diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index cd81171b73..f751a54116 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,9 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.18.7-nightly.2
+ - 3.18.7-nightly.1
+ - 3.18.6
- 3.18.6-nightly.2
- 3.18.6-nightly.1
- 3.18.5
@@ -132,9 +135,6 @@ body:
- 3.15.10-nightly.1
- 3.15.9
- 3.15.9-nightly.2
- - 3.15.9-nightly.1
- - 3.15.8
- - 3.15.8-nightly.3
validations:
required: true
- type: dropdown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14f0bc469f..009150ae7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,132 @@
# Changelog
+## [3.18.6](https://github.com/ynput/OpenPype/tree/3.18.6)
+
+
+[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.5...3.18.6)
+
+### **🚀 Enhancements**
+
+
+
+AYON: Use `SettingsField` from ayon server #6173
+
+This is preparation for new version of pydantic which will require to customize the field class for AYON purposes as raw pydantic Field could not be used.
+
+
+___
+
+
+
+
+
+Nuke: Expose write knobs - OP-7592 #6137
+
+This PR adds `exposed_knobs` to the creator plugins settings at `ayon+settings://nuke/create/CreateWriteRender/exposed_knobs`.When exposed knobs will be linked from the write node to the outside publish group, for users to adjust.
+
+
+___
+
+
+
+
+
+AYON: Remove kitsu addon #6172
+
+Removed kitsu addon from server addons because already has own repository.
+
+
+___
+
+
+
+### **🐛 Bug fixes**
+
+
+
+Fusion: provide better logging for validate saver crash due type error #6082
+
+Handles reported issue for `NoneType` error thrown in conversion `int(tool["Comments"][frame])`. It is most likely happening when saver node has no input connections.There is a validator for that, but it might be not obvious, that this error is caused by missing input connections and it has been already reported by `"Validate Saver Has Input"`.
+
+
+___
+
+
+
+
+
+Workfile Template Builder: Use correct variable in create placeholder #6141
+
+Use correct variable where failed instances are stored for validation.
+
+
+___
+
+
+
+
+
+ExtractOIIOTranscode: Missing product_names to subsets conversion #6159
+
+The `Product Names` filtering should be fixed with this.
+
+
+___
+
+
+
+
+
+Blender: Fix missing animation data when updating blend assets #6165
+
+Fix missing animation data when updating blend assets.
+
+
+___
+
+
+
+
+
+TrayPublisher: Pre-fill of version works in AYON #6180
+
+Use `folderPath` instead of `asset` in AYON mode to calculate next available version.
+
+
+___
+
+
+
+### **🔀 Refactored code**
+
+
+
+Chore: remove Muster #6085
+
+Muster isn't maintained for a long time and it wasn't working anyway. This is removing related code from the code base. If there is renewed interest in Muster, it needs to be re-implemented in modern AYON compatible way.
+
+
+___
+
+
+
+### **Merged pull requests**
+
+
+
+Maya: change label in the render settings to be more readable #6134
+
+AYON replacement for #5713.
+
+
+___
+
+
+
+
+
+
## [3.18.5](https://github.com/ynput/OpenPype/tree/3.18.5)
diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py
index fadfc0c206..b4fb20f922 100644
--- a/openpype/hosts/aftereffects/plugins/create/create_render.py
+++ b/openpype/hosts/aftereffects/plugins/create/create_render.py
@@ -29,6 +29,7 @@ class RenderCreator(Creator):
# Settings
mark_for_review = True
+ force_setting_values = True
def create(self, subset_name_from_ui, data, pre_create_data):
stub = api.get_stub() # only after After Effects is up
@@ -96,7 +97,9 @@ class RenderCreator(Creator):
self._add_instance_to_context(new_instance)
stub.rename_item(comp.id, subset_name)
- set_settings(True, True, [comp.id], print_msg=False)
+
+ if self.force_setting_values:
+ set_settings(True, True, [comp.id], print_msg=False)
def get_pre_create_attr_defs(self):
output = [
@@ -173,6 +176,7 @@ class RenderCreator(Creator):
)
self.mark_for_review = plugin_settings["mark_for_review"]
+ self.force_setting_values = plugin_settings["force_setting_values"]
self.default_variants = plugin_settings.get(
"default_variants",
plugin_settings.get("defaults") or []
diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py
index 0b9ad1a43b..ff4a734928 100644
--- a/openpype/hosts/fusion/api/menu.py
+++ b/openpype/hosts/fusion/api/menu.py
@@ -15,6 +15,7 @@ from openpype.hosts.fusion.api.lib import (
)
from openpype.pipeline import get_current_asset_name
from openpype.resources import get_openpype_icon_filepath
+from openpype.tools.utils import get_qt_app
from .pipeline import FusionEventHandler
from .pulse import FusionPulse
@@ -174,7 +175,8 @@ class OpenPypeMenu(QtWidgets.QWidget):
def launch_openpype_menu():
- app = QtWidgets.QApplication(sys.argv)
+
+ app = get_qt_app()
pype_menu = OpenPypeMenu()
diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py
index ff520348f0..d4d4995e6d 100644
--- a/openpype/hosts/photoshop/api/lib.py
+++ b/openpype/hosts/photoshop/api/lib.py
@@ -3,12 +3,11 @@ import sys
import contextlib
import traceback
-from qtpy import QtWidgets
-
from openpype.lib import env_value_to_bool, Logger
from openpype.modules import ModulesManager
from openpype.pipeline import install_host
from openpype.tools.utils import host_tools
+from openpype.tools.utils import get_openpype_qt_app
from openpype.tests.lib import is_in_tests
from .launch_logic import ProcessLauncher, stub
@@ -30,7 +29,7 @@ def main(*subprocess_args):
# coloring in StdOutBroker
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
- app = QtWidgets.QApplication([])
+ app = get_openpype_qt_app()
app.setQuitOnLastWindowClosed(False)
launcher = ProcessLauncher(subprocess_args)
diff --git a/openpype/hosts/standalonepublisher/addon.py b/openpype/hosts/standalonepublisher/addon.py
index 67204b581b..607c4ecdae 100644
--- a/openpype/hosts/standalonepublisher/addon.py
+++ b/openpype/hosts/standalonepublisher/addon.py
@@ -1,10 +1,13 @@
import os
-import click
-
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
-from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
+from openpype.modules import (
+ click_wrap,
+ OpenPypeModule,
+ ITrayAction,
+ IHostAddon,
+)
STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -37,10 +40,10 @@ class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon):
run_detached_process(args)
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(
+@click_wrap.group(
StandAlonePublishAddon.name,
help="StandalonePublisher related commands.")
def cli_main():
diff --git a/openpype/hosts/traypublisher/addon.py b/openpype/hosts/traypublisher/addon.py
index 3b34f9e6e8..ca60760bab 100644
--- a/openpype/hosts/traypublisher/addon.py
+++ b/openpype/hosts/traypublisher/addon.py
@@ -1,10 +1,13 @@
import os
-import click
-
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
-from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
+from openpype.modules import (
+ click_wrap,
+ OpenPypeModule,
+ ITrayAction,
+ IHostAddon,
+)
TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -38,10 +41,12 @@ class TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction):
run_detached_process(args)
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(TrayPublishAddon.name, help="TrayPublisher related commands.")
+@click_wrap.group(
+ TrayPublishAddon.name,
+ help="TrayPublisher related commands.")
def cli_main():
pass
diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py
index 6859b85a46..a6075f0eb5 100644
--- a/openpype/hosts/traypublisher/api/plugin.py
+++ b/openpype/hosts/traypublisher/api/plugin.py
@@ -32,6 +32,7 @@ SHARED_DATA_KEY = "openpype.traypublisher.instances"
class HiddenTrayPublishCreator(HiddenCreator):
host_name = "traypublisher"
+ settings_category = "traypublisher"
def collect_instances(self):
instances_by_identifier = cache_and_get_instances(
@@ -68,6 +69,7 @@ class HiddenTrayPublishCreator(HiddenCreator):
class TrayPublishCreator(Creator):
create_allow_context_change = True
host_name = "traypublisher"
+ settings_category = "traypublisher"
def collect_instances(self):
instances_by_identifier = cache_and_get_instances(
diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py
index dce4a051fd..3898635254 100644
--- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py
+++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py
@@ -381,15 +381,19 @@ or updating already created. Publishing will create OTIO file.
"""
self.asset_name_check = []
- tracks = otio_timeline.each_child(
- descended_from_type=otio.schema.Track
- )
+ tracks = [
+ track for track in otio_timeline.each_child(
+ descended_from_type=otio.schema.Track)
+ if track.kind == "Video"
+ ]
- # media data for audio sream and reference solving
+ # media data for audio stream and reference solving
media_data = self._get_media_source_metadata(media_path)
for track in tracks:
+ # set track name
track.name = f"{sequence_file_name} - {otio_timeline.name}"
+
try:
track_start_frame = (
abs(track.source_range.start_time.value)
@@ -398,19 +402,19 @@ or updating already created. Publishing will create OTIO file.
except AttributeError:
track_start_frame = 0
-
- for clip in track.each_child():
- if not self._validate_clip_for_processing(clip):
+ for otio_clip in track.each_child():
+ if not self._validate_clip_for_processing(otio_clip):
continue
+
# get available frames info to clip data
- self._create_otio_reference(clip, media_path, media_data)
+ self._create_otio_reference(otio_clip, media_path, media_data)
# convert timeline range to source range
- self._restore_otio_source_range(clip)
+ self._restore_otio_source_range(otio_clip)
base_instance_data = self._get_base_instance_data(
- clip,
+ otio_clip,
instance_data,
track_start_frame
)
@@ -429,7 +433,7 @@ or updating already created. Publishing will create OTIO file.
continue
instance = self._make_subset_instance(
- clip,
+ otio_clip,
_fpreset,
deepcopy(base_instance_data),
parenting_data
diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py
index b99b634da1..43f6518374 100644
--- a/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py
+++ b/openpype/hosts/traypublisher/plugins/publish/collect_shot_instances.py
@@ -79,6 +79,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin):
clip for clip in otio_timeline.each_child(
descended_from_type=otio.schema.Clip)
if clip.name == otio_clip.name
+ if clip.parent().kind == "Video"
]
otio_clip = clips.pop()
diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration
index 63266607ce..a4755d2869 160000
--- a/openpype/hosts/unreal/integration
+++ b/openpype/hosts/unreal/integration
@@ -1 +1 @@
-Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757
+Subproject commit a4755d2869694fcf58c98119298cde8d204e2ce4
diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py
index 4438775b03..810d9aa6c3 100644
--- a/openpype/hosts/webpublisher/addon.py
+++ b/openpype/hosts/webpublisher/addon.py
@@ -1,8 +1,6 @@
import os
-import click
-
-from openpype.modules import OpenPypeModule, IHostAddon
+from openpype.modules import click_wrap, OpenPypeModule, IHostAddon
WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -38,10 +36,10 @@ class WebpublisherAddon(OpenPypeModule, IHostAddon):
)
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(
+@click_wrap.group(
WebpublisherAddon.name,
help="Webpublisher related commands.")
def cli_main():
@@ -49,10 +47,10 @@ def cli_main():
@cli_main.command()
-@click.argument("path")
-@click.option("-u", "--user", help="User email address")
-@click.option("-p", "--project", help="Project")
-@click.option("-t", "--targets", help="Targets", default=None,
+@click_wrap.argument("path")
+@click_wrap.option("-u", "--user", help="User email address")
+@click_wrap.option("-p", "--project", help="Project")
+@click_wrap.option("-t", "--targets", help="Targets", default=None,
multiple=True)
def publish(project, path, user=None, targets=None):
"""Start publishing (Inner command).
@@ -67,11 +65,11 @@ def publish(project, path, user=None, targets=None):
@cli_main.command()
-@click.argument("path")
-@click.option("-p", "--project", help="Project")
-@click.option("-h", "--host", help="Host")
-@click.option("-u", "--user", help="User email address")
-@click.option("-t", "--targets", help="Targets", default=None,
+@click_wrap.argument("path")
+@click_wrap.option("-p", "--project", help="Project")
+@click_wrap.option("-h", "--host", help="Host")
+@click_wrap.option("-u", "--user", help="User email address")
+@click_wrap.option("-t", "--targets", help="Targets", default=None,
multiple=True)
def publishfromapp(project, path, host, user=None, targets=None):
"""Start publishing through application (Inner command).
@@ -86,10 +84,10 @@ def publishfromapp(project, path, host, user=None, targets=None):
@cli_main.command()
-@click.option("-e", "--executable", help="Executable")
-@click.option("-u", "--upload_dir", help="Upload dir")
-@click.option("-h", "--host", help="Host", default=None)
-@click.option("-p", "--port", help="Port", default=None)
+@click_wrap.option("-e", "--executable", help="Executable")
+@click_wrap.option("-u", "--upload_dir", help="Upload dir")
+@click_wrap.option("-h", "--host", help="Host", default=None)
+@click_wrap.option("-p", "--port", help="Port", default=None)
def webserver(executable, upload_dir, host=None, port=None):
"""Start service for communication with Webpublish Front end.
diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py
index c8ddbde061..1cfe9ac14b 100644
--- a/openpype/lib/transcoding.py
+++ b/openpype/lib/transcoding.py
@@ -1227,12 +1227,8 @@ def get_rescaled_command_arguments(
target_par = target_par or 1.0
input_par = 1.0
- # ffmpeg command
- input_file_metadata = get_ffprobe_data(input_path, logger=log)
- stream = input_file_metadata["streams"][0]
- input_width = int(stream["width"])
- input_height = int(stream["height"])
- stream_input_par = stream.get("sample_aspect_ratio")
+ input_height, input_width, stream_input_par = _get_image_dimensions(
+ application, input_path, log)
if stream_input_par:
input_par = (
float(stream_input_par.split(":")[0])
@@ -1345,6 +1341,48 @@ def get_rescaled_command_arguments(
return command_args
+def _get_image_dimensions(application, input_path, log):
+ """Uses 'ffprobe' first and then 'oiiotool' if available to get dim.
+
+ Args:
+ application (str): "oiiotool"|"ffmpeg"
+ input_path (str): path to image file
+ log (Optional[logging.Logger]): Logger used for logging.
+ Returns:
+ (tuple) (int, int, dict) - (height, width, sample_aspect_ratio)
+ Raises:
+ RuntimeError if image dimensions couldn't be parsed out.
+ """
+ # ffmpeg command
+ input_file_metadata = get_ffprobe_data(input_path, logger=log)
+ input_width = input_height = 0
+ stream = next(
+ (
+ s for s in input_file_metadata["streams"]
+ if s.get("codec_type") == "video"
+ ),
+ {}
+ )
+ if stream:
+ input_width = int(stream["width"])
+ input_height = int(stream["height"])
+
+ # fallback for weird files with width=0, height=0
+ if (input_width == 0 or input_height == 0) and application == "oiiotool":
+ # Load info about file from oiio tool
+ input_info = get_oiio_info_for_input(input_path, logger=log)
+ if input_info:
+ input_width = int(input_info["width"])
+ input_height = int(input_info["height"])
+
+ if input_width == 0 or input_height == 0:
+ raise RuntimeError("Couldn't read {} either "
+ "with ffprobe or oiiotool".format(input_path))
+
+ stream_input_par = stream.get("sample_aspect_ratio")
+ return input_height, input_width, stream_input_par
+
+
def convert_color_values(application, color_value):
"""Get color mapping for ffmpeg and oiiotool.
Args:
diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py
index 3097805353..87f3233afc 100644
--- a/openpype/modules/__init__.py
+++ b/openpype/modules/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+from . import click_wrap
from .interfaces import (
ILaunchHookPaths,
IPluginPaths,
@@ -28,6 +29,8 @@ from .base import (
__all__ = (
+ "click_wrap",
+
"ILaunchHookPaths",
"IPluginPaths",
"ITrayModule",
diff --git a/openpype/modules/click_wrap.py b/openpype/modules/click_wrap.py
new file mode 100644
index 0000000000..ed67035ec8
--- /dev/null
+++ b/openpype/modules/click_wrap.py
@@ -0,0 +1,365 @@
+"""Simplified wrapper for 'click' python module.
+
+Module 'click' is used as main cli handler in AYON/OpenPype. Addons can
+register their own subcommands with options. This wrapper allows to define
+commands and options as with 'click', but without any dependency.
+
+Why not to use 'click' directly? Version of 'click' used in AYON/OpenPype
+is not compatible with 'click' version used in some DCCs (e.g. Houdini 20+).
+And updating 'click' would break other DCCs.
+
+How to use it? If you already have cli commands defined in addon, just replace
+'click' with 'click_wrap' and it should work and modify your addon's cli
+method to convert 'click_wrap' object to 'click' object.
+
+Before
+```python
+import click
+from openpype.modules import OpenPypeModule
+
+
+class ExampleAddon(OpenPypeModule):
+ name = "example"
+
+ def cli(self, click_group):
+ click_group.add_command(cli_main)
+
+
+@click.group(ExampleAddon.name, help="Example addon")
+def cli_main():
+ pass
+
+
+@cli_main.command(help="Example command")
+@click.option("--arg1", help="Example argument 1", default="default1")
+@click.option("--arg2", help="Example argument 2", is_flag=True)
+def mycommand(arg1, arg2):
+ print(arg1, arg2)
+```
+
+Now
+```
+from openpype import click_wrap
+from openpype.modules import OpenPypeModule
+
+
+class ExampleAddon(OpenPypeModule):
+ name = "example"
+
+ def cli(self, click_group):
+ click_group.add_command(cli_main.to_click_obj())
+
+
+@click_wrap.group(ExampleAddon.name, help="Example addon")
+def cli_main():
+ pass
+
+
+@cli_main.command(help="Example command")
+@click_wrap.option("--arg1", help="Example argument 1", default="default1")
+@click_wrap.option("--arg2", help="Example argument 2", is_flag=True)
+def mycommand(arg1, arg2):
+ print(arg1, arg2)
+```
+
+
+Added small enhancements:
+- most of the methods can be used as chained calls
+- functions/methods 'command' and 'group' can be used in a way that
+ first argument is callback function and the rest are arguments
+ for click
+
+Example:
+ ```python
+ from openpype import click_wrap
+ from openpype.modules import OpenPypeModule
+
+
+ class ExampleAddon(OpenPypeModule):
+ name = "example"
+
+ def cli(self, click_group):
+ # Define main command (name 'example')
+ main = click_wrap.group(
+ self._cli_main, name=self.name, help="Example addon"
+ )
+ # Add subcommand (name 'mycommand')
+ (
+ main.command(
+ self._cli_command, name="mycommand", help="Example command"
+ )
+ .option(
+ "--arg1", help="Example argument 1", default="default1"
+ )
+ .option(
+ "--arg2", help="Example argument 2", is_flag=True,
+ )
+ )
+ # Convert main command to click object and add it to parent group
+ click_group.add_command(main.to_click_obj())
+
+ def _cli_main(self):
+ pass
+
+ def _cli_command(self, arg1, arg2):
+ print(arg1, arg2)
+ ```
+
+ ```shell
+ openpype_console addon example mycommand --arg1 value1 --arg2
+ ```
+"""
+
+import collections
+
+FUNC_ATTR_NAME = "__ayon_cli_options__"
+
+
+class Command(object):
+ def __init__(self, func, *args, **kwargs):
+ # Command function
+ self._func = func
+ # Command definition arguments
+ self._args = args
+ # Command definition kwargs
+ self._kwargs = kwargs
+ # Both 'options' and 'arguments' are stored to the same variable
+ # - keep order of options and arguments
+ self._options = getattr(func, FUNC_ATTR_NAME, [])
+
+ def to_click_obj(self):
+ """Converts this object to click object.
+
+ Returns:
+ click.Command: Click command object.
+ """
+ return convert_to_click(self)
+
+ # --- Methods for 'convert_to_click' function ---
+ def get_args(self):
+ """
+ Returns:
+ tuple: Command definition arguments.
+ """
+ return self._args
+
+ def get_kwargs(self):
+ """
+ Returns:
+ dict[str, Any]: Command definition kwargs.
+ """
+ return self._kwargs
+
+ def get_func(self):
+ """
+ Returns:
+ Function: Function to invoke on command trigger.
+ """
+ return self._func
+
+ def iter_options(self):
+ """
+ Yields:
+ tuple[str, tuple, dict]: Option type name with args and kwargs.
+ """
+ for item in self._options:
+ yield item
+ # -----------------------------------------------
+
+ def add_option(self, *args, **kwargs):
+ return self.add_option_by_type("option", *args, **kwargs)
+
+ def add_argument(self, *args, **kwargs):
+ return self.add_option_by_type("argument", *args, **kwargs)
+
+ option = add_option
+ argument = add_argument
+
+ def add_option_by_type(self, option_name, *args, **kwargs):
+ self._options.append((option_name, args, kwargs))
+ return self
+
+
+class Group(Command):
+ def __init__(self, func, *args, **kwargs):
+ super(Group, self).__init__(func, *args, **kwargs)
+ # Store sub-groupd and sub-commands to the same variable
+ self._commands = []
+
+ # --- Methods for 'convert_to_click' function ---
+ def iter_commands(self):
+ for command in self._commands:
+ yield command
+ # -----------------------------------------------
+
+ def add_command(self, command):
+ """Add prepared command object as child.
+
+ Args:
+ command (Command): Prepared command object.
+ """
+ if command not in self._commands:
+ self._commands.append(command)
+
+ def add_group(self, group):
+ """Add prepared group object as child.
+
+ Args:
+ group (Group): Prepared group object.
+ """
+ if group not in self._commands:
+ self._commands.append(group)
+
+ def command(self, *args, **kwargs):
+ """Add child command.
+
+ Returns:
+ Union[Command, Function]: New command object, or wrapper function.
+ """
+ return self._add_new(Command, *args, **kwargs)
+
+ def group(self, *args, **kwargs):
+ """Add child group.
+
+ Returns:
+ Union[Group, Function]: New group object, or wrapper function.
+ """
+ return self._add_new(Group, *args, **kwargs)
+
+ def _add_new(self, target_cls, *args, **kwargs):
+ func = None
+ if args and callable(args[0]):
+ args = list(args)
+ func = args.pop(0)
+ args = tuple(args)
+
+ def decorator(_func):
+ out = target_cls(_func, *args, **kwargs)
+ self._commands.append(out)
+ return out
+
+ if func is not None:
+ return decorator(func)
+ return decorator
+
+
+def convert_to_click(obj_to_convert):
+ """Convert wrapped object to click object.
+
+ Args:
+ obj_to_convert (Command): Object to convert to click object.
+
+ Returns:
+ click.Command: Click command object.
+ """
+ import click
+
+ commands_queue = collections.deque()
+ commands_queue.append((obj_to_convert, None))
+ top_obj = None
+ while commands_queue:
+ item = commands_queue.popleft()
+ command_obj, parent_obj = item
+ if not isinstance(command_obj, Command):
+ raise TypeError(
+ "Invalid type '{}' expected 'Command'".format(
+ type(command_obj)
+ )
+ )
+
+ if isinstance(command_obj, Group):
+ click_obj = (
+ click.group(
+ *command_obj.get_args(),
+ **command_obj.get_kwargs()
+ )(command_obj.get_func())
+ )
+
+ else:
+ click_obj = (
+ click.command(
+ *command_obj.get_args(),
+ **command_obj.get_kwargs()
+ )(command_obj.get_func())
+ )
+
+ for item in command_obj.iter_options():
+ option_name, args, kwargs = item
+ if option_name == "option":
+ click.option(*args, **kwargs)(click_obj)
+ elif option_name == "argument":
+ click.argument(*args, **kwargs)(click_obj)
+ else:
+ raise ValueError(
+ "Invalid option name '{}'".format(option_name)
+ )
+
+ if top_obj is None:
+ top_obj = click_obj
+
+ if parent_obj is not None:
+ parent_obj.add_command(click_obj)
+
+ if isinstance(command_obj, Group):
+ for command in command_obj.iter_commands():
+ commands_queue.append((command, click_obj))
+
+ return top_obj
+
+
+def group(*args, **kwargs):
+ func = None
+ if args and callable(args[0]):
+ args = list(args)
+ func = args.pop(0)
+ args = tuple(args)
+
+ def decorator(_func):
+ return Group(_func, *args, **kwargs)
+
+ if func is not None:
+ return decorator(func)
+ return decorator
+
+
+def command(*args, **kwargs):
+ func = None
+ if args and callable(args[0]):
+ args = list(args)
+ func = args.pop(0)
+ args = tuple(args)
+
+ def decorator(_func):
+ return Command(_func, *args, **kwargs)
+
+ if func is not None:
+ return decorator(func)
+ return decorator
+
+
+def argument(*args, **kwargs):
+ def decorator(func):
+ return _add_option_to_func(
+ func, "argument", *args, **kwargs
+ )
+ return decorator
+
+
+def option(*args, **kwargs):
+ def decorator(func):
+ return _add_option_to_func(
+ func, "option", *args, **kwargs
+ )
+ return decorator
+
+
+def _add_option_to_func(func, option_name, *args, **kwargs):
+ if isinstance(func, Command):
+ func.add_option_by_type(option_name, *args, **kwargs)
+ return func
+
+ if not hasattr(func, FUNC_ATTR_NAME):
+ setattr(func, FUNC_ATTR_NAME, [])
+ cli_options = getattr(func, FUNC_ATTR_NAME)
+ cli_options.append((option_name, args, kwargs))
+ return func
diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py
index be1d3ff920..e9de0c1bf5 100644
--- a/openpype/modules/example_addons/example_addon/addon.py
+++ b/openpype/modules/example_addons/example_addon/addon.py
@@ -8,9 +8,9 @@ in global space here until are required or used.
"""
import os
-import click
from openpype.modules import (
+ click_wrap,
JsonFilesSettingsDef,
OpenPypeAddOn,
ModulesManager,
@@ -115,10 +115,12 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction):
}
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(ExampleAddon.name, help="Example addon dynamic cli commands.")
+@click_wrap.group(
+ ExampleAddon.name,
+ help="Example addon dynamic cli commands.")
def cli_main():
pass
diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py
index b5152ff9c4..2042367a7e 100644
--- a/openpype/modules/ftrack/ftrack_module.py
+++ b/openpype/modules/ftrack/ftrack_module.py
@@ -3,9 +3,8 @@ import json
import collections
import platform
-import click
-
from openpype.modules import (
+ click_wrap,
OpenPypeModule,
ITrayModule,
IPluginPaths,
@@ -489,7 +488,7 @@ class FtrackModule(
return cred.get("username"), cred.get("api_key")
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
def _check_ftrack_url(url):
@@ -540,24 +539,24 @@ def resolve_ftrack_url(url, logger=None):
return ftrack_url
-@click.group(FtrackModule.name, help="Ftrack module related commands.")
+@click_wrap.group(FtrackModule.name, help="Ftrack module related commands.")
def cli_main():
pass
@cli_main.command()
-@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
-@click.option("--ftrack-url", envvar="FTRACK_SERVER",
+@click_wrap.option("-d", "--debug", is_flag=True, help="Print debug messages")
+@click_wrap.option("--ftrack-url", envvar="FTRACK_SERVER",
help="Ftrack server url")
-@click.option("--ftrack-user", envvar="FTRACK_API_USER",
+@click_wrap.option("--ftrack-user", envvar="FTRACK_API_USER",
help="Ftrack api user")
-@click.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
+@click_wrap.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
help="Ftrack api key")
-@click.option("--legacy", is_flag=True,
+@click_wrap.option("--legacy", is_flag=True,
help="run event server without mongo storing")
-@click.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
+@click_wrap.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
help="Clockify API key.")
-@click.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE",
+@click_wrap.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE",
help="Clockify workspace")
def eventserver(
debug,
diff --git a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
index ac4e499e41..5c780a51c4 100644
--- a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
+++ b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
@@ -131,6 +131,8 @@ class PostFtrackHook(PostLaunchHook):
for key, value in status_mapping.items():
if key in already_tested:
continue
+
+ value = value.lower()
if actual_status in value or "__any__" in value:
if key != "__ignore__":
next_status_name = key
diff --git a/openpype/modules/job_queue/module.py b/openpype/modules/job_queue/module.py
index 7075fcea14..6792cd2aca 100644
--- a/openpype/modules/job_queue/module.py
+++ b/openpype/modules/job_queue/module.py
@@ -41,8 +41,7 @@ import json
import copy
import platform
-import click
-from openpype.modules import OpenPypeModule
+from openpype.modules import OpenPypeModule, click_wrap
from openpype.settings import get_system_settings
@@ -153,7 +152,7 @@ class JobQueueModule(OpenPypeModule):
return requests.get(api_path).json()
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
@classmethod
def get_server_url_from_settings(cls):
@@ -213,7 +212,7 @@ class JobQueueModule(OpenPypeModule):
return main(str(executable), server_url)
-@click.group(
+@click_wrap.group(
JobQueueModule.name,
help="Application job server. Can be used as render farm."
)
@@ -225,8 +224,8 @@ def cli_main():
"start_server",
help="Start server handling workers and their jobs."
)
-@click.option("--port", help="Server port")
-@click.option("--host", help="Server host (ip address)")
+@click_wrap.option("--port", help="Server port")
+@click_wrap.option("--host", help="Server host (ip address)")
def cli_start_server(port, host):
JobQueueModule.start_server(port, host)
@@ -236,7 +235,9 @@ def cli_start_server(port, host):
"Start a worker for a specific application. (e.g. \"tvpaint/11.5\")"
)
)
-@click.argument("app_name")
-@click.option("--server_url", help="Server url which handle workers and jobs.")
+@click_wrap.argument("app_name")
+@click_wrap.option(
+ "--server_url",
+ help="Server url which handle workers and jobs.")
def cli_start_worker(app_name, server_url):
JobQueueModule.start_worker(app_name, server_url)
diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py
index 8d2d5ccd60..0ab627ba75 100644
--- a/openpype/modules/kitsu/kitsu_module.py
+++ b/openpype/modules/kitsu/kitsu_module.py
@@ -1,9 +1,9 @@
"""Kitsu module."""
-import click
import os
from openpype.modules import (
+ click_wrap,
OpenPypeModule,
IPluginPaths,
ITrayAction,
@@ -98,17 +98,17 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):
}
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(KitsuModule.name, help="Kitsu dynamic cli commands.")
+@click_wrap.group(KitsuModule.name, help="Kitsu dynamic cli commands.")
def cli_main():
pass
@cli_main.command()
-@click.option("--login", envvar="KITSU_LOGIN", help="Kitsu login")
-@click.option(
+@click_wrap.option("--login", envvar="KITSU_LOGIN", help="Kitsu login")
+@click_wrap.option(
"--password", envvar="KITSU_PWD", help="Password for kitsu username"
)
def push_to_zou(login, password):
@@ -124,11 +124,11 @@ def push_to_zou(login, password):
@cli_main.command()
-@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login")
-@click.option(
+@click_wrap.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login")
+@click_wrap.option(
"-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username"
)
-@click.option(
+@click_wrap.option(
"-prj",
"--project",
"projects",
@@ -136,7 +136,7 @@ def push_to_zou(login, password):
default=[],
help="Sync specific kitsu projects",
)
-@click.option(
+@click_wrap.option(
"-lo",
"--listen-only",
"listen_only",
diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py
index 8a92697920..3d6f76ad55 100644
--- a/openpype/modules/sync_server/sync_server_module.py
+++ b/openpype/modules/sync_server/sync_server_module.py
@@ -7,7 +7,6 @@ import copy
import signal
from collections import deque, defaultdict
-import click
from bson.objectid import ObjectId
from openpype.client import (
@@ -15,7 +14,12 @@ from openpype.client import (
get_representations,
get_representation_by_id,
)
-from openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths
+from openpype.modules import (
+ OpenPypeModule,
+ ITrayModule,
+ IPluginPaths,
+ click_wrap,
+)
from openpype.settings import (
get_project_settings,
get_system_settings,
@@ -2405,7 +2409,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
return presets[project_name]['sites'][site_name]['root']
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
# Webserver module implementation
def webserver_initialization(self, server_manager):
@@ -2417,13 +2421,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
)
-@click.group(SyncServerModule.name, help="SyncServer module related commands.")
+@click_wrap.group(
+ SyncServerModule.name,
+ help="SyncServer module related commands.")
def cli_main():
pass
@cli_main.command()
-@click.option(
+@click_wrap.option(
"-a",
"--active_site",
required=True,
diff --git a/openpype/modules/timers_manager/widget_user_idle.py b/openpype/modules/timers_manager/widget_user_idle.py
index 9df328e6b2..8cc78cf102 100644
--- a/openpype/modules/timers_manager/widget_user_idle.py
+++ b/openpype/modules/timers_manager/widget_user_idle.py
@@ -17,6 +17,7 @@ class WidgetUserIdle(QtWidgets.QWidget):
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowMinimizeButtonHint
+ | QtCore.Qt.WindowStaysOnTopHint
)
self._is_showed = False
diff --git a/openpype/plugins/publish/validate_resources.py b/openpype/plugins/publish/validate_resources.py
index 7911c70c2d..ce03515400 100644
--- a/openpype/plugins/publish/validate_resources.py
+++ b/openpype/plugins/publish/validate_resources.py
@@ -17,7 +17,7 @@ class ValidateResources(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder
- label = "Resources"
+ label = "Validate Resources"
def process(self, instance):
diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py
index 4948f2431c..2c851c054d 100644
--- a/openpype/settings/ayon_settings.py
+++ b/openpype/settings/ayon_settings.py
@@ -1458,7 +1458,7 @@ class _AyonSettingsCache:
variant = "production"
if is_dev_mode_enabled():
- variant = cls._get_dev_mode_settings_variant()
+ variant = cls._get_bundle_name()
elif is_staging_enabled():
variant = "staging"
@@ -1474,28 +1474,6 @@ class _AyonSettingsCache:
def _get_bundle_name(cls):
return os.environ["AYON_BUNDLE_NAME"]
- @classmethod
- def _get_dev_mode_settings_variant(cls):
- """Develop mode settings variant.
-
- Returns:
- str: Name of settings variant.
- """
-
- con = get_ayon_server_api_connection()
- bundles = con.get_bundles()
- user = con.get_user()
- username = user["name"]
- for bundle in bundles["bundles"]:
- if (
- bundle.get("isDev")
- and bundle.get("activeUser") == username
- ):
- return bundle["name"]
- # Return fake variant - distribution logic will tell user that he
- # does not have set any dev bundle
- return "dev"
-
@classmethod
def get_value_by_project(cls, project_name):
cache_item = _AyonSettingsCache.cache_by_project_name[project_name]
diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json
index 77ccb74410..9e2ab7334b 100644
--- a/openpype/settings/defaults/project_settings/aftereffects.json
+++ b/openpype/settings/defaults/project_settings/aftereffects.json
@@ -15,7 +15,8 @@
"default_variants": [
"Main"
],
- "mark_for_review": true
+ "mark_for_review": true,
+ "force_setting_values": true
}
},
"publish": {
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json
index 72f09a641d..b0f8a7357f 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json
@@ -42,6 +42,12 @@
"key": "mark_for_review",
"label": "Review",
"default": true
+ },
+ {
+ "type": "boolean",
+ "key": "force_setting_values",
+ "label": "Force resolution and duration values from Asset",
+ "default": true
}
]
}
diff --git a/openpype/tools/ayon_loader/abstract.py b/openpype/tools/ayon_loader/abstract.py
index bf3e81d485..1d93716e07 100644
--- a/openpype/tools/ayon_loader/abstract.py
+++ b/openpype/tools/ayon_loader/abstract.py
@@ -531,6 +531,9 @@ class FrontendLoaderController(_BaseLoaderController):
Product types have defined if are checked for filtering or not.
+ Args:
+ project_name (Union[str, None]): Project name.
+
Returns:
list[ProductTypeItem]: List of product type items for a project.
"""
diff --git a/openpype/tools/ayon_loader/models/products.py b/openpype/tools/ayon_loader/models/products.py
index 135f28df97..40b6474d12 100644
--- a/openpype/tools/ayon_loader/models/products.py
+++ b/openpype/tools/ayon_loader/models/products.py
@@ -179,12 +179,15 @@ class ProductsModel:
"""Product type items for project.
Args:
- project_name (str): Project name.
+ project_name (Union[str, None]): Project name.
Returns:
list[ProductTypeItem]: Product type items.
"""
+ if not project_name:
+ return []
+
cache = self._product_type_items_cache[project_name]
if not cache.is_valid:
product_types = ayon_api.get_project_product_types(project_name)
diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py
index a6d40d52e7..8982d92c0f 100644
--- a/openpype/tools/ayon_loader/ui/window.py
+++ b/openpype/tools/ayon_loader/ui/window.py
@@ -322,6 +322,7 @@ class LoaderWindow(QtWidgets.QWidget):
)
def refresh(self):
+ self._reset_on_show = False
self._controller.reset()
def showEvent(self, event):
@@ -332,6 +333,13 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.start()
+ def closeEvent(self, event):
+ super(LoaderWindow, self).closeEvent(event)
+ # Deselect project so current context will be selected
+ # on next 'showEvent'
+ self._controller.set_selected_project(None)
+ self._reset_on_show = True
+
def keyPressEvent(self, event):
modifiers = event.modifiers()
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
@@ -378,8 +386,7 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.stop()
if self._reset_on_show:
- self._reset_on_show = False
- self._controller.reset()
+ self.refresh()
def _show_group_dialog(self):
project_name = self._projects_combobox.get_selected_project_name()
diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
index 2ebed7f89b..1d1bd1adbc 100644
--- a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
+++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
@@ -1212,12 +1212,12 @@ class SwitchAssetDialog(QtWidgets.QDialog):
))
version_ids = set()
- version_docs_by_parent_id = {}
+ version_docs_by_parent_id_and_name = collections.defaultdict(dict)
for version_doc in version_docs:
- parent_id = version_doc["parent"]
- if parent_id not in version_docs_by_parent_id:
- version_ids.add(version_doc["_id"])
- version_docs_by_parent_id[parent_id] = version_doc
+ version_ids.add(version_doc["_id"])
+ product_id = version_doc["parent"]
+ name = version_doc["name"]
+ version_docs_by_parent_id_and_name[product_id][name] = version_doc
hero_version_docs_by_parent_id = {}
for hero_version_doc in hero_version_docs:
@@ -1242,7 +1242,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
- version_docs_by_parent_id,
+ version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
)
@@ -1256,10 +1256,10 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container,
loader,
selected_folder_id,
- product_name,
+ selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
- version_docs_by_parent_id,
+ version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
):
@@ -1272,15 +1272,18 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container_product_id = container_version["parent"]
container_product = self._product_docs_by_id[container_product_id]
+ container_product_name = container_product["name"]
+
+ container_folder_id = container_product["parent"]
if selected_folder_id:
folder_id = selected_folder_id
else:
- folder_id = container_product["parent"]
+ folder_id = container_folder_id
products_by_name = product_docs_by_parent_and_name[folder_id]
- if product_name:
- product_doc = products_by_name[product_name]
+ if selected_product_name:
+ product_doc = products_by_name[selected_product_name]
else:
product_doc = products_by_name[container_product["name"]]
@@ -1300,7 +1303,26 @@ class SwitchAssetDialog(QtWidgets.QDialog):
repre_doc = _repres.get(container_repre_name)
if not repre_doc:
- version_doc = version_docs_by_parent_id[product_id]
+ version_docs_by_name = (
+ version_docs_by_parent_id_and_name[product_id]
+ )
+ # If asset or subset are selected for switching, we use latest
+ # version else we try to keep the current container version.
+ version_name = None
+ if (
+ selected_folder_id in (None, container_folder_id)
+ and selected_product_name in (None, container_product_name)
+ ):
+ version_name = container_version.get("name")
+
+ version_doc = None
+ if version_name is not None:
+ version_doc = version_docs_by_name.get(version_name)
+
+ if version_doc is None:
+ version_name = max(version_docs_by_name)
+ version_doc = version_docs_by_name[version_name]
+
version_id = version_doc["_id"]
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
if selected_representation:
diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py
index 47e374edf2..13d007dd35 100644
--- a/openpype/tools/publisher/control.py
+++ b/openpype/tools/publisher/control.py
@@ -2329,8 +2329,25 @@ class PublisherController(BasePublisherController):
result = pyblish.plugin.process(
plugin, self._publish_context, None, action.id
)
+ exception = result.get("error")
+ if exception:
+ self._emit_event(
+ "publish.action.failed",
+ {
+ "title": "Action failed",
+ "message": "Action failed.",
+ "traceback": "".join(
+ traceback.format_exception(exception)
+ ),
+ "label": action.__name__,
+ "identifier": action.id
+ }
+ )
+
self._publish_report.add_action_result(action, result)
+ self.emit_card_message("Action finished.")
+
def _publish_next_process(self):
# Validations of progress before using iterator
# - same conditions may be inside iterator but they may be used
diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py
index 5dd6998b24..dcfbbde851 100644
--- a/openpype/tools/publisher/window.py
+++ b/openpype/tools/publisher/window.py
@@ -42,7 +42,7 @@ from .widgets import (
)
-class PublisherWindow(QtWidgets.QWidget):
+class PublisherWindow(QtWidgets.QDialog):
"""Main window of publisher."""
default_width = 1300
default_height = 800
@@ -50,7 +50,7 @@ class PublisherWindow(QtWidgets.QWidget):
publish_footer_spacer = 2
def __init__(self, parent=None, controller=None, reset_on_show=None):
- super(PublisherWindow, self).__init__()
+ super(PublisherWindow, self).__init__(parent)
self.setObjectName("PublishWindow")
@@ -294,12 +294,6 @@ class PublisherWindow(QtWidgets.QWidget):
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
)
- controller.event_system.add_callback(
- "publish.process.instance.changed", self._on_instance_change
- )
- controller.event_system.add_callback(
- "publish.process.plugin.changed", self._on_plugin_change
- )
controller.event_system.add_callback(
"show.card.message", self._on_overlay_message
)
@@ -321,6 +315,9 @@ class PublisherWindow(QtWidgets.QWidget):
controller.event_system.add_callback(
"convertors.find.failed", self._on_convertor_error
)
+ controller.event_system.add_callback(
+ "publish.action.failed", self._on_action_error
+ )
controller.event_system.add_callback(
"export_report.request", self._export_report
)
@@ -328,7 +325,6 @@ class PublisherWindow(QtWidgets.QWidget):
"copy_report.request", self._copy_report
)
-
# Store extra header widget for TrayPublisher
# - can be used to add additional widgets to header between context
# label and help button
@@ -491,8 +487,14 @@ class PublisherWindow(QtWidgets.QWidget):
app.removeEventFilter(self)
def keyPressEvent(self, event):
- # Ignore escape button to close window
- if event.key() == QtCore.Qt.Key_Escape:
+ if event.key() in {
+ # Ignore escape button to close window
+ QtCore.Qt.Key_Escape,
+ # Ignore enter keyboard event which by default triggers
+ # first available button in QDialog
+ QtCore.Qt.Key_Enter,
+ QtCore.Qt.Key_Return,
+ }:
event.accept()
return
@@ -558,18 +560,6 @@ class PublisherWindow(QtWidgets.QWidget):
self._reset_on_show = False
self.reset()
- def _make_sure_on_top(self):
- """Raise window to top and activate it.
-
- This may not work for some DCCs without Qt.
- """
-
- if not self._window_is_visible:
- self.show()
-
- self.setWindowState(QtCore.Qt.WindowActive)
- self.raise_()
-
def _checks_before_save(self, explicit_save):
"""Save of changes may trigger some issues.
@@ -882,12 +872,6 @@ class PublisherWindow(QtWidgets.QWidget):
if self._is_on_create_tab():
self._go_to_publish_tab()
- def _on_instance_change(self):
- self._make_sure_on_top()
-
- def _on_plugin_change(self):
- self._make_sure_on_top()
-
def _on_publish_validated_change(self, event):
if event["value"]:
self._validate_btn.setEnabled(False)
@@ -898,7 +882,6 @@ class PublisherWindow(QtWidgets.QWidget):
self._comment_input.setText("")
def _on_publish_stop(self):
- self._make_sure_on_top()
self._set_publish_overlay_visibility(False)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
@@ -1012,6 +995,18 @@ class PublisherWindow(QtWidgets.QWidget):
event["title"], new_failed_info, "Convertor:"
)
+ def _on_action_error(self, event):
+ self.add_error_message_dialog(
+ event["title"],
+ [{
+ "message": event["message"],
+ "traceback": event["traceback"],
+ "label": event["label"],
+ "identifier": event["identifier"]
+ }],
+ "Action:"
+ )
+
def _update_create_overlay_size(self):
metrics = self._create_overlay_button.fontMetrics()
height = int(metrics.height())
diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py
index 150e369678..695f47b4d4 100644
--- a/openpype/tools/sceneinventory/switch_dialog.py
+++ b/openpype/tools/sceneinventory/switch_dialog.py
@@ -1299,15 +1299,21 @@ class SwitchAssetDialog(QtWidgets.QDialog):
# If asset or subset are selected for switching, we use latest
# version else we try to keep the current container version.
+ version_name = None
if (
- selected_asset not in (None, container_asset_name)
- or selected_subset not in (None, container_subset_name)
+ selected_asset in (None, container_asset_name)
+ and selected_subset in (None, container_subset_name)
):
- version_name = max(version_docs_by_name)
- else:
- version_name = container_version["name"]
+ version_name = container_version.get("name")
+
+ version_doc = None
+ if version_name is not None:
+ version_doc = version_docs_by_name.get(version_name)
+
+ if version_doc is None:
+ version_name = max(version_docs_by_name)
+ version_doc = version_docs_by_name[version_name]
- version_doc = version_docs_by_name[version_name]
version_id = version_doc["_id"]
repres_docs_by_name = repre_docs_by_parent_id_by_name[
version_id
diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py
index 50d50f467a..74702a2a10 100644
--- a/openpype/tools/utils/__init__.py
+++ b/openpype/tools/utils/__init__.py
@@ -32,6 +32,7 @@ from .lib import (
set_style_property,
DynamicQThread,
qt_app_context,
+ get_qt_app,
get_openpype_qt_app,
get_asset_icon,
get_asset_icon_by_name,
diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py
index 365caaafd9..c7f92dd26e 100644
--- a/openpype/tools/utils/lib.py
+++ b/openpype/tools/utils/lib.py
@@ -154,11 +154,15 @@ def qt_app_context():
yield app
-def get_openpype_qt_app():
- """Main Qt application initialized for OpenPype processed.
+def get_qt_app():
+ """Get Qt application.
- This function should be used only inside OpenPype process and never inside
- other processes.
+ The function initializes new Qt application if it is not already
+ initialized. It also sets some attributes to the application to
+ ensure that it will work properly on high DPI displays.
+
+ Returns:
+ QtWidgets.QApplication: Current Qt application.
"""
app = QtWidgets.QApplication.instance()
@@ -184,6 +188,17 @@ def get_openpype_qt_app():
app = QtWidgets.QApplication(sys.argv)
+ return app
+
+
+def get_openpype_qt_app():
+ """Main Qt application initialized for OpenPype processed.
+
+ This function should be used only inside OpenPype process and never inside
+ other processes.
+ """
+
+ app = get_qt_app()
app.setWindowIcon(QtGui.QIcon(get_app_icon_path()))
return app
diff --git a/openpype/version.py b/openpype/version.py
index 6cbe5ba6cd..d105b0169e 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.18.6-nightly.2"
+__version__ = "3.18.7-nightly.2"
diff --git a/pyproject.toml b/pyproject.toml
index 24172aa77f..453833aae2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
-version = "3.18.5" # OpenPype
+version = "3.18.6" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team "]
license = "MIT License"
diff --git a/server_addon/aftereffects/server/settings/creator_plugins.py b/server_addon/aftereffects/server/settings/creator_plugins.py
index 988a036589..5d4ba30cd0 100644
--- a/server_addon/aftereffects/server/settings/creator_plugins.py
+++ b/server_addon/aftereffects/server/settings/creator_plugins.py
@@ -7,6 +7,8 @@ class CreateRenderPlugin(BaseSettingsModel):
default_factory=list,
title="Default Variants"
)
+ force_setting_values: bool = SettingsField(
+ True, title="Force resolution and duration values from Asset")
class AfterEffectsCreatorPlugins(BaseSettingsModel):
diff --git a/server_addon/aftereffects/server/version.py b/server_addon/aftereffects/server/version.py
index df0c92f1e2..e57ad00718 100644
--- a/server_addon/aftereffects/server/version.py
+++ b/server_addon/aftereffects/server/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
-__version__ = "0.1.2"
+__version__ = "0.1.3"
diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json
index b0b12b2003..82dfd3b8d3 100644
--- a/server_addon/applications/server/applications.json
+++ b/server_addon/applications/server/applications.json
@@ -1175,30 +1175,6 @@
}
]
},
- "djvview": {
- "enabled": true,
- "label": "DJV View",
- "icon": "{}/app_icons/djvView.png",
- "host_name": "",
- "environment": "{}",
- "variants": [
- {
- "name": "1-1",
- "label": "1.1",
- "executables": {
- "windows": [],
- "darwin": [],
- "linux": []
- },
- "arguments": {
- "windows": [],
- "darwin": [],
- "linux": []
- },
- "environment": "{}"
- }
- ]
- },
"wrap": {
"enabled": true,
"label": "Wrap",
diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py
index b157ce9e40..a913db16da 100644
--- a/server_addon/fusion/server/settings.py
+++ b/server_addon/fusion/server/settings.py
@@ -57,9 +57,9 @@ class CreateSaverPluginModel(BaseSettingsModel):
enum_resolver=_create_saver_instance_attributes_enum,
title="Instance attributes"
)
- output_formats: list[str] = SettingsField(
- default_factory=list,
- title="Output formats"
+ image_format: str = SettingsField(
+ enum_resolver=_image_format_enum,
+ title="Output Image Format"
)
@@ -90,6 +90,8 @@ class CreateImageSaverModel(CreateSaverPluginModel):
0,
title="Default rendered frame"
)
+
+
class CreatPluginsModel(BaseSettingsModel):
CreateSaver: CreateSaverModel = SettingsField(
default_factory=CreateSaverModel,
diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py
index ae7362549b..bbab0242f6 100644
--- a/server_addon/fusion/server/version.py
+++ b/server_addon/fusion/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.3"
+__version__ = "0.1.4"