ayon-core/openpype/tools/push_to_project/window.py
2023-01-19 03:43:48 +01:00

830 lines
29 KiB
Python

import collections
from qtpy import QtWidgets, QtGui, QtCore
from openpype.style import load_stylesheet, get_app_icon_path
from openpype.tools.utils import (
PlaceholderLineEdit,
SeparatorWidget,
get_asset_icon_by_name,
set_style_property,
)
from openpype.tools.utils.views import DeselectableTreeView
from .control_context import PushToContextController
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1
ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2
ASSET_ID_ROLE = QtCore.Qt.UserRole + 3
TASK_NAME_ROLE = QtCore.Qt.UserRole + 4
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 5
class ProjectsModel(QtGui.QStandardItemModel):
empty_text = "< Empty >"
refreshing_text = "< Refreshing >"
select_project_text = "< Select Project >"
refreshed = QtCore.Signal()
def __init__(self, controller):
super(ProjectsModel, self).__init__()
self._controller = controller
self.event_system.add_callback(
"projects.refresh.finished", self._on_refresh_finish
)
placeholder_item = QtGui.QStandardItem(self.empty_text)
root_item = self.invisibleRootItem()
root_item.appendRows([placeholder_item])
items = {None: placeholder_item}
self._placeholder_item = placeholder_item
self._items = items
@property
def event_system(self):
return self._controller.event_system
def _on_refresh_finish(self):
root_item = self.invisibleRootItem()
project_names = self._controller.model.get_projects()
if not project_names:
placeholder_text = self.empty_text
else:
placeholder_text = self.select_project_text
self._placeholder_item.setData(placeholder_text, QtCore.Qt.DisplayRole)
new_items = []
if None not in self._items:
new_items.append(self._placeholder_item)
current_project_names = set(self._items.keys())
for project_name in current_project_names - set(project_names):
if project_name is None:
continue
item = self._items.pop(project_name)
root_item.takeRow(item.row())
for project_name in project_names:
if project_name in self._items:
continue
item = QtGui.QStandardItem(project_name)
item.setData(project_name, PROJECT_NAME_ROLE)
new_items.append(item)
if new_items:
root_item.appendRows(new_items)
self.refreshed.emit()
class ProjectProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self):
super(ProjectProxyModel, self).__init__()
self._filter_empty_projects = False
def set_filter_empty_project(self, filter_empty_projects):
if filter_empty_projects == self._filter_empty_projects:
return
self._filter_empty_projects = filter_empty_projects
self.invalidate()
def filterAcceptsRow(self, row, parent):
if not self._filter_empty_projects:
return True
model = self.sourceModel()
source_index = model.index(row, self.filterKeyColumn(), parent)
if model.data(source_index, PROJECT_NAME_ROLE) is None:
return False
return True
class AssetsModel(QtGui.QStandardItemModel):
items_changed = QtCore.Signal()
empty_text = "< Empty >"
def __init__(self, controller):
super(AssetsModel, self).__init__()
self._controller = controller
placeholder_item = QtGui.QStandardItem(self.empty_text)
placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled)
root_item = self.invisibleRootItem()
root_item.appendRows([placeholder_item])
self.event_system.add_callback(
"project.changed", self._on_project_change
)
self.event_system.add_callback(
"assets.refresh.started", self._on_refresh_start
)
self.event_system.add_callback(
"assets.refresh.finished", self._on_refresh_finish
)
self._items = {None: placeholder_item}
self._placeholder_item = placeholder_item
self._last_project = None
@property
def event_system(self):
return self._controller.event_system
def _clear(self):
placeholder_in = False
root_item = self.invisibleRootItem()
for row in reversed(range(root_item.rowCount())):
item = root_item.child(row)
asset_id = item.data(ASSET_ID_ROLE)
if asset_id is None:
placeholder_in = True
continue
root_item.removeRow(item.row())
for key in tuple(self._items.keys()):
if key is not None:
self._items.pop(key)
if not placeholder_in:
root_item.appendRows([self._placeholder_item])
self._items[None] = self._placeholder_item
def _on_project_change(self, event):
project_name = event["project_name"]
if project_name == self._last_project:
return
self._last_project = project_name
self._clear()
self.items_changed.emit()
def _on_refresh_start(self, event):
pass
def _on_refresh_finish(self, event):
event_project_name = event["project_name"]
project_name = self._controller.selection_model.project_name
if event_project_name != project_name:
return
self._last_project = event["project_name"]
if project_name is None:
if None not in self._items:
self._clear()
self.items_changed.emit()
return
asset_items_by_id = self._controller.model.get_assets(project_name)
if not asset_items_by_id:
self._clear()
self.items_changed.emit()
return
assets_by_parent_id = collections.defaultdict(list)
for asset_item in asset_items_by_id.values():
assets_by_parent_id[asset_item.parent_id].append(asset_item)
root_item = self.invisibleRootItem()
if None in self._items:
self._items.pop(None)
root_item.takeRow(self._placeholder_item.row())
items_to_remove = set(self._items) - set(asset_items_by_id.keys())
hierarchy_queue = collections.deque()
hierarchy_queue.append((None, root_item))
while hierarchy_queue:
parent_id, parent_item = hierarchy_queue.popleft()
new_items = []
for asset_item in assets_by_parent_id[parent_id]:
item = self._items.get(asset_item.id)
if item is None:
item = QtGui.QStandardItem()
item.setFlags(
QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsEnabled
)
new_items.append(item)
self._items[asset_item.id] = item
elif item.parent() is not parent_item:
new_items.append(item)
icon = get_asset_icon_by_name(
asset_item.icon_name, asset_item.icon_color
)
item.setData(asset_item.name, QtCore.Qt.DisplayRole)
item.setData(icon, QtCore.Qt.DecorationRole)
item.setData(asset_item.id, ASSET_ID_ROLE)
hierarchy_queue.append((asset_item.id, item))
if new_items:
parent_item.appendRows(new_items)
for item_id in items_to_remove:
item = self._items.pop(item_id, None)
if item is None:
continue
row = item.row()
if row < 0:
continue
parent = item.parent()
if parent is None:
parent = root_item
parent.takeRow(row)
self.items_changed.emit()
class TasksModel(QtGui.QStandardItemModel):
items_changed = QtCore.Signal()
empty_text = "< Empty >"
def __init__(self, controller):
super(TasksModel, self).__init__()
self._controller = controller
placeholder_item = QtGui.QStandardItem(self.empty_text)
placeholder_item.setFlags(QtCore.Qt.ItemIsEnabled)
root_item = self.invisibleRootItem()
root_item.appendRows([placeholder_item])
self.event_system.add_callback(
"project.changed", self._on_project_change
)
self.event_system.add_callback(
"assets.refresh.finished", self._on_asset_refresh_finish
)
self.event_system.add_callback(
"asset.changed", self._on_asset_change
)
self._items = {None: placeholder_item}
self._placeholder_item = placeholder_item
self._last_project = None
@property
def event_system(self):
return self._controller.event_system
def _clear(self):
placeholder_in = False
root_item = self.invisibleRootItem()
for row in reversed(range(root_item.rowCount())):
item = root_item.child(row)
task_name = item.data(TASK_NAME_ROLE)
if task_name is None:
placeholder_in = True
continue
root_item.removeRow(item.row())
for key in tuple(self._items.keys()):
if key is not None:
self._items.pop(key)
if not placeholder_in:
root_item.appendRows([self._placeholder_item])
self._items[None] = self._placeholder_item
def _on_project_change(self, event):
project_name = event["project_name"]
if project_name == self._last_project:
return
self._last_project = project_name
self._clear()
self.items_changed.emit()
def _on_asset_refresh_finish(self, event):
self._refresh(event["project_name"])
def _on_asset_change(self, event):
self._refresh(event["project_name"])
def _refresh(self, new_project_name):
project_name = self._controller.selection_model.project_name
if new_project_name != project_name:
return
self._last_project = project_name
if project_name is None:
if None not in self._items:
self._clear()
self.items_changed.emit()
return
asset_id = self._controller.selection_model.asset_id
task_items = self._controller.model.get_tasks(
project_name, asset_id
)
if not task_items:
self._clear()
self.items_changed.emit()
return
root_item = self.invisibleRootItem()
if None in self._items:
self._items.pop(None)
root_item.takeRow(self._placeholder_item.row())
new_items = []
task_names = set()
for task_item in task_items:
task_name = task_item.name
item = self._items.get(task_name)
if item is None:
item = QtGui.QStandardItem()
item.setFlags(
QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsEnabled
)
new_items.append(item)
self._items[task_name] = item
item.setData(task_name, QtCore.Qt.DisplayRole)
item.setData(task_name, TASK_NAME_ROLE)
item.setData(task_item.task_type, TASK_TYPE_ROLE)
if new_items:
root_item.appendRows(new_items)
items_to_remove = set(self._items) - task_names
for item_id in items_to_remove:
item = self._items.pop(item_id, None)
if item is None:
continue
parent = item.parent()
if parent is not None:
parent.removeRow(item.row())
self.items_changed.emit()
class PushToContextSelectWindow(QtWidgets.QWidget):
def __init__(self, controller=None):
super(PushToContextSelectWindow, self).__init__()
if controller is None:
controller = PushToContextController()
self._controller = controller
self.setWindowTitle("Push to project (select context)")
self.setWindowIcon(QtGui.QIcon(get_app_icon_path()))
main_context_widget = QtWidgets.QWidget(self)
header_widget = QtWidgets.QWidget(main_context_widget)
header_label = QtWidgets.QLabel(controller.src_label, header_widget)
header_layout = QtWidgets.QHBoxLayout(header_widget)
header_layout.setContentsMargins(0, 0, 0, 0)
header_layout.addWidget(header_label)
main_splitter = QtWidgets.QSplitter(
QtCore.Qt.Horizontal, main_context_widget
)
context_widget = QtWidgets.QWidget(main_splitter)
project_combobox = QtWidgets.QComboBox(context_widget)
project_model = ProjectsModel(controller)
project_proxy = ProjectProxyModel()
project_proxy.setSourceModel(project_model)
project_proxy.setDynamicSortFilter(True)
project_delegate = QtWidgets.QStyledItemDelegate()
project_combobox.setItemDelegate(project_delegate)
project_combobox.setModel(project_proxy)
asset_task_splitter = QtWidgets.QSplitter(
QtCore.Qt.Vertical, context_widget
)
asset_view = DeselectableTreeView(asset_task_splitter)
asset_view.setHeaderHidden(True)
asset_model = AssetsModel(controller)
asset_proxy = QtCore.QSortFilterProxyModel()
asset_proxy.setSourceModel(asset_model)
asset_proxy.setDynamicSortFilter(True)
asset_view.setModel(asset_proxy)
task_view = QtWidgets.QListView(asset_task_splitter)
task_proxy = QtCore.QSortFilterProxyModel()
task_model = TasksModel(controller)
task_proxy.setSourceModel(task_model)
task_proxy.setDynamicSortFilter(True)
task_view.setModel(task_proxy)
asset_task_splitter.addWidget(asset_view)
asset_task_splitter.addWidget(task_view)
context_layout = QtWidgets.QVBoxLayout(context_widget)
context_layout.setContentsMargins(0, 0, 0, 0)
context_layout.addWidget(project_combobox, 0)
context_layout.addWidget(asset_task_splitter, 1)
# --- Inputs widget ---
inputs_widget = QtWidgets.QWidget(main_splitter)
asset_name_input = PlaceholderLineEdit(inputs_widget)
asset_name_input.setPlaceholderText("< Name of new asset >")
asset_name_input.setObjectName("ValidatedLineEdit")
variant_input = PlaceholderLineEdit(inputs_widget)
variant_input.setPlaceholderText("< Variant >")
variant_input.setObjectName("ValidatedLineEdit")
comment_input = PlaceholderLineEdit(inputs_widget)
comment_input.setPlaceholderText("< Publish comment >")
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
inputs_layout.setContentsMargins(0, 0, 0, 0)
inputs_layout.addRow("New asset name", asset_name_input)
inputs_layout.addRow("Variant", variant_input)
inputs_layout.addRow("Comment", comment_input)
main_splitter.addWidget(context_widget)
main_splitter.addWidget(inputs_widget)
# --- Buttons widget ---
btns_widget = QtWidgets.QWidget(self)
cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget)
publish_btn = QtWidgets.QPushButton("Publish", btns_widget)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addStretch(1)
btns_layout.addWidget(cancel_btn, 0)
btns_layout.addWidget(publish_btn, 0)
sep_1 = SeparatorWidget(parent=main_context_widget)
sep_2 = SeparatorWidget(parent=main_context_widget)
main_context_layout = QtWidgets.QVBoxLayout(main_context_widget)
main_context_layout.addWidget(header_widget, 0)
main_context_layout.addWidget(sep_1, 0)
main_context_layout.addWidget(main_splitter, 1)
main_context_layout.addWidget(sep_2, 0)
main_context_layout.addWidget(btns_widget, 0)
# NOTE This was added in hurry
# - should be reorganized and changed styles
overlay_widget = QtWidgets.QFrame(self)
overlay_widget.setObjectName("OverlayFrame")
overlay_label = QtWidgets.QLabel(overlay_widget)
overlay_label.setAlignment(QtCore.Qt.AlignCenter)
overlay_btns_widget = QtWidgets.QWidget(overlay_widget)
overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# Add try again button (requires changes in controller)
overlay_try_btn = QtWidgets.QPushButton(
"Try again", overlay_btns_widget
)
overlay_close_btn = QtWidgets.QPushButton(
"Close", overlay_btns_widget
)
overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget)
overlay_btns_layout.addStretch(1)
overlay_btns_layout.addWidget(overlay_try_btn, 0)
overlay_btns_layout.addWidget(overlay_close_btn, 0)
overlay_btns_layout.addStretch(1)
overlay_layout = QtWidgets.QVBoxLayout(overlay_widget)
overlay_layout.addWidget(overlay_label, 0)
overlay_layout.addWidget(overlay_btns_widget, 0)
overlay_layout.setAlignment(QtCore.Qt.AlignCenter)
main_layout = QtWidgets.QStackedLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(main_context_widget)
main_layout.addWidget(overlay_widget)
main_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)
main_layout.setCurrentWidget(main_context_widget)
show_timer = QtCore.QTimer()
show_timer.setInterval(1)
main_thread_timer = QtCore.QTimer()
main_thread_timer.setInterval(10)
user_input_changed_timer = QtCore.QTimer()
user_input_changed_timer.setInterval(200)
user_input_changed_timer.setSingleShot(True)
main_thread_timer.timeout.connect(self._on_main_thread_timer)
show_timer.timeout.connect(self._on_show_timer)
user_input_changed_timer.timeout.connect(self._on_user_input_timer)
asset_name_input.textChanged.connect(self._on_new_asset_change)
variant_input.textChanged.connect(self._on_variant_change)
comment_input.textChanged.connect(self._on_comment_change)
project_model.refreshed.connect(self._on_projects_refresh)
project_combobox.currentIndexChanged.connect(self._on_project_change)
asset_view.selectionModel().selectionChanged.connect(
self._on_asset_change
)
asset_model.items_changed.connect(self._on_asset_model_change)
task_view.selectionModel().selectionChanged.connect(
self._on_task_change
)
task_model.items_changed.connect(self._on_task_model_change)
publish_btn.clicked.connect(self._on_select_click)
cancel_btn.clicked.connect(self._on_close_click)
overlay_close_btn.clicked.connect(self._on_close_click)
overlay_try_btn.clicked.connect(self._on_try_again_click)
controller.event_system.add_callback(
"new_asset_name.changed", self._on_controller_new_asset_change
)
controller.event_system.add_callback(
"variant.changed", self._on_controller_variant_change
)
controller.event_system.add_callback(
"comment.changed", self._on_controller_comment_change
)
controller.event_system.add_callback(
"submission.enabled.changed", self._on_submission_change
)
controller.event_system.add_callback(
"source.changed", self._on_controller_source_change
)
controller.event_system.add_callback(
"submit.started", self._on_controller_submit_start
)
controller.event_system.add_callback(
"submit.finished", self._on_controller_submit_end
)
controller.event_system.add_callback(
"push.message.added", self._on_push_message
)
self._main_layout = main_layout
self._main_context_widget = main_context_widget
self._header_label = header_label
self._main_splitter = main_splitter
self._project_combobox = project_combobox
self._project_model = project_model
self._project_proxy = project_proxy
self._project_delegate = project_delegate
self._asset_view = asset_view
self._asset_model = asset_model
self._asset_proxy_model = asset_proxy
self._task_view = task_view
self._task_proxy_model = task_proxy
self._variant_input = variant_input
self._asset_name_input = asset_name_input
self._comment_input = comment_input
self._publish_btn = publish_btn
self._overlay_widget = overlay_widget
self._overlay_close_btn = overlay_close_btn
self._overlay_try_btn = overlay_try_btn
self._overlay_label = overlay_label
self._user_input_changed_timer = user_input_changed_timer
# Store current value on input text change
# The value is unset when is passed to controller
# The goal is to have controll over changes happened during user change
# in UI and controller auto-changes
self._variant_input_text = None
self._new_asset_name_input_text = None
self._comment_input_text = None
self._show_timer = show_timer
self._show_counter = 2
self._first_show = True
self._main_thread_timer = main_thread_timer
self._main_thread_timer_can_stop = True
self._last_submit_message = None
self._process_item = None
publish_btn.setEnabled(False)
overlay_close_btn.setVisible(False)
overlay_try_btn.setVisible(False)
if controller.user_values.new_asset_name:
asset_name_input.setText(controller.user_values.new_asset_name)
if controller.user_values.variant:
variant_input.setText(controller.user_values.variant)
self._invalidate_variant()
self._invalidate_new_asset_name()
@property
def controller(self):
return self._controller
def showEvent(self, event):
super(PushToContextSelectWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self.setStyleSheet(load_stylesheet())
self._invalidate_variant()
self._show_timer.start()
def _on_show_timer(self):
if self._show_counter == 0:
self._show_timer.stop()
return
self._show_counter -= 1
if self._show_counter == 1:
width = 740
height = 640
inputs_width = 360
self.resize(width, height)
self._main_splitter.setSizes([width - inputs_width, inputs_width])
if self._show_counter > 0:
return
self._controller.model.refresh_projects()
def _on_new_asset_change(self, text):
self._new_asset_name_input_text = text
self._user_input_changed_timer.start()
def _on_variant_change(self, text):
self._variant_input_text = text
self._user_input_changed_timer.start()
def _on_comment_change(self, text):
self._comment_input_text = text
self._user_input_changed_timer.start()
def _on_user_input_timer(self):
asset_name = self._new_asset_name_input_text
if asset_name is not None:
self._new_asset_name_input_text = None
self._controller.user_values.set_new_asset(asset_name)
variant = self._variant_input_text
if variant is not None:
self._variant_input_text = None
self._controller.user_values.set_variant(variant)
comment = self._comment_input_text
if comment is not None:
self._comment_input_text = None
self._controller.user_values.set_comment(comment)
def _on_controller_new_asset_change(self, event):
asset_name = event["changes"]["new_asset_name"]["new"]
if (
self._new_asset_name_input_text is None
and asset_name != self._asset_name_input.text()
):
self._asset_name_input.setText(asset_name)
self._invalidate_new_asset_name()
def _on_controller_variant_change(self, event):
is_valid_changes = event["changes"]["is_valid"]
variant = event["changes"]["variant"]["new"]
if (
self._variant_input_text is None
and variant != self._variant_input.text()
):
self._variant_input.setText(variant)
if is_valid_changes["old"] != is_valid_changes["new"]:
self._invalidate_variant()
def _on_controller_comment_change(self, event):
comment = event["comment"]
if (
self._comment_input_text is None
and comment != self._comment_input.text()
):
self._comment_input.setText(comment)
def _on_controller_source_change(self):
self._header_label.setText(self._controller.src_label)
def _invalidate_new_asset_name(self):
asset_name = self._controller.user_values.new_asset_name
self._task_view.setVisible(not asset_name)
valid = None
if asset_name:
valid = self._controller.user_values.is_new_asset_name_valid
state = ""
if valid is True:
state = "valid"
elif valid is False:
state = "invalid"
set_style_property(self._asset_name_input, "state", state)
def _invalidate_variant(self):
valid = self._controller.user_values.is_variant_valid
state = "invalid"
if valid is True:
state = "valid"
set_style_property(self._variant_input, "state", state)
def _on_projects_refresh(self):
self._project_proxy.sort(0, QtCore.Qt.AscendingOrder)
def _on_project_change(self):
idx = self._project_combobox.currentIndex()
if idx < 0:
self._project_proxy.set_filter_empty_project(False)
return
project_name = self._project_combobox.itemData(idx, PROJECT_NAME_ROLE)
self._project_proxy.set_filter_empty_project(project_name is not None)
self._controller.selection_model.select_project(project_name)
def _on_asset_change(self):
indexes = self._asset_view.selectedIndexes()
index = next(iter(indexes), None)
asset_id = None
if index is not None:
model = self._asset_view.model()
asset_id = model.data(index, ASSET_ID_ROLE)
self._controller.selection_model.select_asset(asset_id)
def _on_asset_model_change(self):
self._asset_proxy_model.sort(0, QtCore.Qt.AscendingOrder)
def _on_task_model_change(self):
self._task_proxy_model.sort(0, QtCore.Qt.AscendingOrder)
def _on_task_change(self):
indexes = self._task_view.selectedIndexes()
index = next(iter(indexes), None)
task_name = None
if index is not None:
model = self._task_view.model()
task_name = model.data(index, TASK_NAME_ROLE)
self._controller.selection_model.select_task(task_name)
def _on_submission_change(self, event):
self._publish_btn.setEnabled(event["enabled"])
def _on_close_click(self):
self.close()
def _on_select_click(self):
self._process_item = self._controller.submit(wait=False)
def _on_try_again_click(self):
self._process_item = None
self._last_submit_message = None
self._overlay_close_btn.setVisible(False)
self._overlay_try_btn.setVisible(False)
self._main_layout.setCurrentWidget(self._main_context_widget)
def _on_main_thread_timer(self):
if self._last_submit_message:
self._overlay_label.setText(self._last_submit_message)
self._last_submit_message = None
process_status = self._process_item.status
push_failed = process_status.failed
fail_traceback = process_status.traceback
if self._main_thread_timer_can_stop:
self._main_thread_timer.stop()
self._overlay_close_btn.setVisible(True)
if push_failed and not fail_traceback:
self._overlay_try_btn.setVisible(True)
if push_failed:
message = "Push Failed:\n{}".format(process_status.fail_reason)
if fail_traceback:
message += "\n{}".format(fail_traceback)
self._overlay_label.setText(message)
set_style_property(self._overlay_close_btn, "state", "error")
if self._main_thread_timer_can_stop:
# Join thread in controller
self._controller.wait_for_process_thread()
# Reset process item to None
self._process_item = None
def _on_controller_submit_start(self):
self._main_thread_timer_can_stop = False
self._main_thread_timer.start()
self._main_layout.setCurrentWidget(self._overlay_widget)
self._overlay_label.setText("Submittion started")
def _on_controller_submit_end(self):
self._main_thread_timer_can_stop = True
def _on_push_message(self, event):
self._last_submit_message = event["message"]