mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
1530 lines
50 KiB
Python
1530 lines
50 KiB
Python
# -*- coding: utf-8 -*-
|
|
import os
|
|
import re
|
|
import copy
|
|
import collections
|
|
from Qt import QtWidgets, QtCore, QtGui
|
|
import qtawesome
|
|
|
|
from openpype.lib import TaskNotSetError
|
|
from openpype.widgets.attribute_defs import create_widget_for_attr_def
|
|
from openpype.tools import resources
|
|
from openpype.tools.flickcharm import FlickCharm
|
|
from openpype.tools.utils import (
|
|
PlaceholderLineEdit,
|
|
IconButton,
|
|
PixmapLabel,
|
|
BaseClickableFrame
|
|
)
|
|
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
|
|
from .assets_widget import AssetsDialog
|
|
from .tasks_widget import TasksModel
|
|
from .icons import (
|
|
get_pixmap,
|
|
get_icon_path
|
|
)
|
|
|
|
from ..constants import (
|
|
VARIANT_TOOLTIP
|
|
)
|
|
|
|
|
|
class PublishPixmapLabel(PixmapLabel):
|
|
def _get_pix_size(self):
|
|
size = self.fontMetrics().height()
|
|
size += size % 2
|
|
return size, size
|
|
|
|
|
|
class IconValuePixmapLabel(PublishPixmapLabel):
|
|
"""Label resizing to width and height of font.
|
|
|
|
Handle icon parsing from creators/instances. Using of QAwesome module
|
|
of path to images.
|
|
"""
|
|
fa_prefixes = ["", "fa."]
|
|
default_size = 200
|
|
|
|
def __init__(self, icon_def, parent):
|
|
source_pixmap = self._parse_icon_def(icon_def)
|
|
|
|
super(IconValuePixmapLabel, self).__init__(source_pixmap, parent)
|
|
|
|
def set_icon_def(self, icon_def):
|
|
"""Set icon by it's definition name.
|
|
|
|
Args:
|
|
icon_def (str): Name of FontAwesome icon or path to image.
|
|
"""
|
|
source_pixmap = self._parse_icon_def(icon_def)
|
|
self.set_source_pixmap(source_pixmap)
|
|
|
|
def _default_pixmap(self):
|
|
pix = QtGui.QPixmap(1, 1)
|
|
pix.fill(QtCore.Qt.transparent)
|
|
return pix
|
|
|
|
def _parse_icon_def(self, icon_def):
|
|
if not icon_def:
|
|
return self._default_pixmap()
|
|
|
|
if isinstance(icon_def, QtGui.QPixmap):
|
|
return icon_def
|
|
|
|
if isinstance(icon_def, QtGui.QIcon):
|
|
return icon_def.pixmap(self.default_size, self.default_size)
|
|
|
|
try:
|
|
if os.path.exists(icon_def):
|
|
return QtGui.QPixmap(icon_def)
|
|
except Exception:
|
|
# TODO logging
|
|
pass
|
|
|
|
for prefix in self.fa_prefixes:
|
|
try:
|
|
icon_name = "{}{}".format(prefix, icon_def)
|
|
icon = qtawesome.icon(icon_name, color="white")
|
|
return icon.pixmap(self.default_size, self.default_size)
|
|
except Exception:
|
|
# TODO logging
|
|
continue
|
|
|
|
return self._default_pixmap()
|
|
|
|
|
|
class ContextWarningLabel(PublishPixmapLabel):
|
|
"""Pixmap label with warning icon."""
|
|
def __init__(self, parent):
|
|
pix = get_pixmap("warning")
|
|
|
|
super(ContextWarningLabel, self).__init__(pix, parent)
|
|
|
|
self.setToolTip(
|
|
"Contain invalid context. Please check details."
|
|
)
|
|
self.setObjectName("FamilyIconLabel")
|
|
|
|
|
|
class PublishIconBtn(IconButton):
|
|
"""Button using alpha of source image to redraw with different color.
|
|
|
|
Main class for buttons showed in publisher.
|
|
|
|
TODO:
|
|
Add different states:
|
|
- normal : before publishing
|
|
- publishing : publishing is running
|
|
- validation error : validation error happened
|
|
- error : other error happened
|
|
- success : publishing finished
|
|
"""
|
|
def __init__(self, pixmap_path, *args, **kwargs):
|
|
super(PublishIconBtn, self).__init__(*args, **kwargs)
|
|
|
|
loaded_image = QtGui.QImage(pixmap_path)
|
|
|
|
pixmap = self.paint_image_with_color(loaded_image, QtCore.Qt.white)
|
|
|
|
self._base_image = loaded_image
|
|
self._enabled_icon = QtGui.QIcon(pixmap)
|
|
self._disabled_icon = None
|
|
|
|
self.setIcon(self._enabled_icon)
|
|
|
|
def get_enabled_icon(self):
|
|
"""Enabled icon."""
|
|
return self._enabled_icon
|
|
|
|
def get_disabled_icon(self):
|
|
"""Disabled icon."""
|
|
if self._disabled_icon is None:
|
|
pixmap = self.paint_image_with_color(
|
|
self._base_image, QtCore.Qt.gray
|
|
)
|
|
self._disabled_icon = QtGui.QIcon(pixmap)
|
|
return self._disabled_icon
|
|
|
|
@staticmethod
|
|
def paint_image_with_color(image, color):
|
|
"""Redraw image with single color using it's alpha.
|
|
|
|
It is expected that input image is singlecolor image with alpha.
|
|
|
|
Args:
|
|
image (QImage): Loaded image with alpha.
|
|
color (QColor): Color that will be used to paint image.
|
|
"""
|
|
width = image.width()
|
|
height = image.height()
|
|
partition = 8
|
|
part_w = int(width / partition)
|
|
part_h = int(height / partition)
|
|
part_w -= part_w % 2
|
|
part_h -= part_h % 2
|
|
scaled_image = image.scaled(
|
|
width - (2 * part_w),
|
|
height - (2 * part_h),
|
|
QtCore.Qt.IgnoreAspectRatio,
|
|
QtCore.Qt.SmoothTransformation
|
|
)
|
|
alpha_mask = scaled_image.createAlphaMask()
|
|
alpha_region = QtGui.QRegion(QtGui.QBitmap.fromImage(alpha_mask))
|
|
alpha_region.translate(part_w, part_h)
|
|
|
|
pixmap = QtGui.QPixmap(width, height)
|
|
pixmap.fill(QtCore.Qt.transparent)
|
|
|
|
painter = QtGui.QPainter(pixmap)
|
|
painter.setClipRegion(alpha_region)
|
|
painter.setPen(QtCore.Qt.NoPen)
|
|
painter.setBrush(color)
|
|
painter.drawRect(QtCore.QRect(0, 0, width, height))
|
|
painter.end()
|
|
|
|
return pixmap
|
|
|
|
def setEnabled(self, enabled):
|
|
super(PublishIconBtn, self).setEnabled(enabled)
|
|
if self.isEnabled():
|
|
self.setIcon(self.get_enabled_icon())
|
|
else:
|
|
self.setIcon(self.get_disabled_icon())
|
|
|
|
|
|
class ResetBtn(PublishIconBtn):
|
|
"""Publish reset button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("refresh")
|
|
super(ResetBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Refresh publishing")
|
|
|
|
|
|
class StopBtn(PublishIconBtn):
|
|
"""Publish stop button."""
|
|
def __init__(self, parent):
|
|
icon_path = get_icon_path("stop")
|
|
super(StopBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Stop/Pause publishing")
|
|
|
|
|
|
class ValidateBtn(PublishIconBtn):
|
|
"""Publish validate button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("validate")
|
|
super(ValidateBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Validate")
|
|
|
|
|
|
class PublishBtn(PublishIconBtn):
|
|
"""Publish start publish button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("play")
|
|
super(PublishBtn, self).__init__(icon_path, "Publish", parent)
|
|
self.setToolTip("Publish")
|
|
|
|
|
|
class CreateInstanceBtn(PublishIconBtn):
|
|
"""Create add button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("add")
|
|
super(CreateInstanceBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Create new instance")
|
|
|
|
|
|
class CopyPublishReportBtn(PublishIconBtn):
|
|
"""Copy report button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("copy")
|
|
super(CopyPublishReportBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Copy report")
|
|
|
|
|
|
class SavePublishReportBtn(PublishIconBtn):
|
|
"""Save report button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("download_arrow")
|
|
super(SavePublishReportBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Export and save report")
|
|
|
|
|
|
class ShowPublishReportBtn(PublishIconBtn):
|
|
"""Show report button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("view_report")
|
|
super(ShowPublishReportBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Show details")
|
|
|
|
|
|
class RemoveInstanceBtn(PublishIconBtn):
|
|
"""Create remove button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = resources.get_icon_path("delete")
|
|
super(RemoveInstanceBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Remove selected instances")
|
|
|
|
|
|
class ChangeViewBtn(PublishIconBtn):
|
|
"""Create toggle view button."""
|
|
def __init__(self, parent=None):
|
|
icon_path = get_icon_path("change_view")
|
|
super(ChangeViewBtn, self).__init__(icon_path, parent)
|
|
self.setToolTip("Swap between views")
|
|
|
|
|
|
class AbstractInstanceView(QtWidgets.QWidget):
|
|
"""Abstract class for instance view in creation part."""
|
|
selection_changed = QtCore.Signal()
|
|
active_changed = QtCore.Signal()
|
|
# Refreshed attribute is not changed by view itself
|
|
# - widget which triggers `refresh` is changing the state
|
|
# TODO store that information in widget which cares about refreshing
|
|
refreshed = False
|
|
|
|
def set_refreshed(self, refreshed):
|
|
"""View is refreshed with last instances.
|
|
|
|
Views are not updated all the time. Only if are visible.
|
|
"""
|
|
self.refreshed = refreshed
|
|
|
|
def refresh(self):
|
|
"""Refresh instances in the view from current `CreatedContext`."""
|
|
raise NotImplementedError((
|
|
"{} Method 'refresh' is not implemented."
|
|
).format(self.__class__.__name__))
|
|
|
|
def get_selected_items(self):
|
|
"""Selected instances required for callbacks.
|
|
|
|
Example: When delete button is clicked to know what should be deleted.
|
|
"""
|
|
raise NotImplementedError((
|
|
"{} Method 'get_selected_items' is not implemented."
|
|
).format(self.__class__.__name__))
|
|
|
|
|
|
class ClickableLineEdit(QtWidgets.QLineEdit):
|
|
"""QLineEdit capturing left mouse click.
|
|
|
|
Triggers `clicked` signal on mouse click.
|
|
"""
|
|
clicked = QtCore.Signal()
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ClickableLineEdit, self).__init__(*args, **kwargs)
|
|
self.setReadOnly(True)
|
|
self._mouse_pressed = False
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == QtCore.Qt.LeftButton:
|
|
self._mouse_pressed = True
|
|
event.accept()
|
|
|
|
def mouseMoveEvent(self, event):
|
|
event.accept()
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if self._mouse_pressed:
|
|
self._mouse_pressed = False
|
|
if self.rect().contains(event.pos()):
|
|
self.clicked.emit()
|
|
event.accept()
|
|
|
|
def mouseDoubleClickEvent(self, event):
|
|
event.accept()
|
|
|
|
|
|
class AssetsField(BaseClickableFrame):
|
|
"""Field where asset name of selected instance/s is showed.
|
|
|
|
Click on the field will trigger `AssetsDialog`.
|
|
"""
|
|
value_changed = QtCore.Signal()
|
|
|
|
def __init__(self, controller, parent):
|
|
super(AssetsField, self).__init__(parent)
|
|
|
|
dialog = AssetsDialog(controller, self)
|
|
|
|
name_input = ClickableLineEdit(self)
|
|
name_input.setObjectName("AssetNameInput")
|
|
|
|
layout = QtWidgets.QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.addWidget(name_input, 1)
|
|
|
|
name_input.clicked.connect(self._mouse_release_callback)
|
|
dialog.finished.connect(self._on_dialog_finish)
|
|
|
|
self._dialog = dialog
|
|
self._name_input = name_input
|
|
|
|
self._origin_value = []
|
|
self._origin_selection = []
|
|
self._selected_items = []
|
|
self._has_value_changed = False
|
|
self._is_valid = True
|
|
self._multiselection_text = None
|
|
|
|
def _on_dialog_finish(self, result):
|
|
if not result:
|
|
return
|
|
|
|
asset_name = self._dialog.get_selected_asset()
|
|
if asset_name is None:
|
|
return
|
|
|
|
self._selected_items = [asset_name]
|
|
self._has_value_changed = (
|
|
self._origin_value != self._selected_items
|
|
)
|
|
self.set_text(asset_name)
|
|
self._set_is_valid(True)
|
|
|
|
self.value_changed.emit()
|
|
|
|
def _mouse_release_callback(self):
|
|
self._dialog.set_selected_assets(self._selected_items)
|
|
self._dialog.open()
|
|
|
|
def set_multiselection_text(self, text):
|
|
"""Change text for multiselection of different assets.
|
|
|
|
When there are selected multiple instances at once and they don't have
|
|
same asset in context.
|
|
"""
|
|
self._multiselection_text = text
|
|
|
|
def _set_is_valid(self, valid):
|
|
if valid == self._is_valid:
|
|
return
|
|
self._is_valid = valid
|
|
state = ""
|
|
if not valid:
|
|
state = "invalid"
|
|
self._set_state_property(state)
|
|
|
|
def _set_state_property(self, state):
|
|
current_value = self._name_input.property("state")
|
|
if current_value != state:
|
|
self._name_input.setProperty("state", state)
|
|
self._name_input.style().polish(self._name_input)
|
|
|
|
def is_valid(self):
|
|
"""Is asset valid."""
|
|
return self._is_valid
|
|
|
|
def has_value_changed(self):
|
|
"""Value of asset has changed."""
|
|
return self._has_value_changed
|
|
|
|
def get_selected_items(self):
|
|
"""Selected asset names."""
|
|
return list(self._selected_items)
|
|
|
|
def set_text(self, text):
|
|
"""Set text in text field.
|
|
|
|
Does not change selected items (assets).
|
|
"""
|
|
self._name_input.setText(text)
|
|
|
|
def set_selected_items(self, asset_names=None):
|
|
"""Set asset names for selection of instances.
|
|
|
|
Passed asset names are validated and if there are 2 or more different
|
|
asset names then multiselection text is shown.
|
|
|
|
Args:
|
|
asset_names (list, tuple, set, NoneType): List of asset names.
|
|
"""
|
|
if asset_names is None:
|
|
asset_names = []
|
|
|
|
self._has_value_changed = False
|
|
self._origin_value = list(asset_names)
|
|
self._selected_items = list(asset_names)
|
|
is_valid = True
|
|
if not asset_names:
|
|
self.set_text("")
|
|
|
|
elif len(asset_names) == 1:
|
|
asset_name = tuple(asset_names)[0]
|
|
is_valid = self._dialog.name_is_valid(asset_name)
|
|
self.set_text(asset_name)
|
|
else:
|
|
for asset_name in asset_names:
|
|
is_valid = self._dialog.name_is_valid(asset_name)
|
|
if not is_valid:
|
|
break
|
|
|
|
multiselection_text = self._multiselection_text
|
|
if multiselection_text is None:
|
|
multiselection_text = "|".join(asset_names)
|
|
self.set_text(multiselection_text)
|
|
|
|
self._set_is_valid(is_valid)
|
|
|
|
def reset_to_origin(self):
|
|
"""Change to asset names set with last `set_selected_items` call."""
|
|
self.set_selected_items(self._origin_value)
|
|
|
|
|
|
class TasksComboboxProxy(QtCore.QSortFilterProxyModel):
|
|
def __init__(self, *args, **kwargs):
|
|
super(TasksComboboxProxy, self).__init__(*args, **kwargs)
|
|
self._filter_empty = False
|
|
|
|
def set_filter_empty(self, filter_empty):
|
|
if self._filter_empty is filter_empty:
|
|
return
|
|
self._filter_empty = filter_empty
|
|
self.invalidate()
|
|
|
|
def filterAcceptsRow(self, source_row, parent_index):
|
|
if self._filter_empty:
|
|
model = self.sourceModel()
|
|
source_index = model.index(
|
|
source_row, self.filterKeyColumn(), parent_index
|
|
)
|
|
if not source_index.data(QtCore.Qt.DisplayRole):
|
|
return False
|
|
return True
|
|
|
|
|
|
class TasksCombobox(QtWidgets.QComboBox):
|
|
"""Combobox to show tasks for selected instances.
|
|
|
|
Combobox gives ability to select only from intersection of task names for
|
|
asset names in selected instances.
|
|
|
|
If asset names in selected instances does not have same tasks then combobox
|
|
will be empty.
|
|
"""
|
|
value_changed = QtCore.Signal()
|
|
|
|
def __init__(self, controller, parent):
|
|
super(TasksCombobox, self).__init__(parent)
|
|
self.setObjectName("TasksCombobox")
|
|
|
|
# Set empty delegate to propagate stylesheet to a combobox
|
|
delegate = QtWidgets.QStyledItemDelegate()
|
|
self.setItemDelegate(delegate)
|
|
|
|
model = TasksModel(controller, True)
|
|
proxy_model = TasksComboboxProxy()
|
|
proxy_model.setSourceModel(model)
|
|
self.setModel(proxy_model)
|
|
|
|
self.currentIndexChanged.connect(self._on_index_change)
|
|
|
|
self._delegate = delegate
|
|
self._model = model
|
|
self._proxy_model = proxy_model
|
|
self._origin_value = []
|
|
self._origin_selection = []
|
|
self._selected_items = []
|
|
self._has_value_changed = False
|
|
self._ignore_index_change = False
|
|
self._multiselection_text = None
|
|
self._is_valid = True
|
|
|
|
self._text = None
|
|
|
|
def set_invalid_empty_task(self, invalid=True):
|
|
self._proxy_model.set_filter_empty(invalid)
|
|
if invalid:
|
|
self._set_is_valid(False)
|
|
self.set_text("< One or more subsets require Task selected >")
|
|
else:
|
|
self.set_text(None)
|
|
|
|
def set_multiselection_text(self, text):
|
|
"""Change text shown when multiple different tasks are in context."""
|
|
self._multiselection_text = text
|
|
|
|
def _on_index_change(self):
|
|
if self._ignore_index_change:
|
|
return
|
|
|
|
self.set_text(None)
|
|
text = self.currentText()
|
|
idx = self.findText(text)
|
|
if idx < 0:
|
|
return
|
|
|
|
self._set_is_valid(True)
|
|
self._selected_items = [text]
|
|
self._has_value_changed = (
|
|
self._origin_selection != self._selected_items
|
|
)
|
|
|
|
self.value_changed.emit()
|
|
|
|
def set_text(self, text):
|
|
"""Set context shown in combobox without changing selected items."""
|
|
if text == self._text:
|
|
return
|
|
|
|
self._text = text
|
|
self.repaint()
|
|
|
|
def paintEvent(self, event):
|
|
"""Paint custom text without using QLineEdit.
|
|
|
|
The easiest way how to draw custom text in combobox and keep combobox
|
|
properties and event handling.
|
|
"""
|
|
painter = QtGui.QPainter(self)
|
|
painter.setPen(self.palette().color(QtGui.QPalette.Text))
|
|
opt = QtWidgets.QStyleOptionComboBox()
|
|
self.initStyleOption(opt)
|
|
if self._text is not None:
|
|
opt.currentText = self._text
|
|
|
|
style = self.style()
|
|
style.drawComplexControl(
|
|
QtWidgets.QStyle.CC_ComboBox, opt, painter, self
|
|
)
|
|
style.drawControl(
|
|
QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self
|
|
)
|
|
|
|
def is_valid(self):
|
|
"""Are all selected items valid."""
|
|
return self._is_valid
|
|
|
|
def has_value_changed(self):
|
|
"""Did selection of task changed."""
|
|
return self._has_value_changed
|
|
|
|
def _set_is_valid(self, valid):
|
|
if valid == self._is_valid:
|
|
return
|
|
self._is_valid = valid
|
|
state = ""
|
|
if not valid:
|
|
state = "invalid"
|
|
self._set_state_property(state)
|
|
|
|
def _set_state_property(self, state):
|
|
current_value = self.property("state")
|
|
if current_value != state:
|
|
self.setProperty("state", state)
|
|
self.style().polish(self)
|
|
|
|
def get_selected_items(self):
|
|
"""Get selected tasks.
|
|
|
|
If value has changed then will return list with single item.
|
|
|
|
Returns:
|
|
list: Selected tasks.
|
|
"""
|
|
return list(self._selected_items)
|
|
|
|
def set_asset_names(self, asset_names):
|
|
"""Set asset names for which should show tasks."""
|
|
self._ignore_index_change = True
|
|
|
|
self._model.set_asset_names(asset_names)
|
|
self._proxy_model.set_filter_empty(False)
|
|
self._proxy_model.sort(0)
|
|
|
|
self._ignore_index_change = False
|
|
|
|
# It is a bug if not exactly one asset got here
|
|
if len(asset_names) != 1:
|
|
self.set_selected_item("")
|
|
self._set_is_valid(False)
|
|
return
|
|
|
|
asset_name = tuple(asset_names)[0]
|
|
|
|
is_valid = False
|
|
if self._selected_items:
|
|
is_valid = True
|
|
|
|
valid_task_names = []
|
|
for task_name in self._selected_items:
|
|
_is_valid = self._model.is_task_name_valid(asset_name, task_name)
|
|
if _is_valid:
|
|
valid_task_names.append(task_name)
|
|
else:
|
|
is_valid = _is_valid
|
|
|
|
self._selected_items = valid_task_names
|
|
if len(self._selected_items) == 0:
|
|
self.set_selected_item("")
|
|
|
|
elif len(self._selected_items) == 1:
|
|
self.set_selected_item(self._selected_items[0])
|
|
|
|
else:
|
|
multiselection_text = self._multiselection_text
|
|
if multiselection_text is None:
|
|
multiselection_text = "|".join(self._selected_items)
|
|
self.set_selected_item(multiselection_text)
|
|
|
|
self._set_is_valid(is_valid)
|
|
|
|
def set_selected_items(self, asset_task_combinations=None):
|
|
"""Set items for selected instances.
|
|
|
|
Args:
|
|
asset_task_combinations (list): List of tuples. Each item in
|
|
the list contain asset name and task name.
|
|
"""
|
|
self._proxy_model.set_filter_empty(False)
|
|
self._proxy_model.sort(0)
|
|
|
|
if asset_task_combinations is None:
|
|
asset_task_combinations = []
|
|
|
|
task_names = set()
|
|
task_names_by_asset_name = collections.defaultdict(set)
|
|
for asset_name, task_name in asset_task_combinations:
|
|
task_names.add(task_name)
|
|
task_names_by_asset_name[asset_name].add(task_name)
|
|
asset_names = set(task_names_by_asset_name.keys())
|
|
|
|
self._ignore_index_change = True
|
|
|
|
self._model.set_asset_names(asset_names)
|
|
|
|
self._has_value_changed = False
|
|
|
|
self._origin_value = copy.deepcopy(asset_task_combinations)
|
|
|
|
self._origin_selection = list(task_names)
|
|
self._selected_items = list(task_names)
|
|
# Reset current index
|
|
self.setCurrentIndex(-1)
|
|
is_valid = True
|
|
if not task_names:
|
|
self.set_selected_item("")
|
|
|
|
elif len(task_names) == 1:
|
|
task_name = tuple(task_names)[0]
|
|
idx = self.findText(task_name)
|
|
is_valid = not idx < 0
|
|
if not is_valid and len(asset_names) > 1:
|
|
is_valid = self._validate_task_names_by_asset_names(
|
|
task_names_by_asset_name
|
|
)
|
|
self.set_selected_item(task_name)
|
|
|
|
else:
|
|
for task_name in task_names:
|
|
idx = self.findText(task_name)
|
|
is_valid = not idx < 0
|
|
if not is_valid:
|
|
break
|
|
|
|
if not is_valid and len(asset_names) > 1:
|
|
is_valid = self._validate_task_names_by_asset_names(
|
|
task_names_by_asset_name
|
|
)
|
|
multiselection_text = self._multiselection_text
|
|
if multiselection_text is None:
|
|
multiselection_text = "|".join(task_names)
|
|
self.set_selected_item(multiselection_text)
|
|
|
|
self._set_is_valid(is_valid)
|
|
|
|
self._ignore_index_change = False
|
|
|
|
self.value_changed.emit()
|
|
|
|
def _validate_task_names_by_asset_names(self, task_names_by_asset_name):
|
|
for asset_name, task_names in task_names_by_asset_name.items():
|
|
for task_name in task_names:
|
|
if not self._model.is_task_name_valid(asset_name, task_name):
|
|
return False
|
|
return True
|
|
|
|
def set_selected_item(self, item_name):
|
|
"""Set task which is set on selected instance.
|
|
|
|
Args:
|
|
item_name(str): Task name which should be selected.
|
|
"""
|
|
idx = self.findText(item_name)
|
|
# Set current index (must be set to -1 if is invalid)
|
|
self.setCurrentIndex(idx)
|
|
self.set_text(item_name)
|
|
|
|
def reset_to_origin(self):
|
|
"""Change to task names set with last `set_selected_items` call."""
|
|
self.set_selected_items(self._origin_value)
|
|
|
|
|
|
class VariantInputWidget(PlaceholderLineEdit):
|
|
"""Input widget for variant."""
|
|
value_changed = QtCore.Signal()
|
|
|
|
def __init__(self, parent):
|
|
super(VariantInputWidget, self).__init__(parent)
|
|
|
|
self.setObjectName("VariantInput")
|
|
self.setToolTip(VARIANT_TOOLTIP)
|
|
|
|
name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS)
|
|
self._name_pattern = name_pattern
|
|
self._compiled_name_pattern = re.compile(name_pattern)
|
|
|
|
self._origin_value = []
|
|
self._current_value = []
|
|
|
|
self._ignore_value_change = False
|
|
self._has_value_changed = False
|
|
self._multiselection_text = None
|
|
|
|
self._is_valid = True
|
|
|
|
self.textChanged.connect(self._on_text_change)
|
|
|
|
def is_valid(self):
|
|
"""Is variant text valid."""
|
|
return self._is_valid
|
|
|
|
def has_value_changed(self):
|
|
"""Value of variant has changed."""
|
|
return self._has_value_changed
|
|
|
|
def _set_state_property(self, state):
|
|
current_value = self.property("state")
|
|
if current_value != state:
|
|
self.setProperty("state", state)
|
|
self.style().polish(self)
|
|
|
|
def set_multiselection_text(self, text):
|
|
"""Change text of multiselection."""
|
|
self._multiselection_text = text
|
|
|
|
def _set_is_valid(self, valid):
|
|
if valid == self._is_valid:
|
|
return
|
|
self._is_valid = valid
|
|
state = ""
|
|
if not valid:
|
|
state = "invalid"
|
|
self._set_state_property(state)
|
|
|
|
def _on_text_change(self):
|
|
if self._ignore_value_change:
|
|
return
|
|
|
|
is_valid = bool(self._compiled_name_pattern.match(self.text()))
|
|
self._set_is_valid(is_valid)
|
|
|
|
self._current_value = [self.text()]
|
|
self._has_value_changed = self._current_value != self._origin_value
|
|
|
|
self.value_changed.emit()
|
|
|
|
def reset_to_origin(self):
|
|
"""Set origin value of selected instances."""
|
|
self.set_value(self._origin_value)
|
|
|
|
def get_value(self):
|
|
"""Get current value.
|
|
|
|
Origin value returned if didn't change.
|
|
"""
|
|
return copy.deepcopy(self._current_value)
|
|
|
|
def set_value(self, variants=None):
|
|
"""Set value of currently selected instances."""
|
|
if variants is None:
|
|
variants = []
|
|
|
|
self._ignore_value_change = True
|
|
|
|
self._origin_value = list(variants)
|
|
self._current_value = list(variants)
|
|
|
|
self.setPlaceholderText("")
|
|
if not variants:
|
|
self.setText("")
|
|
|
|
elif len(variants) == 1:
|
|
self.setText(self._current_value[0])
|
|
|
|
else:
|
|
multiselection_text = self._multiselection_text
|
|
if multiselection_text is None:
|
|
multiselection_text = "|".join(variants)
|
|
self.setText("")
|
|
self.setPlaceholderText(multiselection_text)
|
|
|
|
self._ignore_value_change = False
|
|
|
|
|
|
class MultipleItemWidget(QtWidgets.QWidget):
|
|
"""Widget for immutable text which can have more than one value.
|
|
|
|
Content may be bigger than widget's size and does not have scroll but has
|
|
flick widget on top (is possible to move around with clicked mouse).
|
|
"""
|
|
|
|
def __init__(self, parent):
|
|
super(MultipleItemWidget, self).__init__(parent)
|
|
|
|
model = QtGui.QStandardItemModel()
|
|
|
|
view = QtWidgets.QListView(self)
|
|
view.setObjectName("MultipleItemView")
|
|
view.setLayoutMode(QtWidgets.QListView.Batched)
|
|
view.setViewMode(QtWidgets.QListView.IconMode)
|
|
view.setResizeMode(QtWidgets.QListView.Adjust)
|
|
view.setWrapping(False)
|
|
view.setSpacing(2)
|
|
view.setModel(model)
|
|
view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
|
|
flick = FlickCharm(parent=view)
|
|
flick.activateOn(view)
|
|
|
|
layout = QtWidgets.QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.addWidget(view)
|
|
|
|
self._view = view
|
|
self._model = model
|
|
|
|
self._value = []
|
|
|
|
def showEvent(self, event):
|
|
super(MultipleItemWidget, self).showEvent(event)
|
|
tmp_item = None
|
|
if not self._value:
|
|
# Add temp item to be able calculate maximum height of widget
|
|
tmp_item = QtGui.QStandardItem("tmp")
|
|
self._model.appendRow(tmp_item)
|
|
|
|
height = self._view.sizeHintForRow(0)
|
|
self.setMaximumHeight(height + (2 * self._view.spacing()))
|
|
|
|
if tmp_item is not None:
|
|
self._model.clear()
|
|
|
|
def set_value(self, value=None):
|
|
"""Set value/s of currently selected instance."""
|
|
if value is None:
|
|
value = []
|
|
self._value = value
|
|
|
|
self._model.clear()
|
|
for item_text in value:
|
|
item = QtGui.QStandardItem(item_text)
|
|
item.setEditable(False)
|
|
item.setSelectable(False)
|
|
self._model.appendRow(item)
|
|
|
|
|
|
class GlobalAttrsWidget(QtWidgets.QWidget):
|
|
"""Global attributes mainly to define context and subset name of instances.
|
|
|
|
Subset name is or may be affected on context. Gives abiity to modify
|
|
context and subset name of instance. This change is not autopromoted but
|
|
must be submitted.
|
|
|
|
Warning: Until artist hit `Submit` changes must not be propagated to
|
|
instance data.
|
|
|
|
Global attributes contain these widgets:
|
|
Variant: [ text input ]
|
|
Asset: [ asset dialog ]
|
|
Task: [ combobox ]
|
|
Family: [ immutable ]
|
|
Subset name: [ immutable ]
|
|
[Submit] [Cancel]
|
|
"""
|
|
instance_context_changed = QtCore.Signal()
|
|
|
|
multiselection_text = "< Multiselection >"
|
|
unknown_value = "N/A"
|
|
|
|
def __init__(self, controller, parent):
|
|
super(GlobalAttrsWidget, self).__init__(parent)
|
|
|
|
self.controller = controller
|
|
self._current_instances = []
|
|
|
|
variant_input = VariantInputWidget(self)
|
|
asset_value_widget = AssetsField(controller, self)
|
|
task_value_widget = TasksCombobox(controller, self)
|
|
family_value_widget = MultipleItemWidget(self)
|
|
subset_value_widget = MultipleItemWidget(self)
|
|
|
|
variant_input.set_multiselection_text(self.multiselection_text)
|
|
asset_value_widget.set_multiselection_text(self.multiselection_text)
|
|
task_value_widget.set_multiselection_text(self.multiselection_text)
|
|
|
|
variant_input.set_value()
|
|
asset_value_widget.set_selected_items()
|
|
task_value_widget.set_selected_items()
|
|
family_value_widget.set_value()
|
|
subset_value_widget.set_value()
|
|
|
|
submit_btn = QtWidgets.QPushButton("Confirm", self)
|
|
cancel_btn = QtWidgets.QPushButton("Cancel", self)
|
|
submit_btn.setEnabled(False)
|
|
cancel_btn.setEnabled(False)
|
|
|
|
btns_layout = QtWidgets.QHBoxLayout()
|
|
btns_layout.setContentsMargins(0, 0, 0, 0)
|
|
btns_layout.addStretch(1)
|
|
btns_layout.addWidget(submit_btn)
|
|
btns_layout.addWidget(cancel_btn)
|
|
|
|
main_layout = QtWidgets.QFormLayout(self)
|
|
main_layout.addRow("Variant", variant_input)
|
|
main_layout.addRow("Asset", asset_value_widget)
|
|
main_layout.addRow("Task", task_value_widget)
|
|
main_layout.addRow("Family", family_value_widget)
|
|
main_layout.addRow("Subset", subset_value_widget)
|
|
main_layout.addRow(btns_layout)
|
|
|
|
variant_input.value_changed.connect(self._on_variant_change)
|
|
asset_value_widget.value_changed.connect(self._on_asset_change)
|
|
task_value_widget.value_changed.connect(self._on_task_change)
|
|
submit_btn.clicked.connect(self._on_submit)
|
|
cancel_btn.clicked.connect(self._on_cancel)
|
|
|
|
self.variant_input = variant_input
|
|
self.asset_value_widget = asset_value_widget
|
|
self.task_value_widget = task_value_widget
|
|
self.family_value_widget = family_value_widget
|
|
self.subset_value_widget = subset_value_widget
|
|
self.submit_btn = submit_btn
|
|
self.cancel_btn = cancel_btn
|
|
|
|
def _on_submit(self):
|
|
"""Commit changes for selected instances."""
|
|
variant_value = None
|
|
asset_name = None
|
|
task_name = None
|
|
if self.variant_input.has_value_changed():
|
|
variant_value = self.variant_input.get_value()[0]
|
|
|
|
if self.asset_value_widget.has_value_changed():
|
|
asset_name = self.asset_value_widget.get_selected_items()[0]
|
|
|
|
if self.task_value_widget.has_value_changed():
|
|
task_name = self.task_value_widget.get_selected_items()[0]
|
|
|
|
asset_docs_by_name = {}
|
|
asset_names = set()
|
|
if asset_name is None:
|
|
for instance in self._current_instances:
|
|
asset_names.add(instance.get("asset"))
|
|
else:
|
|
asset_names.add(asset_name)
|
|
|
|
for asset_doc in self.controller.get_asset_docs():
|
|
_asset_name = asset_doc["name"]
|
|
if _asset_name in asset_names:
|
|
asset_names.remove(_asset_name)
|
|
asset_docs_by_name[_asset_name] = asset_doc
|
|
|
|
if not asset_names:
|
|
break
|
|
|
|
project_name = self.controller.project_name
|
|
subset_names = set()
|
|
invalid_tasks = False
|
|
for instance in self._current_instances:
|
|
new_variant_value = instance.get("variant")
|
|
new_asset_name = instance.get("asset")
|
|
new_task_name = instance.get("task")
|
|
if variant_value is not None:
|
|
new_variant_value = variant_value
|
|
|
|
if asset_name is not None:
|
|
new_asset_name = asset_name
|
|
|
|
if task_name is not None:
|
|
new_task_name = task_name
|
|
|
|
asset_doc = asset_docs_by_name[new_asset_name]
|
|
|
|
try:
|
|
new_subset_name = instance.creator.get_subset_name(
|
|
new_variant_value, new_task_name, asset_doc, project_name
|
|
)
|
|
except TaskNotSetError:
|
|
invalid_tasks = True
|
|
instance.set_task_invalid(True)
|
|
subset_names.add(instance["subset"])
|
|
continue
|
|
|
|
subset_names.add(new_subset_name)
|
|
if variant_value is not None:
|
|
instance["variant"] = variant_value
|
|
|
|
if asset_name is not None:
|
|
instance["asset"] = asset_name
|
|
instance.set_asset_invalid(False)
|
|
|
|
if task_name is not None:
|
|
instance["task"] = task_name or None
|
|
instance.set_task_invalid(False)
|
|
|
|
instance["subset"] = new_subset_name
|
|
|
|
if invalid_tasks:
|
|
self.task_value_widget.set_invalid_empty_task()
|
|
|
|
self.subset_value_widget.set_value(subset_names)
|
|
|
|
self._set_btns_enabled(False)
|
|
self._set_btns_visible(invalid_tasks)
|
|
|
|
self.instance_context_changed.emit()
|
|
|
|
def _on_cancel(self):
|
|
"""Cancel changes and set back to their irigin value."""
|
|
self.variant_input.reset_to_origin()
|
|
self.asset_value_widget.reset_to_origin()
|
|
self.task_value_widget.reset_to_origin()
|
|
self._set_btns_enabled(False)
|
|
|
|
def _on_value_change(self):
|
|
any_invalid = (
|
|
not self.variant_input.is_valid()
|
|
or not self.asset_value_widget.is_valid()
|
|
or not self.task_value_widget.is_valid()
|
|
)
|
|
any_changed = (
|
|
self.variant_input.has_value_changed()
|
|
or self.asset_value_widget.has_value_changed()
|
|
or self.task_value_widget.has_value_changed()
|
|
)
|
|
self._set_btns_visible(any_changed or any_invalid)
|
|
self.cancel_btn.setEnabled(any_changed)
|
|
self.submit_btn.setEnabled(not any_invalid)
|
|
|
|
def _on_variant_change(self):
|
|
self._on_value_change()
|
|
|
|
def _on_asset_change(self):
|
|
asset_names = self.asset_value_widget.get_selected_items()
|
|
self.task_value_widget.set_asset_names(asset_names)
|
|
self._on_value_change()
|
|
|
|
def _on_task_change(self):
|
|
self._on_value_change()
|
|
|
|
def _set_btns_visible(self, visible):
|
|
self.cancel_btn.setVisible(visible)
|
|
self.submit_btn.setVisible(visible)
|
|
|
|
def _set_btns_enabled(self, enabled):
|
|
self.cancel_btn.setEnabled(enabled)
|
|
self.submit_btn.setEnabled(enabled)
|
|
|
|
def set_current_instances(self, instances):
|
|
"""Set currently selected instances.
|
|
|
|
Args:
|
|
instances(list<CreatedInstance>): List of selected instances.
|
|
Empty instances tells that nothing or context is selected.
|
|
"""
|
|
self._set_btns_visible(False)
|
|
|
|
self._current_instances = instances
|
|
|
|
asset_names = set()
|
|
variants = set()
|
|
families = set()
|
|
subset_names = set()
|
|
|
|
editable = True
|
|
if len(instances) == 0:
|
|
editable = False
|
|
|
|
asset_task_combinations = []
|
|
for instance in instances:
|
|
if instance.creator is None:
|
|
editable = False
|
|
|
|
variants.add(instance.get("variant") or self.unknown_value)
|
|
families.add(instance.get("family") or self.unknown_value)
|
|
asset_name = instance.get("asset") or self.unknown_value
|
|
task_name = instance.get("task") or ""
|
|
asset_names.add(asset_name)
|
|
asset_task_combinations.append((asset_name, task_name))
|
|
subset_names.add(instance.get("subset") or self.unknown_value)
|
|
|
|
self.variant_input.set_value(variants)
|
|
|
|
# Set context of asset widget
|
|
self.asset_value_widget.set_selected_items(asset_names)
|
|
# Set context of task widget
|
|
self.task_value_widget.set_selected_items(asset_task_combinations)
|
|
self.family_value_widget.set_value(families)
|
|
self.subset_value_widget.set_value(subset_names)
|
|
|
|
self.variant_input.setEnabled(editable)
|
|
self.asset_value_widget.setEnabled(editable)
|
|
self.task_value_widget.setEnabled(editable)
|
|
|
|
|
|
class CreatorAttrsWidget(QtWidgets.QWidget):
|
|
"""Widget showing creator specific attributes for selected instances.
|
|
|
|
Attributes are defined on creator so are dynamic. Their look and type is
|
|
based on attribute definitions that are defined in
|
|
`~/openpype/pipeline/lib/attribute_definitions.py` and their widget
|
|
representation in `~/openpype/widgets/attribute_defs/*`.
|
|
|
|
Widgets are disabled if context of instance is not valid.
|
|
|
|
Definitions are shown for all instance no matter if they are created with
|
|
different creators. If creator have same (similar) definitions their
|
|
widgets are merged into one (different label does not count).
|
|
"""
|
|
def __init__(self, controller, parent):
|
|
super(CreatorAttrsWidget, self).__init__(parent)
|
|
|
|
scroll_area = QtWidgets.QScrollArea(self)
|
|
scroll_area.setWidgetResizable(True)
|
|
|
|
main_layout = QtWidgets.QHBoxLayout(self)
|
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
main_layout.setSpacing(0)
|
|
main_layout.addWidget(scroll_area, 1)
|
|
|
|
self._main_layout = main_layout
|
|
|
|
self.controller = controller
|
|
self._scroll_area = scroll_area
|
|
|
|
self._attr_def_id_to_instances = {}
|
|
self._attr_def_id_to_attr_def = {}
|
|
|
|
# To store content of scroll area to prevent garbage collection
|
|
self._content_widget = None
|
|
|
|
def set_instances_valid(self, valid):
|
|
"""Change valid state of current instances."""
|
|
if (
|
|
self._content_widget is not None
|
|
and self._content_widget.isEnabled() != valid
|
|
):
|
|
self._content_widget.setEnabled(valid)
|
|
|
|
def set_current_instances(self, instances):
|
|
"""Set current instances for which are attribute definitions shown."""
|
|
prev_content_widget = self._scroll_area.widget()
|
|
if prev_content_widget:
|
|
self._scroll_area.takeWidget()
|
|
prev_content_widget.hide()
|
|
prev_content_widget.deleteLater()
|
|
|
|
self._content_widget = None
|
|
self._attr_def_id_to_instances = {}
|
|
self._attr_def_id_to_attr_def = {}
|
|
|
|
result = self.controller.get_creator_attribute_definitions(
|
|
instances
|
|
)
|
|
|
|
content_widget = QtWidgets.QWidget(self._scroll_area)
|
|
content_layout = QtWidgets.QFormLayout(content_widget)
|
|
for attr_def, attr_instances, values in result:
|
|
widget = create_widget_for_attr_def(attr_def, content_widget)
|
|
if attr_def.is_value_def:
|
|
if len(values) == 1:
|
|
value = values[0]
|
|
if value is not None:
|
|
widget.set_value(values[0])
|
|
else:
|
|
widget.set_value(values, True)
|
|
|
|
label = attr_def.label or attr_def.key
|
|
content_layout.addRow(label, widget)
|
|
widget.value_changed.connect(self._input_value_changed)
|
|
|
|
self._attr_def_id_to_instances[attr_def.id] = attr_instances
|
|
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
|
|
|
self._scroll_area.setWidget(content_widget)
|
|
self._content_widget = content_widget
|
|
|
|
def _input_value_changed(self, value, attr_id):
|
|
instances = self._attr_def_id_to_instances.get(attr_id)
|
|
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
|
|
if not instances or not attr_def:
|
|
return
|
|
|
|
for instance in instances:
|
|
creator_attributes = instance["creator_attributes"]
|
|
if attr_def.key in creator_attributes:
|
|
creator_attributes[attr_def.key] = value
|
|
|
|
|
|
class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|
"""Widget showing publsish plugin attributes for selected instances.
|
|
|
|
Attributes are defined on publish plugins. Publihs plugin may define
|
|
attribute definitions but must inherit `OpenPypePyblishPluginMixin`
|
|
(~/openpype/pipeline/publish). At the moment requires to implement
|
|
`get_attribute_defs` and `convert_attribute_values` class methods.
|
|
|
|
Look and type of attributes is based on attribute definitions that are
|
|
defined in `~/openpype/pipeline/lib/attribute_definitions.py` and their
|
|
widget representation in `~/openpype/widgets/attribute_defs/*`.
|
|
|
|
Widgets are disabled if context of instance is not valid.
|
|
|
|
Definitions are shown for all instance no matter if they have different
|
|
families. Similar definitions are merged into one (different label
|
|
does not count).
|
|
"""
|
|
def __init__(self, controller, parent):
|
|
super(PublishPluginAttrsWidget, self).__init__(parent)
|
|
|
|
scroll_area = QtWidgets.QScrollArea(self)
|
|
scroll_area.setWidgetResizable(True)
|
|
|
|
main_layout = QtWidgets.QHBoxLayout(self)
|
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
main_layout.setSpacing(0)
|
|
main_layout.addWidget(scroll_area, 1)
|
|
|
|
self._main_layout = main_layout
|
|
|
|
self.controller = controller
|
|
self._scroll_area = scroll_area
|
|
|
|
self._attr_def_id_to_instances = {}
|
|
self._attr_def_id_to_attr_def = {}
|
|
self._attr_def_id_to_plugin_name = {}
|
|
|
|
# Store content of scroll area to prevent garbage collection
|
|
self._content_widget = None
|
|
|
|
def set_instances_valid(self, valid):
|
|
"""Change valid state of current instances."""
|
|
if (
|
|
self._content_widget is not None
|
|
and self._content_widget.isEnabled() != valid
|
|
):
|
|
self._content_widget.setEnabled(valid)
|
|
|
|
def set_current_instances(self, instances, context_selected):
|
|
"""Set current instances for which are attribute definitions shown."""
|
|
prev_content_widget = self._scroll_area.widget()
|
|
if prev_content_widget:
|
|
self._scroll_area.takeWidget()
|
|
prev_content_widget.hide()
|
|
prev_content_widget.deleteLater()
|
|
|
|
self._content_widget = None
|
|
|
|
self._attr_def_id_to_instances = {}
|
|
self._attr_def_id_to_attr_def = {}
|
|
self._attr_def_id_to_plugin_name = {}
|
|
|
|
result = self.controller.get_publish_attribute_definitions(
|
|
instances, context_selected
|
|
)
|
|
|
|
content_widget = QtWidgets.QWidget(self._scroll_area)
|
|
content_layout = QtWidgets.QFormLayout(content_widget)
|
|
for plugin_name, attr_defs, all_plugin_values in result:
|
|
plugin_values = all_plugin_values[plugin_name]
|
|
|
|
for attr_def in attr_defs:
|
|
widget = create_widget_for_attr_def(
|
|
attr_def, content_widget
|
|
)
|
|
label = attr_def.label or attr_def.key
|
|
content_layout.addRow(label, widget)
|
|
|
|
widget.value_changed.connect(self._input_value_changed)
|
|
|
|
attr_values = plugin_values[attr_def.key]
|
|
multivalue = len(attr_values) > 1
|
|
values = []
|
|
instances = []
|
|
for instance, value in attr_values:
|
|
values.append(value)
|
|
instances.append(instance)
|
|
|
|
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
|
self._attr_def_id_to_instances[attr_def.id] = instances
|
|
self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name
|
|
|
|
if multivalue:
|
|
widget.set_value(values, multivalue)
|
|
else:
|
|
widget.set_value(values[0])
|
|
|
|
self._scroll_area.setWidget(content_widget)
|
|
self._content_widget = content_widget
|
|
|
|
def _input_value_changed(self, value, attr_id):
|
|
instances = self._attr_def_id_to_instances.get(attr_id)
|
|
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
|
|
plugin_name = self._attr_def_id_to_plugin_name.get(attr_id)
|
|
if not instances or not attr_def or not plugin_name:
|
|
return
|
|
|
|
for instance in instances:
|
|
plugin_val = instance.publish_attributes[plugin_name]
|
|
plugin_val[attr_def.key] = value
|
|
|
|
|
|
class SubsetAttributesWidget(QtWidgets.QWidget):
|
|
"""Wrapper widget where attributes of instance/s are modified.
|
|
┌─────────────────┬─────────────┐
|
|
│ Global │ │
|
|
│ attributes │ Thumbnail │ TOP
|
|
│ │ │
|
|
├─────────────┬───┴─────────────┤
|
|
│ Family │ Publish │
|
|
│ attributes │ plugin │ BOTTOM
|
|
│ │ attributes │
|
|
└───────────────────────────────┘
|
|
"""
|
|
instance_context_changed = QtCore.Signal()
|
|
|
|
def __init__(self, controller, parent):
|
|
super(SubsetAttributesWidget, self).__init__(parent)
|
|
|
|
# TOP PART
|
|
top_widget = QtWidgets.QWidget(self)
|
|
|
|
# Global attributes
|
|
global_attrs_widget = GlobalAttrsWidget(controller, top_widget)
|
|
thumbnail_widget = ThumbnailWidget(top_widget)
|
|
|
|
top_layout = QtWidgets.QHBoxLayout(top_widget)
|
|
top_layout.setContentsMargins(0, 0, 0, 0)
|
|
top_layout.addWidget(global_attrs_widget, 7)
|
|
top_layout.addWidget(thumbnail_widget, 3)
|
|
|
|
# BOTTOM PART
|
|
bottom_widget = QtWidgets.QWidget(self)
|
|
creator_attrs_widget = CreatorAttrsWidget(
|
|
controller, bottom_widget
|
|
)
|
|
publish_attrs_widget = PublishPluginAttrsWidget(
|
|
controller, bottom_widget
|
|
)
|
|
|
|
bottom_separator = QtWidgets.QWidget(bottom_widget)
|
|
bottom_separator.setObjectName("Separator")
|
|
bottom_separator.setMinimumWidth(1)
|
|
|
|
bottom_layout = QtWidgets.QHBoxLayout(bottom_widget)
|
|
bottom_layout.setContentsMargins(0, 0, 0, 0)
|
|
bottom_layout.addWidget(creator_attrs_widget, 1)
|
|
bottom_layout.addWidget(bottom_separator, 0)
|
|
bottom_layout.addWidget(publish_attrs_widget, 1)
|
|
|
|
top_bottom = QtWidgets.QWidget(self)
|
|
top_bottom.setObjectName("Separator")
|
|
top_bottom.setMinimumHeight(1)
|
|
|
|
layout = QtWidgets.QVBoxLayout(self)
|
|
layout.addWidget(top_widget, 0)
|
|
layout.addWidget(top_bottom, 0)
|
|
layout.addWidget(bottom_widget, 1)
|
|
|
|
self._current_instances = None
|
|
self._context_selected = False
|
|
self._all_instances_valid = True
|
|
|
|
global_attrs_widget.instance_context_changed.connect(
|
|
self._on_instance_context_changed
|
|
)
|
|
|
|
self.controller = controller
|
|
|
|
self.global_attrs_widget = global_attrs_widget
|
|
|
|
self.creator_attrs_widget = creator_attrs_widget
|
|
self.publish_attrs_widget = publish_attrs_widget
|
|
self.thumbnail_widget = thumbnail_widget
|
|
|
|
self.top_bottom = top_bottom
|
|
self.bottom_separator = bottom_separator
|
|
|
|
def _on_instance_context_changed(self):
|
|
all_valid = True
|
|
for instance in self._current_instances:
|
|
if not instance.has_valid_context:
|
|
all_valid = False
|
|
break
|
|
|
|
self._all_instances_valid = all_valid
|
|
self.creator_attrs_widget.set_instances_valid(all_valid)
|
|
self.publish_attrs_widget.set_instances_valid(all_valid)
|
|
|
|
self.instance_context_changed.emit()
|
|
|
|
def set_current_instances(self, instances, context_selected):
|
|
"""Change currently selected items.
|
|
|
|
Args:
|
|
instances(list<CreatedInstance>): List of currently selected
|
|
instances.
|
|
context_selected(bool): Is context selected.
|
|
"""
|
|
all_valid = True
|
|
for instance in instances:
|
|
if not instance.has_valid_context:
|
|
all_valid = False
|
|
break
|
|
|
|
self._current_instances = instances
|
|
self._context_selected = context_selected
|
|
self._all_instances_valid = all_valid
|
|
|
|
self.global_attrs_widget.set_current_instances(instances)
|
|
self.creator_attrs_widget.set_current_instances(instances)
|
|
self.publish_attrs_widget.set_current_instances(
|
|
instances, context_selected
|
|
)
|
|
self.creator_attrs_widget.set_instances_valid(all_valid)
|
|
self.publish_attrs_widget.set_instances_valid(all_valid)
|
|
|
|
|
|
class ThumbnailWidget(QtWidgets.QWidget):
|
|
"""Instance thumbnail widget.
|
|
|
|
Logic implementation of this widget is missing but widget is used
|
|
to offset `GlobalAttrsWidget` inputs visually.
|
|
"""
|
|
def __init__(self, parent):
|
|
super(ThumbnailWidget, self).__init__(parent)
|
|
|
|
# Missing implementation for thumbnail
|
|
# - widget kept to make a visial offset of global attr widget offset
|
|
# default_pix = get_pixmap("thumbnail")
|
|
default_pix = QtGui.QPixmap(10, 10)
|
|
default_pix.fill(QtCore.Qt.transparent)
|
|
|
|
thumbnail_label = QtWidgets.QLabel(self)
|
|
thumbnail_label.setPixmap(
|
|
default_pix.scaled(
|
|
200, 100,
|
|
QtCore.Qt.KeepAspectRatio,
|
|
QtCore.Qt.SmoothTransformation
|
|
)
|
|
)
|
|
|
|
layout = QtWidgets.QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.addWidget(thumbnail_label, alignment=QtCore.Qt.AlignCenter)
|
|
|
|
self.thumbnail_label = thumbnail_label
|
|
self.default_pix = default_pix
|
|
self.current_pix = None
|