mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-27 06:12:19 +01:00
Merge remote-tracking branch 'origin/bugfix/OP-4183_Crashed-save-cause-crash-of-UI' into enhancement/OP-3075_houdini-new-publisher
This commit is contained in:
commit
d5afbcd005
9 changed files with 600 additions and 169 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import sys
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
import collections
|
||||
import inspect
|
||||
from uuid import uuid4
|
||||
|
|
@ -22,6 +24,7 @@ from .creator_plugins import (
|
|||
Creator,
|
||||
AutoCreator,
|
||||
discover_creator_plugins,
|
||||
CreatorError,
|
||||
)
|
||||
|
||||
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
|
||||
|
|
@ -67,6 +70,77 @@ class HostMissRequiredMethod(Exception):
|
|||
super(HostMissRequiredMethod, self).__init__(msg)
|
||||
|
||||
|
||||
class CreatorsOperationFailed(Exception):
|
||||
"""Raised when a creator process crashes in 'CreateContext'.
|
||||
|
||||
The exception contains information about the creator and error. The data
|
||||
are prepared using 'prepare_failed_creator_operation_info' and can be
|
||||
serialized using json.
|
||||
|
||||
Usage is for UI purposes which may not have access to exceptions directly
|
||||
and would not have ability to catch exceptions 'per creator'.
|
||||
|
||||
Args:
|
||||
msg (str): General error message.
|
||||
failed_info (list[dict[str, Any]]): List of failed creators with
|
||||
exception message and optionally formatted traceback.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, failed_info):
|
||||
super(CreatorsOperationFailed, self).__init__(msg)
|
||||
self.failed_info = failed_info
|
||||
|
||||
|
||||
class CreatorsCollectionFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to collect instances"
|
||||
super(CreatorsCollectionFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsSaveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed update instance changes"
|
||||
super(CreatorsSaveFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsRemoveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to remove instances"
|
||||
super(CreatorsRemoveFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
class CreatorsCreateFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Faled to create instances"
|
||||
super(CreatorsCreateFailed, self).__init__(
|
||||
msg, failed_info
|
||||
)
|
||||
|
||||
|
||||
def prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback=True
|
||||
):
|
||||
formatted_traceback = None
|
||||
exc_type, exc_value, exc_traceback = exc_info
|
||||
if add_traceback:
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
|
||||
return {
|
||||
"creator_identifier": identifier,
|
||||
"creator_label": label,
|
||||
"message": str(exc_value),
|
||||
"traceback": formatted_traceback
|
||||
}
|
||||
|
||||
|
||||
class InstanceMember:
|
||||
"""Representation of instance member.
|
||||
|
||||
|
|
@ -1212,7 +1286,65 @@ class CreateContext:
|
|||
with self.bulk_instances_collection():
|
||||
self._bulk_instances_to_process.append(instance)
|
||||
|
||||
def create(self, identifier, *args, **kwargs):
|
||||
"""Wrapper for creators to trigger created.
|
||||
|
||||
Different types of creators may expect different arguments thus the
|
||||
hints for args are blind.
|
||||
|
||||
Args:
|
||||
identifier (str): Creator's identifier.
|
||||
*args (Tuple[Any]): Arguments for create method.
|
||||
**kwargs (Dict[Any, Any]): Keyword argument for create method.
|
||||
"""
|
||||
|
||||
error_message = "Failed to run Creator with identifier \"{}\". {}"
|
||||
creator = self.creators.get(identifier)
|
||||
label = getattr(creator, "label", None)
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
# Fake CreatorError (Could be maybe specific exception?)
|
||||
if creator is None:
|
||||
raise CreatorError(
|
||||
"Creator {} was not found".format(identifier)
|
||||
)
|
||||
|
||||
creator.create(*args, **kwargs)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
raise CreatorsCreateFailed([
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
])
|
||||
|
||||
def creator_removed_instance(self, instance):
|
||||
"""When creator removes instance context should be acknowledged.
|
||||
|
||||
If creator removes instance conext should know about it to avoid
|
||||
possible issues in the session.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): Object of instance which was removed
|
||||
from scene metadata.
|
||||
"""
|
||||
|
||||
self._instances_by_id.pop(instance.id, None)
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -1247,24 +1379,81 @@ class CreateContext:
|
|||
self._instances_by_id = {}
|
||||
|
||||
# Collect instances
|
||||
error_message = "Collection of instances for creator {} failed. {}"
|
||||
failed_info = []
|
||||
for creator in self.creators.values():
|
||||
creator.collect_instances()
|
||||
label = creator.label
|
||||
identifier = creator.identifier
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.collect_instances()
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsCollectionFailed(failed_info)
|
||||
|
||||
def execute_autocreators(self):
|
||||
"""Execute discovered AutoCreator plugins.
|
||||
|
||||
Reset instances if any autocreator executed properly.
|
||||
"""
|
||||
|
||||
error_message = "Failed to run AutoCreator with identifier \"{}\". {}"
|
||||
failed_info = []
|
||||
for identifier, creator in self.autocreators.items():
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
try:
|
||||
creator.create()
|
||||
|
||||
except Exception:
|
||||
# TODO raise report exception if any crashed
|
||||
msg = (
|
||||
"Failed to run AutoCreator with identifier \"{}\" ({})."
|
||||
).format(identifier, inspect.getfile(creator.__class__))
|
||||
self.log.warning(msg, exc_info=True)
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
# Use bare except because some hosts raise their exceptions that
|
||||
# do not inherit from python's `BaseException`
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsCreateFailed(failed_info)
|
||||
|
||||
def validate_instances_context(self, instances=None):
|
||||
"""Validate 'asset' and 'task' instance context."""
|
||||
|
|
@ -1341,17 +1530,48 @@ class CreateContext:
|
|||
identifier = instance.creator_identifier
|
||||
instances_by_identifier[identifier].append(instance)
|
||||
|
||||
for identifier, cretor_instances in instances_by_identifier.items():
|
||||
error_message = "Instances update of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
for identifier, creator_instances in instances_by_identifier.items():
|
||||
update_list = []
|
||||
for instance in cretor_instances:
|
||||
for instance in creator_instances:
|
||||
instance_changes = instance.changes()
|
||||
if instance_changes:
|
||||
update_list.append(UpdateData(instance, instance_changes))
|
||||
|
||||
creator = self.creators[identifier]
|
||||
if update_list:
|
||||
if not update_list:
|
||||
continue
|
||||
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.update_instances(update_list)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(error_message.format(identifier, exc_info[1]))
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""), exc_info=True)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsSaveFailed(failed_info)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Remove instances from context.
|
||||
|
||||
|
|
@ -1359,14 +1579,48 @@ class CreateContext:
|
|||
instances(list<CreatedInstance>): Instances that should be removed
|
||||
from context.
|
||||
"""
|
||||
|
||||
instances_by_identifier = collections.defaultdict(list)
|
||||
for instance in instances:
|
||||
identifier = instance.creator_identifier
|
||||
instances_by_identifier[identifier].append(instance)
|
||||
|
||||
error_message = "Instances removement of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
for identifier, creator_instances in instances_by_identifier.items():
|
||||
creator = self.creators.get(identifier)
|
||||
creator.remove_instances(creator_instances)
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.remove_instances(creator_instances)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, exc_info[1])
|
||||
)
|
||||
|
||||
except:
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsRemoveFailed(failed_info)
|
||||
|
||||
def _get_publish_plugins_with_attr_for_family(self, family):
|
||||
"""Publish plugin attributes for passed family.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ from openpype.pipeline.create import (
|
|||
HiddenCreator,
|
||||
Creator,
|
||||
)
|
||||
from openpype.pipeline.create.context import (
|
||||
CreatorsOperationFailed,
|
||||
)
|
||||
|
||||
# Define constant for plugin orders offset
|
||||
PLUGIN_ORDER_OFFSET = 0.5
|
||||
|
|
@ -299,8 +302,11 @@ class PublishReport:
|
|||
}
|
||||
|
||||
def _extract_context_data(self, context):
|
||||
context_label = "Context"
|
||||
if context is not None:
|
||||
context_label = context.data.get("label")
|
||||
return {
|
||||
"label": context.data.get("label")
|
||||
"label": context_label
|
||||
}
|
||||
|
||||
def _extract_instance_data(self, instance, exists):
|
||||
|
|
@ -1101,6 +1107,8 @@ class AbstractPublisherController(object):
|
|||
options (Dict[str, Any]): Data from pre-create attributes.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes in create context."""
|
||||
|
||||
|
|
@ -1662,11 +1670,8 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
def reset(self):
|
||||
"""Reset everything related to creation and publishing."""
|
||||
# Stop publishing
|
||||
self.stop_publish()
|
||||
|
||||
self.save_changes()
|
||||
|
||||
self.host_is_valid = self._create_context.host_is_valid
|
||||
|
||||
self._create_context.reset_preparation()
|
||||
|
|
@ -1715,8 +1720,28 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
self._create_context.reset_context_data()
|
||||
with self._create_context.bulk_instances_collection():
|
||||
self._create_context.reset_instances()
|
||||
self._create_context.execute_autocreators()
|
||||
try:
|
||||
self._create_context.reset_instances()
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.collection.failed",
|
||||
{
|
||||
"title": "Instance collection failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
self._create_context.execute_autocreators()
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "AutoCreation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._resetting_instances = False
|
||||
|
||||
|
|
@ -1845,16 +1870,42 @@ class PublisherController(BasePublisherController):
|
|||
self, creator_identifier, subset_name, instance_data, options
|
||||
):
|
||||
"""Trigger creation and refresh of instances in UI."""
|
||||
creator = self._creators[creator_identifier]
|
||||
creator.create(subset_name, instance_data, options)
|
||||
|
||||
success = True
|
||||
try:
|
||||
self._create_context.create(
|
||||
creator_identifier, subset_name, instance_data, options
|
||||
)
|
||||
except CreatorsOperationFailed as exc:
|
||||
success = False
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "Creation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._on_create_instance_change()
|
||||
return success
|
||||
|
||||
def save_changes(self):
|
||||
"""Save changes happened during creation."""
|
||||
if self._create_context.host_is_valid:
|
||||
if not self._create_context.host_is_valid:
|
||||
return
|
||||
|
||||
try:
|
||||
self._create_context.save_changes()
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.save.failed",
|
||||
{
|
||||
"title": "Instances save failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
def remove_instances(self, instance_ids):
|
||||
"""Remove instances based on instance ids.
|
||||
|
||||
|
|
@ -1876,7 +1927,16 @@ class PublisherController(BasePublisherController):
|
|||
instances_by_id[instance_id]
|
||||
for instance_id in instance_ids
|
||||
]
|
||||
self._create_context.remove_instances(instances)
|
||||
try:
|
||||
self._create_context.remove_instances(instances)
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.remove.failed",
|
||||
{
|
||||
"title": "Instance removement failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
def _on_create_instance_change(self):
|
||||
self._emit_event("instances.refresh.finished")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from openpype.pipeline.create import (
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
)
|
||||
from openpype.tools.utils import ErrorMessageBox
|
||||
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel,
|
||||
|
|
@ -35,79 +34,6 @@ class VariantInputsWidget(QtWidgets.QWidget):
|
|||
self.resized.emit()
|
||||
|
||||
|
||||
class CreateErrorMessageBox(ErrorMessageBox):
|
||||
def __init__(
|
||||
self,
|
||||
creator_label,
|
||||
subset_name,
|
||||
asset_name,
|
||||
exc_msg,
|
||||
formatted_traceback,
|
||||
parent
|
||||
):
|
||||
self._creator_label = creator_label
|
||||
self._subset_name = subset_name
|
||||
self._asset_name = asset_name
|
||||
self._exc_msg = exc_msg
|
||||
self._formatted_traceback = formatted_traceback
|
||||
super(CreateErrorMessageBox, self).__init__("Creation failed", parent)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
label_widget.setText(
|
||||
"<span style='font-size:18pt;'>Failed to create</span>"
|
||||
)
|
||||
return label_widget
|
||||
|
||||
def _get_report_data(self):
|
||||
report_message = (
|
||||
"{creator}: Failed to create Subset: \"{subset}\""
|
||||
" in Asset: \"{asset}\""
|
||||
"\n\nError: {message}"
|
||||
).format(
|
||||
creator=self._creator_label,
|
||||
subset=self._subset_name,
|
||||
asset=self._asset_name,
|
||||
message=self._exc_msg,
|
||||
)
|
||||
if self._formatted_traceback:
|
||||
report_message += "\n\n{}".format(self._formatted_traceback)
|
||||
return [report_message]
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
item_name_template = (
|
||||
"<span style='font-weight:bold;'>Creator:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Subset:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Asset:</span> {}<br>"
|
||||
)
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
|
||||
line = self._create_line()
|
||||
content_layout.addWidget(line)
|
||||
|
||||
item_name_widget = QtWidgets.QLabel(self)
|
||||
item_name_widget.setText(
|
||||
item_name_template.format(
|
||||
self._creator_label, self._subset_name, self._asset_name
|
||||
)
|
||||
)
|
||||
content_layout.addWidget(item_name_widget)
|
||||
|
||||
message_label_widget = QtWidgets.QLabel(self)
|
||||
message_label_widget.setText(
|
||||
exc_msg_template.format(self.convert_text_for_html(self._exc_msg))
|
||||
)
|
||||
content_layout.addWidget(message_label_widget)
|
||||
|
||||
if self._formatted_traceback:
|
||||
line_widget = self._create_line()
|
||||
tb_widget = self._create_traceback_widget(
|
||||
self._formatted_traceback
|
||||
)
|
||||
content_layout.addWidget(line_widget)
|
||||
content_layout.addWidget(tb_widget)
|
||||
|
||||
|
||||
# TODO add creator identifier/label to details
|
||||
class CreatorShortDescWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
|
|
@ -178,8 +104,6 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
self._prereq_available = False
|
||||
|
||||
self._message_dialog = None
|
||||
|
||||
name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS)
|
||||
self._name_pattern = name_pattern
|
||||
self._compiled_name_pattern = re.compile(name_pattern)
|
||||
|
|
@ -769,7 +693,6 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
index = indexes[0]
|
||||
creator_label = index.data(QtCore.Qt.DisplayRole)
|
||||
creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE)
|
||||
family = index.data(FAMILY_ROLE)
|
||||
variant = self.variant_input.text()
|
||||
|
|
@ -792,40 +715,13 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
"family": family
|
||||
}
|
||||
|
||||
error_msg = None
|
||||
formatted_traceback = None
|
||||
try:
|
||||
self._controller.create(
|
||||
creator_identifier,
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data
|
||||
)
|
||||
success = self._controller.create(
|
||||
creator_identifier,
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data
|
||||
)
|
||||
|
||||
except CreatorError as exc:
|
||||
error_msg = str(exc)
|
||||
|
||||
# Use bare except because some hosts raise their exceptions that
|
||||
# do not inherit from python's `BaseException`
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
error_msg = str(exc_value)
|
||||
|
||||
if error_msg is None:
|
||||
if success:
|
||||
self._set_creator(self._selected_creator)
|
||||
self._controller.emit_card_message("Creation finished...")
|
||||
else:
|
||||
box = CreateErrorMessageBox(
|
||||
creator_label,
|
||||
subset_name,
|
||||
asset_name,
|
||||
error_msg,
|
||||
formatted_traceback,
|
||||
parent=self
|
||||
)
|
||||
box.show()
|
||||
# Store dialog so is not garbage collected before is shown
|
||||
self._message_dialog = box
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
main_layout.addWidget(subset_content_widget, 1)
|
||||
|
||||
change_anim = QtCore.QVariantAnimation()
|
||||
change_anim.setStartValue(0)
|
||||
change_anim.setEndValue(self.anim_end_value)
|
||||
change_anim.setStartValue(float(0))
|
||||
change_anim.setEndValue(float(self.anim_end_value))
|
||||
change_anim.setDuration(self.anim_duration)
|
||||
change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad)
|
||||
|
||||
|
|
@ -264,9 +264,10 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
+ (self._subset_content_layout.spacing() * 2)
|
||||
)
|
||||
)
|
||||
subset_attrs_width = int(float(width) / self.anim_end_value) * value
|
||||
subset_attrs_width = int((float(width) / self.anim_end_value) * value)
|
||||
if subset_attrs_width > width:
|
||||
subset_attrs_width = width
|
||||
|
||||
create_width = width - subset_attrs_width
|
||||
|
||||
self._create_widget.setMinimumWidth(create_width)
|
||||
|
|
|
|||
|
|
@ -248,13 +248,13 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
hint = self._top_content_widget.minimumSizeHint()
|
||||
end = hint.height()
|
||||
|
||||
self._shrunk_anim.setStartValue(start)
|
||||
self._shrunk_anim.setEndValue(end)
|
||||
self._shrunk_anim.setStartValue(float(start))
|
||||
self._shrunk_anim.setEndValue(float(end))
|
||||
if not anim_is_running:
|
||||
self._shrunk_anim.start()
|
||||
|
||||
def _on_shrunk_anim(self, value):
|
||||
diff = self._top_content_widget.height() - value
|
||||
diff = self._top_content_widget.height() - int(value)
|
||||
if not self._top_content_widget.isVisible():
|
||||
diff -= self._content_layout.spacing()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import collections
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype import (
|
||||
|
|
@ -5,6 +6,7 @@ from openpype import (
|
|||
style
|
||||
)
|
||||
from openpype.tools.utils import (
|
||||
ErrorMessageBox,
|
||||
PlaceholderLineEdit,
|
||||
MessageOverlayObject,
|
||||
PixmapLabel,
|
||||
|
|
@ -222,6 +224,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
# Floating publish frame
|
||||
publish_frame = PublishFrame(controller, self.footer_border, self)
|
||||
|
||||
creators_dialog_message_timer = QtCore.QTimer()
|
||||
creators_dialog_message_timer.setInterval(100)
|
||||
creators_dialog_message_timer.timeout.connect(
|
||||
self._on_creators_message_timeout
|
||||
)
|
||||
|
||||
help_btn.clicked.connect(self._on_help_click)
|
||||
tabs_widget.tab_changed.connect(self._on_tab_change)
|
||||
overview_widget.active_changed.connect(
|
||||
|
|
@ -259,6 +267,18 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller.event_system.add_callback(
|
||||
"show.card.message", self._on_overlay_message
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.collection.failed", self._instance_collection_failed
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.save.failed", self._instance_save_failed
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.remove.failed", self._instance_remove_failed
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"instances.create.failed", self._instance_create_failed
|
||||
)
|
||||
|
||||
# Store extra header widget for TrayPublisher
|
||||
# - can be used to add additional widgets to header between context
|
||||
|
|
@ -298,10 +318,16 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._controller = controller
|
||||
|
||||
self._first_show = True
|
||||
self._reset_on_show = reset_on_show
|
||||
# This is a little bit confusing but 'reset_on_first_show' is too long
|
||||
# forin init
|
||||
self._reset_on_first_show = reset_on_show
|
||||
self._reset_on_show = True
|
||||
self._restart_timer = None
|
||||
self._publish_frame_visible = None
|
||||
|
||||
self._creators_messages_to_show = collections.deque()
|
||||
self._creators_dialog_message_timer = creators_dialog_message_timer
|
||||
|
||||
self._set_publish_visibility(False)
|
||||
|
||||
@property
|
||||
|
|
@ -314,6 +340,18 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
if not self._reset_on_show:
|
||||
return
|
||||
|
||||
self._reset_on_show = False
|
||||
# Detach showing - give OS chance to draw the window
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(1)
|
||||
timer.timeout.connect(self._on_show_restart_timer)
|
||||
self._restart_timer = timer
|
||||
timer.start()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PublisherWindow, self).resizeEvent(event)
|
||||
self._update_publish_frame_rect()
|
||||
|
|
@ -324,16 +362,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
def _on_first_show(self):
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
if not self._reset_on_show:
|
||||
return
|
||||
|
||||
# Detach showing - give OS chance to draw the window
|
||||
timer = QtCore.QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(1)
|
||||
timer.timeout.connect(self._on_show_restart_timer)
|
||||
self._restart_timer = timer
|
||||
timer.start()
|
||||
self._reset_on_show = self._reset_on_first_show
|
||||
|
||||
def _on_show_restart_timer(self):
|
||||
"""Callback for '_restart_timer' timer."""
|
||||
|
|
@ -342,9 +371,13 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self.reset()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._controller.save_changes()
|
||||
self.save_changes()
|
||||
self._reset_on_show = True
|
||||
super(PublisherWindow, self).closeEvent(event)
|
||||
|
||||
def save_changes(self):
|
||||
self._controller.save_changes()
|
||||
|
||||
def reset(self):
|
||||
self._controller.reset()
|
||||
|
||||
|
|
@ -436,7 +469,8 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._update_publish_frame_rect()
|
||||
|
||||
def _on_reset_clicked(self):
|
||||
self._controller.reset()
|
||||
self.save_changes()
|
||||
self.reset()
|
||||
|
||||
def _on_stop_clicked(self):
|
||||
self._controller.stop_publish()
|
||||
|
|
@ -472,7 +506,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._update_publish_details_widget()
|
||||
if (
|
||||
not self._tabs_widget.is_current_tab("create")
|
||||
or not self._tabs_widget.is_current_tab("publish")
|
||||
and not self._tabs_widget.is_current_tab("publish")
|
||||
):
|
||||
self._tabs_widget.set_current_tab("publish")
|
||||
|
||||
|
|
@ -569,3 +603,129 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._publish_frame.move(
|
||||
0, window_size.height() - height
|
||||
)
|
||||
|
||||
def add_message_dialog(self, title, failed_info):
|
||||
self._creators_messages_to_show.append((title, failed_info))
|
||||
self._creators_dialog_message_timer.start()
|
||||
|
||||
def _on_creators_message_timeout(self):
|
||||
if not self._creators_messages_to_show:
|
||||
self._creators_dialog_message_timer.stop()
|
||||
return
|
||||
|
||||
item = self._creators_messages_to_show.popleft()
|
||||
title, failed_info = item
|
||||
dialog = CreatorsErrorMessageBox(title, failed_info, self)
|
||||
dialog.exec_()
|
||||
dialog.deleteLater()
|
||||
|
||||
def _instance_collection_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
def _instance_save_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
def _instance_remove_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
def _instance_create_failed(self, event):
|
||||
self.add_message_dialog(event["title"], event["failed_info"])
|
||||
|
||||
|
||||
class CreatorsErrorMessageBox(ErrorMessageBox):
|
||||
def __init__(self, error_title, failed_info, parent):
|
||||
self._failed_info = failed_info
|
||||
self._info_with_id = [
|
||||
# Id must be string when used in tab widget
|
||||
{"id": str(idx), "info": info}
|
||||
for idx, info in enumerate(failed_info)
|
||||
]
|
||||
self._widgets_by_id = {}
|
||||
self._tabs_widget = None
|
||||
self._stack_layout = None
|
||||
|
||||
super(CreatorsErrorMessageBox, self).__init__(error_title, parent)
|
||||
|
||||
layout = self.layout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
footer_layout = self._footer_widget.layout()
|
||||
footer_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
return None
|
||||
|
||||
def _get_report_data(self):
|
||||
output = []
|
||||
for info in self._failed_info:
|
||||
creator_label = info["creator_label"]
|
||||
creator_identifier = info["creator_identifier"]
|
||||
report_message = "Creator:"
|
||||
if creator_label:
|
||||
report_message += " {} ({})".format(
|
||||
creator_label, creator_identifier)
|
||||
else:
|
||||
report_message += " {}".format(creator_identifier)
|
||||
|
||||
report_message += "\n\nError: {}".format(info["message"])
|
||||
formatted_traceback = info["traceback"]
|
||||
if formatted_traceback:
|
||||
report_message += "\n\n{}".format(formatted_traceback)
|
||||
output.append(report_message)
|
||||
return output
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
tabs_widget = PublisherTabsWidget(self)
|
||||
|
||||
stack_widget = QtWidgets.QFrame(self._content_widget)
|
||||
stack_layout = QtWidgets.QStackedLayout(stack_widget)
|
||||
|
||||
first = True
|
||||
for item in self._info_with_id:
|
||||
item_id = item["id"]
|
||||
info = item["info"]
|
||||
message = info["message"]
|
||||
formatted_traceback = info["traceback"]
|
||||
creator_label = info["creator_label"]
|
||||
creator_identifier = info["creator_identifier"]
|
||||
if not creator_label:
|
||||
creator_label = creator_identifier
|
||||
|
||||
msg_widget = QtWidgets.QWidget(stack_widget)
|
||||
msg_layout = QtWidgets.QVBoxLayout(msg_widget)
|
||||
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
message_label_widget = QtWidgets.QLabel(msg_widget)
|
||||
message_label_widget.setText(
|
||||
exc_msg_template.format(self.convert_text_for_html(message))
|
||||
)
|
||||
msg_layout.addWidget(message_label_widget, 0)
|
||||
|
||||
if formatted_traceback:
|
||||
line_widget = self._create_line(msg_widget)
|
||||
tb_widget = self._create_traceback_widget(formatted_traceback)
|
||||
msg_layout.addWidget(line_widget, 0)
|
||||
msg_layout.addWidget(tb_widget, 0)
|
||||
|
||||
msg_layout.addStretch(1)
|
||||
|
||||
tabs_widget.add_tab(creator_label, item_id)
|
||||
stack_layout.addWidget(msg_widget)
|
||||
if first:
|
||||
first = False
|
||||
stack_layout.setCurrentWidget(msg_widget)
|
||||
|
||||
self._widgets_by_id[item_id] = msg_widget
|
||||
|
||||
content_layout.addWidget(tabs_widget, 0)
|
||||
content_layout.addWidget(stack_widget, 1)
|
||||
|
||||
tabs_widget.tab_changed.connect(self._on_tab_change)
|
||||
|
||||
self._tabs_widget = tabs_widget
|
||||
self._stack_layout = stack_layout
|
||||
|
||||
def _on_tab_change(self, old_identifier, identifier):
|
||||
widget = self._widgets_by_id[identifier]
|
||||
self._stack_layout.setCurrentWidget(widget)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from .widgets import (
|
|||
ExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
SeparatorWidget,
|
||||
)
|
||||
from .views import DeselectableTreeView
|
||||
from .error_dialog import ErrorMessageBox
|
||||
|
|
@ -37,6 +38,7 @@ __all__ = (
|
|||
"ExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
"SeparatorWidget",
|
||||
|
||||
"DeselectableTreeView",
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from .widgets import ClickableFrame, ExpandBtn
|
||||
from .widgets import ClickableFrame, ExpandBtn, SeparatorWidget
|
||||
|
||||
|
||||
def convert_text_for_html(text):
|
||||
def escape_text_for_html(text):
|
||||
return (
|
||||
text
|
||||
.replace("<", "<")
|
||||
|
|
@ -19,7 +19,7 @@ class TracebackWidget(QtWidgets.QWidget):
|
|||
|
||||
# Modify text to match html
|
||||
# - add more replacements when needed
|
||||
tb_text = convert_text_for_html(tb_text)
|
||||
tb_text = escape_text_for_html(tb_text)
|
||||
expand_btn = ExpandBtn(self)
|
||||
|
||||
clickable_frame = ClickableFrame(self)
|
||||
|
|
@ -85,17 +85,20 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
copy_report_btn = QtWidgets.QPushButton("Copy report", self)
|
||||
ok_btn = QtWidgets.QPushButton("OK", self)
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout()
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addWidget(copy_report_btn, 0)
|
||||
footer_layout.addStretch(1)
|
||||
footer_layout.addWidget(ok_btn, 0)
|
||||
|
||||
bottom_line = self._create_line()
|
||||
body_layout = QtWidgets.QVBoxLayout(self)
|
||||
body_layout.addWidget(top_widget, 0)
|
||||
body_layout.addWidget(content_scroll, 1)
|
||||
body_layout.addWidget(bottom_line, 0)
|
||||
body_layout.addLayout(footer_layout, 0)
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
if top_widget is not None:
|
||||
main_layout.addWidget(top_widget, 0)
|
||||
main_layout.addWidget(content_scroll, 1)
|
||||
main_layout.addWidget(bottom_line, 0)
|
||||
main_layout.addWidget(footer_widget, 0)
|
||||
|
||||
copy_report_btn.clicked.connect(self._on_copy_report)
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
|
|
@ -106,11 +109,13 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
if not report_data:
|
||||
copy_report_btn.setVisible(False)
|
||||
|
||||
self._content_scroll = content_scroll
|
||||
self._footer_widget = footer_widget
|
||||
self._report_data = report_data
|
||||
|
||||
@staticmethod
|
||||
def convert_text_for_html(text):
|
||||
return convert_text_for_html(text)
|
||||
return escape_text_for_html(text)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
|
|
@ -131,7 +136,8 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
self.close()
|
||||
|
||||
def _on_copy_report(self):
|
||||
report_text = (10 * "*").join(self._report_data)
|
||||
sep = "\n{}\n".format(10 * "*")
|
||||
report_text = sep.join(self._report_data)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(report_text)
|
||||
|
|
@ -139,12 +145,10 @@ class ErrorMessageBox(QtWidgets.QDialog):
|
|||
mime_data
|
||||
)
|
||||
|
||||
def _create_line(self):
|
||||
line = QtWidgets.QFrame(self)
|
||||
line.setObjectName("Separator")
|
||||
line.setMinimumHeight(2)
|
||||
line.setMaximumHeight(2)
|
||||
return line
|
||||
def _create_line(self, parent=None):
|
||||
if parent is None:
|
||||
parent = self
|
||||
return SeparatorWidget(2, parent=parent)
|
||||
|
||||
def _create_traceback_widget(self, traceback_text, parent=None):
|
||||
if parent is None:
|
||||
|
|
|
|||
|
|
@ -448,3 +448,57 @@ class OptionDialog(QtWidgets.QDialog):
|
|||
|
||||
def parse(self):
|
||||
return self._options.copy()
|
||||
|
||||
|
||||
class SeparatorWidget(QtWidgets.QFrame):
|
||||
"""Prepared widget that can be used as separator with predefined color.
|
||||
|
||||
Args:
|
||||
size (int): Size of separator (width or height).
|
||||
orientation (Qt.Horizontal|Qt.Vertical): Orintation of widget.
|
||||
parent (QtWidgets.QWidget): Parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, size=2, orientation=QtCore.Qt.Horizontal, parent=None):
|
||||
super(SeparatorWidget, self).__init__(parent)
|
||||
|
||||
self.setObjectName("Separator")
|
||||
|
||||
maximum_width = self.maximumWidth()
|
||||
maximum_height = self.maximumHeight()
|
||||
|
||||
self._size = None
|
||||
self._orientation = orientation
|
||||
self._maximum_width = maximum_width
|
||||
self._maximum_height = maximum_height
|
||||
self.set_size(size)
|
||||
|
||||
def set_size(self, size):
|
||||
if size == self._size:
|
||||
return
|
||||
if self._orientation == QtCore.Qt.Vertical:
|
||||
self.setMinimumWidth(size)
|
||||
self.setMaximumWidth(size)
|
||||
else:
|
||||
self.setMinimumHeight(size)
|
||||
self.setMaximumHeight(size)
|
||||
|
||||
self._size = size
|
||||
|
||||
def set_orientation(self, orientation):
|
||||
if self._orientation == orientation:
|
||||
return
|
||||
|
||||
# Reset min/max sizes in opossite direction
|
||||
if self._orientation == QtCore.Qt.Vertical:
|
||||
self.setMinimumHeight(0)
|
||||
self.setMaximumHeight(self._maximum_height)
|
||||
else:
|
||||
self.setMinimumWidth(0)
|
||||
self.setMaximumWidth(self._maximum_width)
|
||||
|
||||
self._orientation = orientation
|
||||
|
||||
size = self._size
|
||||
self._size = None
|
||||
self.set_size(size)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue