Merge branch 'develop' into release/3.15.x

This commit is contained in:
Jakub Jezek 2022-10-26 11:37:32 +02:00
commit 6807c05c27
No known key found for this signature in database
GPG key ID: 730D7C02726179A7
14 changed files with 1087 additions and 238 deletions

28
.github/workflows/milestone_assign.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Milestone - assign to PRs
on:
pull_request_target:
types: [opened, reopened, edited]
jobs:
run_if_release:
if: startsWith(github.base_ref, 'release/')
runs-on: ubuntu-latest
steps:
- name: 'Assign Milestone [next-minor]'
if: github.event.pull_request.milestone == null
uses: zoispag/action-assign-milestone@v1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
milestone: 'next-minor'
run_if_develop:
if: ${{ github.base_ref == 'develop' }}
runs-on: ubuntu-latest
steps:
- name: 'Assign Milestone [next-patch]'
if: github.event.pull_request.milestone == null
uses: zoispag/action-assign-milestone@v1
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
milestone: 'next-patch'

62
.github/workflows/milestone_create.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: Milestone - create default
on:
milestone:
types: [closed, edited]
jobs:
generate-next-patch:
runs-on: ubuntu-latest
steps:
- name: 'Get Milestones'
uses: "WyriHaximus/github-action-get-milestones@master"
id: milestones
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
id: querymilestone
env:
MILESTONES: ${{ steps.milestones.outputs.milestones }}
MILESTONE: "next-patch"
- name: Read output
run: |
echo "${{ steps.querymilestone.outputs.number }}"
- name: 'Create `next-patch` milestone'
if: steps.querymilestone.outputs.number == ''
id: createmilestone
uses: "WyriHaximus/github-action-create-milestone@v1"
with:
title: 'next-patch'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
generate-next-minor:
runs-on: ubuntu-latest
steps:
- name: 'Get Milestones'
uses: "WyriHaximus/github-action-get-milestones@master"
id: milestones
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number')
id: querymilestone
env:
MILESTONES: ${{ steps.milestones.outputs.milestones }}
MILESTONE: "next-minor"
- name: Read output
run: |
echo "${{ steps.querymilestone.outputs.number }}"
- name: 'Create `next-minor` milestone'
if: steps.querymilestone.outputs.number == ''
id: createmilestone
uses: "WyriHaximus/github-action-create-milestone@v1"
with:
title: 'next-minor'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View file

@ -24,6 +24,7 @@ from .creator_plugins import (
Creator,
AutoCreator,
discover_creator_plugins,
discover_convertor_plugins,
CreatorError,
)
@ -70,6 +71,41 @@ class HostMissRequiredMethod(Exception):
super(HostMissRequiredMethod, self).__init__(msg)
class ConvertorsOperationFailed(Exception):
def __init__(self, msg, failed_info):
super(ConvertorsOperationFailed, self).__init__(msg)
self.failed_info = failed_info
class ConvertorsFindFailed(ConvertorsOperationFailed):
def __init__(self, failed_info):
msg = "Failed to find incompatible subsets"
super(ConvertorsFindFailed, self).__init__(
msg, failed_info
)
class ConvertorsConversionFailed(ConvertorsOperationFailed):
def __init__(self, failed_info):
msg = "Failed to convert incompatible subsets"
super(ConvertorsConversionFailed, self).__init__(
msg, failed_info
)
def prepare_failed_convertor_operation_info(identifier, exc_info):
exc_type, exc_value, exc_traceback = exc_info
formatted_traceback = "".join(traceback.format_exception(
exc_type, exc_value, exc_traceback
))
return {
"convertor_identifier": identifier,
"message": str(exc_value),
"traceback": formatted_traceback
}
class CreatorsOperationFailed(Exception):
"""Raised when a creator process crashes in 'CreateContext'.
@ -926,6 +962,37 @@ class CreatedInstance:
self[key] = new_value
class ConvertorItem(object):
"""Item representing convertor plugin.
Args:
identifier (str): Identifier of convertor.
label (str): Label which will be shown in UI.
"""
def __init__(self, identifier, label):
self._id = str(uuid4())
self.identifier = identifier
self.label = label
@property
def id(self):
return self._id
def to_data(self):
return {
"id": self.id,
"identifier": self.identifier,
"label": self.label
}
@classmethod
def from_data(cls, data):
obj = cls(data["identifier"], data["label"])
obj._id = data["id"]
return obj
class CreateContext:
"""Context of instance creation.
@ -991,6 +1058,9 @@ class CreateContext:
# Manual creators
self.manual_creators = {}
self.convertors_plugins = {}
self.convertor_items_by_id = {}
self.publish_discover_result = None
self.publish_plugins_mismatch_targets = []
self.publish_plugins = []
@ -1071,6 +1141,7 @@ class CreateContext:
with self.bulk_instances_collection():
self.reset_instances()
self.find_convertor_items()
self.execute_autocreators()
self.reset_finalization()
@ -1125,6 +1196,12 @@ class CreateContext:
Reloads creators from preregistered paths and can load publish plugins
if it's enabled on context.
"""
self._reset_publish_plugins(discover_publish_plugins)
self._reset_creator_plugins()
self._reset_convertor_plugins()
def _reset_publish_plugins(self, discover_publish_plugins):
import pyblish.logic
from openpype.pipeline import OpenPypePyblishPluginMixin
@ -1166,6 +1243,7 @@ class CreateContext:
self.publish_plugins = plugins_by_targets
self.plugins_with_defs = plugins_with_defs
def _reset_creator_plugins(self):
# Prepare settings
system_settings = get_system_settings()
project_settings = get_project_settings(self.project_name)
@ -1217,6 +1295,27 @@ class CreateContext:
self.creators = creators
def _reset_convertor_plugins(self):
convertors_plugins = {}
for convertor_class in discover_convertor_plugins():
if inspect.isabstract(convertor_class):
self.log.info(
"Skipping abstract Creator {}".format(str(convertor_class))
)
continue
convertor_identifier = convertor_class.identifier
if convertor_identifier in convertors_plugins:
self.log.warning((
"Duplicated Converter identifier. "
"Using first and skipping following"
))
continue
convertors_plugins[convertor_identifier] = convertor_class(self)
self.convertors_plugins = convertors_plugins
def reset_context_data(self):
"""Reload context data using host implementation.
@ -1346,6 +1445,14 @@ class CreateContext:
self._instances_by_id.pop(instance.id, None)
def add_convertor_item(self, convertor_identifier, label):
self.convertor_items_by_id[convertor_identifier] = ConvertorItem(
convertor_identifier, label
)
def remove_convertor_item(self, convertor_identifier):
self.convertor_items_by_id.pop(convertor_identifier, None)
@contextmanager
def bulk_instances_collection(self):
"""Validate context of instances in bulk.
@ -1413,6 +1520,37 @@ class CreateContext:
if failed_info:
raise CreatorsCollectionFailed(failed_info)
def find_convertor_items(self):
"""Go through convertor plugins to look for items to convert.
Raises:
ConvertorsFindFailed: When one or more convertors fails during
finding.
"""
self.convertor_items_by_id = {}
failed_info = []
for convertor in self.convertors_plugins.values():
try:
convertor.find_instances()
except:
failed_info.append(
prepare_failed_convertor_operation_info(
convertor.identifier, sys.exc_info()
)
)
self.log.warning(
"Failed to find instances of convertor \"{}\"".format(
convertor.identifier
),
exc_info=True
)
if failed_info:
raise ConvertorsFindFailed(failed_info)
def execute_autocreators(self):
"""Execute discovered AutoCreator plugins.
@ -1668,3 +1806,51 @@ class CreateContext:
"Accessed Collection shared data out of collection phase"
)
return self._collection_shared_data
def run_convertor(self, convertor_identifier):
"""Run convertor plugin by it's idenfitifier.
Conversion is skipped if convertor is not available.
Args:
convertor_identifier (str): Identifier of convertor.
"""
convertor = self.convertors_plugins.get(convertor_identifier)
if convertor is not None:
convertor.convert()
def run_convertors(self, convertor_identifiers):
"""Run convertor plugins by idenfitifiers.
Conversion is skipped if convertor is not available. It is recommended
to trigger reset after conversion to reload instances.
Args:
convertor_identifiers (Iterator[str]): Identifiers of convertors
to run.
Raises:
ConvertorsConversionFailed: When one or more convertors fails.
"""
failed_info = []
for convertor_identifier in convertor_identifiers:
try:
self.run_convertor(convertor_identifier)
except:
failed_info.append(
prepare_failed_convertor_operation_info(
convertor_identifier, sys.exc_info()
)
)
self.log.warning(
"Failed to convert instances of convertor \"{}\"".format(
convertor_identifier
),
exc_info=True
)
if failed_info:
raise ConvertorsConversionFailed(failed_info)

View file

@ -33,6 +33,111 @@ class CreatorError(Exception):
super(CreatorError, self).__init__(message)
@six.add_metaclass(ABCMeta)
class SubsetConvertorPlugin(object):
"""Helper for conversion of instances created using legacy creators.
Conversion from legacy creators would mean to loose legacy instances,
convert them automatically or write a script which must user run. All of
these solutions are workign but will happen without asking or user must
know about them. This plugin can be used to show legacy instances in
Publisher and give user ability to run conversion script.
Convertor logic should be very simple. Method 'find_instances' is to
look for legacy instances in scene a possibly call
pre-implemented 'add_convertor_item'.
User will have ability to trigger conversion which is executed by calling
'convert' which should call 'remove_convertor_item' when is done.
It does make sense to add only one or none legacy item to create context
for convertor as it's not possible to choose which instace are converted
and which are not.
Convertor can use 'collection_shared_data' property like creators. Also
can store any information to it's object for conversion purposes.
Args:
create_context
"""
_log = None
def __init__(self, create_context):
self._create_context = create_context
@property
def log(self):
"""Logger of the plugin.
Returns:
logging.Logger: Logger with name of the plugin.
"""
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
@abstractproperty
def identifier(self):
"""Converted identifier.
Returns:
str: Converted identifier unique for all converters in host.
"""
pass
@abstractmethod
def find_instances(self):
"""Look for legacy instances in the scene.
Should call 'add_convertor_item' if there is at least one instance to
convert.
"""
pass
@abstractmethod
def convert(self):
"""Conversion code."""
pass
@property
def create_context(self):
"""Quick access to create context."""
return self._create_context
@property
def collection_shared_data(self):
"""Access to shared data that can be used during 'find_instances'.
Retruns:
Dict[str, Any]: Shared data.
Raises:
UnavailableSharedData: When called out of collection phase.
"""
return self._create_context.collection_shared_data
def add_convertor_item(self, label):
"""Add item to CreateContext.
Args:
label (str): Label of item which will show in UI.
"""
self._create_context.add_convertor_item(self.identifier, label)
def remove_convertor_item(self):
"""Remove legacy item from create context when conversion finished."""
self._create_context.remove_convertor_item(self.identifier)
@six.add_metaclass(ABCMeta)
class BaseCreator:
"""Plugin that create and modify instance data before publishing process.
@ -469,6 +574,10 @@ def discover_creator_plugins():
return discover(BaseCreator)
def discover_convertor_plugins():
return discover(SubsetConvertorPlugin)
def discover_legacy_creator_plugins():
from openpype.lib import Logger
@ -526,6 +635,9 @@ def register_creator_plugin(plugin):
elif issubclass(plugin, LegacyCreator):
register_plugin(LegacyCreator, plugin)
elif issubclass(plugin, SubsetConvertorPlugin):
register_plugin(SubsetConvertorPlugin, plugin)
def deregister_creator_plugin(plugin):
if issubclass(plugin, BaseCreator):
@ -534,12 +646,17 @@ def deregister_creator_plugin(plugin):
elif issubclass(plugin, LegacyCreator):
deregister_plugin(LegacyCreator, plugin)
elif issubclass(plugin, SubsetConvertorPlugin):
deregister_plugin(SubsetConvertorPlugin, plugin)
def register_creator_plugin_path(path):
register_plugin_path(BaseCreator, path)
register_plugin_path(LegacyCreator, path)
register_plugin_path(SubsetConvertorPlugin, path)
def deregister_creator_plugin_path(path):
deregister_plugin_path(BaseCreator, path)
deregister_plugin_path(LegacyCreator, path)
deregister_plugin_path(SubsetConvertorPlugin, path)

View file

@ -64,7 +64,9 @@
"overlay-messages": {
"close-btn": "#D3D8DE",
"bg-success": "#458056",
"bg-success-hover": "#55a066"
"bg-success-hover": "#55a066",
"bg-error": "#AD2E2E",
"bg-error-hover": "#C93636"
},
"tab-widget": {
"bg": "#21252B",

View file

@ -688,22 +688,23 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
}
/* Messages overlay */
#OverlayMessageWidget {
OverlayMessageWidget {
border-radius: 0.2em;
background: {color:bg-buttons};
}
#OverlayMessageWidget:hover {
background: {color:bg-button-hover};
}
#OverlayMessageWidget {
background: {color:overlay-messages:bg-success};
}
#OverlayMessageWidget:hover {
OverlayMessageWidget:hover {
background: {color:overlay-messages:bg-success-hover};
}
#OverlayMessageWidget QWidget {
OverlayMessageWidget[type="error"] {
background: {color:overlay-messages:bg-error};
}
OverlayMessageWidget[type="error"]:hover {
background: {color:overlay-messages:bg-error-hover};
}
OverlayMessageWidget QWidget {
background: transparent;
}

View file

@ -3,6 +3,10 @@ from Qt import QtCore
# ID of context item in instance view
CONTEXT_ID = "context"
CONTEXT_LABEL = "Options"
# Not showed anywhere - used as identifier
CONTEXT_GROUP = "__ContextGroup__"
CONVERTOR_ITEM_GROUP = "Incompatible subsets"
# Allowed symbols for subset name (and variant)
# - characters, numbers, unsercore and dash
@ -17,6 +21,8 @@ SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2
IS_GROUP_ROLE = QtCore.Qt.UserRole + 3
CREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4
FAMILY_ROLE = QtCore.Qt.UserRole + 5
GROUP_ROLE = QtCore.Qt.UserRole + 6
CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 7
__all__ = (

View file

@ -33,12 +33,18 @@ from openpype.pipeline.create import (
)
from openpype.pipeline.create.context import (
CreatorsOperationFailed,
ConvertorsOperationFailed,
)
# Define constant for plugin orders offset
PLUGIN_ORDER_OFFSET = 0.5
class CardMessageTypes:
standard = None
error = "error"
class MainThreadItem:
"""Callback with args and kwargs."""
@ -1242,6 +1248,14 @@ class AbstractPublisherController(object):
pass
@abstractproperty
def convertor_items(self):
pass
@abstractmethod
def trigger_convertor_items(self, convertor_identifiers):
pass
@abstractmethod
def set_comment(self, comment):
"""Set comment on pyblish context.
@ -1255,7 +1269,9 @@ class AbstractPublisherController(object):
pass
@abstractmethod
def emit_card_message(self, message):
def emit_card_message(
self, message, message_type=CardMessageTypes.standard
):
"""Emit a card message which can have a lifetime.
This is for UI purposes. Method can be extended to more arguments
@ -1606,6 +1622,10 @@ class PublisherController(BasePublisherController):
"""Current instances in create context."""
return self._create_context.instances_by_id
@property
def convertor_items(self):
return self._create_context.convertor_items_by_id
@property
def _creators(self):
"""All creators loaded in create context."""
@ -1731,6 +1751,17 @@ class PublisherController(BasePublisherController):
}
)
try:
self._create_context.find_convertor_items()
except ConvertorsOperationFailed as exc:
self._emit_event(
"convertors.find.failed",
{
"title": "Collection of unsupported subset failed",
"failed_info": exc.failed_info
}
)
try:
self._create_context.execute_autocreators()
@ -1747,8 +1778,16 @@ class PublisherController(BasePublisherController):
self._on_create_instance_change()
def emit_card_message(self, message):
self._emit_event("show.card.message", {"message": message})
def emit_card_message(
self, message, message_type=CardMessageTypes.standard
):
self._emit_event(
"show.card.message",
{
"message": message,
"message_type": message_type
}
)
def get_creator_attribute_definitions(self, instances):
"""Collect creator attribute definitions for multuple instances.
@ -1866,6 +1905,30 @@ class PublisherController(BasePublisherController):
variant, task_name, asset_doc, project_name, instance=instance
)
def trigger_convertor_items(self, convertor_identifiers):
self.save_changes()
success = True
try:
self._create_context.run_convertors(convertor_identifiers)
except ConvertorsOperationFailed as exc:
success = False
self._emit_event(
"convertors.convert.failed",
{
"title": "Conversion failed",
"failed_info": exc.failed_info
}
)
if success:
self.emit_card_message("Conversion finished")
else:
self.emit_card_message("Conversion failed", CardMessageTypes.error)
self.reset()
def create(
self, creator_identifier, subset_name, instance_data, options
):
@ -1912,7 +1975,6 @@ class PublisherController(BasePublisherController):
Args:
instance_ids (List[str]): List of instance ids to remove.
"""
# TODO expect instance ids instead of instances
# QUESTION Expect that instances are really removed? In that case save
# reset is not required and save changes too.
self.save_changes()

View file

@ -37,7 +37,9 @@ from .widgets import (
)
from ..constants import (
CONTEXT_ID,
CONTEXT_LABEL
CONTEXT_LABEL,
CONTEXT_GROUP,
CONVERTOR_ITEM_GROUP,
)
@ -57,15 +59,12 @@ class SelectionTypes:
extend_to = SelectionType("extend_to")
class GroupWidget(QtWidgets.QWidget):
"""Widget wrapping instances under group."""
class BaseGroupWidget(QtWidgets.QWidget):
selected = QtCore.Signal(str, str, SelectionType)
active_changed = QtCore.Signal()
removed_selected = QtCore.Signal()
def __init__(self, group_name, group_icons, parent):
super(GroupWidget, self).__init__(parent)
def __init__(self, group_name, parent):
super(BaseGroupWidget, self).__init__(parent)
label_widget = QtWidgets.QLabel(group_name, self)
@ -86,10 +85,9 @@ class GroupWidget(QtWidgets.QWidget):
layout.addLayout(label_layout, 0)
self._group = group_name
self._group_icons = group_icons
self._widgets_by_id = {}
self._ordered_instance_ids = []
self._ordered_item_ids = []
self._label_widget = label_widget
self._content_layout = layout
@ -104,7 +102,12 @@ class GroupWidget(QtWidgets.QWidget):
return self._group
def get_selected_instance_ids(self):
def get_widget_by_item_id(self, item_id):
"""Get instance widget by it's id."""
return self._widgets_by_id.get(item_id)
def get_selected_item_ids(self):
"""Selected instance ids.
Returns:
@ -139,13 +142,80 @@ class GroupWidget(QtWidgets.QWidget):
return [
self._widgets_by_id[instance_id]
for instance_id in self._ordered_instance_ids
for instance_id in self._ordered_item_ids
]
def get_widget_by_instance_id(self, instance_id):
"""Get instance widget by it's id."""
def _remove_all_except(self, item_ids):
item_ids = set(item_ids)
# Remove instance widgets that are not in passed instances
for item_id in tuple(self._widgets_by_id.keys()):
if item_id in item_ids:
continue
return self._widgets_by_id.get(instance_id)
widget = self._widgets_by_id.pop(item_id)
if widget.is_selected:
self.removed_selected.emit()
widget.setVisible(False)
self._content_layout.removeWidget(widget)
widget.deleteLater()
def _update_ordered_item_ids(self):
ordered_item_ids = []
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
widget = item.widget()
if widget is not None:
ordered_item_ids.append(widget.id)
self._ordered_item_ids = ordered_item_ids
def _on_widget_selection(self, instance_id, group_id, selection_type):
self.selected.emit(instance_id, group_id, selection_type)
class ConvertorItemsGroupWidget(BaseGroupWidget):
def update_items(self, items_by_id):
items_by_label = collections.defaultdict(list)
for item in items_by_id.values():
items_by_label[item.label].append(item)
# Remove instance widgets that are not in passed instances
self._remove_all_except(items_by_id.keys())
# Sort instances by subset name
sorted_labels = list(sorted(items_by_label.keys()))
# Add new instances to widget
widget_idx = 1
for label in sorted_labels:
for item in items_by_label[label]:
if item.id in self._widgets_by_id:
widget = self._widgets_by_id[item.id]
widget.update_item(item)
else:
widget = ConvertorItemCardWidget(item, self)
widget.selected.connect(self._on_widget_selection)
self._widgets_by_id[item.id] = widget
self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1
self._update_ordered_item_ids()
class InstanceGroupWidget(BaseGroupWidget):
"""Widget wrapping instances under group."""
active_changed = QtCore.Signal()
def __init__(self, group_icons, *args, **kwargs):
super(InstanceGroupWidget, self).__init__(*args, **kwargs)
self._group_icons = group_icons
def update_icons(self, group_icons):
self._group_icons = group_icons
def update_instance_values(self):
"""Trigger update on instance widgets."""
@ -153,14 +223,6 @@ class GroupWidget(QtWidgets.QWidget):
for widget in self._widgets_by_id.values():
widget.update_instance_values()
def confirm_remove_instance_id(self, instance_id):
"""Delete widget by instance id."""
widget = self._widgets_by_id.pop(instance_id)
widget.setVisible(False)
self._content_layout.removeWidget(widget)
widget.deleteLater()
def update_instances(self, instances):
"""Update instances for the group.
@ -178,17 +240,7 @@ class GroupWidget(QtWidgets.QWidget):
instances_by_subset_name[subset_name].append(instance)
# Remove instance widgets that are not in passed instances
for instance_id in tuple(self._widgets_by_id.keys()):
if instance_id in instances_by_id:
continue
widget = self._widgets_by_id.pop(instance_id)
if widget.is_selected:
self.removed_selected.emit()
widget.setVisible(False)
self._content_layout.removeWidget(widget)
widget.deleteLater()
self._remove_all_except(instances_by_id.keys())
# Sort instances by subset name
sorted_subset_names = list(sorted(instances_by_subset_name.keys()))
@ -211,18 +263,7 @@ class GroupWidget(QtWidgets.QWidget):
self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1
ordered_instance_ids = []
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
widget = item.widget()
if widget is not None:
ordered_instance_ids.append(widget.id)
self._ordered_instance_ids = ordered_instance_ids
def _on_widget_selection(self, instance_id, group_id, selection_type):
self.selected.emit(instance_id, group_id, selection_type)
self._update_ordered_item_ids()
class CardWidget(BaseClickableFrame):
@ -284,7 +325,7 @@ class ContextCardWidget(CardWidget):
super(ContextCardWidget, self).__init__(parent)
self._id = CONTEXT_ID
self._group_identifier = ""
self._group_identifier = CONTEXT_GROUP
icon_widget = PublishPixmapLabel(None, self)
icon_widget.setObjectName("FamilyIconLabel")
@ -304,6 +345,40 @@ class ContextCardWidget(CardWidget):
self._label_widget = label_widget
class ConvertorItemCardWidget(CardWidget):
"""Card for global context.
Is not visually under group widget and is always at the top of card view.
"""
def __init__(self, item, parent):
super(ConvertorItemCardWidget, self).__init__(parent)
self._id = item.id
self.identifier = item.identifier
self._group_identifier = CONVERTOR_ITEM_GROUP
icon_widget = IconValuePixmapLabel("fa.magic", self)
icon_widget.setObjectName("FamilyIconLabel")
label_widget = QtWidgets.QLabel(item.label, self)
icon_layout = QtWidgets.QHBoxLayout()
icon_layout.setContentsMargins(10, 5, 5, 5)
icon_layout.addWidget(icon_widget)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 5, 10, 5)
layout.addLayout(icon_layout, 0)
layout.addWidget(label_widget, 1)
self._icon_widget = icon_widget
self._label_widget = label_widget
def update_instance_values(self):
pass
class InstanceCardWidget(CardWidget):
"""Card widget representing instance."""
@ -481,6 +556,7 @@ class InstanceCardView(AbstractInstanceView):
self._content_widget = content_widget
self._context_widget = None
self._convertor_items_group = None
self._widgets_by_group = {}
self._ordered_groups = []
@ -513,6 +589,9 @@ class InstanceCardView(AbstractInstanceView):
):
output.append(self._context_widget)
if self._convertor_items_group is not None:
output.extend(self._convertor_items_group.get_selected_widgets())
for group_widget in self._widgets_by_group.values():
for widget in group_widget.get_selected_widgets():
output.append(widget)
@ -526,23 +605,19 @@ class InstanceCardView(AbstractInstanceView):
):
output.append(CONTEXT_ID)
if self._convertor_items_group is not None:
output.extend(self._convertor_items_group.get_selected_item_ids())
for group_widget in self._widgets_by_group.values():
output.extend(group_widget.get_selected_instance_ids())
output.extend(group_widget.get_selected_item_ids())
return output
def refresh(self):
"""Refresh instances in view based on CreatedContext."""
# Create context item if is not already existing
# - this must be as first thing to do as context item should be at the
# top
if self._context_widget is None:
widget = ContextCardWidget(self._content_widget)
widget.selected.connect(self._on_widget_selection)
self._context_widget = widget
self._make_sure_context_widget_exists()
self.selection_changed.emit()
self._content_layout.insertWidget(0, widget)
self._update_convertor_items_group()
# Prepare instances by group and identifiers by group
instances_by_group = collections.defaultdict(list)
@ -573,17 +648,21 @@ class InstanceCardView(AbstractInstanceView):
# Keep track of widget indexes
# - we start with 1 because Context item as at the top
widget_idx = 1
if self._convertor_items_group is not None:
widget_idx += 1
for group_name in sorted_group_names:
group_icons = {
idenfier: self._controller.get_creator_icon(idenfier)
for idenfier in identifiers_by_group[group_name]
}
if group_name in self._widgets_by_group:
group_widget = self._widgets_by_group[group_name]
else:
group_icons = {
idenfier: self._controller.get_creator_icon(idenfier)
for idenfier in identifiers_by_group[group_name]
}
group_widget.update_icons(group_icons)
group_widget = GroupWidget(
group_name, group_icons, self._content_widget
else:
group_widget = InstanceGroupWidget(
group_icons, group_name, self._content_widget
)
group_widget.active_changed.connect(self._on_active_changed)
group_widget.selected.connect(self._on_widget_selection)
@ -595,7 +674,10 @@ class InstanceCardView(AbstractInstanceView):
instances_by_group[group_name]
)
ordered_group_names = [""]
self._update_ordered_group_nameS()
def _update_ordered_group_nameS(self):
ordered_group_names = [CONTEXT_GROUP]
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
@ -605,6 +687,43 @@ class InstanceCardView(AbstractInstanceView):
self._ordered_groups = ordered_group_names
def _make_sure_context_widget_exists(self):
# Create context item if is not already existing
# - this must be as first thing to do as context item should be at the
# top
if self._context_widget is not None:
return
widget = ContextCardWidget(self._content_widget)
widget.selected.connect(self._on_widget_selection)
self._context_widget = widget
self.selection_changed.emit()
self._content_layout.insertWidget(0, widget)
def _update_convertor_items_group(self):
convertor_items = self._controller.convertor_items
if not convertor_items and self._convertor_items_group is None:
return
if not convertor_items:
self._convertor_items_group.setVisible(False)
self._content_layout.removeWidget(self._convertor_items_group)
self._convertor_items_group.deleteLater()
self._convertor_items_group = None
return
if self._convertor_items_group is None:
group_widget = ConvertorItemsGroupWidget(
CONVERTOR_ITEM_GROUP, self._content_widget
)
group_widget.selected.connect(self._on_widget_selection)
self._content_layout.insertWidget(1, group_widget)
self._convertor_items_group = group_widget
self._convertor_items_group.update_items(convertor_items)
def refresh_instance_states(self):
"""Trigger update of instances on group widgets."""
for widget in self._widgets_by_group.values():
@ -621,9 +740,13 @@ class InstanceCardView(AbstractInstanceView):
"""
if instance_id == CONTEXT_ID:
new_widget = self._context_widget
else:
group_widget = self._widgets_by_group[group_name]
new_widget = group_widget.get_widget_by_instance_id(instance_id)
if group_name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[group_name]
new_widget = group_widget.get_widget_by_item_id(instance_id)
if selection_type is SelectionTypes.clear:
self._select_item_clear(instance_id, group_name, new_widget)
@ -668,7 +791,10 @@ class InstanceCardView(AbstractInstanceView):
if instance_id == CONTEXT_ID:
remove_group = True
else:
group_widget = self._widgets_by_group[group_name]
if group_name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[group_name]
if not group_widget.get_selected_widgets():
remove_group = True
@ -749,7 +875,7 @@ class InstanceCardView(AbstractInstanceView):
# If start group is not set then use context item group name
if start_group is None:
start_group = ""
start_group = CONTEXT_GROUP
# If start instance id is not filled then use context id (similar to
# group)
@ -777,10 +903,13 @@ class InstanceCardView(AbstractInstanceView):
# Go through ordered groups (from top to bottom) and change selection
for name in self._ordered_groups:
# Prepare sorted instance widgets
if name == "":
if name == CONTEXT_GROUP:
sorted_widgets = [self._context_widget]
else:
group_widget = self._widgets_by_group[name]
if name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[name]
sorted_widgets = group_widget.get_ordered_widgets()
# Change selection based on explicit selection if start group
@ -892,6 +1021,8 @@ class InstanceCardView(AbstractInstanceView):
def get_selected_items(self):
"""Get selected instance ids and context."""
convertor_identifiers = []
instances = []
selected_widgets = self._get_selected_widgets()
@ -899,37 +1030,56 @@ class InstanceCardView(AbstractInstanceView):
for widget in selected_widgets:
if widget is self._context_widget:
context_selected = True
else:
elif isinstance(widget, InstanceCardWidget):
instances.append(widget.id)
return instances, context_selected
elif isinstance(widget, ConvertorItemCardWidget):
convertor_identifiers.append(widget.identifier)
def set_selected_items(self, instance_ids, context_selected):
return instances, context_selected, convertor_identifiers
def set_selected_items(
self, instance_ids, context_selected, convertor_identifiers
):
s_instance_ids = set(instance_ids)
cur_ids, cur_context = self.get_selected_items()
s_convertor_identifiers = set(convertor_identifiers)
cur_ids, cur_context, cur_convertor_identifiers = (
self.get_selected_items()
)
if (
set(cur_ids) == s_instance_ids
and cur_context == context_selected
and set(cur_convertor_identifiers) == s_convertor_identifiers
):
return
selected_groups = []
selected_instances = []
if context_selected:
selected_groups.append("")
selected_groups.append(CONTEXT_GROUP)
selected_instances.append(CONTEXT_ID)
self._context_widget.set_selected(context_selected)
for group_name in self._ordered_groups:
if group_name == "":
if group_name == CONTEXT_GROUP:
continue
group_widget = self._widgets_by_group[group_name]
is_convertor_group = group_name == CONVERTOR_ITEM_GROUP
if is_convertor_group:
group_widget = self._convertor_items_group
else:
group_widget = self._widgets_by_group[group_name]
group_selected = False
for widget in group_widget.get_ordered_widgets():
select = False
if widget.id in s_instance_ids:
if is_convertor_group:
is_in = widget.identifier in s_convertor_identifiers
else:
is_in = widget.id in s_instance_ids
if is_in:
selected_instances.append(widget.id)
group_selected = True
select = True

View file

@ -35,7 +35,10 @@ from ..constants import (
SORT_VALUE_ROLE,
IS_GROUP_ROLE,
CONTEXT_ID,
CONTEXT_LABEL
CONTEXT_LABEL,
GROUP_ROLE,
CONVERTER_IDENTIFIER_ROLE,
CONVERTOR_ITEM_GROUP,
)
@ -330,6 +333,9 @@ class InstanceTreeView(QtWidgets.QTreeView):
"""Ids of selected instances."""
instance_ids = set()
for index in self.selectionModel().selectedIndexes():
if index.data(CONVERTER_IDENTIFIER_ROLE) is not None:
continue
instance_id = index.data(INSTANCE_ID_ROLE)
if instance_id is not None:
instance_ids.add(instance_id)
@ -439,26 +445,35 @@ class InstanceListView(AbstractInstanceView):
self._group_items = {}
self._group_widgets = {}
self._widgets_by_id = {}
# Group by instance id for handling of active state
self._group_by_instance_id = {}
self._context_item = None
self._context_widget = None
self._convertor_group_item = None
self._convertor_group_widget = None
self._convertor_items_by_id = {}
self._instance_view = instance_view
self._instance_delegate = instance_delegate
self._instance_model = instance_model
self._proxy_model = proxy_model
def _on_expand(self, index):
group_name = index.data(SORT_VALUE_ROLE)
group_widget = self._group_widgets.get(group_name)
if group_widget:
group_widget.set_expanded(True)
self._update_widget_expand_state(index, True)
def _on_collapse(self, index):
group_name = index.data(SORT_VALUE_ROLE)
group_widget = self._group_widgets.get(group_name)
self._update_widget_expand_state(index, False)
def _update_widget_expand_state(self, index, expanded):
group_name = index.data(GROUP_ROLE)
if group_name == CONVERTOR_ITEM_GROUP:
group_widget = self._convertor_group_widget
else:
group_widget = self._group_widgets.get(group_name)
if group_widget:
group_widget.set_expanded(False)
group_widget.set_expanded(expanded)
def _on_toggle_request(self, toggle):
selected_instance_ids = self._instance_view.get_selected_instance_ids()
@ -517,6 +532,16 @@ class InstanceListView(AbstractInstanceView):
def refresh(self):
"""Refresh instances in the view."""
# Sort view at the end of refresh
# - is turned off until any change in view happens
sort_at_the_end = False
# Create or use already existing context item
# - context widget does not change so we don't have to update anything
if self._make_sure_context_item_exists():
sort_at_the_end = True
self._update_convertor_items_group()
# Prepare instances by their groups
instances_by_group_name = collections.defaultdict(list)
group_names = set()
@ -525,75 +550,12 @@ class InstanceListView(AbstractInstanceView):
group_names.add(group_label)
instances_by_group_name[group_label].append(instance)
# Sort view at the end of refresh
# - is turned off until any change in view happens
sort_at_the_end = False
# Access to root item of main model
root_item = self._instance_model.invisibleRootItem()
# Create or use already existing context item
# - context widget does not change so we don't have to update anything
context_item = None
if self._context_item is None:
sort_at_the_end = True
context_item = QtGui.QStandardItem()
context_item.setData(0, SORT_VALUE_ROLE)
context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE)
root_item.appendRow(context_item)
index = self._instance_model.index(
context_item.row(), context_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
widget = ListContextWidget(self._instance_view)
self._instance_view.setIndexWidget(proxy_index, widget)
self._context_widget = widget
self._context_item = context_item
# Create new groups based on prepared `instances_by_group_name`
new_group_items = []
for group_name in group_names:
if group_name in self._group_items:
continue
group_item = QtGui.QStandardItem()
group_item.setData(group_name, SORT_VALUE_ROLE)
group_item.setData(True, IS_GROUP_ROLE)
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
self._group_items[group_name] = group_item
new_group_items.append(group_item)
# Add new group items to root item if there are any
if new_group_items:
# Trigger sort at the end
if self._make_sure_groups_exists(group_names):
sort_at_the_end = True
root_item.appendRows(new_group_items)
# Create widget for each new group item and store it for future usage
for group_item in new_group_items:
index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
group_name = group_item.data(SORT_VALUE_ROLE)
widget = InstanceListGroupWidget(group_name, self._instance_view)
widget.expand_changed.connect(self._on_group_expand_request)
widget.toggle_requested.connect(self._on_group_toggle_request)
self._group_widgets[group_name] = widget
self._instance_view.setIndexWidget(proxy_index, widget)
# Remove groups that are not available anymore
for group_name in tuple(self._group_items.keys()):
if group_name in group_names:
continue
group_item = self._group_items.pop(group_name)
root_item.removeRow(group_item.row())
widget = self._group_widgets.pop(group_name)
widget.deleteLater()
self._remove_groups_except(group_names)
# Store which groups should be expanded at the end
expand_groups = set()
@ -652,6 +614,7 @@ class InstanceListView(AbstractInstanceView):
# Create new item and store it as new
item = QtGui.QStandardItem()
item.setData(instance["subset"], SORT_VALUE_ROLE)
item.setData(instance["subset"], GROUP_ROLE)
item.setData(instance_id, INSTANCE_ID_ROLE)
new_items.append(item)
new_items_with_instance.append((item, instance))
@ -717,13 +680,152 @@ class InstanceListView(AbstractInstanceView):
self._instance_view.expand(proxy_index)
def _make_sure_context_item_exists(self):
if self._context_item is not None:
return False
root_item = self._instance_model.invisibleRootItem()
context_item = QtGui.QStandardItem()
context_item.setData(0, SORT_VALUE_ROLE)
context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE)
root_item.appendRow(context_item)
index = self._instance_model.index(
context_item.row(), context_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
widget = ListContextWidget(self._instance_view)
self._instance_view.setIndexWidget(proxy_index, widget)
self._context_widget = widget
self._context_item = context_item
return True
def _update_convertor_items_group(self):
created_new_items = False
convertor_items_by_id = self._controller.convertor_items
group_item = self._convertor_group_item
if not convertor_items_by_id and group_item is None:
return created_new_items
root_item = self._instance_model.invisibleRootItem()
if not convertor_items_by_id:
root_item.removeRow(group_item.row())
self._convertor_group_widget.deleteLater()
self._convertor_group_widget = None
self._convertor_items_by_id = {}
return created_new_items
if group_item is None:
created_new_items = True
group_item = QtGui.QStandardItem()
group_item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)
group_item.setData(1, SORT_VALUE_ROLE)
group_item.setData(True, IS_GROUP_ROLE)
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
root_item.appendRow(group_item)
index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
widget = InstanceListGroupWidget(
CONVERTOR_ITEM_GROUP, self._instance_view
)
widget.toggle_checkbox.setVisible(False)
widget.expand_changed.connect(
self._on_convertor_group_expand_request
)
self._instance_view.setIndexWidget(proxy_index, widget)
self._convertor_group_item = group_item
self._convertor_group_widget = widget
for row in reversed(range(group_item.rowCount())):
child_item = group_item.child(row)
child_identifier = child_item.data(CONVERTER_IDENTIFIER_ROLE)
if child_identifier not in convertor_items_by_id:
self._convertor_items_by_id.pop(child_identifier, None)
group_item.removeRows(row, 1)
new_items = []
for identifier, convertor_item in convertor_items_by_id.items():
item = self._convertor_items_by_id.get(identifier)
if item is None:
created_new_items = True
item = QtGui.QStandardItem(convertor_item.label)
new_items.append(item)
item.setData(convertor_item.id, INSTANCE_ID_ROLE)
item.setData(convertor_item.label, SORT_VALUE_ROLE)
item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE)
item.setData(
convertor_item.identifier, CONVERTER_IDENTIFIER_ROLE
)
self._convertor_items_by_id[identifier] = item
if new_items:
group_item.appendRows(new_items)
return created_new_items
def _make_sure_groups_exists(self, group_names):
new_group_items = []
for group_name in group_names:
if group_name in self._group_items:
continue
group_item = QtGui.QStandardItem()
group_item.setData(group_name, GROUP_ROLE)
group_item.setData(group_name, SORT_VALUE_ROLE)
group_item.setData(True, IS_GROUP_ROLE)
group_item.setFlags(QtCore.Qt.ItemIsEnabled)
self._group_items[group_name] = group_item
new_group_items.append(group_item)
# Add new group items to root item if there are any
if not new_group_items:
return False
# Access to root item of main model
root_item = self._instance_model.invisibleRootItem()
root_item.appendRows(new_group_items)
# Create widget for each new group item and store it for future usage
for group_item in new_group_items:
index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(index)
group_name = group_item.data(GROUP_ROLE)
widget = InstanceListGroupWidget(group_name, self._instance_view)
widget.expand_changed.connect(self._on_group_expand_request)
widget.toggle_requested.connect(self._on_group_toggle_request)
self._group_widgets[group_name] = widget
self._instance_view.setIndexWidget(proxy_index, widget)
return True
def _remove_groups_except(self, group_names):
# Remove groups that are not available anymore
root_item = self._instance_model.invisibleRootItem()
for group_name in tuple(self._group_items.keys()):
if group_name in group_names:
continue
group_item = self._group_items.pop(group_name)
root_item.removeRow(group_item.row())
widget = self._group_widgets.pop(group_name)
widget.deleteLater()
def refresh_instance_states(self):
"""Trigger update of all instances."""
for widget in self._widgets_by_id.values():
widget.update_instance_values()
def _on_active_changed(self, changed_instance_id, new_value):
selected_instance_ids, _ = self.get_selected_items()
selected_instance_ids, _, _ = self.get_selected_items()
selected_ids = set()
found = False
@ -774,6 +876,16 @@ class InstanceListView(AbstractInstanceView):
proxy_index = self._proxy_model.mapFromSource(group_index)
self._instance_view.setExpanded(proxy_index, expanded)
def _on_convertor_group_expand_request(self, _, expanded):
group_item = self._convertor_group_item
if not group_item:
return
group_index = self._instance_model.index(
group_item.row(), group_item.column()
)
proxy_index = self._proxy_model.mapFromSource(group_index)
self._instance_view.setExpanded(proxy_index, expanded)
def _on_group_toggle_request(self, group_name, state):
if state == QtCore.Qt.PartiallyChecked:
return
@ -807,10 +919,17 @@ class InstanceListView(AbstractInstanceView):
tuple<list, bool>: Selected instance ids and boolean if context
is selected.
"""
instance_ids = []
convertor_identifiers = []
context_selected = False
for index in self._instance_view.selectionModel().selectedIndexes():
convertor_identifier = index.data(CONVERTER_IDENTIFIER_ROLE)
if convertor_identifier is not None:
convertor_identifiers.append(convertor_identifier)
continue
instance_id = index.data(INSTANCE_ID_ROLE)
if not context_selected and instance_id == CONTEXT_ID:
context_selected = True
@ -818,14 +937,20 @@ class InstanceListView(AbstractInstanceView):
elif instance_id is not None:
instance_ids.append(instance_id)
return instance_ids, context_selected
return instance_ids, context_selected, convertor_identifiers
def set_selected_items(self, instance_ids, context_selected):
def set_selected_items(
self, instance_ids, context_selected, convertor_identifiers
):
s_instance_ids = set(instance_ids)
cur_ids, cur_context = self.get_selected_items()
s_convertor_identifiers = set(convertor_identifiers)
cur_ids, cur_context, cur_convertor_identifiers = (
self.get_selected_items()
)
if (
set(cur_ids) == s_instance_ids
and cur_context == context_selected
and set(cur_convertor_identifiers) == s_convertor_identifiers
):
return
@ -851,20 +976,35 @@ class InstanceListView(AbstractInstanceView):
(item.child(row), list(new_parent_items))
)
instance_id = item.data(INSTANCE_ID_ROLE)
if not instance_id:
convertor_identifier = item.data(CONVERTER_IDENTIFIER_ROLE)
select = False
expand_parent = True
if convertor_identifier is not None:
if convertor_identifier in s_convertor_identifiers:
select = True
else:
instance_id = item.data(INSTANCE_ID_ROLE)
if instance_id == CONTEXT_ID:
if context_selected:
select = True
expand_parent = False
elif instance_id in s_instance_ids:
select = True
if not select:
continue
if instance_id in s_instance_ids:
select_indexes.append(item.index())
for parent_item in parent_items:
index = parent_item.index()
proxy_index = proxy_model.mapFromSource(index)
if not view.isExpanded(proxy_index):
view.expand(proxy_index)
select_indexes.append(item.index())
if not expand_parent:
continue
elif context_selected and instance_id == CONTEXT_ID:
select_indexes.append(item.index())
for parent_item in parent_items:
index = parent_item.index()
proxy_index = proxy_model.mapFromSource(index)
if not view.isExpanded(proxy_index):
view.expand(proxy_index)
selection_model = view.selectionModel()
if not select_indexes:

View file

@ -124,6 +124,9 @@ class OverviewWidget(QtWidgets.QFrame):
subset_attributes_widget.instance_context_changed.connect(
self._on_instance_context_change
)
subset_attributes_widget.convert_requested.connect(
self._on_convert_requested
)
# --- Controller callbacks ---
controller.event_system.add_callback(
@ -201,7 +204,7 @@ class OverviewWidget(QtWidgets.QFrame):
self.create_requested.emit()
def _on_delete_clicked(self):
instance_ids, _ = self.get_selected_items()
instance_ids, _, _ = self.get_selected_items()
# Ask user if he really wants to remove instances
dialog = QtWidgets.QMessageBox(self)
@ -235,7 +238,9 @@ class OverviewWidget(QtWidgets.QFrame):
if self._refreshing_instances:
return
instance_ids, context_selected = self.get_selected_items()
instance_ids, context_selected, convertor_identifiers = (
self.get_selected_items()
)
# Disable delete button if nothing is selected
self._delete_btn.setEnabled(len(instance_ids) > 0)
@ -246,7 +251,7 @@ class OverviewWidget(QtWidgets.QFrame):
for instance_id in instance_ids
]
self._subset_attributes_widget.set_current_instances(
instances, context_selected
instances, context_selected, convertor_identifiers
)
def _on_active_changed(self):
@ -315,6 +320,10 @@ class OverviewWidget(QtWidgets.QFrame):
self.instance_context_changed.emit()
def _on_convert_requested(self):
_, _, convertor_identifiers = self.get_selected_items()
self._controller.trigger_convertor_items(convertor_identifiers)
def get_selected_items(self):
view = self._subset_views_layout.currentWidget()
return view.get_selected_items()
@ -332,8 +341,12 @@ class OverviewWidget(QtWidgets.QFrame):
else:
new_view.refresh_instance_states()
instance_ids, context_selected = old_view.get_selected_items()
new_view.set_selected_items(instance_ids, context_selected)
instance_ids, context_selected, convertor_identifiers = (
old_view.get_selected_items()
)
new_view.set_selected_items(
instance_ids, context_selected, convertor_identifiers
)
self._subset_views_layout.setCurrentIndex(new_idx)

View file

@ -1461,6 +1461,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
"""
instance_context_changed = QtCore.Signal()
convert_requested = QtCore.Signal()
def __init__(self, controller, parent):
super(SubsetAttributesWidget, self).__init__(parent)
@ -1479,9 +1480,53 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
# BOTTOM PART
bottom_widget = QtWidgets.QWidget(self)
creator_attrs_widget = CreatorAttrsWidget(
controller, bottom_widget
# Wrap Creator attributes to widget to be able add convert button
creator_widget = QtWidgets.QWidget(bottom_widget)
# Convert button widget (with layout to handle stretch)
convert_widget = QtWidgets.QWidget(creator_widget)
convert_label = QtWidgets.QLabel(creator_widget)
# Set the label text with 'setText' to apply html
convert_label.setText(
(
"Found old publishable subsets"
" incompatible with new publisher."
"<br/><br/>Press the <b>update subsets</b> button"
" to automatically update them"
" to be able to publish again."
)
)
convert_label.setWordWrap(True)
convert_label.setAlignment(QtCore.Qt.AlignCenter)
convert_btn = QtWidgets.QPushButton(
"Update subsets", convert_widget
)
convert_separator = QtWidgets.QFrame(convert_widget)
convert_separator.setObjectName("Separator")
convert_separator.setMinimumHeight(1)
convert_separator.setMaximumHeight(1)
convert_layout = QtWidgets.QGridLayout(convert_widget)
convert_layout.setContentsMargins(5, 0, 5, 0)
convert_layout.setVerticalSpacing(10)
convert_layout.addWidget(convert_label, 0, 0, 1, 3)
convert_layout.addWidget(convert_btn, 1, 1)
convert_layout.addWidget(convert_separator, 2, 0, 1, 3)
convert_layout.setColumnStretch(0, 1)
convert_layout.setColumnStretch(1, 0)
convert_layout.setColumnStretch(2, 1)
# Creator attributes widget
creator_attrs_widget = CreatorAttrsWidget(
controller, creator_widget
)
creator_layout = QtWidgets.QVBoxLayout(creator_widget)
creator_layout.setContentsMargins(0, 0, 0, 0)
creator_layout.addWidget(convert_widget, 0)
creator_layout.addWidget(creator_attrs_widget, 1)
publish_attrs_widget = PublishPluginAttrsWidget(
controller, bottom_widget
)
@ -1492,7 +1537,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
bottom_layout = QtWidgets.QHBoxLayout(bottom_widget)
bottom_layout.setContentsMargins(0, 0, 0, 0)
bottom_layout.addWidget(creator_attrs_widget, 1)
bottom_layout.addWidget(creator_widget, 1)
bottom_layout.addWidget(bottom_separator, 0)
bottom_layout.addWidget(publish_attrs_widget, 1)
@ -1505,6 +1550,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
layout.addWidget(top_bottom, 0)
layout.addWidget(bottom_widget, 1)
self._convertor_identifiers = None
self._current_instances = None
self._context_selected = False
self._all_instances_valid = True
@ -1512,9 +1558,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
global_attrs_widget.instance_context_changed.connect(
self._on_instance_context_changed
)
convert_btn.clicked.connect(self._on_convert_click)
self._controller = controller
self._convert_widget = convert_widget
self.global_attrs_widget = global_attrs_widget
self.creator_attrs_widget = creator_attrs_widget
@ -1537,7 +1586,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
self.instance_context_changed.emit()
def set_current_instances(self, instances, context_selected):
def _on_convert_click(self):
self.convert_requested.emit()
def set_current_instances(
self, instances, context_selected, convertor_identifiers
):
"""Change currently selected items.
Args:
@ -1551,10 +1605,13 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
all_valid = False
break
s_convertor_identifiers = set(convertor_identifiers)
self._convertor_identifiers = s_convertor_identifiers
self._current_instances = instances
self._context_selected = context_selected
self._all_instances_valid = all_valid
self._convert_widget.setVisible(len(s_convertor_identifiers) > 0)
self.global_attrs_widget.set_current_instances(instances)
self.creator_attrs_widget.set_current_instances(instances)
self.publish_attrs_widget.set_current_instances(

View file

@ -1,4 +1,5 @@
import collections
import copy
from Qt import QtWidgets, QtCore, QtGui
from openpype import (
@ -224,10 +225,10 @@ 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
errors_dialog_message_timer = QtCore.QTimer()
errors_dialog_message_timer.setInterval(100)
errors_dialog_message_timer.timeout.connect(
self._on_errors_message_timeout
)
help_btn.clicked.connect(self._on_help_click)
@ -268,16 +269,22 @@ class PublisherWindow(QtWidgets.QDialog):
"show.card.message", self._on_overlay_message
)
controller.event_system.add_callback(
"instances.collection.failed", self._instance_collection_failed
"instances.collection.failed", self._on_creator_error
)
controller.event_system.add_callback(
"instances.save.failed", self._instance_save_failed
"instances.save.failed", self._on_creator_error
)
controller.event_system.add_callback(
"instances.remove.failed", self._instance_remove_failed
"instances.remove.failed", self._on_creator_error
)
controller.event_system.add_callback(
"instances.create.failed", self._instance_create_failed
"instances.create.failed", self._on_creator_error
)
controller.event_system.add_callback(
"convertors.convert.failed", self._on_convertor_error
)
controller.event_system.add_callback(
"convertors.find.failed", self._on_convertor_error
)
# Store extra header widget for TrayPublisher
@ -325,8 +332,8 @@ class PublisherWindow(QtWidgets.QDialog):
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._error_messages_to_show = collections.deque()
self._errors_dialog_message_timer = errors_dialog_message_timer
self._set_publish_visibility(False)
@ -357,7 +364,10 @@ class PublisherWindow(QtWidgets.QDialog):
self._update_publish_frame_rect()
def _on_overlay_message(self, event):
self._overlay_object.add_message(event["message"])
self._overlay_object.add_message(
event["message"],
event.get("message_type")
)
def _on_first_show(self):
self.resize(self.default_width, self.default_height)
@ -604,37 +614,49 @@ class PublisherWindow(QtWidgets.QDialog):
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 add_error_message_dialog(self, title, failed_info, message_start=None):
self._error_messages_to_show.append(
(title, failed_info, message_start)
)
self._errors_dialog_message_timer.start()
def _on_creators_message_timeout(self):
if not self._creators_messages_to_show:
self._creators_dialog_message_timer.stop()
def _on_errors_message_timeout(self):
if not self._error_messages_to_show:
self._errors_dialog_message_timer.stop()
return
item = self._creators_messages_to_show.popleft()
title, failed_info = item
dialog = CreatorsErrorMessageBox(title, failed_info, self)
item = self._error_messages_to_show.popleft()
title, failed_info, message_start = item
dialog = ErrorsMessageBox(
title, failed_info, message_start, self
)
dialog.exec_()
dialog.deleteLater()
def _instance_collection_failed(self, event):
self.add_message_dialog(event["title"], event["failed_info"])
def _on_creator_error(self, event):
new_failed_info = []
for item in event["failed_info"]:
new_item = copy.deepcopy(item)
new_item["label"] = new_item.pop("creator_label")
new_item["identifier"] = new_item.pop("creator_identifier")
new_failed_info.append(new_item)
self.add_error_message_dialog(event["title"], new_failed_info, "Creator:")
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"])
def _on_convertor_error(self, event):
new_failed_info = []
for item in event["failed_info"]:
new_item = copy.deepcopy(item)
new_item["identifier"] = new_item.pop("convertor_identifier")
new_failed_info.append(new_item)
self.add_error_message_dialog(
event["title"], new_failed_info, "Convertor:"
)
class CreatorsErrorMessageBox(ErrorMessageBox):
def __init__(self, error_title, failed_info, parent):
class ErrorsMessageBox(ErrorMessageBox):
def __init__(self, error_title, failed_info, message_start, parent):
self._failed_info = failed_info
self._message_start = message_start
self._info_with_id = [
# Id must be string when used in tab widget
{"id": str(idx), "info": info}
@ -644,7 +666,7 @@ class CreatorsErrorMessageBox(ErrorMessageBox):
self._tabs_widget = None
self._stack_layout = None
super(CreatorsErrorMessageBox, self).__init__(error_title, parent)
super(ErrorsMessageBox, self).__init__(error_title, parent)
layout = self.layout()
layout.setContentsMargins(0, 0, 0, 0)
@ -659,17 +681,21 @@ class CreatorsErrorMessageBox(ErrorMessageBox):
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)
item_label = info.get("label")
item_identifier = info["identifier"]
if item_label:
report_message = "{} ({})".format(
item_label, item_identifier)
else:
report_message += " {}".format(creator_identifier)
report_message = "{}".format(item_identifier)
if self._message_start:
report_message = "{} {}".format(
self._message_start, report_message
)
report_message += "\n\nError: {}".format(info["message"])
formatted_traceback = info["traceback"]
formatted_traceback = info.get("traceback")
if formatted_traceback:
report_message += "\n\n{}".format(formatted_traceback)
output.append(report_message)
@ -686,11 +712,10 @@ class CreatorsErrorMessageBox(ErrorMessageBox):
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
formatted_traceback = info.get("traceback")
item_label = info.get("label")
if not item_label:
item_label = info["identifier"]
msg_widget = QtWidgets.QWidget(stack_widget)
msg_layout = QtWidgets.QVBoxLayout(msg_widget)
@ -710,7 +735,7 @@ class CreatorsErrorMessageBox(ErrorMessageBox):
msg_layout.addStretch(1)
tabs_widget.add_tab(creator_label, item_id)
tabs_widget.add_tab(item_label, item_id)
stack_layout.addWidget(msg_widget)
if first:
first = False

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.14.5"
__version__ = "3.14.6-nightly.1"