diff --git a/client/ayon_core/hooks/pre_add_last_workfile_arg.py b/client/ayon_core/hooks/pre_add_last_workfile_arg.py index daea8c5502..bafc075888 100644 --- a/client/ayon_core/hooks/pre_add_last_workfile_arg.py +++ b/client/ayon_core/hooks/pre_add_last_workfile_arg.py @@ -30,7 +30,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "aftereffects", "wrap", "openrv", - "cinema4d" + "cinema4d", + "silhouette", } launch_types = {LaunchTypes.local} diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 78fc8c78de..9f5c8c7339 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -21,7 +21,8 @@ class OCIOEnvHook(PreLaunchHook): "hiero", "resolve", "openrv", - "cinema4d" + "cinema4d", + "silhouette", } launch_types = set() diff --git a/client/ayon_core/pipeline/create/legacy_create.py b/client/ayon_core/pipeline/create/legacy_create.py index ec9b23ac62..f6427d9bd1 100644 --- a/client/ayon_core/pipeline/create/legacy_create.py +++ b/client/ayon_core/pipeline/create/legacy_create.py @@ -9,7 +9,7 @@ import os import logging import collections -from ayon_core.pipeline.constants import AVALON_INSTANCE_ID +from ayon_core.pipeline.constants import AYON_INSTANCE_ID from .product_name import get_product_name @@ -34,7 +34,7 @@ class LegacyCreator: # Default data self.data = collections.OrderedDict() # TODO use 'AYON_INSTANCE_ID' when all hosts support it - self.data["id"] = AVALON_INSTANCE_ID + self.data["id"] = AYON_INSTANCE_ID self.data["productType"] = self.product_type self.data["folderPath"] = folder_path self.data["productName"] = name diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index a1a4d5f8ef..17bb85b720 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -1,6 +1,7 @@ import copy import collections from uuid import uuid4 +import typing from typing import Optional, Dict, List, Any from ayon_core.lib.attribute_definitions import ( @@ -17,6 +18,9 @@ from ayon_core.pipeline import ( from .exceptions import ImmutableKeyError from .changes import TrackChangesItem +if typing.TYPE_CHECKING: + from .creator_plugins import BaseCreator + class ConvertorItem: """Item representing convertor plugin. @@ -444,10 +448,11 @@ class CreatedInstance: def __init__( self, - product_type, - product_name, - data, - creator, + product_type: str, + product_name: str, + data: Dict[str, Any], + creator: "BaseCreator", + transient_data: Optional[Dict[str, Any]] = None, ): self._creator = creator creator_identifier = creator.identifier @@ -462,7 +467,9 @@ class CreatedInstance: self._members = [] # Data that can be used for lifetime of object - self._transient_data = {} + if transient_data is None: + transient_data = {} + self._transient_data = transient_data # Create a copy of passed data to avoid changing them on the fly data = copy.deepcopy(data or {}) @@ -492,7 +499,7 @@ class CreatedInstance: item_id = data.get("id") # TODO use only 'AYON_INSTANCE_ID' when all hosts support it if item_id not in {AYON_INSTANCE_ID, AVALON_INSTANCE_ID}: - item_id = AVALON_INSTANCE_ID + item_id = AYON_INSTANCE_ID self._data["id"] = item_id self._data["productType"] = product_type self._data["productName"] = product_name @@ -787,16 +794,26 @@ class CreatedInstance: self._create_context.instance_create_attr_defs_changed(self.id) @classmethod - def from_existing(cls, instance_data, creator): + def from_existing( + cls, + instance_data: Dict[str, Any], + creator: "BaseCreator", + transient_data: Optional[Dict[str, Any]] = None, + ) -> "CreatedInstance": """Convert instance data from workfile to CreatedInstance. Args: instance_data (Dict[str, Any]): Data in a structure ready for 'CreatedInstance' object. creator (BaseCreator): Creator plugin which is creating the - instance of for which the instance belong. - """ + instance of for which the instance belongs. + transient_data (Optional[dict[str, Any]]): Instance transient + data. + Returns: + CreatedInstance: Instance object. + + """ instance_data = copy.deepcopy(instance_data) product_type = instance_data.get("productType") @@ -809,7 +826,11 @@ class CreatedInstance: product_name = instance_data.get("subset") return cls( - product_type, product_name, instance_data, creator + product_type, + product_name, + instance_data, + creator, + transient_data=transient_data, ) def attribute_value_changed(self, key, changes): diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index f52998cef3..4f9e84aee0 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -37,7 +37,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): label = "Validate File Saved" order = pyblish.api.ValidatorOrder - 0.1 hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter", - "cinema4d"] + "cinema4d", "silhouette"] actions = [SaveByVersionUpAction, ShowWorkfilesAction] def process(self, context): diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index a5e54453cc..0e19702d53 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -1171,6 +1171,8 @@ ValidationArtistMessage QLabel { #PublishLogMessage { font-family: "Noto Sans Mono"; + border: none; + padding: 0; } #PublishInstanceLogsLabel { diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index b7afcf470a..1e46e7e52c 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -1117,6 +1117,57 @@ class LogIconFrame(QtWidgets.QFrame): painter.end() +class LogItemMessage(QtWidgets.QTextEdit): + def __init__(self, msg, parent): + super().__init__(parent) + + # Set as plain text to propagate new line characters + self.setPlainText(msg) + + self.setObjectName("PublishLogMessage") + self.setReadOnly(True) + self.setFrameStyle(QtWidgets.QFrame.NoFrame) + self.setLineWidth(0) + self.setMidLineWidth(0) + pal = self.palette() + pal.setColor(QtGui.QPalette.Base, QtCore.Qt.transparent) + self.setPalette(pal) + self.setContentsMargins(0, 0, 0, 0) + viewport = self.viewport() + viewport.setContentsMargins(0, 0, 0, 0) + + self.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction) + self.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + self.setLineWrapMode(QtWidgets.QTextEdit.WidgetWidth) + self.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere + ) + self.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, + QtWidgets.QSizePolicy.Maximum + ) + document = self.document() + document.documentLayout().documentSizeChanged.connect( + self._adjust_minimum_size + ) + document.setDocumentMargin(0.0) + self._height = None + + def _adjust_minimum_size(self, size): + self._height = size.height() + (2 * self.frameWidth()) + self.updateGeometry() + + def sizeHint(self): + size = super().sizeHint() + if self._height is not None: + size.setHeight(self._height) + return size + + def minimumSizeHint(self): + return self.sizeHint() + + class LogItemWidget(QtWidgets.QWidget): log_level_to_flag = { 10: LOG_DEBUG_VISIBLE, @@ -1132,12 +1183,7 @@ class LogItemWidget(QtWidgets.QWidget): type_flag, level_n = self._get_log_info(log) icon_label = LogIconFrame( self, log["type"], level_n, log.get("is_validation_error")) - message_label = QtWidgets.QLabel(log["msg"].rstrip(), self) - message_label.setObjectName("PublishLogMessage") - message_label.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction) - message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) - message_label.setWordWrap(True) + message_label = LogItemMessage(log["msg"].rstrip(), self) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -1290,6 +1336,7 @@ class InstanceLogsWidget(QtWidgets.QWidget): label_widget = QtWidgets.QLabel(instance.label, self) label_widget.setObjectName("PublishInstanceLogsLabel") + label_widget.setWordWrap(True) logs_grid = LogsWithIconsView(instance.logs, self) layout = QtWidgets.QVBoxLayout(self) @@ -1329,9 +1376,11 @@ class InstancesLogsView(QtWidgets.QFrame): content_wrap_widget = QtWidgets.QWidget(scroll_area) content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + content_wrap_widget.setMinimumWidth(80) content_widget = QtWidgets.QWidget(content_wrap_widget) content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(8, 8, 8, 8) content_layout.setSpacing(15) diff --git a/client/ayon_core/tools/tray/webserver/server.py b/client/ayon_core/tools/tray/webserver/server.py index d2a9b0fc6b..70d1fc8c0f 100644 --- a/client/ayon_core/tools/tray/webserver/server.py +++ b/client/ayon_core/tools/tray/webserver/server.py @@ -28,7 +28,7 @@ def find_free_port( exclude_ports (list, tuple, set): List of ports that won't be checked form entered range. host (str): Host where will check for free ports. Set to - "localhost" by default. + "127.0.0.1" by default. """ if port_from is None: port_from = 8079 @@ -42,7 +42,7 @@ def find_free_port( # Default host is localhost but it is possible to look for other hosts if host is None: - host = "localhost" + host = "127.0.0.1" found_port = None while True: @@ -78,7 +78,7 @@ class WebServerManager: self._log = None self.port = port or 8079 - self.host = host or "localhost" + self.host = host or "127.0.0.1" self.on_stop_callbacks = [] diff --git a/client/ayon_core/tools/utils/views.py b/client/ayon_core/tools/utils/views.py index b501f1ff11..d8ae94bf0c 100644 --- a/client/ayon_core/tools/utils/views.py +++ b/client/ayon_core/tools/utils/views.py @@ -1,7 +1,6 @@ -from ayon_core.resources import get_image_path from ayon_core.tools.flickcharm import FlickCharm -from qtpy import QtWidgets, QtCore, QtGui, QtSvg +from qtpy import QtWidgets, QtCore, QtGui class DeselectableTreeView(QtWidgets.QTreeView): @@ -19,48 +18,6 @@ class DeselectableTreeView(QtWidgets.QTreeView): QtWidgets.QTreeView.mousePressEvent(self, event) -class TreeViewSpinner(QtWidgets.QTreeView): - size = 160 - - def __init__(self, parent=None): - super(TreeViewSpinner, self).__init__(parent=parent) - - loading_image_path = get_image_path("spinner-200.svg") - - self.spinner = QtSvg.QSvgRenderer(loading_image_path) - - self.is_loading = False - self.is_empty = True - - def paint_loading(self, event): - rect = event.rect() - rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) - rect.moveTo( - rect.x() + rect.width() / 2 - self.size / 2, - rect.y() + rect.height() / 2 - self.size / 2 - ) - rect.setSize(QtCore.QSizeF(self.size, self.size)) - painter = QtGui.QPainter(self.viewport()) - self.spinner.render(painter, rect) - - def paint_empty(self, event): - painter = QtGui.QPainter(self.viewport()) - rect = event.rect() - rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) - qtext_opt = QtGui.QTextOption( - QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter - ) - painter.drawText(rect, "No Data", qtext_opt) - - def paintEvent(self, event): - if self.is_loading: - self.paint_loading(event) - elif self.is_empty: - self.paint_empty(event) - else: - super(TreeViewSpinner, self).paintEvent(event) - - class TreeView(QtWidgets.QTreeView): """Ultimate TreeView with flick charm and double click signals. diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index a268a9bd0e..c621a44937 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -184,9 +184,10 @@ class WorkareaModel: return items for filename in os.listdir(workdir): + # We want to support both files and folders. e.g. Silhoutte uses + # folders as its project files. So we do not check whether it is + # a file or not. filepath = os.path.join(workdir, filename) - if not os.path.isfile(filepath): - continue ext = os.path.splitext(filename)[1].lower() if ext not in self._extensions: diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 1bf2e853cf..18e7d67f90 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1033,7 +1033,8 @@ DEFAULT_PUBLISH_VALUES = { "maya", "nuke", "photoshop", - "substancepainter" + "substancepainter", + "silhouette", ], "enabled": True, "optional": False, @@ -1053,7 +1054,8 @@ DEFAULT_PUBLISH_VALUES = { "harmony", "photoshop", "aftereffects", - "fusion" + "fusion", + "silhouette", ], "enabled": True, "optional": True,