mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' into feature/OP-2461_slate-generator-handling
This commit is contained in:
commit
ed86f542ab
56 changed files with 3206 additions and 1297 deletions
41
CHANGELOG.md
41
CHANGELOG.md
|
|
@ -1,16 +1,31 @@
|
|||
# Changelog
|
||||
|
||||
## [3.8.2-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.1...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.1...3.8.2)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- TVPaint: Image loaders also work on review family [\#2638](https://github.com/pypeclub/OpenPype/pull/2638)
|
||||
- General: Project backup tools [\#2629](https://github.com/pypeclub/OpenPype/pull/2629)
|
||||
- nuke: adding clear button to write nodes [\#2627](https://github.com/pypeclub/OpenPype/pull/2627)
|
||||
- Ftrack: Family to Asset type mapping is in settings [\#2602](https://github.com/pypeclub/OpenPype/pull/2602)
|
||||
- Nuke: load color space from representation data [\#2576](https://github.com/pypeclub/OpenPype/pull/2576)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Global: fix broken otio review extractor [\#2590](https://github.com/pypeclub/OpenPype/pull/2590)
|
||||
- Fix pulling of cx\_freeze 6.10 [\#2628](https://github.com/pypeclub/OpenPype/pull/2628)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Cosmetics: Fix common typos in openpype/website [\#2617](https://github.com/pypeclub/OpenPype/pull/2617)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Docker: enhance dockerfiles with metadata, fix pyenv initialization [\#2647](https://github.com/pypeclub/OpenPype/pull/2647)
|
||||
- WebPublisher: fix instance duplicates [\#2641](https://github.com/pypeclub/OpenPype/pull/2641)
|
||||
- Fix - safer pulling of task name for webpublishing from PS [\#2613](https://github.com/pypeclub/OpenPype/pull/2613)
|
||||
- Webpublisher: Skip version collect [\#2591](https://github.com/pypeclub/OpenPype/pull/2591)
|
||||
|
||||
## [3.8.1](https://github.com/pypeclub/OpenPype/tree/3.8.1) (2022-02-01)
|
||||
|
||||
|
|
@ -19,6 +34,7 @@
|
|||
**🚀 Enhancements**
|
||||
|
||||
- Webpublisher: Thumbnail extractor [\#2600](https://github.com/pypeclub/OpenPype/pull/2600)
|
||||
- Webpublisher: Added endpoint to reprocess batch through UI [\#2555](https://github.com/pypeclub/OpenPype/pull/2555)
|
||||
- Loader: Allow to toggle default family filters between "include" or "exclude" filtering [\#2541](https://github.com/pypeclub/OpenPype/pull/2541)
|
||||
- Launcher: Added context menu to to skip opening last workfile [\#2536](https://github.com/pypeclub/OpenPype/pull/2536)
|
||||
|
||||
|
|
@ -28,6 +44,7 @@
|
|||
- hotfix: OIIO tool path - add extension on windows [\#2618](https://github.com/pypeclub/OpenPype/pull/2618)
|
||||
- Settings: Enum does not store empty string if has single item to select [\#2615](https://github.com/pypeclub/OpenPype/pull/2615)
|
||||
- switch distutils to sysconfig for `get\_platform\(\)` [\#2594](https://github.com/pypeclub/OpenPype/pull/2594)
|
||||
- Global: fix broken otio review extractor [\#2590](https://github.com/pypeclub/OpenPype/pull/2590)
|
||||
- Fix poetry index and speedcopy update [\#2589](https://github.com/pypeclub/OpenPype/pull/2589)
|
||||
- Webpublisher: Fix - subset names from processed .psd used wrong value for task [\#2586](https://github.com/pypeclub/OpenPype/pull/2586)
|
||||
- `vrscene` creator Deadline webservice URL handling [\#2580](https://github.com/pypeclub/OpenPype/pull/2580)
|
||||
|
|
@ -37,17 +54,12 @@
|
|||
**Merged pull requests:**
|
||||
|
||||
- Bump pillow from 8.4.0 to 9.0.0 [\#2595](https://github.com/pypeclub/OpenPype/pull/2595)
|
||||
- Webpublisher: Skip version collect [\#2591](https://github.com/pypeclub/OpenPype/pull/2591)
|
||||
- build\(deps\): bump pillow from 8.4.0 to 9.0.0 [\#2523](https://github.com/pypeclub/OpenPype/pull/2523)
|
||||
|
||||
## [3.8.0](https://github.com/pypeclub/OpenPype/tree/3.8.0) (2022-01-24)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.0-nightly.7...3.8.0)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Variable in docs renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
- Flame: extracting segments with trans-coding [\#2547](https://github.com/pypeclub/OpenPype/pull/2547)
|
||||
|
|
@ -60,7 +72,6 @@
|
|||
|
||||
- Webpublisher: Moved error at the beginning of the log [\#2559](https://github.com/pypeclub/OpenPype/pull/2559)
|
||||
- Ftrack: Use ApplicationManager to get DJV path [\#2558](https://github.com/pypeclub/OpenPype/pull/2558)
|
||||
- Webpublisher: Added endpoint to reprocess batch through UI [\#2555](https://github.com/pypeclub/OpenPype/pull/2555)
|
||||
- Settings: PathInput strip passed string [\#2550](https://github.com/pypeclub/OpenPype/pull/2550)
|
||||
- Global: Exctract Review anatomy fill data with output name [\#2548](https://github.com/pypeclub/OpenPype/pull/2548)
|
||||
- Cosmetics: Clean up some cosmetics / typos [\#2542](https://github.com/pypeclub/OpenPype/pull/2542)
|
||||
|
|
@ -92,6 +103,10 @@
|
|||
- Maya: reset empty string attributes correctly to "" instead of "None" [\#2506](https://github.com/pypeclub/OpenPype/pull/2506)
|
||||
- Improve FusionPreLaunch hook errors [\#2505](https://github.com/pypeclub/OpenPype/pull/2505)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Variable in docs renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- AfterEffects: Move implementation to OpenPype [\#2543](https://github.com/pypeclub/OpenPype/pull/2543)
|
||||
|
|
@ -104,14 +119,6 @@
|
|||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.7.0-nightly.14...3.7.0)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- General: Workdir extra folders [\#2462](https://github.com/pypeclub/OpenPype/pull/2462)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- TVPaint: Create render layer dialog is in front [\#2471](https://github.com/pypeclub/OpenPype/pull/2471)
|
||||
|
||||
## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.7.0-nightly.1...3.6.4)
|
||||
|
|
|
|||
35
Dockerfile
35
Dockerfile
|
|
@ -1,13 +1,18 @@
|
|||
# Build Pype docker image
|
||||
FROM debian:bookworm-slim AS builder
|
||||
FROM ubuntu:focal AS builder
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.7.12
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
|
||||
LABEL maintainer="info@openpype.io"
|
||||
LABEL description="Docker Image to build and run OpenPype"
|
||||
LABEL description="Docker Image to build and run OpenPype under Ubuntu 20.04"
|
||||
LABEL org.opencontainers.image.name="pypeclub/openpype"
|
||||
LABEL org.opencontainers.image.title="OpenPype Docker Image"
|
||||
LABEL org.opencontainers.image.url="https://openpype.io/"
|
||||
LABEL org.opencontainers.image.source="https://github.com/pypeclub/pype"
|
||||
LABEL org.opencontainers.image.source="https://github.com/pypeclub/OpenPype"
|
||||
LABEL org.opencontainers.image.documentation="https://openpype.io/docs/system_introduction"
|
||||
LABEL org.opencontainers.image.created=$BUILD_DATE
|
||||
LABEL org.opencontainers.image.version=$VERSION
|
||||
|
||||
USER root
|
||||
|
||||
|
|
@ -42,14 +47,19 @@ RUN apt-get update \
|
|||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
|
||||
RUN mkdir /opt/openpype
|
||||
|
||||
# download and install pyenv
|
||||
RUN curl https://pyenv.run | bash \
|
||||
&& echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc \
|
||||
&& echo 'eval "$(pyenv init --path)"' >> $HOME/.bashrc \
|
||||
&& source $HOME/.bashrc && pyenv install ${OPENPYPE_PYTHON_VERSION}
|
||||
&& echo 'export PATH="$HOME/.pyenv/bin:$PATH"'>> $HOME/init_pyenv.sh \
|
||||
&& echo 'eval "$(pyenv init -)"' >> $HOME/init_pyenv.sh \
|
||||
&& echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/init_pyenv.sh \
|
||||
&& echo 'eval "$(pyenv init --path)"' >> $HOME/init_pyenv.sh
|
||||
|
||||
# install python with pyenv
|
||||
RUN source $HOME/init_pyenv.sh \
|
||||
&& pyenv install ${OPENPYPE_PYTHON_VERSION}
|
||||
|
||||
COPY . /opt/openpype/
|
||||
|
||||
|
|
@ -57,13 +67,16 @@ RUN chmod +x /opt/openpype/tools/create_env.sh && chmod +x /opt/openpype/tools/b
|
|||
|
||||
WORKDIR /opt/openpype
|
||||
|
||||
# set local python version
|
||||
RUN cd /opt/openpype \
|
||||
&& source $HOME/.bashrc \
|
||||
&& source $HOME/init_pyenv.sh \
|
||||
&& pyenv local ${OPENPYPE_PYTHON_VERSION}
|
||||
|
||||
RUN source $HOME/.bashrc \
|
||||
# fetch third party tools/libraries
|
||||
RUN source $HOME/init_pyenv.sh \
|
||||
&& ./tools/create_env.sh \
|
||||
&& ./tools/fetch_thirdparty_libs.sh
|
||||
|
||||
RUN source $HOME/.bashrc \
|
||||
# build openpype
|
||||
RUN source $HOME/init_pyenv.sh \
|
||||
&& bash ./tools/build.sh
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
# Build Pype docker image
|
||||
FROM centos:7 AS builder
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.7.10
|
||||
ARG OPENPYPE_PYTHON_VERSION=3.7.12
|
||||
|
||||
LABEL org.opencontainers.image.name="pypeclub/openpype"
|
||||
LABEL org.opencontainers.image.title="OpenPype Docker Image"
|
||||
LABEL org.opencontainers.image.url="https://openpype.io/"
|
||||
LABEL org.opencontainers.image.source="https://github.com/pypeclub/pype"
|
||||
LABEL org.opencontainers.image.documentation="https://openpype.io/docs/system_introduction"
|
||||
LABEL org.opencontainers.image.created=$BUILD_DATE
|
||||
LABEL org.opencontainers.image.version=$VERSION
|
||||
|
||||
|
||||
USER root
|
||||
|
||||
|
|
|
|||
|
|
@ -412,3 +412,23 @@ def repack_version(directory):
|
|||
directory name.
|
||||
"""
|
||||
PypeCommands().repack_version(directory)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("--project", help="Project name")
|
||||
@click.option(
|
||||
"--dirpath", help="Directory where package is stored", default=None
|
||||
)
|
||||
def pack_project(project, dirpath):
|
||||
"""Create a package of project with all files and database dump."""
|
||||
PypeCommands().pack_project(project, dirpath)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("--zipfile", help="Path to zip file")
|
||||
@click.option(
|
||||
"--root", help="Replace root which was stored in project", default=None
|
||||
)
|
||||
def unpack_project(zipfile, root):
|
||||
"""Create a package of project with all files and database dump."""
|
||||
PypeCommands().unpack_project(zipfile, root)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ from .lib import (
|
|||
maintained_selection
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
colorspace_exists_on_node,
|
||||
get_colorspace_list
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"file_extensions",
|
||||
|
|
@ -54,4 +58,7 @@ __all__ = (
|
|||
"update_container",
|
||||
|
||||
"maintained_selection",
|
||||
|
||||
"colorspace_exists_on_node",
|
||||
"get_colorspace_list"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -753,7 +753,7 @@ def script_name():
|
|||
|
||||
def add_button_write_to_read(node):
|
||||
name = "createReadNode"
|
||||
label = "Create Read From Rendered"
|
||||
label = "Read From Rendered"
|
||||
value = "import write_to_read;\
|
||||
write_to_read.write_to_read(nuke.thisNode(), allow_relative=False)"
|
||||
knob = nuke.PyScript_Knob(name, label, value)
|
||||
|
|
@ -761,6 +761,15 @@ def add_button_write_to_read(node):
|
|||
node.addKnob(knob)
|
||||
|
||||
|
||||
def add_button_clear_rendered(node, path):
|
||||
name = "clearRendered"
|
||||
label = "Clear Rendered"
|
||||
value = "import clear_rendered;\
|
||||
clear_rendered.clear_rendered(\"{}\")".format(path)
|
||||
knob = nuke.PyScript_Knob(name, label, value)
|
||||
node.addKnob(knob)
|
||||
|
||||
|
||||
def create_write_node(name, data, input=None, prenodes=None,
|
||||
review=True, linked_knobs=None, farm=True):
|
||||
''' Creating write node which is group node
|
||||
|
|
@ -988,6 +997,9 @@ def create_write_node(name, data, input=None, prenodes=None,
|
|||
# adding write to read button
|
||||
add_button_write_to_read(GN)
|
||||
|
||||
# adding write to read button
|
||||
add_button_clear_rendered(GN, os.path.dirname(fpath))
|
||||
|
||||
# Deadline tab.
|
||||
add_deadline_tab(GN)
|
||||
|
||||
|
|
|
|||
|
|
@ -82,3 +82,50 @@ def bake_gizmos_recursively(in_group=None):
|
|||
|
||||
if node.Class() == "Group":
|
||||
bake_gizmos_recursively(node)
|
||||
|
||||
|
||||
def colorspace_exists_on_node(node, colorspace_name):
|
||||
""" Check if colorspace exists on node
|
||||
|
||||
Look through all options in the colorpsace knob, and see if we have an
|
||||
exact match to one of the items.
|
||||
|
||||
Args:
|
||||
node (nuke.Node): nuke node object
|
||||
colorspace_name (str): color profile name
|
||||
|
||||
Returns:
|
||||
bool: True if exists
|
||||
"""
|
||||
try:
|
||||
colorspace_knob = node['colorspace']
|
||||
except ValueError:
|
||||
# knob is not available on input node
|
||||
return False
|
||||
all_clrs = get_colorspace_list(colorspace_knob)
|
||||
|
||||
return colorspace_name in all_clrs
|
||||
|
||||
|
||||
def get_colorspace_list(colorspace_knob):
|
||||
"""Get available colorspace profile names
|
||||
|
||||
Args:
|
||||
colorspace_knob (nuke.Knob): nuke knob object
|
||||
|
||||
Returns:
|
||||
list: list of strings names of profiles
|
||||
"""
|
||||
|
||||
all_clrs = list(colorspace_knob.values())
|
||||
reduced_clrs = []
|
||||
|
||||
if not colorspace_knob.getFlag(nuke.STRIP_CASCADE_PREFIX):
|
||||
return all_clrs
|
||||
|
||||
# strip colorspace with nested path
|
||||
for clrs in all_clrs:
|
||||
clrs = clrs.split('/')[-1]
|
||||
reduced_clrs.append(clrs)
|
||||
|
||||
return reduced_clrs
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ from openpype.hosts.nuke.api.lib import (
|
|||
from openpype.hosts.nuke.api import (
|
||||
containerise,
|
||||
update_container,
|
||||
viewer_update_and_undo_stop
|
||||
viewer_update_and_undo_stop,
|
||||
colorspace_exists_on_node
|
||||
)
|
||||
from openpype.hosts.nuke.api import plugin
|
||||
|
||||
|
|
@ -66,11 +67,11 @@ class LoadClip(plugin.NukeLoader):
|
|||
)
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
|
||||
repre = context["representation"]
|
||||
# reste container id so it is always unique for each instance
|
||||
self.reset_container_id()
|
||||
|
||||
is_sequence = len(context["representation"]["files"]) > 1
|
||||
is_sequence = len(repre["files"]) > 1
|
||||
|
||||
file = self.fname.replace("\\", "/")
|
||||
|
||||
|
|
@ -79,14 +80,13 @@ class LoadClip(plugin.NukeLoader):
|
|||
|
||||
version = context['version']
|
||||
version_data = version.get("data", {})
|
||||
repr_id = context["representation"]["_id"]
|
||||
colorspace = version_data.get("colorspace")
|
||||
iio_colorspace = get_imageio_input_colorspace(file)
|
||||
repr_cont = context["representation"]["context"]
|
||||
repre_id = repre["_id"]
|
||||
|
||||
repre_cont = repre["context"]
|
||||
|
||||
self.log.info("version_data: {}\n".format(version_data))
|
||||
self.log.debug(
|
||||
"Representation id `{}` ".format(repr_id))
|
||||
"Representation id `{}` ".format(repre_id))
|
||||
|
||||
self.handle_start = version_data.get("handleStart", 0)
|
||||
self.handle_end = version_data.get("handleEnd", 0)
|
||||
|
|
@ -101,7 +101,7 @@ class LoadClip(plugin.NukeLoader):
|
|||
first = 1
|
||||
last = first + duration
|
||||
elif "#" not in file:
|
||||
frame = repr_cont.get("frame")
|
||||
frame = repre_cont.get("frame")
|
||||
assert frame, "Representation is not sequence"
|
||||
|
||||
padding = len(frame)
|
||||
|
|
@ -113,10 +113,10 @@ class LoadClip(plugin.NukeLoader):
|
|||
|
||||
if not file:
|
||||
self.log.warning(
|
||||
"Representation id `{}` is failing to load".format(repr_id))
|
||||
"Representation id `{}` is failing to load".format(repre_id))
|
||||
return
|
||||
|
||||
read_name = self._get_node_name(context["representation"])
|
||||
read_name = self._get_node_name(repre)
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
read_node = nuke.createNode(
|
||||
|
|
@ -128,11 +128,8 @@ class LoadClip(plugin.NukeLoader):
|
|||
with viewer_update_and_undo_stop():
|
||||
read_node["file"].setValue(file)
|
||||
|
||||
# Set colorspace defined in version data
|
||||
if colorspace:
|
||||
read_node["colorspace"].setValue(str(colorspace))
|
||||
elif iio_colorspace is not None:
|
||||
read_node["colorspace"].setValue(iio_colorspace)
|
||||
used_colorspace = self._set_colorspace(
|
||||
read_node, version_data, repre["data"])
|
||||
|
||||
self._set_range_to_node(read_node, first, last, start_at_workfile)
|
||||
|
||||
|
|
@ -145,6 +142,12 @@ class LoadClip(plugin.NukeLoader):
|
|||
for k in add_keys:
|
||||
if k == 'version':
|
||||
data_imprint.update({k: context["version"]['name']})
|
||||
elif k == 'colorspace':
|
||||
colorspace = repre["data"].get(k)
|
||||
colorspace = colorspace or version_data.get(k)
|
||||
data_imprint["db_colorspace"] = colorspace
|
||||
if used_colorspace:
|
||||
data_imprint["used_colorspace"] = used_colorspace
|
||||
else:
|
||||
data_imprint.update(
|
||||
{k: context["version"]['data'].get(k, str(None))})
|
||||
|
|
@ -192,10 +195,13 @@ class LoadClip(plugin.NukeLoader):
|
|||
"_id": representation["parent"]
|
||||
})
|
||||
version_data = version.get("data", {})
|
||||
repr_id = representation["_id"]
|
||||
colorspace = version_data.get("colorspace")
|
||||
iio_colorspace = get_imageio_input_colorspace(file)
|
||||
repr_cont = representation["context"]
|
||||
repre_id = representation["_id"]
|
||||
|
||||
repre_cont = representation["context"]
|
||||
|
||||
# colorspace profile
|
||||
colorspace = representation["data"].get("colorspace")
|
||||
colorspace = colorspace or version_data.get("colorspace")
|
||||
|
||||
self.handle_start = version_data.get("handleStart", 0)
|
||||
self.handle_end = version_data.get("handleEnd", 0)
|
||||
|
|
@ -210,7 +216,7 @@ class LoadClip(plugin.NukeLoader):
|
|||
first = 1
|
||||
last = first + duration
|
||||
elif "#" not in file:
|
||||
frame = repr_cont.get("frame")
|
||||
frame = repre_cont.get("frame")
|
||||
assert frame, "Representation is not sequence"
|
||||
|
||||
padding = len(frame)
|
||||
|
|
@ -218,7 +224,7 @@ class LoadClip(plugin.NukeLoader):
|
|||
|
||||
if not file:
|
||||
self.log.warning(
|
||||
"Representation id `{}` is failing to load".format(repr_id))
|
||||
"Representation id `{}` is failing to load".format(repre_id))
|
||||
return
|
||||
|
||||
read_name = self._get_node_name(representation)
|
||||
|
|
@ -229,12 +235,9 @@ class LoadClip(plugin.NukeLoader):
|
|||
# to avoid multiple undo steps for rest of process
|
||||
# we will switch off undo-ing
|
||||
with viewer_update_and_undo_stop():
|
||||
|
||||
# Set colorspace defined in version data
|
||||
if colorspace:
|
||||
read_node["colorspace"].setValue(str(colorspace))
|
||||
elif iio_colorspace is not None:
|
||||
read_node["colorspace"].setValue(iio_colorspace)
|
||||
used_colorspace = self._set_colorspace(
|
||||
read_node, version_data, representation["data"],
|
||||
path=file)
|
||||
|
||||
self._set_range_to_node(read_node, first, last, start_at_workfile)
|
||||
|
||||
|
|
@ -243,7 +246,7 @@ class LoadClip(plugin.NukeLoader):
|
|||
"frameStart": str(first),
|
||||
"frameEnd": str(last),
|
||||
"version": str(version.get("name")),
|
||||
"colorspace": colorspace,
|
||||
"db_colorspace": colorspace,
|
||||
"source": version_data.get("source"),
|
||||
"handleStart": str(self.handle_start),
|
||||
"handleEnd": str(self.handle_end),
|
||||
|
|
@ -251,6 +254,10 @@ class LoadClip(plugin.NukeLoader):
|
|||
"author": version_data.get("author")
|
||||
}
|
||||
|
||||
# add used colorspace if found any
|
||||
if used_colorspace:
|
||||
updated_dict["used_colorspace"] = used_colorspace
|
||||
|
||||
# change color of read_node
|
||||
# get all versions in list
|
||||
versions = io.find({
|
||||
|
|
@ -365,14 +372,37 @@ class LoadClip(plugin.NukeLoader):
|
|||
|
||||
def _get_node_name(self, representation):
|
||||
|
||||
repr_cont = representation["context"]
|
||||
repre_cont = representation["context"]
|
||||
name_data = {
|
||||
"asset": repr_cont["asset"],
|
||||
"subset": repr_cont["subset"],
|
||||
"asset": repre_cont["asset"],
|
||||
"subset": repre_cont["subset"],
|
||||
"representation": representation["name"],
|
||||
"ext": repr_cont["representation"],
|
||||
"ext": repre_cont["representation"],
|
||||
"id": representation["_id"],
|
||||
"class_name": self.__class__.__name__
|
||||
}
|
||||
|
||||
return self.node_name_template.format(**name_data)
|
||||
|
||||
def _set_colorspace(self, node, version_data, repre_data, path=None):
|
||||
output_color = None
|
||||
path = path or self.fname.replace("\\", "/")
|
||||
# get colorspace
|
||||
colorspace = repre_data.get("colorspace")
|
||||
colorspace = colorspace or version_data.get("colorspace")
|
||||
|
||||
# colorspace from `project_anatomy/imageio/nuke/regexInputs`
|
||||
iio_colorspace = get_imageio_input_colorspace(path)
|
||||
|
||||
# Set colorspace defined in version data
|
||||
if (
|
||||
colorspace is not None
|
||||
and colorspace_exists_on_node(node, str(colorspace))
|
||||
):
|
||||
node["colorspace"].setValue(str(colorspace))
|
||||
output_color = str(colorspace)
|
||||
elif iio_colorspace is not None:
|
||||
node["colorspace"].setValue(iio_colorspace)
|
||||
output_color = iio_colorspace
|
||||
|
||||
return output_color
|
||||
|
|
|
|||
11
openpype/hosts/nuke/startup/clear_rendered.py
Normal file
11
openpype/hosts/nuke/startup/clear_rendered.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import os
|
||||
|
||||
from openpype.api import Logger
|
||||
log = Logger().get_logger(__name__)
|
||||
|
||||
|
||||
def clear_rendered(dir_path):
|
||||
for _f in os.listdir(dir_path):
|
||||
_f_path = os.path.join(dir_path, _f)
|
||||
log.info("Removing: `{}`".format(_f_path))
|
||||
os.remove(_f_path)
|
||||
|
|
@ -10,6 +10,7 @@ import avalon.api
|
|||
from openpype.api import Logger
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.lib.remote_publish import headless_publish
|
||||
from openpype.lib import env_value_to_bool
|
||||
|
||||
from .launch_logic import ProcessLauncher, stub
|
||||
|
||||
|
|
@ -34,20 +35,19 @@ def main(*subprocess_args):
|
|||
launcher = ProcessLauncher(subprocess_args)
|
||||
launcher.start()
|
||||
|
||||
if os.environ.get("HEADLESS_PUBLISH"):
|
||||
if env_value_to_bool("HEADLESS_PUBLISH"):
|
||||
launcher.execute_in_main_thread(
|
||||
headless_publish,
|
||||
log,
|
||||
"ClosePS",
|
||||
os.environ.get("IS_TEST")
|
||||
)
|
||||
elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
|
||||
save = False
|
||||
if os.getenv("WORKFILES_SAVE_AS"):
|
||||
save = True
|
||||
elif env_value_to_bool("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH",
|
||||
default=True):
|
||||
|
||||
launcher.execute_in_main_thread(
|
||||
host_tools.show_workfiles, save=save
|
||||
host_tools.show_workfiles,
|
||||
save=env_value_to_bool("WORKFILES_SAVE_AS")
|
||||
)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ class CreateImage(openpype.api.Creator):
|
|||
create_group = False
|
||||
|
||||
stub = photoshop.stub()
|
||||
useSelection = False
|
||||
if (self.options or {}).get("useSelection"):
|
||||
useSelection = True
|
||||
multiple_instances = False
|
||||
selection = stub.get_selected_layers()
|
||||
self.log.info("selection {}".format(selection))
|
||||
|
|
@ -64,8 +62,7 @@ class CreateImage(openpype.api.Creator):
|
|||
# No selection creates an empty group.
|
||||
create_group = True
|
||||
else:
|
||||
stub.select_layers(stub.get_layers())
|
||||
group = stub.group_selected_layers(self.name)
|
||||
group = stub.create_group(self.name)
|
||||
groups.append(group)
|
||||
|
||||
if create_group:
|
||||
|
|
@ -77,16 +74,15 @@ class CreateImage(openpype.api.Creator):
|
|||
group = stub.group_selected_layers(layer.name)
|
||||
groups.append(group)
|
||||
|
||||
creator_subset_name = self.data["subset"]
|
||||
for group in groups:
|
||||
long_names = []
|
||||
group.name = group.name.replace(stub.PUBLISH_ICON, ''). \
|
||||
replace(stub.LOADED_ICON, '')
|
||||
|
||||
if useSelection:
|
||||
subset_name = self.data["subset"] + group.name
|
||||
else:
|
||||
# use value provided by user from Creator
|
||||
subset_name = self.data["subset"]
|
||||
subset_name = creator_subset_name
|
||||
if len(groups) > 1:
|
||||
subset_name += group.name.title().replace(" ", "")
|
||||
|
||||
if group.long_name:
|
||||
for directory in group.long_name[::-1]:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype.hosts.photoshop import api as photoshop
|
|||
|
||||
|
||||
class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
||||
"""Gather instances configured color code of a layer.
|
||||
"""Creates instances for configured color code of a layer.
|
||||
|
||||
Used in remote publishing when artists marks publishable layers by color-
|
||||
coding.
|
||||
|
|
@ -46,6 +46,11 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
|||
stub = photoshop.stub()
|
||||
layers = stub.get_layers()
|
||||
|
||||
existing_subset_names = []
|
||||
for instance in context:
|
||||
if instance.data.get('publish'):
|
||||
existing_subset_names.append(instance.data.get('subset'))
|
||||
|
||||
asset, task_name, task_type = get_batch_asset_task_info(
|
||||
task_data["context"])
|
||||
|
||||
|
|
@ -55,6 +60,10 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
|||
instance_names = []
|
||||
for layer in layers:
|
||||
self.log.debug("Layer:: {}".format(layer))
|
||||
if layer.parents:
|
||||
self.log.debug("!!! Not a top layer, skip")
|
||||
continue
|
||||
|
||||
resolved_family, resolved_subset_template = self._resolve_mapping(
|
||||
layer
|
||||
)
|
||||
|
|
@ -66,8 +75,19 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
|||
self.log.debug("!!! Not found family or template, skip")
|
||||
continue
|
||||
|
||||
if layer.parents:
|
||||
self.log.debug("!!! Not a top layer, skip")
|
||||
fill_pairs = {
|
||||
"variant": variant,
|
||||
"family": resolved_family,
|
||||
"task": task_name,
|
||||
"layer": layer.name
|
||||
}
|
||||
|
||||
subset = resolved_subset_template.format(
|
||||
**prepare_template_data(fill_pairs))
|
||||
|
||||
if subset in existing_subset_names:
|
||||
self.log.info(
|
||||
"Subset {} already created, skipping.".format(subset))
|
||||
continue
|
||||
|
||||
instance = context.create_instance(layer.name)
|
||||
|
|
@ -76,15 +96,6 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
|||
instance.data["publish"] = layer.visible
|
||||
instance.data["asset"] = asset
|
||||
instance.data["task"] = task_name
|
||||
|
||||
fill_pairs = {
|
||||
"variant": variant,
|
||||
"family": instance.data["family"],
|
||||
"task": instance.data["task"],
|
||||
"layer": layer.name
|
||||
}
|
||||
subset = resolved_subset_template.format(
|
||||
**prepare_template_data(fill_pairs))
|
||||
instance.data["subset"] = subset
|
||||
|
||||
instance_names.append(layer.name)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import collections
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
|
|
@ -16,11 +17,14 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin):
|
|||
subset_names = []
|
||||
|
||||
for instance in context:
|
||||
self.log.info("instance:: {}".format(instance.data))
|
||||
if instance.data.get('publish'):
|
||||
subset_names.append(instance.data.get('subset'))
|
||||
|
||||
msg = (
|
||||
"Instance subset names are not unique. " +
|
||||
"Remove duplicates via SubsetManager."
|
||||
)
|
||||
assert len(subset_names) == len(set(subset_names)), msg
|
||||
non_unique = \
|
||||
[item
|
||||
for item, count in collections.Counter(subset_names).items()
|
||||
if count > 1]
|
||||
msg = ("Instance subset names {} are not unique. ".format(non_unique) +
|
||||
"Remove duplicates via SubsetManager.")
|
||||
assert not non_unique, msg
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class MyAutoCreator(AutoCreator):
|
|||
identifier = "workfile"
|
||||
family = "workfile"
|
||||
|
||||
def get_attribute_defs(self):
|
||||
def get_instance_attr_defs(self):
|
||||
output = [
|
||||
lib.NumberDef("number_key", label="Number")
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from openpype import resources
|
||||
from openpype.hosts.testhost.api import pipeline
|
||||
from openpype.pipeline import (
|
||||
|
|
@ -13,6 +14,8 @@ class TestCreatorOne(Creator):
|
|||
family = "test"
|
||||
description = "Testing creator of testhost"
|
||||
|
||||
create_allow_context_change = False
|
||||
|
||||
def get_icon(self):
|
||||
return resources.get_openpype_splash_filepath()
|
||||
|
||||
|
|
@ -33,7 +36,10 @@ class TestCreatorOne(Creator):
|
|||
for instance in instances:
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def create(self, subset_name, data, options=None):
|
||||
def create(self, subset_name, data, pre_create_data):
|
||||
print("Data that can be used in create:\n{}".format(
|
||||
json.dumps(pre_create_data, indent=4)
|
||||
))
|
||||
new_instance = CreatedInstance(self.family, subset_name, data, self)
|
||||
pipeline.HostContext.add_instance(new_instance.data_to_store())
|
||||
self.log.info(new_instance.data)
|
||||
|
|
@ -46,9 +52,21 @@ class TestCreatorOne(Creator):
|
|||
"different_variant"
|
||||
]
|
||||
|
||||
def get_attribute_defs(self):
|
||||
def get_instance_attr_defs(self):
|
||||
output = [
|
||||
lib.NumberDef("number_key", label="Number")
|
||||
lib.NumberDef("number_key", label="Number"),
|
||||
]
|
||||
return output
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
output = [
|
||||
lib.BoolDef("use_selection", label="Use selection"),
|
||||
lib.UISeparatorDef(),
|
||||
lib.UILabelDef("Testing label"),
|
||||
lib.FileDef("filepath", folders=True, label="Filepath"),
|
||||
lib.FileDef(
|
||||
"filepath_2", multipath=True, folders=True, label="Filepath 2"
|
||||
)
|
||||
]
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class TestCreatorTwo(Creator):
|
|||
def get_icon(self):
|
||||
return "cube"
|
||||
|
||||
def create(self, subset_name, data, options=None):
|
||||
def create(self, subset_name, data, pre_create_data):
|
||||
new_instance = CreatedInstance(self.family, subset_name, data, self)
|
||||
pipeline.HostContext.add_instance(new_instance.data_to_store())
|
||||
self.log.info(new_instance.data)
|
||||
|
|
@ -38,7 +38,7 @@ class TestCreatorTwo(Creator):
|
|||
for instance in instances:
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def get_attribute_defs(self):
|
||||
def get_instance_attr_defs(self):
|
||||
output = [
|
||||
lib.NumberDef("number_key"),
|
||||
lib.TextDef("text_key")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class CollectContextDataTestHost(
|
|||
hosts = ["testhost"]
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
def get_instance_attr_defs(cls):
|
||||
return [
|
||||
attribute_definitions.BoolDef(
|
||||
"test_bool",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class CollectInstanceOneTestHost(
|
|||
hosts = ["testhost"]
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
def get_instance_attr_defs(cls):
|
||||
return [
|
||||
attribute_definitions.NumberDef(
|
||||
"version",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from openpype.hosts.tvpaint.api import lib, plugin
|
|||
class ImportImage(plugin.Loader):
|
||||
"""Load image or image sequence to TVPaint as new layer."""
|
||||
|
||||
families = ["render", "image", "background", "plate"]
|
||||
families = ["render", "image", "background", "plate", "review"]
|
||||
representations = ["*"]
|
||||
|
||||
label = "Import Image"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from openpype.hosts.tvpaint.api import lib, pipeline, plugin
|
|||
class LoadImage(plugin.Loader):
|
||||
"""Load image or image sequence to TVPaint as new layer."""
|
||||
|
||||
families = ["render", "image", "background", "plate"]
|
||||
families = ["render", "image", "background", "plate", "review"]
|
||||
representations = ["*"]
|
||||
|
||||
label = "Load Image"
|
||||
|
|
|
|||
247
openpype/lib/project_backpack.py
Normal file
247
openpype/lib/project_backpack.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""These lib functions are primarily for development purposes.
|
||||
|
||||
WARNING: This is not meant for production data.
|
||||
|
||||
Goal is to be able create package of current state of project with related
|
||||
documents from mongo and files from disk to zip file and then be able recreate
|
||||
the project based on the zip.
|
||||
|
||||
This gives ability to create project where a changes and tests can be done.
|
||||
|
||||
Keep in mind that to be able create a package of project has few requirements.
|
||||
Possible requirement should be listed in 'pack_project' function.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import platform
|
||||
import tempfile
|
||||
import shutil
|
||||
import datetime
|
||||
|
||||
import zipfile
|
||||
from bson.json_util import (
|
||||
loads,
|
||||
dumps,
|
||||
CANONICAL_JSON_OPTIONS
|
||||
)
|
||||
|
||||
from avalon.api import AvalonMongoDB
|
||||
|
||||
DOCUMENTS_FILE_NAME = "database"
|
||||
METADATA_FILE_NAME = "metadata"
|
||||
PROJECT_FILES_DIR = "project_files"
|
||||
|
||||
|
||||
def add_timestamp(filepath):
|
||||
"""Add timestamp string to a file."""
|
||||
base, ext = os.path.splitext(filepath)
|
||||
timestamp = datetime.datetime.now().strftime("%y%m%d_%H%M%S")
|
||||
new_base = "{}_{}".format(base, timestamp)
|
||||
return new_base + ext
|
||||
|
||||
|
||||
def pack_project(project_name, destination_dir=None):
|
||||
"""Make a package of a project with mongo documents and files.
|
||||
|
||||
This function has few restrictions:
|
||||
- project must have only one root
|
||||
- project must have all templates starting with
|
||||
"{root[...]}/{project[name]}"
|
||||
|
||||
Args:
|
||||
project_name(str): Project that should be packaged.
|
||||
destination_dir(str): Optinal path where zip will be stored. Project's
|
||||
root is used if not passed.
|
||||
"""
|
||||
print("Creating package of project \"{}\"".format(project_name))
|
||||
# Validate existence of project
|
||||
dbcon = AvalonMongoDB()
|
||||
dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
project_doc = dbcon.find_one({"type": "project"})
|
||||
if not project_doc:
|
||||
raise ValueError("Project \"{}\" was not found in database".format(
|
||||
project_name
|
||||
))
|
||||
|
||||
roots = project_doc["config"]["roots"]
|
||||
# Determine root directory of project
|
||||
source_root = None
|
||||
source_root_name = None
|
||||
for root_name, root_value in roots.items():
|
||||
if source_root is not None:
|
||||
raise ValueError(
|
||||
"Packaging is supported only for single root projects"
|
||||
)
|
||||
source_root = root_value
|
||||
source_root_name = root_name
|
||||
|
||||
root_path = source_root[platform.system().lower()]
|
||||
print("Using root \"{}\" with path \"{}\"".format(
|
||||
source_root_name, root_path
|
||||
))
|
||||
|
||||
project_source_path = os.path.join(root_path, project_name)
|
||||
if not os.path.exists(project_source_path):
|
||||
raise ValueError("Didn't find source of project files")
|
||||
|
||||
# Determine zip filepath where data will be stored
|
||||
if not destination_dir:
|
||||
destination_dir = root_path
|
||||
|
||||
destination_dir = os.path.normpath(destination_dir)
|
||||
if not os.path.exists(destination_dir):
|
||||
os.makedirs(destination_dir)
|
||||
|
||||
zip_path = os.path.join(destination_dir, project_name + ".zip")
|
||||
|
||||
print("Project will be packaged into \"{}\"".format(zip_path))
|
||||
# Rename already existing zip
|
||||
if os.path.exists(zip_path):
|
||||
dst_filepath = add_timestamp(zip_path)
|
||||
os.rename(zip_path, dst_filepath)
|
||||
|
||||
# We can add more data
|
||||
metadata = {
|
||||
"project_name": project_name,
|
||||
"root": source_root,
|
||||
"version": 1
|
||||
}
|
||||
# Create temp json file where metadata are stored
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as s:
|
||||
temp_metadata_json = s.name
|
||||
|
||||
with open(temp_metadata_json, "w") as stream:
|
||||
json.dump(metadata, stream)
|
||||
|
||||
# Create temp json file where database documents are stored
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as s:
|
||||
temp_docs_json = s.name
|
||||
|
||||
# Query all project documents and store them to temp json
|
||||
docs = list(dbcon.find({}))
|
||||
data = dumps(
|
||||
docs, json_options=CANONICAL_JSON_OPTIONS
|
||||
)
|
||||
with open(temp_docs_json, "w") as stream:
|
||||
stream.write(data)
|
||||
|
||||
print("Packing files into zip")
|
||||
# Write all to zip file
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zip_stream:
|
||||
# Add metadata file
|
||||
zip_stream.write(temp_metadata_json, METADATA_FILE_NAME + ".json")
|
||||
# Add database documents
|
||||
zip_stream.write(temp_docs_json, DOCUMENTS_FILE_NAME + ".json")
|
||||
# Add project files to zip
|
||||
for root, _, filenames in os.walk(project_source_path):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(root, filename)
|
||||
# TODO add one more folder
|
||||
archive_name = os.path.join(
|
||||
PROJECT_FILES_DIR,
|
||||
os.path.relpath(filepath, root_path)
|
||||
)
|
||||
zip_stream.write(filepath, archive_name)
|
||||
|
||||
print("Cleaning up")
|
||||
# Cleanup
|
||||
os.remove(temp_docs_json)
|
||||
os.remove(temp_metadata_json)
|
||||
dbcon.uninstall()
|
||||
print("*** Packing finished ***")
|
||||
|
||||
|
||||
def unpack_project(path_to_zip, new_root=None):
|
||||
"""Unpack project zip file to recreate project.
|
||||
|
||||
Args:
|
||||
path_to_zip(str): Path to zip which was created using 'pack_project'
|
||||
function.
|
||||
new_root(str): Optional way how to set different root path for unpacked
|
||||
project.
|
||||
"""
|
||||
print("Unpacking project from zip {}".format(path_to_zip))
|
||||
if not os.path.exists(path_to_zip):
|
||||
print("Zip file does not exists: {}".format(path_to_zip))
|
||||
return
|
||||
|
||||
tmp_dir = tempfile.mkdtemp(prefix="unpack_")
|
||||
print("Zip is extracted to temp: {}".format(tmp_dir))
|
||||
with zipfile.ZipFile(path_to_zip, "r") as zip_stream:
|
||||
zip_stream.extractall(tmp_dir)
|
||||
|
||||
metadata_json_path = os.path.join(tmp_dir, METADATA_FILE_NAME + ".json")
|
||||
with open(metadata_json_path, "r") as stream:
|
||||
metadata = json.load(stream)
|
||||
|
||||
docs_json_path = os.path.join(tmp_dir, DOCUMENTS_FILE_NAME + ".json")
|
||||
with open(docs_json_path, "r") as stream:
|
||||
content = stream.readlines()
|
||||
docs = loads("".join(content))
|
||||
|
||||
low_platform = platform.system().lower()
|
||||
project_name = metadata["project_name"]
|
||||
source_root = metadata["root"]
|
||||
root_path = source_root[low_platform]
|
||||
|
||||
# Drop existing collection
|
||||
dbcon = AvalonMongoDB()
|
||||
database = dbcon.database
|
||||
if project_name in database.list_collection_names():
|
||||
database.drop_collection(project_name)
|
||||
print("Removed existing project collection")
|
||||
|
||||
print("Creating project documents ({})".format(len(docs)))
|
||||
# Create new collection with loaded docs
|
||||
collection = database[project_name]
|
||||
collection.insert_many(docs)
|
||||
|
||||
# Skip change of root if is the same as the one stored in metadata
|
||||
if (
|
||||
new_root
|
||||
and (os.path.normpath(new_root) == os.path.normpath(root_path))
|
||||
):
|
||||
new_root = None
|
||||
|
||||
if new_root:
|
||||
print("Using different root path {}".format(new_root))
|
||||
root_path = new_root
|
||||
|
||||
project_doc = collection.find_one({"type": "project"})
|
||||
roots = project_doc["config"]["roots"]
|
||||
key = tuple(roots.keys())[0]
|
||||
update_key = "config.roots.{}.{}".format(key, low_platform)
|
||||
collection.update_one(
|
||||
{"_id": project_doc["_id"]},
|
||||
{"$set": {
|
||||
update_key: new_root
|
||||
}}
|
||||
)
|
||||
|
||||
# Make sure root path exists
|
||||
if not os.path.exists(root_path):
|
||||
os.makedirs(root_path)
|
||||
|
||||
src_project_files_dir = os.path.join(
|
||||
tmp_dir, PROJECT_FILES_DIR, project_name
|
||||
)
|
||||
dst_project_files_dir = os.path.normpath(
|
||||
os.path.join(root_path, project_name)
|
||||
)
|
||||
if os.path.exists(dst_project_files_dir):
|
||||
new_path = add_timestamp(dst_project_files_dir)
|
||||
print("Project folder already exists. Renamed \"{}\" -> \"{}\"".format(
|
||||
dst_project_files_dir, new_path
|
||||
))
|
||||
os.rename(dst_project_files_dir, new_path)
|
||||
|
||||
print("Moving project files from temp \"{}\" -> \"{}\"".format(
|
||||
src_project_files_dir, dst_project_files_dir
|
||||
))
|
||||
shutil.move(src_project_files_dir, dst_project_files_dir)
|
||||
|
||||
# CLeanup
|
||||
print("Cleaning up")
|
||||
shutil.rmtree(tmp_dir)
|
||||
dbcon.uninstall()
|
||||
print("*** Unpack finished ***")
|
||||
|
|
@ -88,7 +88,6 @@ def publish(log, close_plugin_name=None):
|
|||
if close_plugin: # close host app explicitly after error
|
||||
context = pyblish.api.Context()
|
||||
close_plugin().process(context)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None):
|
||||
|
|
@ -137,7 +136,7 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None):
|
|||
if close_plugin: # close host app explicitly after error
|
||||
context = pyblish.api.Context()
|
||||
close_plugin().process(context)
|
||||
sys.exit(1)
|
||||
return
|
||||
elif processed % log_every == 0:
|
||||
# pyblish returns progress in 0.0 - 2.0
|
||||
progress = min(round(result["progress"] / 2 * 100), 99)
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class AttributeValues:
|
|||
attr_defs_by_key = {
|
||||
attr_def.key: attr_def
|
||||
for attr_def in attr_defs
|
||||
if attr_def.is_value_def
|
||||
}
|
||||
for key, value in values.items():
|
||||
if key not in attr_defs_by_key:
|
||||
|
|
@ -306,8 +307,6 @@ class PublishAttributes:
|
|||
self._plugin_names_order = []
|
||||
self._missing_plugins = []
|
||||
self.attr_plugins = attr_plugins or []
|
||||
if not attr_plugins:
|
||||
return
|
||||
|
||||
origin_data = self._origin_data
|
||||
data = self._data
|
||||
|
|
@ -420,7 +419,7 @@ class CreatedInstance:
|
|||
# Stored creator specific attribute values
|
||||
# {key: value}
|
||||
creator_values = copy.deepcopy(orig_creator_attributes)
|
||||
creator_attr_defs = creator.get_attribute_defs()
|
||||
creator_attr_defs = creator.get_instance_attr_defs()
|
||||
|
||||
self._data["creator_attributes"] = CreatorAttributeValues(
|
||||
self, creator_attr_defs, creator_values, orig_creator_attributes
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class BaseCreator:
|
|||
self.create_context.creator_removed_instance(instance)
|
||||
|
||||
@abstractmethod
|
||||
def create(self, options=None):
|
||||
def create(self):
|
||||
"""Create new instance.
|
||||
|
||||
Replacement of `process` method from avalon implementation.
|
||||
|
|
@ -163,7 +163,7 @@ class BaseCreator:
|
|||
dynamic_data=dynamic_data
|
||||
)
|
||||
|
||||
def get_attribute_defs(self):
|
||||
def get_instance_attr_defs(self):
|
||||
"""Plugin attribute definitions.
|
||||
|
||||
Attribute definitions of plugin that hold data about created instance
|
||||
|
|
@ -199,15 +199,22 @@ class Creator(BaseCreator):
|
|||
# - may not be used if `get_detail_description` is overriden
|
||||
detailed_description = None
|
||||
|
||||
# It does make sense to change context on creation
|
||||
# - in some cases it may confuse artists because it would not be used
|
||||
# e.g. for buld creators
|
||||
create_allow_context_change = True
|
||||
|
||||
@abstractmethod
|
||||
def create(self, subset_name, instance_data, options=None):
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
"""Create new instance and store it.
|
||||
|
||||
Ideally should be stored to workfile using host implementation.
|
||||
|
||||
Args:
|
||||
subset_name(str): Subset name of created instance.
|
||||
instance_data(dict):
|
||||
instance_data(dict): Base data for instance.
|
||||
pre_create_data(dict): Data based on pre creation attributes.
|
||||
Those may affect how creator works.
|
||||
"""
|
||||
|
||||
# instance = CreatedInstance(
|
||||
|
|
@ -258,6 +265,19 @@ class Creator(BaseCreator):
|
|||
|
||||
return None
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
"""Plugin attribute definitions needed for creation.
|
||||
Attribute definitions of plugin that define how creation will work.
|
||||
Values of these definitions are passed to `create` method.
|
||||
NOTE:
|
||||
Convert method should be implemented which should care about updating
|
||||
keys/values when plugin attributes change.
|
||||
Returns:
|
||||
list<AbtractAttrDef>: Attribute definitions that can be tweaked for
|
||||
created instance.
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class AutoCreator(BaseCreator):
|
||||
"""Creator which is automatically triggered without user interaction.
|
||||
|
|
|
|||
|
|
@ -5,11 +5,17 @@ from .events import (
|
|||
|
||||
from .attribute_definitions import (
|
||||
AbtractAttrDef,
|
||||
|
||||
UIDef,
|
||||
UISeparatorDef,
|
||||
UILabelDef,
|
||||
|
||||
UnknownDef,
|
||||
NumberDef,
|
||||
TextDef,
|
||||
EnumDef,
|
||||
BoolDef
|
||||
BoolDef,
|
||||
FileDef,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -18,9 +24,15 @@ __all__ = (
|
|||
"BeforeWorkfileSave",
|
||||
|
||||
"AbtractAttrDef",
|
||||
|
||||
"UIDef",
|
||||
"UISeparatorDef",
|
||||
"UILabelDef",
|
||||
|
||||
"UnknownDef",
|
||||
"NumberDef",
|
||||
"TextDef",
|
||||
"EnumDef",
|
||||
"BoolDef"
|
||||
"BoolDef",
|
||||
"FileDef",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,13 +38,21 @@ class AbtractAttrDef:
|
|||
key(str): Under which key will be attribute value stored.
|
||||
label(str): Attribute label.
|
||||
tooltip(str): Attribute tooltip.
|
||||
is_label_horizontal(bool): UI specific argument. Specify if label is
|
||||
next to value input or ahead.
|
||||
"""
|
||||
is_value_def = True
|
||||
|
||||
def __init__(self, key, default, label=None, tooltip=None):
|
||||
def __init__(
|
||||
self, key, default, label=None, tooltip=None, is_label_horizontal=None
|
||||
):
|
||||
if is_label_horizontal is None:
|
||||
is_label_horizontal = True
|
||||
self.key = key
|
||||
self.label = label
|
||||
self.tooltip = tooltip
|
||||
self.default = default
|
||||
self.is_label_horizontal = is_label_horizontal
|
||||
self._id = uuid.uuid4()
|
||||
|
||||
self.__init__class__ = AbtractAttrDef
|
||||
|
|
@ -68,8 +76,39 @@ class AbtractAttrDef:
|
|||
pass
|
||||
|
||||
|
||||
# -----------------------------------------
|
||||
# UI attribute definitoins won't hold value
|
||||
# -----------------------------------------
|
||||
|
||||
class UIDef(AbtractAttrDef):
|
||||
is_value_def = False
|
||||
|
||||
def __init__(self, key=None, default=None, *args, **kwargs):
|
||||
super(UIDef, self).__init__(key, default, *args, **kwargs)
|
||||
|
||||
def convert_value(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class UISeparatorDef(UIDef):
|
||||
pass
|
||||
|
||||
|
||||
class UILabelDef(UIDef):
|
||||
def __init__(self, label):
|
||||
super(UILabelDef, self).__init__(label=label)
|
||||
|
||||
|
||||
# ---------------------------------------
|
||||
# Attribute defintioins should hold value
|
||||
# ---------------------------------------
|
||||
|
||||
class UnknownDef(AbtractAttrDef):
|
||||
"""Definition is not known because definition is not available."""
|
||||
"""Definition is not known because definition is not available.
|
||||
|
||||
This attribute can be used to keep existing data unchanged but does not
|
||||
have known definition of type.
|
||||
"""
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
super(UnknownDef, self).__init__(key, **kwargs)
|
||||
|
|
@ -261,3 +300,90 @@ class BoolDef(AbtractAttrDef):
|
|||
if isinstance(value, bool):
|
||||
return value
|
||||
return self.default
|
||||
|
||||
|
||||
class FileDef(AbtractAttrDef):
|
||||
"""File definition.
|
||||
It is possible to define filters of allowed file extensions and if supports
|
||||
folders.
|
||||
Args:
|
||||
multipath(bool): Allow multiple path.
|
||||
folders(bool): Allow folder paths.
|
||||
extensions(list<str>): Allow files with extensions. Empty list will
|
||||
allow all extensions and None will disable files completely.
|
||||
default(str, list<str>): Defautl value.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, key, multipath=False, folders=None, extensions=None,
|
||||
default=None, **kwargs
|
||||
):
|
||||
if folders is None and extensions is None:
|
||||
folders = True
|
||||
extensions = []
|
||||
|
||||
if default is None:
|
||||
if multipath:
|
||||
default = []
|
||||
else:
|
||||
default = ""
|
||||
else:
|
||||
if multipath:
|
||||
if not isinstance(default, (tuple, list, set)):
|
||||
raise TypeError((
|
||||
"'default' argument must be 'list', 'tuple' or 'set'"
|
||||
", not '{}'"
|
||||
).format(type(default)))
|
||||
|
||||
else:
|
||||
if not isinstance(default, six.string_types):
|
||||
raise TypeError((
|
||||
"'default' argument must be 'str' not '{}'"
|
||||
).format(type(default)))
|
||||
default = default.strip()
|
||||
|
||||
# Change horizontal label
|
||||
is_label_horizontal = kwargs.get("is_label_horizontal")
|
||||
if is_label_horizontal is None:
|
||||
is_label_horizontal = True
|
||||
if multipath:
|
||||
is_label_horizontal = False
|
||||
kwargs["is_label_horizontal"] = is_label_horizontal
|
||||
|
||||
self.multipath = multipath
|
||||
self.folders = folders
|
||||
self.extensions = extensions
|
||||
super(FileDef, self).__init__(key, default=default, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(FileDef, self).__eq__(other):
|
||||
return False
|
||||
|
||||
return (
|
||||
self.multipath == other.multipath
|
||||
and self.folders == other.folders
|
||||
and self.extensions == other.extensions
|
||||
)
|
||||
|
||||
def convert_value(self, value):
|
||||
if isinstance(value, six.string_types):
|
||||
if self.multipath:
|
||||
value = [value.strip()]
|
||||
else:
|
||||
value = value.strip()
|
||||
return value
|
||||
|
||||
if isinstance(value, (tuple, list, set)):
|
||||
_value = []
|
||||
for item in value:
|
||||
if isinstance(item, six.string_types):
|
||||
_value.append(item.strip())
|
||||
|
||||
if self.multipath:
|
||||
return _value
|
||||
|
||||
if not _value:
|
||||
return self.default
|
||||
return _value[0].strip()
|
||||
|
||||
return str(value).strip()
|
||||
|
|
|
|||
|
|
@ -251,7 +251,10 @@ class PypeCommands:
|
|||
|
||||
data = {
|
||||
"last_workfile_path": workfile_path,
|
||||
"start_last_workfile": True
|
||||
"start_last_workfile": True,
|
||||
"project_name": project,
|
||||
"asset_name": asset,
|
||||
"task_name": task_name
|
||||
}
|
||||
|
||||
launched_app = application_manager.launch(app_name, **data)
|
||||
|
|
@ -433,3 +436,13 @@ class PypeCommands:
|
|||
|
||||
version_packer = VersionRepacker(directory)
|
||||
version_packer.process()
|
||||
|
||||
def pack_project(self, project_name, dirpath):
|
||||
from openpype.lib.project_backpack import pack_project
|
||||
|
||||
pack_project(project_name, dirpath)
|
||||
|
||||
def unpack_project(self, zip_filepath, new_root):
|
||||
from openpype.lib.project_backpack import unpack_project
|
||||
|
||||
unpack_project(zip_filepath, new_root)
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
},
|
||||
"nice-checkbox": {
|
||||
"bg-checked": "#56a06f",
|
||||
"bg-unchecked": "#434b56",
|
||||
"bg-unchecked": "#21252B",
|
||||
"bg-checker": "#D3D8DE",
|
||||
"bg-checker-hover": "#F0F2F5"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -387,10 +387,16 @@ QHeaderView::section:only-one {
|
|||
|
||||
QHeaderView::down-arrow {
|
||||
image: url(:/openpype/images/down_arrow.png);
|
||||
padding-right: 4px;
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
}
|
||||
|
||||
QHeaderView::up-arrow {
|
||||
image: url(:/openpype/images/up_arrow.png);
|
||||
padding-right: 4px;
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
}
|
||||
|
||||
/* Checkboxes */
|
||||
|
|
@ -1198,6 +1204,10 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
font-size: 36pt;
|
||||
}
|
||||
|
||||
#OverlayFrame {
|
||||
background: rgba(0, 0, 0, 127);
|
||||
}
|
||||
|
||||
#BreadcrumbsPathInput {
|
||||
padding: 2px;
|
||||
font-size: 9pt;
|
||||
|
|
|
|||
|
|
@ -605,7 +605,9 @@ class PublisherController:
|
|||
found_idx = idx
|
||||
break
|
||||
|
||||
value = instance.creator_attributes[attr_def.key]
|
||||
value = None
|
||||
if attr_def.is_value_def:
|
||||
value = instance.creator_attributes[attr_def.key]
|
||||
if found_idx is None:
|
||||
idx = len(output)
|
||||
output.append((attr_def, [instance], [value]))
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ from .border_label_widget import (
|
|||
from .widgets import (
|
||||
SubsetAttributesWidget,
|
||||
|
||||
PixmapLabel,
|
||||
|
||||
StopBtn,
|
||||
ResetBtn,
|
||||
ValidateBtn,
|
||||
|
|
@ -44,8 +42,6 @@ __all__ = (
|
|||
"SubsetAttributesWidget",
|
||||
"BorderedLabelWidget",
|
||||
|
||||
"PixmapLabel",
|
||||
|
||||
"StopBtn",
|
||||
"ResetBtn",
|
||||
"ValidateBtn",
|
||||
|
|
|
|||
273
openpype/tools/publisher/widgets/assets_widget.py
Normal file
273
openpype/tools/publisher/widgets/assets_widget.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import collections
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
RecursiveSortFilterProxyModel
|
||||
)
|
||||
from openpype.tools.utils.assets_widget import (
|
||||
SingleSelectAssetsWidget,
|
||||
ASSET_ID_ROLE,
|
||||
ASSET_NAME_ROLE
|
||||
)
|
||||
|
||||
|
||||
class CreateDialogAssetsWidget(SingleSelectAssetsWidget):
|
||||
current_context_required = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
self._controller = controller
|
||||
super(CreateDialogAssetsWidget, self).__init__(None, parent)
|
||||
|
||||
self.set_refresh_btn_visibility(False)
|
||||
self.set_current_asset_btn_visibility(False)
|
||||
|
||||
self._current_asset_name = None
|
||||
self._last_selection = None
|
||||
self._enabled = None
|
||||
|
||||
def _on_current_asset_click(self):
|
||||
self.current_context_required.emit()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
if self._enabled == enabled:
|
||||
return
|
||||
self._enabled = enabled
|
||||
if not enabled:
|
||||
self._last_selection = self.get_selected_asset_id()
|
||||
self._clear_selection()
|
||||
elif self._last_selection is not None:
|
||||
self.select_asset(self._last_selection)
|
||||
|
||||
def _select_indexes(self, *args, **kwargs):
|
||||
super(CreateDialogAssetsWidget, self)._select_indexes(*args, **kwargs)
|
||||
if self._enabled:
|
||||
return
|
||||
self._last_selection = self.get_selected_asset_id()
|
||||
self._clear_selection()
|
||||
|
||||
def set_current_asset_name(self, asset_name):
|
||||
self._current_asset_name = asset_name
|
||||
# Hide set current asset if there is no one
|
||||
self.set_current_asset_btn_visibility(asset_name is not None)
|
||||
|
||||
def _get_current_session_asset(self):
|
||||
return self._current_asset_name
|
||||
|
||||
def _create_source_model(self):
|
||||
return AssetsHierarchyModel(self._controller)
|
||||
|
||||
def _refresh_model(self):
|
||||
self._model.reset()
|
||||
self._on_model_refresh(self._model.rowCount() > 0)
|
||||
|
||||
|
||||
class AssetsHierarchyModel(QtGui.QStandardItemModel):
|
||||
"""Assets hiearrchy model.
|
||||
|
||||
For selecting asset for which should beinstance created.
|
||||
|
||||
Uses controller to load asset hierarchy. All asset documents are stored by
|
||||
their parents.
|
||||
"""
|
||||
def __init__(self, controller):
|
||||
super(AssetsHierarchyModel, self).__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._items_by_name = {}
|
||||
self._items_by_asset_id = {}
|
||||
|
||||
def reset(self):
|
||||
self.clear()
|
||||
|
||||
self._items_by_name = {}
|
||||
self._items_by_asset_id = {}
|
||||
assets_by_parent_id = self._controller.get_asset_hierarchy()
|
||||
|
||||
items_by_name = {}
|
||||
items_by_asset_id = {}
|
||||
_queue = collections.deque()
|
||||
_queue.append((self.invisibleRootItem(), None))
|
||||
while _queue:
|
||||
parent_item, parent_id = _queue.popleft()
|
||||
children = assets_by_parent_id.get(parent_id)
|
||||
if not children:
|
||||
continue
|
||||
|
||||
children_by_name = {
|
||||
child["name"]: child
|
||||
for child in children
|
||||
}
|
||||
items = []
|
||||
for name in sorted(children_by_name.keys()):
|
||||
child = children_by_name[name]
|
||||
child_id = child["_id"]
|
||||
item = QtGui.QStandardItem(name)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
item.setData(child_id, ASSET_ID_ROLE)
|
||||
item.setData(name, ASSET_NAME_ROLE)
|
||||
|
||||
items_by_name[name] = item
|
||||
items_by_asset_id[child_id] = item
|
||||
items.append(item)
|
||||
_queue.append((item, child_id))
|
||||
|
||||
parent_item.appendRows(items)
|
||||
|
||||
self._items_by_name = items_by_name
|
||||
self._items_by_asset_id = items_by_asset_id
|
||||
|
||||
def get_index_by_asset_id(self, asset_id):
|
||||
item = self._items_by_asset_id.get(asset_id)
|
||||
if item is not None:
|
||||
return item.index()
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def get_index_by_asset_name(self, asset_name):
|
||||
item = self._items_by_name.get(asset_name)
|
||||
if item is None:
|
||||
return QtCore.QModelIndex()
|
||||
return item.index()
|
||||
|
||||
def name_is_valid(self, item_name):
|
||||
return item_name in self._items_by_name
|
||||
|
||||
|
||||
class AssetsDialog(QtWidgets.QDialog):
|
||||
"""Dialog to select asset for a context of instance."""
|
||||
def __init__(self, controller, parent):
|
||||
super(AssetsDialog, self).__init__(parent)
|
||||
self.setWindowTitle("Select asset")
|
||||
|
||||
model = AssetsHierarchyModel(controller)
|
||||
proxy_model = RecursiveSortFilterProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
filter_input = PlaceholderLineEdit(self)
|
||||
filter_input.setPlaceholderText("Filter assets..")
|
||||
|
||||
asset_view = QtWidgets.QTreeView(self)
|
||||
asset_view.setModel(proxy_model)
|
||||
asset_view.setHeaderHidden(True)
|
||||
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
asset_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
asset_view.setAlternatingRowColors(True)
|
||||
asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
|
||||
asset_view.setAllColumnsShowFocus(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(asset_view, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
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._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._asset_view = asset_view
|
||||
|
||||
self._selected_asset = None
|
||||
# Soft refresh is enabled
|
||||
# - reset will happen at all cost if soft reset is enabled
|
||||
# - adds ability to call reset on multiple places without repeating
|
||||
self._soft_reset_enabled = True
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Refresh asset model on show."""
|
||||
super(AssetsDialog, self).showEvent(event)
|
||||
# Refresh on show
|
||||
self.reset(False)
|
||||
|
||||
def reset(self, force=True):
|
||||
"""Reset asset model."""
|
||||
if not force and not self._soft_reset_enabled:
|
||||
return
|
||||
|
||||
if self._soft_reset_enabled:
|
||||
self._soft_reset_enabled = False
|
||||
|
||||
self._model.reset()
|
||||
|
||||
def name_is_valid(self, name):
|
||||
"""Is asset name valid.
|
||||
|
||||
Args:
|
||||
name(str): Asset name that should be checked.
|
||||
"""
|
||||
# Make sure we're reset
|
||||
self.reset(False)
|
||||
# Valid the name by model
|
||||
return self._model.name_is_valid(name)
|
||||
|
||||
def _on_filter_change(self, text):
|
||||
"""Trigger change of filter of assets."""
|
||||
self._proxy_model.setFilterFixedString(text)
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self.done(0)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
index = self._asset_view.currentIndex()
|
||||
asset_name = None
|
||||
if index.isValid():
|
||||
asset_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self._selected_asset = asset_name
|
||||
self.done(1)
|
||||
|
||||
def set_selected_assets(self, asset_names):
|
||||
"""Change preselected asset before showing the dialog.
|
||||
|
||||
This also resets model and clean filter.
|
||||
"""
|
||||
self.reset(False)
|
||||
self._asset_view.collapseAll()
|
||||
self._filter_input.setText("")
|
||||
|
||||
indexes = []
|
||||
for asset_name in asset_names:
|
||||
index = self._model.get_index_by_asset_name(asset_name)
|
||||
if index.isValid():
|
||||
indexes.append(index)
|
||||
|
||||
if not indexes:
|
||||
return
|
||||
|
||||
index_deque = collections.deque()
|
||||
for index in indexes:
|
||||
index_deque.append(index)
|
||||
|
||||
all_indexes = []
|
||||
while index_deque:
|
||||
index = index_deque.popleft()
|
||||
all_indexes.append(index)
|
||||
|
||||
parent_index = index.parent()
|
||||
if parent_index.isValid():
|
||||
index_deque.append(parent_index)
|
||||
|
||||
for index in all_indexes:
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
self._asset_view.expand(proxy_index)
|
||||
|
||||
def get_selected_asset(self):
|
||||
"""Get selected asset name."""
|
||||
return self._selected_asset
|
||||
|
|
@ -27,12 +27,12 @@ from Qt import QtWidgets, QtCore
|
|||
|
||||
from openpype.widgets.nice_checkbox import NiceCheckbox
|
||||
|
||||
from openpype.tools.utils import BaseClickableFrame
|
||||
from .widgets import (
|
||||
AbstractInstanceView,
|
||||
ContextWarningLabel,
|
||||
ClickableFrame,
|
||||
IconValuePixmapLabel,
|
||||
TransparentPixmapLabel
|
||||
PublishPixmapLabel
|
||||
)
|
||||
from ..constants import (
|
||||
CONTEXT_ID,
|
||||
|
|
@ -140,7 +140,7 @@ class GroupWidget(QtWidgets.QWidget):
|
|||
widget_idx += 1
|
||||
|
||||
|
||||
class CardWidget(ClickableFrame):
|
||||
class CardWidget(BaseClickableFrame):
|
||||
"""Clickable card used as bigger button."""
|
||||
selected = QtCore.Signal(str, str)
|
||||
# Group identifier of card
|
||||
|
|
@ -184,7 +184,7 @@ class ContextCardWidget(CardWidget):
|
|||
self._id = CONTEXT_ID
|
||||
self._group_identifier = ""
|
||||
|
||||
icon_widget = TransparentPixmapLabel(self)
|
||||
icon_widget = PublishPixmapLabel(None, self)
|
||||
icon_widget.setObjectName("FamilyIconLabel")
|
||||
|
||||
label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ from openpype.pipeline.create import (
|
|||
)
|
||||
|
||||
from .widgets import IconValuePixmapLabel
|
||||
from .assets_widget import CreateDialogAssetsWidget
|
||||
from .tasks_widget import CreateDialogTasksWidget
|
||||
from .precreate_widget import PreCreateWidget
|
||||
from ..constants import (
|
||||
VARIANT_TOOLTIP,
|
||||
CREATOR_IDENTIFIER_ROLE,
|
||||
|
|
@ -202,7 +205,23 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
self._name_pattern = name_pattern
|
||||
self._compiled_name_pattern = re.compile(name_pattern)
|
||||
|
||||
context_widget = QtWidgets.QWidget(self)
|
||||
|
||||
assets_widget = CreateDialogAssetsWidget(controller, context_widget)
|
||||
tasks_widget = CreateDialogTasksWidget(controller, context_widget)
|
||||
|
||||
context_layout = QtWidgets.QVBoxLayout(context_widget)
|
||||
context_layout.setContentsMargins(0, 0, 0, 0)
|
||||
context_layout.setSpacing(0)
|
||||
context_layout.addWidget(assets_widget, 2)
|
||||
context_layout.addWidget(tasks_widget, 1)
|
||||
|
||||
# Precreate attributes widgets
|
||||
pre_create_widget = PreCreateWidget(self)
|
||||
|
||||
# TODO add HELP button
|
||||
creator_description_widget = CreatorDescriptionWidget(self)
|
||||
creator_description_widget.setVisible(False)
|
||||
|
||||
creators_view = QtWidgets.QListView(self)
|
||||
creators_model = QtGui.QStandardItemModel()
|
||||
|
|
@ -235,27 +254,46 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
form_layout.addRow("Name:", variant_layout)
|
||||
form_layout.addRow("Subset:", subset_name_input)
|
||||
|
||||
left_layout = QtWidgets.QVBoxLayout()
|
||||
left_layout.addWidget(QtWidgets.QLabel("Choose family:", self))
|
||||
left_layout.addWidget(creators_view, 1)
|
||||
left_layout.addLayout(form_layout, 0)
|
||||
left_layout.addWidget(create_btn, 0)
|
||||
mid_widget = QtWidgets.QWidget(self)
|
||||
mid_layout = QtWidgets.QVBoxLayout(mid_widget)
|
||||
mid_layout.setContentsMargins(0, 0, 0, 0)
|
||||
mid_layout.addWidget(QtWidgets.QLabel("Choose family:", self))
|
||||
mid_layout.addWidget(creators_view, 1)
|
||||
mid_layout.addLayout(form_layout, 0)
|
||||
mid_layout.addWidget(create_btn, 0)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addLayout(left_layout, 0)
|
||||
layout.addSpacing(5)
|
||||
layout.addWidget(creator_description_widget, 1)
|
||||
layout.setSpacing(10)
|
||||
layout.addWidget(context_widget, 1)
|
||||
layout.addWidget(mid_widget, 1)
|
||||
layout.addWidget(pre_create_widget, 1)
|
||||
|
||||
prereq_timer = QtCore.QTimer()
|
||||
prereq_timer.setInterval(50)
|
||||
prereq_timer.setSingleShot(True)
|
||||
|
||||
prereq_timer.timeout.connect(self._on_prereq_timer)
|
||||
|
||||
create_btn.clicked.connect(self._on_create)
|
||||
variant_input.returnPressed.connect(self._on_create)
|
||||
variant_input.textChanged.connect(self._on_variant_change)
|
||||
creators_view.selectionModel().currentChanged.connect(
|
||||
self._on_item_change
|
||||
self._on_creator_item_change
|
||||
)
|
||||
variant_hints_menu.triggered.connect(self._on_variant_action)
|
||||
assets_widget.selection_changed.connect(self._on_asset_change)
|
||||
assets_widget.current_context_required.connect(
|
||||
self._on_current_session_context_request
|
||||
)
|
||||
tasks_widget.task_changed.connect(self._on_task_change)
|
||||
|
||||
controller.add_plugins_refresh_callback(self._on_plugins_refresh)
|
||||
|
||||
self._pre_create_widget = pre_create_widget
|
||||
|
||||
self._context_widget = context_widget
|
||||
self._assets_widget = assets_widget
|
||||
self._tasks_widget = tasks_widget
|
||||
self.creator_description_widget = creator_description_widget
|
||||
|
||||
self.subset_name_input = subset_name_input
|
||||
|
|
@ -269,12 +307,54 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
self.creators_view = creators_view
|
||||
self.create_btn = create_btn
|
||||
|
||||
self._prereq_timer = prereq_timer
|
||||
|
||||
def _context_change_is_enabled(self):
|
||||
return self._context_widget.isEnabled()
|
||||
|
||||
def _get_asset_name(self):
|
||||
asset_name = None
|
||||
if self._context_change_is_enabled():
|
||||
asset_name = self._assets_widget.get_selected_asset_name()
|
||||
|
||||
if asset_name is None:
|
||||
asset_name = self._asset_name
|
||||
return asset_name
|
||||
|
||||
def _get_task_name(self):
|
||||
task_name = None
|
||||
if self._context_change_is_enabled():
|
||||
# Don't use selection of task if asset is not set
|
||||
asset_name = self._assets_widget.get_selected_asset_name()
|
||||
if asset_name:
|
||||
task_name = self._tasks_widget.get_selected_task_name()
|
||||
|
||||
if not task_name:
|
||||
task_name = self._task_name
|
||||
return task_name
|
||||
|
||||
@property
|
||||
def dbcon(self):
|
||||
return self.controller.dbcon
|
||||
|
||||
def _set_context_enabled(self, enabled):
|
||||
self._assets_widget.set_enabled(enabled)
|
||||
self._tasks_widget.set_enabled(enabled)
|
||||
self._context_widget.setEnabled(enabled)
|
||||
|
||||
def refresh(self):
|
||||
self._prereq_available = True
|
||||
# Get context before refresh to keep selection of asset and
|
||||
# task widgets
|
||||
asset_name = self._get_asset_name()
|
||||
task_name = self._get_task_name()
|
||||
|
||||
self._prereq_available = False
|
||||
|
||||
# Disable context widget so refresh of asset will use context asset
|
||||
# name
|
||||
self._set_context_enabled(False)
|
||||
|
||||
self._assets_widget.refresh()
|
||||
|
||||
# Refresh data before update of creators
|
||||
self._refresh_asset()
|
||||
|
|
@ -282,21 +362,36 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
# data
|
||||
self._refresh_creators()
|
||||
|
||||
self._assets_widget.set_current_asset_name(self._asset_name)
|
||||
self._assets_widget.select_asset_by_name(asset_name)
|
||||
self._tasks_widget.set_asset_name(asset_name)
|
||||
self._tasks_widget.select_task_name(task_name)
|
||||
|
||||
self._invalidate_prereq()
|
||||
|
||||
def _invalidate_prereq(self):
|
||||
self._prereq_timer.start()
|
||||
|
||||
def _on_prereq_timer(self):
|
||||
prereq_available = True
|
||||
if self.creators_model.rowCount() < 1:
|
||||
prereq_available = False
|
||||
|
||||
if self._asset_doc is None:
|
||||
# QUESTION how to handle invalid asset?
|
||||
self.subset_name_input.setText("< Asset is not set >")
|
||||
self._prereq_available = False
|
||||
prereq_available = False
|
||||
|
||||
if self.creators_model.rowCount() < 1:
|
||||
self._prereq_available = False
|
||||
if prereq_available != self._prereq_available:
|
||||
self._prereq_available = prereq_available
|
||||
|
||||
self.create_btn.setEnabled(self._prereq_available)
|
||||
self.creators_view.setEnabled(self._prereq_available)
|
||||
self.variant_input.setEnabled(self._prereq_available)
|
||||
self.variant_hints_btn.setEnabled(self._prereq_available)
|
||||
self.create_btn.setEnabled(prereq_available)
|
||||
self.creators_view.setEnabled(prereq_available)
|
||||
self.variant_input.setEnabled(prereq_available)
|
||||
self.variant_hints_btn.setEnabled(prereq_available)
|
||||
self._on_variant_change()
|
||||
|
||||
def _refresh_asset(self):
|
||||
asset_name = self._asset_name
|
||||
asset_name = self._get_asset_name()
|
||||
|
||||
# Skip if asset did not change
|
||||
if self._asset_doc and self._asset_doc["name"] == asset_name:
|
||||
|
|
@ -324,6 +419,9 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
)
|
||||
self._subset_names = set(subset_docs.distinct("name"))
|
||||
|
||||
if not asset_doc:
|
||||
self.subset_name_input.setText("< Asset is not set >")
|
||||
|
||||
def _refresh_creators(self):
|
||||
# Refresh creators and add their families to list
|
||||
existing_items = {}
|
||||
|
|
@ -366,25 +464,60 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
if not indexes:
|
||||
index = self.creators_model.index(0, 0)
|
||||
self.creators_view.setCurrentIndex(index)
|
||||
else:
|
||||
index = indexes[0]
|
||||
|
||||
identifier = index.data(CREATOR_IDENTIFIER_ROLE)
|
||||
|
||||
self._set_creator(identifier)
|
||||
|
||||
def _on_plugins_refresh(self):
|
||||
# Trigger refresh only if is visible
|
||||
if self.isVisible():
|
||||
self.refresh()
|
||||
|
||||
def _on_item_change(self, new_index, _old_index):
|
||||
def _on_asset_change(self):
|
||||
self._refresh_asset()
|
||||
|
||||
asset_name = self._assets_widget.get_selected_asset_name()
|
||||
self._tasks_widget.set_asset_name(asset_name)
|
||||
if self._context_change_is_enabled():
|
||||
self._invalidate_prereq()
|
||||
|
||||
def _on_task_change(self):
|
||||
if self._context_change_is_enabled():
|
||||
self._invalidate_prereq()
|
||||
|
||||
def _on_current_session_context_request(self):
|
||||
self._assets_widget.set_current_session_asset()
|
||||
if self._task_name:
|
||||
self._tasks_widget.select_task_name(self._task_name)
|
||||
|
||||
def _on_creator_item_change(self, new_index, _old_index):
|
||||
identifier = None
|
||||
if new_index.isValid():
|
||||
identifier = new_index.data(CREATOR_IDENTIFIER_ROLE)
|
||||
self._set_creator(identifier)
|
||||
|
||||
def _set_creator(self, identifier):
|
||||
creator = self.controller.manual_creators.get(identifier)
|
||||
|
||||
self.creator_description_widget.set_plugin(creator)
|
||||
self._pre_create_widget.set_plugin(creator)
|
||||
|
||||
self._selected_creator = creator
|
||||
|
||||
if not creator:
|
||||
self._set_context_enabled(False)
|
||||
return
|
||||
|
||||
if (
|
||||
creator.create_allow_context_change
|
||||
!= self._context_change_is_enabled()
|
||||
):
|
||||
self._set_context_enabled(creator.create_allow_context_change)
|
||||
self._refresh_asset()
|
||||
|
||||
default_variants = creator.get_default_variants()
|
||||
if not default_variants:
|
||||
default_variants = ["Main"]
|
||||
|
|
@ -410,12 +543,19 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
if self.variant_input.text() != value:
|
||||
self.variant_input.setText(value)
|
||||
|
||||
def _on_variant_change(self, variant_value):
|
||||
if not self._prereq_available or not self._selected_creator:
|
||||
def _on_variant_change(self, variant_value=None):
|
||||
if not self._prereq_available:
|
||||
return
|
||||
|
||||
# This should probably never happen?
|
||||
if not self._selected_creator:
|
||||
if self.subset_name_input.text():
|
||||
self.subset_name_input.setText("")
|
||||
return
|
||||
|
||||
if variant_value is None:
|
||||
variant_value = self.variant_input.text()
|
||||
|
||||
match = self._compiled_name_pattern.match(variant_value)
|
||||
valid = bool(match)
|
||||
self.create_btn.setEnabled(valid)
|
||||
|
|
@ -425,7 +565,7 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
return
|
||||
|
||||
project_name = self.controller.project_name
|
||||
task_name = self._task_name
|
||||
task_name = self._get_task_name()
|
||||
|
||||
asset_doc = copy.deepcopy(self._asset_doc)
|
||||
# Calculate subset name with Creator plugin
|
||||
|
|
@ -522,9 +662,9 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
family = index.data(FAMILY_ROLE)
|
||||
subset_name = self.subset_name_input.text()
|
||||
variant = self.variant_input.text()
|
||||
asset_name = self._asset_name
|
||||
task_name = self._task_name
|
||||
options = {}
|
||||
asset_name = self._get_asset_name()
|
||||
task_name = self._get_task_name()
|
||||
pre_create_data = self._pre_create_widget.current_value()
|
||||
# Where to define these data?
|
||||
# - what data show be stored?
|
||||
instance_data = {
|
||||
|
|
@ -537,7 +677,7 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
error_info = None
|
||||
try:
|
||||
self.controller.create(
|
||||
creator_identifier, subset_name, instance_data, options
|
||||
creator_identifier, subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
except CreatorError as exc:
|
||||
|
|
|
|||
133
openpype/tools/publisher/widgets/precreate_widget.py
Normal file
133
openpype/tools/publisher/widgets/precreate_widget.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from openpype.widgets.attribute_defs import create_widget_for_attr_def
|
||||
|
||||
|
||||
class PreCreateWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(PreCreateWidget, self).__init__(parent)
|
||||
|
||||
# Precreate attribute defininitions of Creator
|
||||
scroll_area = QtWidgets.QScrollArea(self)
|
||||
contet_widget = QtWidgets.QWidget(scroll_area)
|
||||
scroll_area.setWidget(contet_widget)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
|
||||
attributes_widget = AttributesWidget(contet_widget)
|
||||
contet_layout = QtWidgets.QVBoxLayout(contet_widget)
|
||||
contet_layout.setContentsMargins(0, 0, 0, 0)
|
||||
contet_layout.addWidget(attributes_widget, 0)
|
||||
contet_layout.addStretch(1)
|
||||
|
||||
# Widget showed when there are no attribute definitions from creator
|
||||
empty_widget = QtWidgets.QWidget(self)
|
||||
empty_widget.setVisible(False)
|
||||
|
||||
# Label showed when creator is not selected
|
||||
no_creator_label = QtWidgets.QLabel(
|
||||
"Creator is not selected",
|
||||
empty_widget
|
||||
)
|
||||
no_creator_label.setWordWrap(True)
|
||||
|
||||
# Creator does not have precreate attributes
|
||||
empty_label = QtWidgets.QLabel(
|
||||
"This creator has no configurable options",
|
||||
empty_widget
|
||||
)
|
||||
empty_label.setWordWrap(True)
|
||||
empty_label.setVisible(False)
|
||||
|
||||
empty_layout = QtWidgets.QVBoxLayout(empty_widget)
|
||||
empty_layout.setContentsMargins(0, 0, 0, 0)
|
||||
empty_layout.addWidget(empty_label, 0, QtCore.Qt.AlignCenter)
|
||||
empty_layout.addWidget(no_creator_label, 0, QtCore.Qt.AlignCenter)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(scroll_area, 1)
|
||||
main_layout.addWidget(empty_widget, 1)
|
||||
|
||||
self._scroll_area = scroll_area
|
||||
self._empty_widget = empty_widget
|
||||
|
||||
self._empty_label = empty_label
|
||||
self._no_creator_label = no_creator_label
|
||||
self._attributes_widget = attributes_widget
|
||||
|
||||
def current_value(self):
|
||||
return self._attributes_widget.current_value()
|
||||
|
||||
def set_plugin(self, creator):
|
||||
attr_defs = []
|
||||
creator_selected = False
|
||||
if creator is not None:
|
||||
creator_selected = True
|
||||
attr_defs = creator.get_pre_create_attr_defs()
|
||||
|
||||
self._attributes_widget.set_attr_defs(attr_defs)
|
||||
|
||||
attr_defs_available = len(attr_defs) > 0
|
||||
self._scroll_area.setVisible(attr_defs_available)
|
||||
self._empty_widget.setVisible(not attr_defs_available)
|
||||
|
||||
self._empty_label.setVisible(creator_selected)
|
||||
self._no_creator_label.setVisible(not creator_selected)
|
||||
|
||||
|
||||
class AttributesWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(AttributesWidget, self).__init__(parent)
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self._layout = layout
|
||||
|
||||
self._widgets = []
|
||||
|
||||
def current_value(self):
|
||||
output = {}
|
||||
for widget in self._widgets:
|
||||
attr_def = widget.attr_def
|
||||
if attr_def.is_value_def:
|
||||
output[attr_def.key] = widget.current_value()
|
||||
return output
|
||||
|
||||
def clear_attr_defs(self):
|
||||
while self._layout.count():
|
||||
item = self._layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
|
||||
self._widgets = []
|
||||
|
||||
def set_attr_defs(self, attr_defs):
|
||||
self.clear_attr_defs()
|
||||
|
||||
row = 0
|
||||
for attr_def in attr_defs:
|
||||
widget = create_widget_for_attr_def(attr_def, self)
|
||||
|
||||
expand_cols = 2
|
||||
if attr_def.is_value_def and attr_def.is_label_horizontal:
|
||||
expand_cols = 1
|
||||
|
||||
col_num = 2 - expand_cols
|
||||
|
||||
if attr_def.label:
|
||||
label_widget = QtWidgets.QLabel(attr_def.label, self)
|
||||
self._layout.addWidget(
|
||||
label_widget, row, 0, 1, expand_cols
|
||||
)
|
||||
if not attr_def.is_label_horizontal:
|
||||
row += 1
|
||||
|
||||
self._layout.addWidget(
|
||||
widget, row, col_num, 1, expand_cols
|
||||
)
|
||||
self._widgets.append(widget)
|
||||
|
||||
row += 1
|
||||
|
|
@ -1,62 +1,6 @@
|
|||
import re
|
||||
import collections
|
||||
|
||||
from Qt import QtCore, QtGui
|
||||
|
||||
|
||||
class AssetsHierarchyModel(QtGui.QStandardItemModel):
|
||||
"""Assets hiearrchy model.
|
||||
|
||||
For selecting asset for which should beinstance created.
|
||||
|
||||
Uses controller to load asset hierarchy. All asset documents are stored by
|
||||
their parents.
|
||||
"""
|
||||
def __init__(self, controller):
|
||||
super(AssetsHierarchyModel, self).__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._items_by_name = {}
|
||||
|
||||
def reset(self):
|
||||
self.clear()
|
||||
|
||||
self._items_by_name = {}
|
||||
assets_by_parent_id = self._controller.get_asset_hierarchy()
|
||||
|
||||
items_by_name = {}
|
||||
_queue = collections.deque()
|
||||
_queue.append((self.invisibleRootItem(), None))
|
||||
while _queue:
|
||||
parent_item, parent_id = _queue.popleft()
|
||||
children = assets_by_parent_id.get(parent_id)
|
||||
if not children:
|
||||
continue
|
||||
|
||||
children_by_name = {
|
||||
child["name"]: child
|
||||
for child in children
|
||||
}
|
||||
items = []
|
||||
for name in sorted(children_by_name.keys()):
|
||||
child = children_by_name[name]
|
||||
item = QtGui.QStandardItem(name)
|
||||
items_by_name[name] = item
|
||||
items.append(item)
|
||||
_queue.append((item, child["_id"]))
|
||||
|
||||
parent_item.appendRows(items)
|
||||
|
||||
self._items_by_name = items_by_name
|
||||
|
||||
def name_is_valid(self, item_name):
|
||||
return item_name in self._items_by_name
|
||||
|
||||
def get_index_by_name(self, item_name):
|
||||
item = self._items_by_name.get(item_name)
|
||||
if item:
|
||||
return item.index()
|
||||
return QtCore.QModelIndex()
|
||||
from openpype.tools.utils.tasks_widget import TasksWidget, TASK_NAME_ROLE
|
||||
|
||||
|
||||
class TasksModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -75,6 +19,7 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
"""
|
||||
def __init__(self, controller):
|
||||
super(TasksModel, self).__init__()
|
||||
|
||||
self._controller = controller
|
||||
self._items_by_name = {}
|
||||
self._asset_names = []
|
||||
|
|
@ -141,6 +86,7 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
task_names_by_asset_name = (
|
||||
self._controller.get_task_names_by_asset_names(self._asset_names)
|
||||
)
|
||||
|
||||
self._task_names_by_asset_name = task_names_by_asset_name
|
||||
|
||||
new_task_names = self.get_intersection_of_tasks(
|
||||
|
|
@ -162,40 +108,62 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
continue
|
||||
|
||||
item = QtGui.QStandardItem(task_name)
|
||||
item.setData(task_name, TASK_NAME_ROLE)
|
||||
self._items_by_name[task_name] = item
|
||||
new_items.append(item)
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
# Show nice labels in the header
|
||||
if section == 0:
|
||||
if (
|
||||
role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
):
|
||||
return "Tasks"
|
||||
|
||||
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Recursive proxy model.
|
||||
return super(TasksModel, self).headerData(section, orientation, role)
|
||||
|
||||
Item is not filtered if any children match the filter.
|
||||
|
||||
Use case: Filtering by string - parent won't be filtered if does not match
|
||||
the filter string but first checks if any children does.
|
||||
"""
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
regex = self.filterRegExp()
|
||||
if not regex.isEmpty():
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(
|
||||
row, self.filterKeyColumn(), parent_index
|
||||
)
|
||||
if source_index.isValid():
|
||||
pattern = regex.pattern()
|
||||
class CreateDialogTasksWidget(TasksWidget):
|
||||
def __init__(self, controller, parent):
|
||||
self._controller = controller
|
||||
super(CreateDialogTasksWidget, self).__init__(None, parent)
|
||||
|
||||
# Check current index itself
|
||||
value = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, value, re.IGNORECASE):
|
||||
return True
|
||||
self._enabled = None
|
||||
|
||||
rows = model.rowCount(source_index)
|
||||
for idx in range(rows):
|
||||
if self.filterAcceptsRow(idx, source_index):
|
||||
return True
|
||||
return False
|
||||
def _create_source_model(self):
|
||||
return TasksModel(self._controller)
|
||||
|
||||
return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow(
|
||||
row, parent_index
|
||||
)
|
||||
def set_asset_name(self, asset_name):
|
||||
current = self.get_selected_task_name()
|
||||
if current:
|
||||
self._last_selected_task_name = current
|
||||
|
||||
self._tasks_model.set_asset_names([asset_name])
|
||||
if self._last_selected_task_name and self._enabled:
|
||||
self.select_task_name(self._last_selected_task_name)
|
||||
|
||||
# Force a task changed emit.
|
||||
self.task_changed.emit()
|
||||
|
||||
def select_task_name(self, task_name):
|
||||
super(CreateDialogTasksWidget, self).select_task_name(task_name)
|
||||
if not self._enabled:
|
||||
current = self.get_selected_task_name()
|
||||
if current:
|
||||
self._last_selected_task_name = current
|
||||
self._clear_selection()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self._enabled = enabled
|
||||
if not enabled:
|
||||
last_selected_task_name = self.get_selected_task_name()
|
||||
if last_selected_task_name:
|
||||
self._last_selected_task_name = last_selected_task_name
|
||||
self._clear_selection()
|
||||
|
||||
elif self._last_selected_task_name is not None:
|
||||
self.select_task_name(self._last_selected_task_name)
|
||||
|
|
@ -6,8 +6,8 @@ except Exception:
|
|||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import BaseClickableFrame
|
||||
from .widgets import (
|
||||
ClickableFrame,
|
||||
IconValuePixmapLabel
|
||||
)
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
self._error_info = error_info
|
||||
self._selected = False
|
||||
|
||||
title_frame = ClickableFrame(self)
|
||||
title_frame = BaseClickableFrame(self)
|
||||
title_frame.setObjectName("ValidationErrorTitleFrame")
|
||||
title_frame._mouse_release_callback = self._mouse_release_callback
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
|
||||
|
||||
class ActionButton(ClickableFrame):
|
||||
class ActionButton(BaseClickableFrame):
|
||||
"""Plugin's action callback button.
|
||||
|
||||
Action may have label or icon or both.
|
||||
|
|
|
|||
|
|
@ -8,14 +8,17 @@ from Qt import QtWidgets, QtCore, QtGui
|
|||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.widgets.attribute_defs import create_widget_for_attr_def
|
||||
from openpype.tools import resources
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
from .models import (
|
||||
AssetsHierarchyModel,
|
||||
TasksModel,
|
||||
RecursiveSortFilterProxyModel,
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
IconButton,
|
||||
PixmapLabel,
|
||||
BaseClickableFrame
|
||||
)
|
||||
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
||||
from .assets_widget import AssetsDialog
|
||||
from .tasks_widget import TasksModel
|
||||
from .icons import (
|
||||
get_pixmap,
|
||||
get_icon_path
|
||||
|
|
@ -26,49 +29,14 @@ from ..constants import (
|
|||
)
|
||||
|
||||
|
||||
class PixmapLabel(QtWidgets.QLabel):
|
||||
"""Label resizing image to height of font."""
|
||||
def __init__(self, pixmap, parent):
|
||||
super(PixmapLabel, self).__init__(parent)
|
||||
self._source_pixmap = pixmap
|
||||
|
||||
def set_source_pixmap(self, pixmap):
|
||||
"""Change source image."""
|
||||
self._source_pixmap = pixmap
|
||||
self._set_resized_pix()
|
||||
|
||||
def _set_resized_pix(self):
|
||||
class PublishPixmapLabel(PixmapLabel):
|
||||
def _get_pix_size(self):
|
||||
size = self.fontMetrics().height()
|
||||
size += size % 2
|
||||
self.setPixmap(
|
||||
self._source_pixmap.scaled(
|
||||
size,
|
||||
size,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._set_resized_pix()
|
||||
super(PixmapLabel, self).resizeEvent(event)
|
||||
return size, size
|
||||
|
||||
|
||||
class TransparentPixmapLabel(QtWidgets.QLabel):
|
||||
"""Transparent label resizing to width and height of font."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TransparentPixmapLabel, self).__init__(*args, **kwargs)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
size = self.fontMetrics().height()
|
||||
size += size % 2
|
||||
pix = QtGui.QPixmap(size, size)
|
||||
pix.fill(QtCore.Qt.transparent)
|
||||
self.setPixmap(pix)
|
||||
super(TransparentPixmapLabel, self).resizeEvent(event)
|
||||
|
||||
|
||||
class IconValuePixmapLabel(PixmapLabel):
|
||||
class IconValuePixmapLabel(PublishPixmapLabel):
|
||||
"""Label resizing to width and height of font.
|
||||
|
||||
Handle icon parsing from creators/instances. Using of QAwesome module
|
||||
|
|
@ -125,7 +93,7 @@ class IconValuePixmapLabel(PixmapLabel):
|
|||
return self._default_pixmap()
|
||||
|
||||
|
||||
class ContextWarningLabel(PixmapLabel):
|
||||
class ContextWarningLabel(PublishPixmapLabel):
|
||||
"""Pixmap label with warning icon."""
|
||||
def __init__(self, parent):
|
||||
pix = get_pixmap("warning")
|
||||
|
|
@ -138,29 +106,6 @@ class ContextWarningLabel(PixmapLabel):
|
|||
self.setObjectName("FamilyIconLabel")
|
||||
|
||||
|
||||
class IconButton(QtWidgets.QPushButton):
|
||||
"""PushButton with icon and size of font.
|
||||
|
||||
Using font metrics height as icon size reference.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IconButton, self).__init__(*args, **kwargs)
|
||||
self.setObjectName("IconButton")
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(IconButton, self).sizeHint()
|
||||
icon_h = self.iconSize().height()
|
||||
font_height = self.fontMetrics().height()
|
||||
text_set = bool(self.text())
|
||||
if not text_set and icon_h < font_height:
|
||||
new_size = result.height() - icon_h + font_height
|
||||
result.setHeight(new_size)
|
||||
result.setWidth(new_size)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PublishIconBtn(IconButton):
|
||||
"""Button using alpha of source image to redraw with different color.
|
||||
|
||||
|
|
@ -314,7 +259,7 @@ class ShowPublishReportBtn(PublishIconBtn):
|
|||
class RemoveInstanceBtn(PublishIconBtn):
|
||||
"""Create remove button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("delete")
|
||||
icon_path = resources.get_icon_path("delete")
|
||||
super(RemoveInstanceBtn, self).__init__(icon_path, parent)
|
||||
self.setToolTip("Remove selected instances")
|
||||
|
||||
|
|
@ -359,170 +304,6 @@ class AbstractInstanceView(QtWidgets.QWidget):
|
|||
).format(self.__class__.__name__))
|
||||
|
||||
|
||||
class ClickableFrame(QtWidgets.QFrame):
|
||||
"""Widget that catch left mouse click and can trigger a callback.
|
||||
|
||||
Callback is defined by overriding `_mouse_release_callback`.
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
super(ClickableFrame, self).__init__(parent)
|
||||
|
||||
self._mouse_pressed = False
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
pass
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(ClickableFrame, self).mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
self._mouse_pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self._mouse_release_callback()
|
||||
|
||||
super(ClickableFrame, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class AssetsDialog(QtWidgets.QDialog):
|
||||
"""Dialog to select asset for a context of instance."""
|
||||
def __init__(self, controller, parent):
|
||||
super(AssetsDialog, self).__init__(parent)
|
||||
self.setWindowTitle("Select asset")
|
||||
|
||||
model = AssetsHierarchyModel(controller)
|
||||
proxy_model = RecursiveSortFilterProxyModel()
|
||||
proxy_model.setSourceModel(model)
|
||||
proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
filter_input = PlaceholderLineEdit(self)
|
||||
filter_input.setPlaceholderText("Filter assets..")
|
||||
|
||||
asset_view = QtWidgets.QTreeView(self)
|
||||
asset_view.setModel(proxy_model)
|
||||
asset_view.setHeaderHidden(True)
|
||||
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
asset_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
asset_view.setAlternatingRowColors(True)
|
||||
asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
|
||||
asset_view.setAllColumnsShowFocus(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(asset_view, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
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._model = model
|
||||
self._proxy_model = proxy_model
|
||||
|
||||
self._asset_view = asset_view
|
||||
|
||||
self._selected_asset = None
|
||||
# Soft refresh is enabled
|
||||
# - reset will happen at all cost if soft reset is enabled
|
||||
# - adds ability to call reset on multiple places without repeating
|
||||
self._soft_reset_enabled = True
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Refresh asset model on show."""
|
||||
super(AssetsDialog, self).showEvent(event)
|
||||
# Refresh on show
|
||||
self.reset(False)
|
||||
|
||||
def reset(self, force=True):
|
||||
"""Reset asset model."""
|
||||
if not force and not self._soft_reset_enabled:
|
||||
return
|
||||
|
||||
if self._soft_reset_enabled:
|
||||
self._soft_reset_enabled = False
|
||||
|
||||
self._model.reset()
|
||||
|
||||
def name_is_valid(self, name):
|
||||
"""Is asset name valid.
|
||||
|
||||
Args:
|
||||
name(str): Asset name that should be checked.
|
||||
"""
|
||||
# Make sure we're reset
|
||||
self.reset(False)
|
||||
# Valid the name by model
|
||||
return self._model.name_is_valid(name)
|
||||
|
||||
def _on_filter_change(self, text):
|
||||
"""Trigger change of filter of assets."""
|
||||
self._proxy_model.setFilterFixedString(text)
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self.done(0)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
index = self._asset_view.currentIndex()
|
||||
asset_name = None
|
||||
if index.isValid():
|
||||
asset_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self._selected_asset = asset_name
|
||||
self.done(1)
|
||||
|
||||
def set_selected_assets(self, asset_names):
|
||||
"""Change preselected asset before showing the dialog.
|
||||
|
||||
This also resets model and clean filter.
|
||||
"""
|
||||
self.reset(False)
|
||||
self._asset_view.collapseAll()
|
||||
self._filter_input.setText("")
|
||||
|
||||
indexes = []
|
||||
for asset_name in asset_names:
|
||||
index = self._model.get_index_by_name(asset_name)
|
||||
if index.isValid():
|
||||
indexes.append(index)
|
||||
|
||||
if not indexes:
|
||||
return
|
||||
|
||||
index_deque = collections.deque()
|
||||
for index in indexes:
|
||||
index_deque.append(index)
|
||||
|
||||
all_indexes = []
|
||||
while index_deque:
|
||||
index = index_deque.popleft()
|
||||
all_indexes.append(index)
|
||||
|
||||
parent_index = index.parent()
|
||||
if parent_index.isValid():
|
||||
index_deque.append(parent_index)
|
||||
|
||||
for index in all_indexes:
|
||||
proxy_index = self._proxy_model.mapFromSource(index)
|
||||
self._asset_view.expand(proxy_index)
|
||||
|
||||
def get_selected_asset(self):
|
||||
"""Get selected asset name."""
|
||||
return self._selected_asset
|
||||
|
||||
|
||||
class ClickableLineEdit(QtWidgets.QLineEdit):
|
||||
"""QLineEdit capturing left mouse click.
|
||||
|
||||
|
|
@ -554,7 +335,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
event.accept()
|
||||
|
||||
|
||||
class AssetsField(ClickableFrame):
|
||||
class AssetsField(BaseClickableFrame):
|
||||
"""Field where asset name of selected instance/s is showed.
|
||||
|
||||
Click on the field will trigger `AssetsDialog`.
|
||||
|
|
@ -1394,12 +1175,13 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
content_layout = QtWidgets.QFormLayout(content_widget)
|
||||
for attr_def, attr_instances, values in result:
|
||||
widget = create_widget_for_attr_def(attr_def, content_widget)
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if value is not None:
|
||||
widget.set_value(values[0])
|
||||
else:
|
||||
widget.set_value(values, True)
|
||||
if attr_def.is_value_def:
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if value is not None:
|
||||
widget.set_value(values[0])
|
||||
else:
|
||||
widget.set_value(values, True)
|
||||
|
||||
label = attr_def.label or attr_def.key
|
||||
content_layout.addRow(label, widget)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ from openpype import (
|
|||
resources,
|
||||
style
|
||||
)
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
PixmapLabel
|
||||
)
|
||||
from .control import PublisherController
|
||||
from .widgets import (
|
||||
BorderedLabelWidget,
|
||||
|
|
@ -14,8 +17,6 @@ from .widgets import (
|
|||
InstanceListView,
|
||||
CreateDialog,
|
||||
|
||||
PixmapLabel,
|
||||
|
||||
StopBtn,
|
||||
ResetBtn,
|
||||
ValidateBtn,
|
||||
|
|
@ -32,7 +33,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
default_width = 1000
|
||||
default_height = 600
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent=None, reset_on_show=None):
|
||||
super(PublisherWindow, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("OpenPype publisher")
|
||||
|
|
@ -40,6 +41,9 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
if reset_on_show is None:
|
||||
reset_on_show = True
|
||||
|
||||
if parent is None:
|
||||
on_top_flag = QtCore.Qt.WindowStaysOnTopHint
|
||||
else:
|
||||
|
|
@ -54,6 +58,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
| on_top_flag
|
||||
)
|
||||
|
||||
self._reset_on_show = reset_on_show
|
||||
self._first_show = True
|
||||
self._refreshing_instances = False
|
||||
|
||||
|
|
@ -116,12 +121,16 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
subset_view_btns_layout.addWidget(change_view_btn)
|
||||
|
||||
# Layout of view and buttons
|
||||
subset_view_layout = QtWidgets.QVBoxLayout()
|
||||
# - widget 'subset_view_widget' is necessary
|
||||
# - only layout won't be resized automatically to minimum size hint
|
||||
# on child resize request!
|
||||
subset_view_widget = QtWidgets.QWidget(subset_views_widget)
|
||||
subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget)
|
||||
subset_view_layout.setContentsMargins(0, 0, 0, 0)
|
||||
subset_view_layout.addLayout(subset_views_layout, 1)
|
||||
subset_view_layout.addLayout(subset_view_btns_layout, 0)
|
||||
|
||||
subset_views_widget.set_center_widget(subset_view_layout)
|
||||
subset_views_widget.set_center_widget(subset_view_widget)
|
||||
|
||||
# Whole subset layout with attributes and details
|
||||
subset_content_widget = QtWidgets.QWidget(subset_frame)
|
||||
|
|
@ -248,7 +257,8 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._first_show = False
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.reset()
|
||||
if self._reset_on_show:
|
||||
self.reset()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.controller.save_changes()
|
||||
|
|
@ -381,6 +391,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
context_title = self.controller.get_context_title()
|
||||
self.set_context_label(context_title)
|
||||
|
||||
# Give a change to process Resize Request
|
||||
QtWidgets.QApplication.processEvents()
|
||||
# Trigger update geometry of
|
||||
widget = self.subset_views_layout.currentWidget()
|
||||
widget.updateGeometry()
|
||||
|
||||
def _on_subset_change(self, *_args):
|
||||
# Ignore changes if in middle of refreshing
|
||||
if self._refreshing_instances:
|
||||
|
|
|
|||
45
openpype/tools/resources/__init__.py
Normal file
45
openpype/tools/resources/__init__.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import os
|
||||
|
||||
from Qt import QtGui
|
||||
|
||||
|
||||
def get_icon_path(icon_name=None, filename=None):
|
||||
"""Path to image in './images' folder."""
|
||||
if icon_name is None and filename is None:
|
||||
return None
|
||||
|
||||
if filename is None:
|
||||
filename = "{}.png".format(icon_name)
|
||||
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"images",
|
||||
filename
|
||||
)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def get_image(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QImage."""
|
||||
path = get_icon_path(icon_name, filename)
|
||||
if path:
|
||||
return QtGui.QImage(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_pixmap(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QPixmap."""
|
||||
path = get_icon_path(icon_name, filename)
|
||||
if path:
|
||||
return QtGui.QPixmap(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_icon(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QICon."""
|
||||
pix = get_pixmap(icon_name, filename)
|
||||
if pix:
|
||||
return QtGui.QIcon(pix)
|
||||
return None
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
openpype/tools/resources/images/file.png
Normal file
BIN
openpype/tools/resources/images/file.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
openpype/tools/resources/images/files.png
Normal file
BIN
openpype/tools/resources/images/files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3 KiB |
BIN
openpype/tools/resources/images/folder.png
Normal file
BIN
openpype/tools/resources/images/folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -3,6 +3,8 @@ from .widgets import (
|
|||
BaseClickableFrame,
|
||||
ClickableFrame,
|
||||
ExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
)
|
||||
|
||||
from .error_dialog import ErrorMessageBox
|
||||
|
|
@ -11,15 +13,22 @@ from .lib import (
|
|||
paint_image_with_color
|
||||
)
|
||||
|
||||
from .models import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"PlaceholderLineEdit",
|
||||
"BaseClickableFrame",
|
||||
"ClickableFrame",
|
||||
"ExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
|
||||
"ErrorMessageBox",
|
||||
|
||||
"WrappedCallbackItem",
|
||||
"paint_image_with_color",
|
||||
|
||||
"RecursiveSortFilterProxyModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -635,9 +635,10 @@ class AssetsWidget(QtWidgets.QWidget):
|
|||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
refresh_btn.clicked.connect(self.refresh)
|
||||
current_asset_btn.clicked.connect(self.set_current_session_asset)
|
||||
current_asset_btn.clicked.connect(self._on_current_asset_click)
|
||||
view.doubleClicked.connect(self.double_clicked)
|
||||
|
||||
self._refresh_btn = refresh_btn
|
||||
self._current_asset_btn = current_asset_btn
|
||||
self._model = model
|
||||
self._proxy = proxy
|
||||
|
|
@ -668,11 +669,30 @@ class AssetsWidget(QtWidgets.QWidget):
|
|||
def stop_refresh(self):
|
||||
self._model.stop_refresh()
|
||||
|
||||
def _get_current_session_asset(self):
|
||||
return self.dbcon.Session.get("AVALON_ASSET")
|
||||
|
||||
def _on_current_asset_click(self):
|
||||
"""Trigger change of asset to current context asset.
|
||||
This separation gives ability to override this method and use it
|
||||
in differnt way.
|
||||
"""
|
||||
self.set_current_session_asset()
|
||||
|
||||
def set_current_session_asset(self):
|
||||
asset_name = self.dbcon.Session.get("AVALON_ASSET")
|
||||
asset_name = self._get_current_session_asset()
|
||||
if asset_name:
|
||||
self.select_asset_by_name(asset_name)
|
||||
|
||||
def set_refresh_btn_visibility(self, visible=None):
|
||||
"""Hide set refresh button.
|
||||
Some tools may have their global refresh button or do not support
|
||||
refresh at all.
|
||||
"""
|
||||
if visible is None:
|
||||
visible = not self._refresh_btn.isVisible()
|
||||
self._refresh_btn.setVisible(visible)
|
||||
|
||||
def set_current_asset_btn_visibility(self, visible=None):
|
||||
"""Hide set current asset button.
|
||||
|
||||
|
|
@ -727,6 +747,10 @@ class AssetsWidget(QtWidgets.QWidget):
|
|||
def _set_loading_state(self, loading, empty):
|
||||
self._view.set_loading_state(loading, empty)
|
||||
|
||||
def _clear_selection(self):
|
||||
selection_model = self._view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
def _select_indexes(self, indexes):
|
||||
valid_indexes = [
|
||||
index
|
||||
|
|
|
|||
|
|
@ -199,31 +199,37 @@ class Item(dict):
|
|||
|
||||
|
||||
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filters to the regex if any of the children matches allow parent"""
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
"""Recursive proxy model.
|
||||
Item is not filtered if any children match the filter.
|
||||
Use case: Filtering by string - parent won't be filtered if does not match
|
||||
the filter string but first checks if any children does.
|
||||
"""
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
regex = self.filterRegExp()
|
||||
if not regex.isEmpty():
|
||||
pattern = regex.pattern()
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
source_index = model.index(
|
||||
row, self.filterKeyColumn(), parent_index
|
||||
)
|
||||
if source_index.isValid():
|
||||
pattern = regex.pattern()
|
||||
|
||||
# Check current index itself
|
||||
key = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
value = model.data(source_index, self.filterRole())
|
||||
if re.search(pattern, value, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
# Check children
|
||||
rows = model.rowCount(source_index)
|
||||
for i in range(rows):
|
||||
if self.filterAcceptsRow(i, source_index):
|
||||
for idx in range(rows):
|
||||
if self.filterAcceptsRow(idx, source_index):
|
||||
return True
|
||||
|
||||
# Otherwise filter it
|
||||
return False
|
||||
|
||||
return super(
|
||||
RecursiveSortFilterProxyModel, self
|
||||
).filterAcceptsRow(row, parent)
|
||||
return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow(
|
||||
row, parent_index
|
||||
)
|
||||
|
||||
|
||||
class ProjectModel(QtGui.QStandardItemModel):
|
||||
|
|
|
|||
|
|
@ -255,6 +255,10 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
# Force a task changed emit.
|
||||
self.task_changed.emit()
|
||||
|
||||
def _clear_selection(self):
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
def select_task_name(self, task_name):
|
||||
"""Select a task by name.
|
||||
|
||||
|
|
@ -285,6 +289,10 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
self._tasks_view.setCurrentIndex(index)
|
||||
break
|
||||
|
||||
last_selected_task_name = self.get_selected_task_name()
|
||||
if last_selected_task_name:
|
||||
self._last_selected_task_name = last_selected_task_name
|
||||
|
||||
def get_selected_task_name(self):
|
||||
"""Return name of task at current index (selected)
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,65 @@ class ImageButton(QtWidgets.QPushButton):
|
|||
return self.iconSize()
|
||||
|
||||
|
||||
class IconButton(QtWidgets.QPushButton):
|
||||
"""PushButton with icon and size of font.
|
||||
|
||||
Using font metrics height as icon size reference.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(IconButton, self).__init__(*args, **kwargs)
|
||||
self.setObjectName("IconButton")
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(IconButton, self).sizeHint()
|
||||
icon_h = self.iconSize().height()
|
||||
font_height = self.fontMetrics().height()
|
||||
text_set = bool(self.text())
|
||||
if not text_set and icon_h < font_height:
|
||||
new_size = result.height() - icon_h + font_height
|
||||
result.setHeight(new_size)
|
||||
result.setWidth(new_size)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PixmapLabel(QtWidgets.QLabel):
|
||||
"""Label resizing image to height of font."""
|
||||
def __init__(self, pixmap, parent):
|
||||
super(PixmapLabel, self).__init__(parent)
|
||||
self._empty_pixmap = QtGui.QPixmap(0, 0)
|
||||
self._source_pixmap = pixmap
|
||||
|
||||
def set_source_pixmap(self, pixmap):
|
||||
"""Change source image."""
|
||||
self._source_pixmap = pixmap
|
||||
self._set_resized_pix()
|
||||
|
||||
def _get_pix_size(self):
|
||||
size = self.fontMetrics().height()
|
||||
size += size % 2
|
||||
return size, size
|
||||
|
||||
def _set_resized_pix(self):
|
||||
if self._source_pixmap is None:
|
||||
self.setPixmap(self._empty_pixmap)
|
||||
return
|
||||
width, height = self._get_pix_size()
|
||||
self.setPixmap(
|
||||
self._source_pixmap.scaled(
|
||||
width,
|
||||
height,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._set_resized_pix()
|
||||
super(PixmapLabel, self).resizeEvent(event)
|
||||
|
||||
|
||||
class OptionalMenu(QtWidgets.QMenu):
|
||||
"""A subclass of `QtWidgets.QMenu` to work with `OptionalAction`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.8.2-nightly.1"
|
||||
__version__ = "3.8.2"
|
||||
|
|
|
|||
645
openpype/widgets/attribute_defs/files_widget.py
Normal file
645
openpype/widgets/attribute_defs/files_widget.py
Normal file
|
|
@ -0,0 +1,645 @@
|
|||
import os
|
||||
import collections
|
||||
import uuid
|
||||
import clique
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import paint_image_with_color
|
||||
# TODO change imports
|
||||
from openpype.tools.resources import (
|
||||
get_pixmap,
|
||||
get_image,
|
||||
)
|
||||
from openpype.tools.utils import (
|
||||
IconButton,
|
||||
PixmapLabel
|
||||
)
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2
|
||||
ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3
|
||||
FILENAMES_ROLE = QtCore.Qt.UserRole + 4
|
||||
DIRPATH_ROLE = QtCore.Qt.UserRole + 5
|
||||
IS_DIR_ROLE = QtCore.Qt.UserRole + 6
|
||||
EXT_ROLE = QtCore.Qt.UserRole + 7
|
||||
|
||||
|
||||
class DropEmpty(QtWidgets.QWidget):
|
||||
_drop_enabled_text = "Drag & Drop\n(drop files here)"
|
||||
|
||||
def __init__(self, parent):
|
||||
super(DropEmpty, self).__init__(parent)
|
||||
label_widget = QtWidgets.QLabel(self._drop_enabled_text, self)
|
||||
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addSpacing(10)
|
||||
layout.addWidget(
|
||||
label_widget,
|
||||
alignment=QtCore.Qt.AlignCenter
|
||||
)
|
||||
layout.addSpacing(10)
|
||||
|
||||
self._label_widget = label_widget
|
||||
|
||||
def paintEvent(self, event):
|
||||
super(DropEmpty, self).paintEvent(event)
|
||||
painter = QtGui.QPainter(self)
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
content_margins = self.layout().contentsMargins()
|
||||
|
||||
left_m = content_margins.left()
|
||||
top_m = content_margins.top()
|
||||
rect = QtCore.QRect(
|
||||
left_m,
|
||||
top_m,
|
||||
(
|
||||
self.rect().width()
|
||||
- (left_m + content_margins.right() + pen.width())
|
||||
),
|
||||
(
|
||||
self.rect().height()
|
||||
- (top_m + content_margins.bottom() + pen.width())
|
||||
)
|
||||
)
|
||||
painter.drawRect(rect)
|
||||
|
||||
|
||||
class FilesModel(QtGui.QStandardItemModel):
|
||||
sequence_exts = [
|
||||
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal",
|
||||
".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits",
|
||||
".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer",
|
||||
".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2",
|
||||
".jng", ".jpeg", ".jpeg-ls", ".2000", ".jpg", ".xr",
|
||||
".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd",
|
||||
".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf",
|
||||
".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras",
|
||||
".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep",
|
||||
".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf",
|
||||
".xpm", ".xwd"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(FilesModel, self).__init__()
|
||||
self._filenames_by_dirpath = collections.defaultdict(set)
|
||||
self._items_by_dirpath = collections.defaultdict(list)
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
if not filepaths:
|
||||
return
|
||||
|
||||
new_dirpaths = set()
|
||||
for filepath in filepaths:
|
||||
filename = os.path.basename(filepath)
|
||||
dirpath = os.path.dirname(filepath)
|
||||
filenames = self._filenames_by_dirpath[dirpath]
|
||||
if filename not in filenames:
|
||||
new_dirpaths.add(dirpath)
|
||||
filenames.add(filename)
|
||||
self._refresh_items(new_dirpaths)
|
||||
|
||||
def remove_item_by_ids(self, item_ids):
|
||||
if not item_ids:
|
||||
return
|
||||
|
||||
remaining_ids = set(item_ids)
|
||||
result = collections.defaultdict(list)
|
||||
for dirpath, items in self._items_by_dirpath.items():
|
||||
if not remaining_ids:
|
||||
break
|
||||
for item in items:
|
||||
if not remaining_ids:
|
||||
break
|
||||
item_id = item.data(ITEM_ID_ROLE)
|
||||
if item_id in remaining_ids:
|
||||
remaining_ids.remove(item_id)
|
||||
result[dirpath].append(item)
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
dirpaths = set(result.keys())
|
||||
for dirpath, items in result.items():
|
||||
filenames_cache = self._filenames_by_dirpath[dirpath]
|
||||
for item in items:
|
||||
filenames = item.data(FILENAMES_ROLE)
|
||||
|
||||
self._items_by_dirpath[dirpath].remove(item)
|
||||
self.removeRows(item.row(), 1)
|
||||
for filename in filenames:
|
||||
if filename in filenames_cache:
|
||||
filenames_cache.remove(filename)
|
||||
|
||||
self._refresh_items(dirpaths)
|
||||
|
||||
def _refresh_items(self, dirpaths=None):
|
||||
if dirpaths is None:
|
||||
dirpaths = set(self._items_by_dirpath.keys())
|
||||
|
||||
new_items = []
|
||||
for dirpath in dirpaths:
|
||||
items_to_remove = list(self._items_by_dirpath[dirpath])
|
||||
cols, remainders = clique.assemble(
|
||||
self._filenames_by_dirpath[dirpath]
|
||||
)
|
||||
filtered_cols = []
|
||||
for collection in cols:
|
||||
filenames = set(collection)
|
||||
valid_col = True
|
||||
for filename in filenames:
|
||||
ext = os.path.splitext(filename)[-1]
|
||||
valid_col = ext in self.sequence_exts
|
||||
break
|
||||
|
||||
if valid_col:
|
||||
filtered_cols.append(collection)
|
||||
else:
|
||||
for filename in filenames:
|
||||
remainders.append(filename)
|
||||
|
||||
for filename in remainders:
|
||||
found = False
|
||||
for item in items_to_remove:
|
||||
item_filenames = item.data(FILENAMES_ROLE)
|
||||
if filename in item_filenames and len(item_filenames) == 1:
|
||||
found = True
|
||||
items_to_remove.remove(item)
|
||||
break
|
||||
|
||||
if found:
|
||||
continue
|
||||
|
||||
fullpath = os.path.join(dirpath, filename)
|
||||
if os.path.isdir(fullpath):
|
||||
icon_pixmap = get_pixmap(filename="folder.png")
|
||||
else:
|
||||
icon_pixmap = get_pixmap(filename="file.png")
|
||||
label = filename
|
||||
filenames = [filename]
|
||||
item = self._create_item(
|
||||
label, filenames, dirpath, icon_pixmap
|
||||
)
|
||||
new_items.append(item)
|
||||
self._items_by_dirpath[dirpath].append(item)
|
||||
|
||||
for collection in filtered_cols:
|
||||
filenames = set(collection)
|
||||
found = False
|
||||
for item in items_to_remove:
|
||||
item_filenames = item.data(FILENAMES_ROLE)
|
||||
if item_filenames == filenames:
|
||||
found = True
|
||||
items_to_remove.remove(item)
|
||||
break
|
||||
|
||||
if found:
|
||||
continue
|
||||
|
||||
col_range = collection.format("{ranges}")
|
||||
label = "{}<{}>{}".format(
|
||||
collection.head, col_range, collection.tail
|
||||
)
|
||||
icon_pixmap = get_pixmap(filename="files.png")
|
||||
item = self._create_item(
|
||||
label, filenames, dirpath, icon_pixmap
|
||||
)
|
||||
new_items.append(item)
|
||||
self._items_by_dirpath[dirpath].append(item)
|
||||
|
||||
for item in items_to_remove:
|
||||
self._items_by_dirpath[dirpath].remove(item)
|
||||
self.removeRows(item.row(), 1)
|
||||
|
||||
if new_items:
|
||||
self.invisibleRootItem().appendRows(new_items)
|
||||
|
||||
def _create_item(self, label, filenames, dirpath, icon_pixmap=None):
|
||||
first_filename = None
|
||||
for filename in filenames:
|
||||
first_filename = filename
|
||||
break
|
||||
ext = os.path.splitext(first_filename)[-1]
|
||||
is_dir = False
|
||||
if len(filenames) == 1:
|
||||
filepath = os.path.join(dirpath, first_filename)
|
||||
is_dir = os.path.isdir(filepath)
|
||||
|
||||
item = QtGui.QStandardItem()
|
||||
item.setData(str(uuid.uuid4()), ITEM_ID_ROLE)
|
||||
item.setData(label, ITEM_LABEL_ROLE)
|
||||
item.setData(filenames, FILENAMES_ROLE)
|
||||
item.setData(dirpath, DIRPATH_ROLE)
|
||||
item.setData(icon_pixmap, ITEM_ICON_ROLE)
|
||||
item.setData(ext, EXT_ROLE)
|
||||
item.setData(is_dir, IS_DIR_ROLE)
|
||||
|
||||
return item
|
||||
|
||||
|
||||
class FilesProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilesProxyModel, self).__init__(*args, **kwargs)
|
||||
self._allow_folders = False
|
||||
self._allowed_extensions = None
|
||||
|
||||
def set_allow_folders(self, allow=None):
|
||||
if allow is None:
|
||||
allow = not self._allow_folders
|
||||
|
||||
if allow == self._allow_folders:
|
||||
return
|
||||
self._allow_folders = allow
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_allowed_extensions(self, extensions=None):
|
||||
if extensions is not None:
|
||||
extensions = set(extensions)
|
||||
|
||||
if self._allowed_extensions != extensions:
|
||||
self._allowed_extensions = extensions
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
model = self.sourceModel()
|
||||
index = model.index(row, self.filterKeyColumn(), parent_index)
|
||||
# First check if item is folder and if folders are enabled
|
||||
if index.data(IS_DIR_ROLE):
|
||||
if not self._allow_folders:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Check if there are any allowed extensions
|
||||
if self._allowed_extensions is None:
|
||||
return False
|
||||
|
||||
if index.data(EXT_ROLE) not in self._allowed_extensions:
|
||||
return False
|
||||
return True
|
||||
|
||||
def lessThan(self, left, right):
|
||||
left_comparison = left.data(DIRPATH_ROLE)
|
||||
right_comparison = right.data(DIRPATH_ROLE)
|
||||
if left_comparison == right_comparison:
|
||||
left_comparison = left.data(ITEM_LABEL_ROLE)
|
||||
right_comparison = right.data(ITEM_LABEL_ROLE)
|
||||
|
||||
if sorted((left_comparison, right_comparison))[0] == left_comparison:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ItemWidget(QtWidgets.QWidget):
|
||||
remove_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, item_id, label, pixmap_icon, parent=None):
|
||||
self._item_id = item_id
|
||||
|
||||
super(ItemWidget, self).__init__(parent)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
icon_widget = PixmapLabel(pixmap_icon, self)
|
||||
label_widget = QtWidgets.QLabel(label, self)
|
||||
pixmap = paint_image_with_color(
|
||||
get_image(filename="delete.png"), QtCore.Qt.white
|
||||
)
|
||||
remove_btn = IconButton(self)
|
||||
remove_btn.setIcon(QtGui.QIcon(pixmap))
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(icon_widget, 0)
|
||||
layout.addWidget(label_widget, 1)
|
||||
layout.addWidget(remove_btn, 0)
|
||||
|
||||
remove_btn.clicked.connect(self._on_remove_clicked)
|
||||
|
||||
self._icon_widget = icon_widget
|
||||
self._label_widget = label_widget
|
||||
self._remove_btn = remove_btn
|
||||
|
||||
def _on_remove_clicked(self):
|
||||
self.remove_requested.emit(self._item_id)
|
||||
|
||||
|
||||
class FilesView(QtWidgets.QListView):
|
||||
"""View showing instances and their groups."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilesView, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setEditTriggers(QtWidgets.QListView.NoEditTriggers)
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
|
||||
def get_selected_item_ids(self):
|
||||
"""Ids of selected instances."""
|
||||
selected_item_ids = set()
|
||||
for index in self.selectionModel().selectedIndexes():
|
||||
instance_id = index.data(ITEM_ID_ROLE)
|
||||
if instance_id is not None:
|
||||
selected_item_ids.add(instance_id)
|
||||
return selected_item_ids
|
||||
|
||||
def event(self, event):
|
||||
if not event.type() == QtCore.QEvent.KeyPress:
|
||||
pass
|
||||
|
||||
elif event.key() == QtCore.Qt.Key_Space:
|
||||
self.toggle_requested.emit(-1)
|
||||
return True
|
||||
|
||||
elif event.key() == QtCore.Qt.Key_Backspace:
|
||||
self.toggle_requested.emit(0)
|
||||
return True
|
||||
|
||||
elif event.key() == QtCore.Qt.Key_Return:
|
||||
self.toggle_requested.emit(1)
|
||||
return True
|
||||
|
||||
return super(FilesView, self).event(event)
|
||||
|
||||
|
||||
class MultiFilesWidget(QtWidgets.QFrame):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(MultiFilesWidget, self).__init__(parent)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
empty_widget = DropEmpty(self)
|
||||
|
||||
files_model = FilesModel()
|
||||
files_proxy_model = FilesProxyModel()
|
||||
files_proxy_model.setSourceModel(files_model)
|
||||
files_view = FilesView(self)
|
||||
files_view.setModel(files_proxy_model)
|
||||
files_view.setVisible(False)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(empty_widget, 1)
|
||||
layout.addWidget(files_view, 1)
|
||||
|
||||
files_proxy_model.rowsInserted.connect(self._on_rows_inserted)
|
||||
files_proxy_model.rowsRemoved.connect(self._on_rows_removed)
|
||||
|
||||
self._in_set_value = False
|
||||
|
||||
self._empty_widget = empty_widget
|
||||
self._files_model = files_model
|
||||
self._files_proxy_model = files_proxy_model
|
||||
self._files_view = files_view
|
||||
|
||||
self._widgets_by_id = {}
|
||||
|
||||
def set_value(self, value, multivalue):
|
||||
self._in_set_value = True
|
||||
widget_ids = set(self._widgets_by_id.keys())
|
||||
self._remove_item_by_ids(widget_ids)
|
||||
|
||||
# TODO how to display multivalue?
|
||||
all_same = True
|
||||
if multivalue:
|
||||
new_value = set()
|
||||
item_row = None
|
||||
for _value in value:
|
||||
_value_set = set(_value)
|
||||
new_value |= _value_set
|
||||
if item_row is None:
|
||||
item_row = _value_set
|
||||
elif item_row != _value_set:
|
||||
all_same = False
|
||||
value = new_value
|
||||
|
||||
if value:
|
||||
self._add_filepaths(value)
|
||||
self._in_set_value = False
|
||||
|
||||
def current_value(self):
|
||||
model = self._files_proxy_model
|
||||
filepaths = set()
|
||||
for row in range(model.rowCount()):
|
||||
index = model.index(row, 0)
|
||||
dirpath = index.data(DIRPATH_ROLE)
|
||||
filenames = index.data(FILENAMES_ROLE)
|
||||
for filename in filenames:
|
||||
filepaths.add(os.path.join(dirpath, filename))
|
||||
return filepaths
|
||||
|
||||
def set_filters(self, folders_allowed, exts_filter):
|
||||
self._files_proxy_model.set_allow_folders(folders_allowed)
|
||||
self._files_proxy_model.set_allowed_extensions(exts_filter)
|
||||
|
||||
def _on_rows_inserted(self, parent_index, start_row, end_row):
|
||||
for row in range(start_row, end_row + 1):
|
||||
index = self._files_proxy_model.index(row, 0, parent_index)
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id in self._widgets_by_id:
|
||||
continue
|
||||
label = index.data(ITEM_LABEL_ROLE)
|
||||
pixmap_icon = index.data(ITEM_ICON_ROLE)
|
||||
|
||||
widget = ItemWidget(item_id, label, pixmap_icon)
|
||||
self._files_view.setIndexWidget(index, widget)
|
||||
self._files_proxy_model.setData(
|
||||
index, widget.sizeHint(), QtCore.Qt.SizeHintRole
|
||||
)
|
||||
widget.remove_requested.connect(self._on_remove_request)
|
||||
self._widgets_by_id[item_id] = widget
|
||||
|
||||
self._files_proxy_model.sort(0)
|
||||
|
||||
if not self._in_set_value:
|
||||
self.value_changed.emit()
|
||||
|
||||
def _on_rows_removed(self, parent_index, start_row, end_row):
|
||||
available_item_ids = set()
|
||||
for row in range(self._files_proxy_model.rowCount()):
|
||||
index = self._files_proxy_model.index(row, 0)
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
available_item_ids.add(index.data(ITEM_ID_ROLE))
|
||||
|
||||
widget_ids = set(self._widgets_by_id.keys())
|
||||
for item_id in available_item_ids:
|
||||
if item_id in widget_ids:
|
||||
widget_ids.remove(item_id)
|
||||
|
||||
for item_id in widget_ids:
|
||||
widget = self._widgets_by_id.pop(item_id)
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
|
||||
if not self._in_set_value:
|
||||
self.value_changed.emit()
|
||||
|
||||
def _on_remove_request(self, item_id):
|
||||
found_index = None
|
||||
for row in range(self._files_model.rowCount()):
|
||||
index = self._files_model.index(row, 0)
|
||||
_item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id == _item_id:
|
||||
found_index = index
|
||||
break
|
||||
|
||||
if found_index is None:
|
||||
return
|
||||
|
||||
items_to_delete = self._files_view.get_selected_item_ids()
|
||||
if item_id not in items_to_delete:
|
||||
items_to_delete = [item_id]
|
||||
|
||||
self._remove_item_by_ids(items_to_delete)
|
||||
|
||||
def sizeHint(self):
|
||||
# Get size hints of widget and visible widgets
|
||||
result = super(MultiFilesWidget, self).sizeHint()
|
||||
if not self._files_view.isVisible():
|
||||
not_visible_hint = self._files_view.sizeHint()
|
||||
else:
|
||||
not_visible_hint = self._empty_widget.sizeHint()
|
||||
|
||||
# Get margins of this widget
|
||||
margins = self.layout().contentsMargins()
|
||||
|
||||
# Change size hint based on result of maximum size hint of widgets
|
||||
result.setWidth(max(
|
||||
result.width(),
|
||||
not_visible_hint.width() + margins.left() + margins.right()
|
||||
))
|
||||
result.setHeight(max(
|
||||
result.height(),
|
||||
not_visible_hint.height() + margins.top() + margins.bottom()
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
filepaths = []
|
||||
for url in mime_data.urls():
|
||||
filepath = url.toLocalFile()
|
||||
if os.path.exists(filepath):
|
||||
filepaths.append(filepath)
|
||||
if filepaths:
|
||||
self._add_filepaths(filepaths)
|
||||
event.accept()
|
||||
|
||||
def _add_filepaths(self, filepaths):
|
||||
self._files_model.add_filepaths(filepaths)
|
||||
self._update_visibility()
|
||||
|
||||
def _remove_item_by_ids(self, item_ids):
|
||||
self._files_model.remove_item_by_ids(item_ids)
|
||||
self._update_visibility()
|
||||
|
||||
def _update_visibility(self):
|
||||
files_exists = self._files_model.rowCount() > 0
|
||||
self._files_view.setVisible(files_exists)
|
||||
self._empty_widget.setVisible(not files_exists)
|
||||
|
||||
|
||||
class SingleFileWidget(QtWidgets.QWidget):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(SingleFileWidget, self).__init__(parent)
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
filepath_input = QtWidgets.QLineEdit(self)
|
||||
|
||||
browse_btn = QtWidgets.QPushButton("Browse", self)
|
||||
browse_btn.setVisible(False)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(filepath_input, 1)
|
||||
layout.addWidget(browse_btn, 0)
|
||||
|
||||
browse_btn.clicked.connect(self._on_browse_clicked)
|
||||
filepath_input.textChanged.connect(self._on_text_change)
|
||||
|
||||
self._in_set_value = False
|
||||
|
||||
self._filepath_input = filepath_input
|
||||
self._folders_allowed = False
|
||||
self._exts_filter = []
|
||||
|
||||
def set_value(self, value, multivalue):
|
||||
self._in_set_value = True
|
||||
|
||||
if multivalue:
|
||||
set_value = set(value)
|
||||
if len(set_value) == 1:
|
||||
value = tuple(set_value)[0]
|
||||
else:
|
||||
value = "< Multiselection >"
|
||||
self._filepath_input.setText(value)
|
||||
|
||||
self._in_set_value = False
|
||||
|
||||
def current_value(self):
|
||||
return self._filepath_input.text()
|
||||
|
||||
def set_filters(self, folders_allowed, exts_filter):
|
||||
self._folders_allowed = folders_allowed
|
||||
self._exts_filter = exts_filter
|
||||
|
||||
def _on_text_change(self, text):
|
||||
if not self._in_set_value:
|
||||
self.value_changed.emit()
|
||||
|
||||
def _on_browse_clicked(self):
|
||||
# TODO implement file dialog logic in '_on_browse_clicked'
|
||||
print("_on_browse_clicked")
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if not mime_data.hasUrls():
|
||||
return
|
||||
|
||||
filepaths = []
|
||||
for url in mime_data.urls():
|
||||
filepath = url.toLocalFile()
|
||||
if os.path.exists(filepath):
|
||||
filepaths.append(filepath)
|
||||
|
||||
# TODO add folder, extensions check
|
||||
if len(filepaths) == 1:
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
filepaths = []
|
||||
for url in mime_data.urls():
|
||||
filepath = url.toLocalFile()
|
||||
if os.path.exists(filepath):
|
||||
filepaths.append(filepath)
|
||||
# TODO filter check
|
||||
if len(filepaths) == 1:
|
||||
self.set_value(filepaths[0], False)
|
||||
event.accept()
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
import uuid
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from openpype.pipeline.lib import (
|
||||
AbtractAttrDef,
|
||||
UnknownDef,
|
||||
NumberDef,
|
||||
TextDef,
|
||||
EnumDef,
|
||||
BoolDef
|
||||
BoolDef,
|
||||
FileDef,
|
||||
UISeparatorDef,
|
||||
UILabelDef
|
||||
)
|
||||
from openpype.widgets.nice_checkbox import NiceCheckbox
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
def create_widget_for_attr_def(attr_def, parent=None):
|
||||
|
|
@ -32,12 +37,22 @@ def create_widget_for_attr_def(attr_def, parent=None):
|
|||
if isinstance(attr_def, UnknownDef):
|
||||
return UnknownAttrWidget(attr_def, parent)
|
||||
|
||||
if isinstance(attr_def, FileDef):
|
||||
return FileAttrWidget(attr_def, parent)
|
||||
|
||||
if isinstance(attr_def, UISeparatorDef):
|
||||
return SeparatorAttrWidget(attr_def, parent)
|
||||
|
||||
if isinstance(attr_def, UILabelDef):
|
||||
return LabelAttrWidget(attr_def, parent)
|
||||
|
||||
raise ValueError("Unknown attribute definition \"{}\"".format(
|
||||
str(type(attr_def))
|
||||
))
|
||||
|
||||
|
||||
class _BaseAttrDefWidget(QtWidgets.QWidget):
|
||||
# Type 'object' may not work with older PySide versions
|
||||
value_changed = QtCore.Signal(object, uuid.UUID)
|
||||
|
||||
def __init__(self, attr_def, parent):
|
||||
|
|
@ -68,12 +83,36 @@ class _BaseAttrDefWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_value(self, value, multivalue=False):
|
||||
raise NotImplementedError(
|
||||
"Method 'current_value' is not implemented. {}".format(
|
||||
"Method 'set_value' is not implemented. {}".format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SeparatorAttrWidget(_BaseAttrDefWidget):
|
||||
def _ui_init(self):
|
||||
input_widget = QtWidgets.QWidget(self)
|
||||
input_widget.setObjectName("Separator")
|
||||
input_widget.setMinimumHeight(2)
|
||||
input_widget.setMaximumHeight(2)
|
||||
|
||||
self._input_widget = input_widget
|
||||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
|
||||
class LabelAttrWidget(_BaseAttrDefWidget):
|
||||
def _ui_init(self):
|
||||
input_widget = QtWidgets.QLabel(self)
|
||||
label = self.attr_def.label
|
||||
if label:
|
||||
input_widget.setText(str(label))
|
||||
|
||||
self._input_widget = input_widget
|
||||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
|
||||
class NumberAttrWidget(_BaseAttrDefWidget):
|
||||
def _ui_init(self):
|
||||
decimals = self.attr_def.decimals
|
||||
|
|
@ -83,6 +122,9 @@ class NumberAttrWidget(_BaseAttrDefWidget):
|
|||
else:
|
||||
input_widget = QtWidgets.QSpinBox(self)
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
input_widget.setMinimum(self.attr_def.minimum)
|
||||
input_widget.setMaximum(self.attr_def.maximum)
|
||||
input_widget.setValue(self.attr_def.default)
|
||||
|
|
@ -136,6 +178,9 @@ class TextAttrWidget(_BaseAttrDefWidget):
|
|||
):
|
||||
input_widget.setPlaceholderText(self.attr_def.placeholder)
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
if self.attr_def.default:
|
||||
if self.multiline:
|
||||
input_widget.setPlainText(self.attr_def.default)
|
||||
|
|
@ -184,6 +229,9 @@ class BoolAttrWidget(_BaseAttrDefWidget):
|
|||
input_widget = NiceCheckbox(parent=self)
|
||||
input_widget.setChecked(self.attr_def.default)
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
input_widget.stateChanged.connect(self._on_value_change)
|
||||
|
||||
self._input_widget = input_widget
|
||||
|
|
@ -220,6 +268,9 @@ class EnumAttrWidget(_BaseAttrDefWidget):
|
|||
combo_delegate = QtWidgets.QStyledItemDelegate(input_widget)
|
||||
input_widget.setItemDelegate(combo_delegate)
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
items = self.attr_def.items
|
||||
for key, label in items.items():
|
||||
input_widget.addItem(label, key)
|
||||
|
|
@ -281,3 +332,40 @@ class UnknownAttrWidget(_BaseAttrDefWidget):
|
|||
if str_value != self._value:
|
||||
self._value = str_value
|
||||
self._input_widget.setText(str_value)
|
||||
|
||||
|
||||
class FileAttrWidget(_BaseAttrDefWidget):
|
||||
def _ui_init(self):
|
||||
self.multipath = self.attr_def.multipath
|
||||
if self.multipath:
|
||||
from .files_widget import MultiFilesWidget
|
||||
|
||||
input_widget = MultiFilesWidget(self)
|
||||
|
||||
else:
|
||||
from .files_widget import SingleFileWidget
|
||||
|
||||
input_widget = SingleFileWidget(self)
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
input_widget.set_filters(
|
||||
self.attr_def.folders, self.attr_def.extensions
|
||||
)
|
||||
|
||||
input_widget.value_changed.connect(self._on_value_change)
|
||||
|
||||
self._input_widget = input_widget
|
||||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
def _on_value_change(self):
|
||||
new_value = self.current_value()
|
||||
self.value_changed.emit(new_value, self.attr_def.id)
|
||||
|
||||
def current_value(self):
|
||||
return self._input_widget.current_value()
|
||||
|
||||
def set_value(self, value, multivalue=False):
|
||||
self._input_widget.set_value(value, multivalue)
|
||||
|
|
|
|||
1678
poetry.lock
generated
1678
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.8.2-nightly.1" # OpenPype
|
||||
version = "3.8.2" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
@ -71,7 +71,7 @@ dropbox = "^11.20.0"
|
|||
flake8 = "^3.7"
|
||||
autopep8 = "^1.4"
|
||||
coverage = "*"
|
||||
cx_freeze = { version = "6.7", source = "openpype" }
|
||||
cx_freeze = "~6.9"
|
||||
GitPython = "^3.1.17"
|
||||
jedi = "^0.13"
|
||||
Jinja2 = "^2.11"
|
||||
|
|
|
|||
|
|
@ -194,7 +194,6 @@ main () {
|
|||
"$POETRY_HOME/bin/poetry" run pip install setuptools==49.6.0
|
||||
"$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall wheel
|
||||
"$POETRY_HOME/bin/poetry" run python -m pip install --disable-pip-version-check --force-reinstall pip
|
||||
"$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall cx_freeze -i $openpype_index --extra-index-url https://pypi.org/simple
|
||||
}
|
||||
|
||||
return_code=0
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ main () {
|
|||
|
||||
echo -e "${BIGreen}>>>${RST} Running docker build ..."
|
||||
# docker build --pull --no-cache -t pypeclub/openpype:$openpype_version .
|
||||
docker build --pull --iidfile $openpype_root/build/docker-image.id -t pypeclub/openpype:$openpype_version -f $dockerfile .
|
||||
docker build --pull --iidfile $openpype_root/build/docker-image.id --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg VERSION=$openpype_version -t pypeclub/openpype:$openpype_version -f $dockerfile .
|
||||
if [ $? -ne 0 ] ; then
|
||||
echo $?
|
||||
echo -e "${BIRed}!!!${RST} Docker build failed."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue