creator dialog has context widget and creator's attributes

This commit is contained in:
iLLiCiTiT 2022-01-18 17:25:34 +01:00
parent 9c6a57aa58
commit f0b7f72325
5 changed files with 201 additions and 369 deletions

View file

@ -15,6 +15,9 @@ from openpype.pipeline.create import (
)
from .widgets import IconValuePixmapLabel
from .assets_widget import CreateDialogAssetsWidget
from .tasks_widget import CreateDialogTasksWidget
from .precreate_widget import AttributesWidget
from ..constants import (
VARIANT_TOOLTIP,
CREATOR_IDENTIFIER_ROLE,
@ -202,7 +205,34 @@ class CreateDialog(QtWidgets.QDialog):
self._name_pattern = name_pattern
self._compiled_name_pattern = re.compile(name_pattern)
context_widget = QtWidgets.QWidget(self)
assets_widget = CreateDialogAssetsWidget(controller, context_widget)
tasks_widget = CreateDialogTasksWidget(controller, context_widget)
context_layout = QtWidgets.QVBoxLayout(context_widget)
context_layout.setContentsMargins(0, 0, 0, 0)
context_layout.setSpacing(0)
context_layout.addWidget(assets_widget, 2)
context_layout.addWidget(tasks_widget, 1)
pre_create_scroll_area = QtWidgets.QScrollArea(self)
pre_create_contet_widget = QtWidgets.QWidget(pre_create_scroll_area)
pre_create_scroll_area.setWidget(pre_create_contet_widget)
pre_create_scroll_area.setWidgetResizable(True)
pre_create_contet_layout = QtWidgets.QVBoxLayout(
pre_create_contet_widget
)
pre_create_attributes_widget = AttributesWidget(
pre_create_contet_widget
)
pre_create_contet_layout.addWidget(pre_create_attributes_widget, 0)
pre_create_contet_layout.addStretch(1)
creator_description_widget = CreatorDescriptionWidget(self)
# TODO add HELP button
creator_description_widget.setVisible(False)
creators_view = QtWidgets.QListView(self)
creators_model = QtGui.QStandardItemModel()
@ -235,6 +265,14 @@ class CreateDialog(QtWidgets.QDialog):
form_layout.addRow("Name:", variant_layout)
form_layout.addRow("Subset:", subset_name_input)
mid_widget = QtWidgets.QWidget(self)
mid_layout = QtWidgets.QVBoxLayout(mid_widget)
mid_layout.setContentsMargins(0, 0, 0, 0)
mid_layout.addWidget(QtWidgets.QLabel("Choose family:", self))
mid_layout.addWidget(creators_view, 1)
mid_layout.addLayout(form_layout, 0)
mid_layout.addWidget(create_btn, 0)
left_layout = QtWidgets.QVBoxLayout()
left_layout.addWidget(QtWidgets.QLabel("Choose family:", self))
left_layout.addWidget(creators_view, 1)
@ -242,20 +280,36 @@ class CreateDialog(QtWidgets.QDialog):
left_layout.addWidget(create_btn, 0)
layout = QtWidgets.QHBoxLayout(self)
layout.addLayout(left_layout, 0)
layout.addSpacing(5)
layout.addWidget(creator_description_widget, 1)
layout.setSpacing(10)
layout.addWidget(context_widget, 1)
layout.addWidget(mid_widget, 1)
layout.addWidget(pre_create_scroll_area, 1)
prereq_timer = QtCore.QTimer()
prereq_timer.setInterval(50)
prereq_timer.setSingleShot(True)
prereq_timer.timeout.connect(self._on_prereq_timer)
create_btn.clicked.connect(self._on_create)
variant_input.returnPressed.connect(self._on_create)
variant_input.textChanged.connect(self._on_variant_change)
creators_view.selectionModel().currentChanged.connect(
self._on_item_change
self._on_creator_item_change
)
variant_hints_menu.triggered.connect(self._on_variant_action)
assets_widget.selection_changed.connect(self._on_asset_change)
assets_widget.current_context_required.connect(
self._on_current_session_context_request
)
tasks_widget.task_changed.connect(self._on_task_change)
controller.add_plugins_refresh_callback(self._on_plugins_refresh)
self._pre_create_attributes_widget = pre_create_attributes_widget
self._context_widget = context_widget
self._assets_widget = assets_widget
self._tasks_widget = tasks_widget
self.creator_description_widget = creator_description_widget
self.subset_name_input = subset_name_input
@ -269,12 +323,54 @@ class CreateDialog(QtWidgets.QDialog):
self.creators_view = creators_view
self.create_btn = create_btn
self._prereq_timer = prereq_timer
def _context_change_is_enabled(self):
return self._context_widget.isEnabled()
def _get_asset_name(self):
asset_name = None
if self._context_change_is_enabled():
asset_name = self._assets_widget.get_selected_asset_name()
if asset_name is None:
asset_name = self._asset_name
return asset_name
def _get_task_name(self):
task_name = None
if self._context_change_is_enabled():
# Don't use selection of task if asset is not set
asset_name = self._assets_widget.get_selected_asset_name()
if asset_name:
task_name = self._tasks_widget.get_selected_task_name()
if not task_name:
task_name = self._task_name
return task_name
@property
def dbcon(self):
return self.controller.dbcon
def _set_context_enabled(self, enabled):
self._assets_widget.set_enabled(enabled)
self._tasks_widget.set_enabled(enabled)
self._context_widget.setEnabled(enabled)
def refresh(self):
self._prereq_available = True
# Get context before refresh to keep selection of asset and
# task widgets
asset_name = self._get_asset_name()
task_name = self._get_task_name()
self._prereq_available = False
# Disable context widget so refresh of asset will use context asset
# name
self._set_context_enabled(False)
self._assets_widget.refresh()
# Refresh data before update of creators
self._refresh_asset()
@ -282,21 +378,36 @@ class CreateDialog(QtWidgets.QDialog):
# data
self._refresh_creators()
self._assets_widget.set_current_asset_name(self._asset_name)
self._assets_widget.select_asset_by_name(asset_name)
self._tasks_widget.set_asset_name(asset_name)
self._tasks_widget.select_task_name(task_name)
self._invalidate_prereq()
def _invalidate_prereq(self):
self._prereq_timer.start()
def _on_prereq_timer(self):
prereq_available = True
if self.creators_model.rowCount() < 1:
prereq_available = False
if self._asset_doc is None:
# QUESTION how to handle invalid asset?
self.subset_name_input.setText("< Asset is not set >")
self._prereq_available = False
prereq_available = False
if self.creators_model.rowCount() < 1:
self._prereq_available = False
if prereq_available != self._prereq_available:
self._prereq_available = prereq_available
self.create_btn.setEnabled(self._prereq_available)
self.creators_view.setEnabled(self._prereq_available)
self.variant_input.setEnabled(self._prereq_available)
self.variant_hints_btn.setEnabled(self._prereq_available)
self.create_btn.setEnabled(prereq_available)
self.creators_view.setEnabled(prereq_available)
self.variant_input.setEnabled(prereq_available)
self.variant_hints_btn.setEnabled(prereq_available)
self._on_variant_change()
def _refresh_asset(self):
asset_name = self._asset_name
asset_name = self._get_asset_name()
# Skip if asset did not change
if self._asset_doc and self._asset_doc["name"] == asset_name:
@ -324,6 +435,9 @@ class CreateDialog(QtWidgets.QDialog):
)
self._subset_names = set(subset_docs.distinct("name"))
if not asset_doc:
self.subset_name_input.setText("< Asset is not set >")
def _refresh_creators(self):
# Refresh creators and add their families to list
existing_items = {}
@ -366,25 +480,62 @@ class CreateDialog(QtWidgets.QDialog):
if not indexes:
index = self.creators_model.index(0, 0)
self.creators_view.setCurrentIndex(index)
else:
index = indexes[0]
identifier = index.data(CREATOR_IDENTIFIER_ROLE)
self._set_creator(identifier)
def _on_plugins_refresh(self):
# Trigger refresh only if is visible
if self.isVisible():
self.refresh()
def _on_item_change(self, new_index, _old_index):
def _on_asset_change(self):
self._refresh_asset()
asset_name = self._assets_widget.get_selected_asset_name()
self._tasks_widget.set_asset_name(asset_name)
if self._context_change_is_enabled():
self._invalidate_prereq()
def _on_task_change(self):
if self._context_change_is_enabled():
self._invalidate_prereq()
def _on_current_session_context_request(self):
self._assets_widget.set_current_session_asset()
if self._task_name:
self._tasks_widget.select_task_name(self._task_name)
def _on_creator_item_change(self, new_index, _old_index):
identifier = None
if new_index.isValid():
identifier = new_index.data(CREATOR_IDENTIFIER_ROLE)
self._set_creator(identifier)
def _set_creator(self, identifier):
creator = self.controller.manual_creators.get(identifier)
self.creator_description_widget.set_plugin(creator)
self._selected_creator = creator
if not creator:
self._pre_create_attributes_widget.set_attr_defs([])
self._set_context_enabled(False)
return
if (
creator.create_allow_context_change
!= self._context_change_is_enabled()
):
self._set_context_enabled(creator.create_allow_context_change)
self._refresh_asset()
attr_defs = creator.get_pre_create_attr_defs()
self._pre_create_attributes_widget.set_attr_defs(attr_defs)
default_variants = creator.get_default_variants()
if not default_variants:
default_variants = ["Main"]
@ -410,12 +561,19 @@ class CreateDialog(QtWidgets.QDialog):
if self.variant_input.text() != value:
self.variant_input.setText(value)
def _on_variant_change(self, variant_value):
if not self._prereq_available or not self._selected_creator:
def _on_variant_change(self, variant_value=None):
if not self._prereq_available:
return
# This should probably never happen?
if not self._selected_creator:
if self.subset_name_input.text():
self.subset_name_input.setText("")
return
if variant_value is None:
variant_value = self.variant_input.text()
match = self._compiled_name_pattern.match(variant_value)
valid = bool(match)
self.create_btn.setEnabled(valid)
@ -425,7 +583,7 @@ class CreateDialog(QtWidgets.QDialog):
return
project_name = self.controller.project_name
task_name = self._task_name
task_name = self._get_task_name()
asset_doc = copy.deepcopy(self._asset_doc)
# Calculate subset name with Creator plugin
@ -522,9 +680,9 @@ class CreateDialog(QtWidgets.QDialog):
family = index.data(FAMILY_ROLE)
subset_name = self.subset_name_input.text()
variant = self.variant_input.text()
asset_name = self._asset_name
task_name = self._task_name
options = {}
asset_name = self._get_asset_name()
task_name = self._get_task_name()
pre_create_data = self._pre_create_attributes_widget.current_value()
# Where to define these data?
# - what data show be stored?
instance_data = {
@ -537,7 +695,7 @@ class CreateDialog(QtWidgets.QDialog):
error_info = None
try:
self.controller.create(
creator_identifier, subset_name, instance_data, options
creator_identifier, subset_name, instance_data, pre_create_data
)
except CreatorError as exc:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,201 +0,0 @@
import re
import collections
from Qt import QtCore, QtGui
class AssetsHierarchyModel(QtGui.QStandardItemModel):
"""Assets hiearrchy model.
For selecting asset for which should beinstance created.
Uses controller to load asset hierarchy. All asset documents are stored by
their parents.
"""
def __init__(self, controller):
super(AssetsHierarchyModel, self).__init__()
self._controller = controller
self._items_by_name = {}
def reset(self):
self.clear()
self._items_by_name = {}
assets_by_parent_id = self._controller.get_asset_hierarchy()
items_by_name = {}
_queue = collections.deque()
_queue.append((self.invisibleRootItem(), None))
while _queue:
parent_item, parent_id = _queue.popleft()
children = assets_by_parent_id.get(parent_id)
if not children:
continue
children_by_name = {
child["name"]: child
for child in children
}
items = []
for name in sorted(children_by_name.keys()):
child = children_by_name[name]
item = QtGui.QStandardItem(name)
items_by_name[name] = item
items.append(item)
_queue.append((item, child["_id"]))
parent_item.appendRows(items)
self._items_by_name = items_by_name
def name_is_valid(self, item_name):
return item_name in self._items_by_name
def get_index_by_name(self, item_name):
item = self._items_by_name.get(item_name)
if item:
return item.index()
return QtCore.QModelIndex()
class TasksModel(QtGui.QStandardItemModel):
"""Tasks model.
Task model must have set context of asset documents.
Items in model are based on 0-infinite asset documents. Always contain
an interserction of context asset tasks. When no assets are in context
them model is empty if 2 or more are in context assets that don't have
tasks with same names then model is empty too.
Args:
controller (PublisherController): Controller which handles creation and
publishing.
"""
def __init__(self, controller):
super(TasksModel, self).__init__()
self._controller = controller
self._items_by_name = {}
self._asset_names = []
self._task_names_by_asset_name = {}
def set_asset_names(self, asset_names):
"""Set assets context."""
self._asset_names = asset_names
self.reset()
@staticmethod
def get_intersection_of_tasks(task_names_by_asset_name):
"""Calculate intersection of task names from passed data.
Example:
```
# Passed `task_names_by_asset_name`
{
"asset_1": ["compositing", "animation"],
"asset_2": ["compositing", "editorial"]
}
```
Result:
```
# Set
{"compositing"}
```
Args:
task_names_by_asset_name (dict): Task names in iterable by parent.
"""
tasks = None
for task_names in task_names_by_asset_name.values():
if tasks is None:
tasks = set(task_names)
else:
tasks &= set(task_names)
if not tasks:
break
return tasks or set()
def is_task_name_valid(self, asset_name, task_name):
"""Is task name available for asset.
Args:
asset_name (str): Name of asset where should look for task.
task_name (str): Name of task which should be available in asset's
tasks.
"""
task_names = self._task_names_by_asset_name.get(asset_name)
if task_names and task_name in task_names:
return True
return False
def reset(self):
"""Update model by current context."""
if not self._asset_names:
self._items_by_name = {}
self._task_names_by_asset_name = {}
self.clear()
return
task_names_by_asset_name = (
self._controller.get_task_names_by_asset_names(self._asset_names)
)
self._task_names_by_asset_name = task_names_by_asset_name
new_task_names = self.get_intersection_of_tasks(
task_names_by_asset_name
)
old_task_names = set(self._items_by_name.keys())
if new_task_names == old_task_names:
return
root_item = self.invisibleRootItem()
for task_name in old_task_names:
if task_name not in new_task_names:
item = self._items_by_name.pop(task_name)
root_item.removeRow(item.row())
new_items = []
for task_name in new_task_names:
if task_name in self._items_by_name:
continue
item = QtGui.QStandardItem(task_name)
self._items_by_name[task_name] = item
new_items.append(item)
root_item.appendRows(new_items)
class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel):
"""Recursive proxy model.
Item is not filtered if any children match the filter.
Use case: Filtering by string - parent won't be filtered if does not match
the filter string but first checks if any children does.
"""
def filterAcceptsRow(self, row, parent_index):
regex = self.filterRegExp()
if not regex.isEmpty():
model = self.sourceModel()
source_index = model.index(
row, self.filterKeyColumn(), parent_index
)
if source_index.isValid():
pattern = regex.pattern()
# Check current index itself
value = model.data(source_index, self.filterRole())
if re.search(pattern, value, re.IGNORECASE):
return True
rows = model.rowCount(source_index)
for idx in range(rows):
if self.filterAcceptsRow(idx, source_index):
return True
return False
return super(RecursiveSortFilterProxyModel, self).filterAcceptsRow(
row, parent_index
)

View file

@ -10,11 +10,6 @@ from avalon.vendor import qtawesome
from openpype.widgets.attribute_defs import create_widget_for_attr_def
from openpype.tools import resources
from openpype.tools.flickcharm import FlickCharm
from .models import (
AssetsHierarchyModel,
TasksModel,
RecursiveSortFilterProxyModel,
)
from openpype.tools.utils import (
PlaceholderLineEdit,
IconButton,
@ -22,6 +17,8 @@ from openpype.tools.utils import (
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
@ -307,143 +304,6 @@ class AbstractInstanceView(QtWidgets.QWidget):
).format(self.__class__.__name__))
class AssetsDialog(QtWidgets.QDialog):
"""Dialog to select asset for a context of instance."""
def __init__(self, controller, parent):
super(AssetsDialog, self).__init__(parent)
self.setWindowTitle("Select asset")
model = AssetsHierarchyModel(controller)
proxy_model = RecursiveSortFilterProxyModel()
proxy_model.setSourceModel(model)
proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
filter_input = PlaceholderLineEdit(self)
filter_input.setPlaceholderText("Filter assets..")
asset_view = QtWidgets.QTreeView(self)
asset_view.setModel(proxy_model)
asset_view.setHeaderHidden(True)
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
asset_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
asset_view.setAlternatingRowColors(True)
asset_view.setSelectionBehavior(QtWidgets.QTreeView.SelectRows)
asset_view.setAllColumnsShowFocus(True)
ok_btn = QtWidgets.QPushButton("OK", self)
cancel_btn = QtWidgets.QPushButton("Cancel", self)
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(ok_btn)
btns_layout.addWidget(cancel_btn)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(filter_input, 0)
layout.addWidget(asset_view, 1)
layout.addLayout(btns_layout, 0)
filter_input.textChanged.connect(self._on_filter_change)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
self._filter_input = filter_input
self._ok_btn = ok_btn
self._cancel_btn = cancel_btn
self._model = model
self._proxy_model = proxy_model
self._asset_view = asset_view
self._selected_asset = None
# Soft refresh is enabled
# - reset will happen at all cost if soft reset is enabled
# - adds ability to call reset on multiple places without repeating
self._soft_reset_enabled = True
def showEvent(self, event):
"""Refresh asset model on show."""
super(AssetsDialog, self).showEvent(event)
# Refresh on show
self.reset(False)
def reset(self, force=True):
"""Reset asset model."""
if not force and not self._soft_reset_enabled:
return
if self._soft_reset_enabled:
self._soft_reset_enabled = False
self._model.reset()
def name_is_valid(self, name):
"""Is asset name valid.
Args:
name(str): Asset name that should be checked.
"""
# Make sure we're reset
self.reset(False)
# Valid the name by model
return self._model.name_is_valid(name)
def _on_filter_change(self, text):
"""Trigger change of filter of assets."""
self._proxy_model.setFilterFixedString(text)
def _on_cancel_clicked(self):
self.done(0)
def _on_ok_clicked(self):
index = self._asset_view.currentIndex()
asset_name = None
if index.isValid():
asset_name = index.data(QtCore.Qt.DisplayRole)
self._selected_asset = asset_name
self.done(1)
def set_selected_assets(self, asset_names):
"""Change preselected asset before showing the dialog.
This also resets model and clean filter.
"""
self.reset(False)
self._asset_view.collapseAll()
self._filter_input.setText("")
indexes = []
for asset_name in asset_names:
index = self._model.get_index_by_name(asset_name)
if index.isValid():
indexes.append(index)
if not indexes:
return
index_deque = collections.deque()
for index in indexes:
index_deque.append(index)
all_indexes = []
while index_deque:
index = index_deque.popleft()
all_indexes.append(index)
parent_index = index.parent()
if parent_index.isValid():
index_deque.append(parent_index)
for index in all_indexes:
proxy_index = self._proxy_model.mapFromSource(index)
self._asset_view.expand(proxy_index)
def get_selected_asset(self):
"""Get selected asset name."""
return self._selected_asset
class ClickableLineEdit(QtWidgets.QLineEdit):
"""QLineEdit capturing left mouse click.

View file

@ -33,7 +33,7 @@ class PublisherWindow(QtWidgets.QDialog):
default_width = 1000
default_height = 600
def __init__(self, parent=None):
def __init__(self, parent=None, reset_on_show=None):
super(PublisherWindow, self).__init__(parent)
self.setWindowTitle("OpenPype publisher")
@ -41,6 +41,9 @@ class PublisherWindow(QtWidgets.QDialog):
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
self.setWindowIcon(icon)
if reset_on_show is None:
reset_on_show = True
if parent is None:
on_top_flag = QtCore.Qt.WindowStaysOnTopHint
else:
@ -55,6 +58,7 @@ class PublisherWindow(QtWidgets.QDialog):
| on_top_flag
)
self._reset_on_show = reset_on_show
self._first_show = True
self._refreshing_instances = False
@ -117,12 +121,16 @@ class PublisherWindow(QtWidgets.QDialog):
subset_view_btns_layout.addWidget(change_view_btn)
# Layout of view and buttons
subset_view_layout = QtWidgets.QVBoxLayout()
# - widget 'subset_view_widget' is necessary
# - only layout won't be resized automatically to minimum size hint
# on child resize request!
subset_view_widget = QtWidgets.QWidget(subset_views_widget)
subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget)
subset_view_layout.setContentsMargins(0, 0, 0, 0)
subset_view_layout.addLayout(subset_views_layout, 1)
subset_view_layout.addLayout(subset_view_btns_layout, 0)
subset_views_widget.set_center_widget(subset_view_layout)
subset_views_widget.set_center_widget(subset_view_widget)
# Whole subset layout with attributes and details
subset_content_widget = QtWidgets.QWidget(subset_frame)
@ -249,7 +257,8 @@ class PublisherWindow(QtWidgets.QDialog):
self._first_show = False
self.resize(self.default_width, self.default_height)
self.setStyleSheet(style.load_stylesheet())
self.reset()
if self._reset_on_show:
self.reset()
def closeEvent(self, event):
self.controller.save_changes()
@ -382,6 +391,12 @@ class PublisherWindow(QtWidgets.QDialog):
context_title = self.controller.get_context_title()
self.set_context_label(context_title)
# Give a change to process Resize Request
QtWidgets.QApplication.processEvents()
# Trigger update geometry of
widget = self.subset_views_layout.currentWidget()
widget.updateGeometry()
def _on_subset_change(self, *_args):
# Ignore changes if in middle of refreshing
if self._refreshing_instances: