mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 08:54:53 +01:00
enhanced report viewer
This commit is contained in:
parent
42f47c868b
commit
f69091bd8c
5 changed files with 734 additions and 129 deletions
|
|
@ -1,3 +1,6 @@
|
|||
from .report_items import (
|
||||
PublishReport
|
||||
)
|
||||
from .widgets import (
|
||||
PublishReportViewerWidget
|
||||
)
|
||||
|
|
@ -8,6 +11,8 @@ from .window import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"PublishReport",
|
||||
|
||||
"PublishReportViewerWidget",
|
||||
|
||||
"PublishReportViewerWindow",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ class InstancesModel(QtGui.QStandardItemModel):
|
|||
self.clear()
|
||||
self._items_by_id.clear()
|
||||
self._plugin_items_by_id.clear()
|
||||
if not report_item:
|
||||
return
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
|
|
@ -119,6 +121,8 @@ class PluginsModel(QtGui.QStandardItemModel):
|
|||
self.clear()
|
||||
self._items_by_id.clear()
|
||||
self._plugin_items_by_id.clear()
|
||||
if not report_item:
|
||||
return
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
|
|
|
|||
126
openpype/tools/publisher/publish_report_viewer/report_items.py
Normal file
126
openpype/tools/publisher/publish_report_viewer/report_items.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import uuid
|
||||
import collections
|
||||
import copy
|
||||
|
||||
|
||||
class PluginItem:
|
||||
def __init__(self, plugin_data):
|
||||
self._id = uuid.uuid4()
|
||||
|
||||
self.name = plugin_data["name"]
|
||||
self.label = plugin_data["label"]
|
||||
self.order = plugin_data["order"]
|
||||
self.skipped = plugin_data["skipped"]
|
||||
self.passed = plugin_data["passed"]
|
||||
|
||||
errored = False
|
||||
for instance_data in plugin_data["instances_data"]:
|
||||
for log_item in instance_data["logs"]:
|
||||
errored = log_item["type"] == "error"
|
||||
if errored:
|
||||
break
|
||||
if errored:
|
||||
break
|
||||
|
||||
self.errored = errored
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
|
||||
class InstanceItem:
|
||||
def __init__(self, instance_id, instance_data, logs_by_instance_id):
|
||||
self._id = instance_id
|
||||
self.label = instance_data.get("label") or instance_data.get("name")
|
||||
self.family = instance_data.get("family")
|
||||
self.removed = not instance_data.get("exists", True)
|
||||
|
||||
logs = logs_by_instance_id.get(instance_id) or []
|
||||
errored = False
|
||||
for log_item in logs:
|
||||
if log_item.errored:
|
||||
errored = True
|
||||
break
|
||||
|
||||
self.errored = errored
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
|
||||
class LogItem:
|
||||
def __init__(self, log_item_data, plugin_id, instance_id):
|
||||
self._instance_id = instance_id
|
||||
self._plugin_id = plugin_id
|
||||
self._errored = log_item_data["type"] == "error"
|
||||
self.data = log_item_data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.data[key]
|
||||
|
||||
@property
|
||||
def errored(self):
|
||||
return self._errored
|
||||
|
||||
@property
|
||||
def instance_id(self):
|
||||
return self._instance_id
|
||||
|
||||
@property
|
||||
def plugin_id(self):
|
||||
return self._plugin_id
|
||||
|
||||
|
||||
class PublishReport:
|
||||
def __init__(self, report_data):
|
||||
data = copy.deepcopy(report_data)
|
||||
|
||||
context_data = data["context"]
|
||||
context_data["name"] = "context"
|
||||
context_data["label"] = context_data["label"] or "Context"
|
||||
|
||||
logs = []
|
||||
plugins_items_by_id = {}
|
||||
plugins_id_order = []
|
||||
for plugin_data in data["plugins_data"]:
|
||||
item = PluginItem(plugin_data)
|
||||
plugins_id_order.append(item.id)
|
||||
plugins_items_by_id[item.id] = item
|
||||
for instance_data_item in plugin_data["instances_data"]:
|
||||
instance_id = instance_data_item["id"]
|
||||
for log_item_data in instance_data_item["logs"]:
|
||||
log_item = LogItem(
|
||||
copy.deepcopy(log_item_data), item.id, instance_id
|
||||
)
|
||||
logs.append(log_item)
|
||||
|
||||
logs_by_instance_id = collections.defaultdict(list)
|
||||
for log_item in logs:
|
||||
logs_by_instance_id[log_item.instance_id].append(log_item)
|
||||
|
||||
instance_items_by_id = {}
|
||||
instance_items_by_family = {}
|
||||
context_item = InstanceItem(None, context_data, logs_by_instance_id)
|
||||
instance_items_by_id[context_item.id] = context_item
|
||||
instance_items_by_family[context_item.family] = [context_item]
|
||||
|
||||
for instance_id, instance_data in data["instances"].items():
|
||||
item = InstanceItem(
|
||||
instance_id, instance_data, logs_by_instance_id
|
||||
)
|
||||
instance_items_by_id[item.id] = item
|
||||
if item.family not in instance_items_by_family:
|
||||
instance_items_by_family[item.family] = []
|
||||
instance_items_by_family[item.family].append(item)
|
||||
|
||||
self.instance_items_by_id = instance_items_by_id
|
||||
self.instance_items_by_family = instance_items_by_family
|
||||
|
||||
self.plugins_id_order = plugins_id_order
|
||||
self.plugins_items_by_id = plugins_items_by_id
|
||||
|
||||
self.logs = logs
|
||||
|
||||
self.crashed_plugin_paths = report_data["crashed_file_paths"]
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import copy
|
||||
import uuid
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.widgets.nice_checkbox import NiceCheckbox
|
||||
|
||||
# from openpype.tools.utils import DeselectableTreeView
|
||||
from .constants import (
|
||||
ITEM_ID_ROLE,
|
||||
ITEM_IS_GROUP_ROLE
|
||||
|
|
@ -16,98 +14,127 @@ from .model import (
|
|||
PluginsModel,
|
||||
PluginProxyModel
|
||||
)
|
||||
from .report_items import PublishReport
|
||||
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 1
|
||||
TRACEBACK_ROLE = QtCore.Qt.UserRole + 2
|
||||
IS_DETAIL_ITEM_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
||||
|
||||
class PluginItem:
|
||||
def __init__(self, plugin_data):
|
||||
self._id = uuid.uuid4()
|
||||
class PluginLoadReportModel(QtGui.QStandardItemModel):
|
||||
def set_report(self, report):
|
||||
parent = self.invisibleRootItem()
|
||||
parent.removeRows(0, parent.rowCount())
|
||||
|
||||
self.name = plugin_data["name"]
|
||||
self.label = plugin_data["label"]
|
||||
self.order = plugin_data["order"]
|
||||
self.skipped = plugin_data["skipped"]
|
||||
self.passed = plugin_data["passed"]
|
||||
new_items = []
|
||||
new_items_by_filepath = {}
|
||||
for filepath in report.crashed_plugin_paths.keys():
|
||||
item = QtGui.QStandardItem(filepath)
|
||||
new_items.append(item)
|
||||
new_items_by_filepath[filepath] = item
|
||||
|
||||
logs = []
|
||||
errored = False
|
||||
for instance_data in plugin_data["instances_data"]:
|
||||
for log_item in instance_data["logs"]:
|
||||
if not errored:
|
||||
errored = log_item["type"] == "error"
|
||||
logs.append(copy.deepcopy(log_item))
|
||||
if not new_items:
|
||||
return
|
||||
|
||||
self.errored = errored
|
||||
self.logs = logs
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
parent.appendRows(new_items)
|
||||
for filepath, item in new_items_by_filepath.items():
|
||||
traceback_txt = report.crashed_plugin_paths[filepath]
|
||||
detail_item = QtGui.QStandardItem()
|
||||
detail_item.setData(filepath, FILEPATH_ROLE)
|
||||
detail_item.setData(traceback_txt, TRACEBACK_ROLE)
|
||||
detail_item.setData(True, IS_DETAIL_ITEM_ROLE)
|
||||
item.appendRow(detail_item)
|
||||
|
||||
|
||||
class InstanceItem:
|
||||
def __init__(self, instance_id, instance_data, report_data):
|
||||
self._id = instance_id
|
||||
self.label = instance_data.get("label") or instance_data.get("name")
|
||||
self.family = instance_data.get("family")
|
||||
self.removed = not instance_data.get("exists", True)
|
||||
class DetailWidget(QtWidgets.QTextEdit):
|
||||
def __init__(self, text, *args, **kwargs):
|
||||
super(DetailWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
logs = []
|
||||
for plugin_data in report_data["plugins_data"]:
|
||||
for instance_data_item in plugin_data["instances_data"]:
|
||||
if instance_data_item["id"] == self._id:
|
||||
logs.extend(copy.deepcopy(instance_data_item["logs"]))
|
||||
self.setReadOnly(True)
|
||||
self.setHtml(text)
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||
self.setWordWrapMode(
|
||||
QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere
|
||||
)
|
||||
|
||||
errored = False
|
||||
for log in logs:
|
||||
if log["type"] == "error":
|
||||
errored = True
|
||||
break
|
||||
|
||||
self.errored = errored
|
||||
self.logs = logs
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
def sizeHint(self):
|
||||
content_margins = (
|
||||
self.contentsMargins().top()
|
||||
+ self.contentsMargins().bottom()
|
||||
)
|
||||
size = self.document().documentLayout().documentSize().toSize()
|
||||
size.setHeight(size.height() + content_margins)
|
||||
return size
|
||||
|
||||
|
||||
class PublishReport:
|
||||
def __init__(self, report_data):
|
||||
data = copy.deepcopy(report_data)
|
||||
class PluginLoadReportWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(PluginLoadReportWidget, self).__init__(parent)
|
||||
|
||||
context_data = data["context"]
|
||||
context_data["name"] = "context"
|
||||
context_data["label"] = context_data["label"] or "Context"
|
||||
view = QtWidgets.QTreeView(self)
|
||||
view.setEditTriggers(view.NoEditTriggers)
|
||||
view.setTextElideMode(QtCore.Qt.ElideLeft)
|
||||
view.setHeaderHidden(True)
|
||||
view.setAlternatingRowColors(True)
|
||||
view.setVerticalScrollMode(view.ScrollPerPixel)
|
||||
|
||||
instance_items_by_id = {}
|
||||
instance_items_by_family = {}
|
||||
context_item = InstanceItem(None, context_data, data)
|
||||
instance_items_by_id[context_item.id] = context_item
|
||||
instance_items_by_family[context_item.family] = [context_item]
|
||||
model = PluginLoadReportModel()
|
||||
view.setModel(model)
|
||||
|
||||
for instance_id, instance_data in data["instances"].items():
|
||||
item = InstanceItem(instance_id, instance_data, data)
|
||||
instance_items_by_id[item.id] = item
|
||||
if item.family not in instance_items_by_family:
|
||||
instance_items_by_family[item.family] = []
|
||||
instance_items_by_family[item.family].append(item)
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(view, 1)
|
||||
|
||||
all_logs = []
|
||||
plugins_items_by_id = {}
|
||||
plugins_id_order = []
|
||||
for plugin_data in data["plugins_data"]:
|
||||
item = PluginItem(plugin_data)
|
||||
plugins_id_order.append(item.id)
|
||||
plugins_items_by_id[item.id] = item
|
||||
all_logs.extend(copy.deepcopy(item.logs))
|
||||
view.expanded.connect(self._on_expand)
|
||||
|
||||
self.instance_items_by_id = instance_items_by_id
|
||||
self.instance_items_by_family = instance_items_by_family
|
||||
self._view = view
|
||||
self._model = model
|
||||
self._widgets_by_filepath = {}
|
||||
|
||||
self.plugins_id_order = plugins_id_order
|
||||
self.plugins_items_by_id = plugins_items_by_id
|
||||
def _on_expand(self, index):
|
||||
for row in range(self._model.rowCount(index)):
|
||||
child_index = self._model.index(row, index.column(), index)
|
||||
self._create_widget(child_index)
|
||||
|
||||
self.logs = all_logs
|
||||
def showEvent(self, event):
|
||||
super(PluginLoadReportWidget, self).showEvent(event)
|
||||
self._update_widgets_size_hints()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PluginLoadReportWidget, self).resizeEvent(event)
|
||||
self._update_widgets_size_hints()
|
||||
|
||||
def _update_widgets_size_hints(self):
|
||||
for item in self._widgets_by_filepath.values():
|
||||
widget, index = item
|
||||
if not widget.isVisible():
|
||||
continue
|
||||
self._model.setData(
|
||||
index, widget.sizeHint(), QtCore.Qt.SizeHintRole
|
||||
)
|
||||
|
||||
def _create_widget(self, index):
|
||||
if not index.data(IS_DETAIL_ITEM_ROLE):
|
||||
return
|
||||
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
if filepath in self._widgets_by_filepath:
|
||||
return
|
||||
|
||||
traceback_txt = index.data(TRACEBACK_ROLE)
|
||||
detail_text = (
|
||||
"<b>Filepath:</b><br/>"
|
||||
"{}<br/><br/>"
|
||||
"<b>Traceback:</b><br/>"
|
||||
"{}"
|
||||
).format(filepath, traceback_txt.replace("\n", "<br/>"))
|
||||
widget = DetailWidget(detail_text, self)
|
||||
self._view.setIndexWidget(index, widget)
|
||||
self._widgets_by_filepath[filepath] = (widget, index)
|
||||
|
||||
def set_report(self, report):
|
||||
self._widgets_by_filepath = {}
|
||||
self._model.set_report(report)
|
||||
|
||||
|
||||
class DetailsWidget(QtWidgets.QWidget):
|
||||
|
|
@ -123,11 +150,50 @@ class DetailsWidget(QtWidgets.QWidget):
|
|||
layout.addWidget(output_widget)
|
||||
|
||||
self._output_widget = output_widget
|
||||
self._report_item = None
|
||||
self._instance_filter = set()
|
||||
self._plugin_filter = set()
|
||||
|
||||
def clear(self):
|
||||
self._output_widget.setPlainText("")
|
||||
|
||||
def set_logs(self, logs):
|
||||
def set_report(self, report):
|
||||
self._report_item = report
|
||||
self._plugin_filter = set()
|
||||
self._instance_filter = set()
|
||||
self._update_logs()
|
||||
|
||||
def set_plugin_filter(self, plugin_filter):
|
||||
self._plugin_filter = plugin_filter
|
||||
self._update_logs()
|
||||
|
||||
def set_instance_filter(self, instance_filter):
|
||||
self._instance_filter = instance_filter
|
||||
self._update_logs()
|
||||
|
||||
def _update_logs(self):
|
||||
if not self._report_item:
|
||||
self._output_widget.setPlainText("")
|
||||
return
|
||||
|
||||
filtered_logs = []
|
||||
for log in self._report_item.logs:
|
||||
if (
|
||||
self._instance_filter
|
||||
and log.instance_id not in self._instance_filter
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
self._plugin_filter
|
||||
and log.plugin_id not in self._plugin_filter
|
||||
):
|
||||
continue
|
||||
filtered_logs.append(log)
|
||||
|
||||
self._set_logs(filtered_logs)
|
||||
|
||||
def _set_logs(self, logs):
|
||||
lines = []
|
||||
for log in logs:
|
||||
if log["type"] == "record":
|
||||
|
|
@ -148,6 +214,59 @@ class DetailsWidget(QtWidgets.QWidget):
|
|||
self._output_widget.setPlainText(text)
|
||||
|
||||
|
||||
class DeselectableTreeView(QtWidgets.QTreeView):
|
||||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
index = self.indexAt(event.pos())
|
||||
clear_selection = False
|
||||
if not index.isValid():
|
||||
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||
if modifiers == QtCore.Qt.ShiftModifier:
|
||||
return
|
||||
elif modifiers == QtCore.Qt.ControlModifier:
|
||||
return
|
||||
clear_selection = True
|
||||
else:
|
||||
indexes = self.selectedIndexes()
|
||||
if len(indexes) == 1 and index in indexes:
|
||||
clear_selection = True
|
||||
|
||||
if clear_selection:
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
event.accept()
|
||||
return
|
||||
|
||||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
|
||||
|
||||
class DetailsPopup(QtWidgets.QDialog):
|
||||
closed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent, center_widget):
|
||||
super(DetailsPopup, self).__init__(parent)
|
||||
self.setWindowTitle("Report Details")
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
||||
self._center_widget = center_widget
|
||||
self._first_show = True
|
||||
|
||||
def showEvent(self, event):
|
||||
layout = self.layout()
|
||||
layout.insertWidget(0, self._center_widget)
|
||||
super(DetailsPopup, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.resize(700, 400)
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(DetailsPopup, self).closeEvent(event)
|
||||
self.closed.emit()
|
||||
|
||||
|
||||
class PublishReportViewerWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(PublishReportViewerWidget, self).__init__(parent)
|
||||
|
|
@ -171,12 +290,13 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
removed_instances_layout.addWidget(removed_instances_check, 0)
|
||||
removed_instances_layout.addWidget(removed_instances_label, 1)
|
||||
|
||||
instances_view = QtWidgets.QTreeView(self)
|
||||
instances_view = DeselectableTreeView(self)
|
||||
instances_view.setObjectName("PublishDetailViews")
|
||||
instances_view.setModel(instances_proxy)
|
||||
instances_view.setIndentation(0)
|
||||
instances_view.setHeaderHidden(True)
|
||||
instances_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
instances_view.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
|
||||
instances_view.setExpandsOnDoubleClick(False)
|
||||
|
||||
instances_delegate = GroupItemDelegate(instances_view)
|
||||
|
|
@ -191,29 +311,49 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
skipped_plugins_layout.addWidget(skipped_plugins_check, 0)
|
||||
skipped_plugins_layout.addWidget(skipped_plugins_label, 1)
|
||||
|
||||
plugins_view = QtWidgets.QTreeView(self)
|
||||
plugins_view = DeselectableTreeView(self)
|
||||
plugins_view.setObjectName("PublishDetailViews")
|
||||
plugins_view.setModel(plugins_proxy)
|
||||
plugins_view.setIndentation(0)
|
||||
plugins_view.setHeaderHidden(True)
|
||||
plugins_view.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
|
||||
plugins_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
plugins_view.setExpandsOnDoubleClick(False)
|
||||
|
||||
plugins_delegate = GroupItemDelegate(plugins_view)
|
||||
plugins_view.setItemDelegate(plugins_delegate)
|
||||
|
||||
details_widget = DetailsWidget(self)
|
||||
details_widget = QtWidgets.QWidget(self)
|
||||
details_tab_widget = QtWidgets.QTabWidget(details_widget)
|
||||
details_popup_btn = QtWidgets.QPushButton("PopUp", details_widget)
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
details_layout = QtWidgets.QVBoxLayout(details_widget)
|
||||
details_layout.setContentsMargins(0, 0, 0, 0)
|
||||
details_layout.addWidget(details_tab_widget, 1)
|
||||
details_layout.addWidget(details_popup_btn, 0)
|
||||
|
||||
details_popup = DetailsPopup(self, details_tab_widget)
|
||||
|
||||
logs_text_widget = DetailsWidget(details_tab_widget)
|
||||
plugin_load_report_widget = PluginLoadReportWidget(details_tab_widget)
|
||||
|
||||
details_tab_widget.addTab(logs_text_widget, "Logs")
|
||||
details_tab_widget.addTab(plugin_load_report_widget, "Crashed plugins")
|
||||
|
||||
middle_widget = QtWidgets.QWidget(self)
|
||||
middle_layout = QtWidgets.QGridLayout(middle_widget)
|
||||
middle_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# Row 1
|
||||
layout.addLayout(removed_instances_layout, 0, 0)
|
||||
layout.addLayout(skipped_plugins_layout, 0, 1)
|
||||
middle_layout.addLayout(removed_instances_layout, 0, 0)
|
||||
middle_layout.addLayout(skipped_plugins_layout, 0, 1)
|
||||
# Row 2
|
||||
layout.addWidget(instances_view, 1, 0)
|
||||
layout.addWidget(plugins_view, 1, 1)
|
||||
layout.addWidget(details_widget, 1, 2)
|
||||
middle_layout.addWidget(instances_view, 1, 0)
|
||||
middle_layout.addWidget(plugins_view, 1, 1)
|
||||
|
||||
layout.setColumnStretch(2, 1)
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(middle_widget, 0)
|
||||
layout.addWidget(details_widget, 1)
|
||||
|
||||
instances_view.selectionModel().selectionChanged.connect(
|
||||
self._on_instance_change
|
||||
|
|
@ -230,10 +370,13 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
removed_instances_check.stateChanged.connect(
|
||||
self._on_removed_instances_check
|
||||
)
|
||||
details_popup_btn.clicked.connect(self._on_details_popup)
|
||||
details_popup.closed.connect(self._on_popup_close)
|
||||
|
||||
self._ignore_selection_changes = False
|
||||
self._report_item = None
|
||||
self._details_widget = details_widget
|
||||
self._logs_text_widget = logs_text_widget
|
||||
self._plugin_load_report_widget = plugin_load_report_widget
|
||||
|
||||
self._removed_instances_check = removed_instances_check
|
||||
self._instances_view = instances_view
|
||||
|
|
@ -248,6 +391,10 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
self._plugins_model = plugins_model
|
||||
self._plugins_proxy = plugins_proxy
|
||||
|
||||
self._details_widget = details_widget
|
||||
self._details_tab_widget = details_tab_widget
|
||||
self._details_popup = details_popup
|
||||
|
||||
def _on_instance_view_clicked(self, index):
|
||||
if not index.isValid() or not index.data(ITEM_IS_GROUP_ROLE):
|
||||
return
|
||||
|
|
@ -266,62 +413,46 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
else:
|
||||
self._plugins_view.expand(index)
|
||||
|
||||
def set_report(self, report_data):
|
||||
def set_report_data(self, report_data):
|
||||
report = PublishReport(report_data)
|
||||
self.set_report(report)
|
||||
|
||||
def set_report(self, report):
|
||||
self._ignore_selection_changes = True
|
||||
|
||||
report_item = PublishReport(report_data)
|
||||
self._report_item = report_item
|
||||
self._report_item = report
|
||||
|
||||
self._instances_model.set_report(report_item)
|
||||
self._plugins_model.set_report(report_item)
|
||||
self._details_widget.set_logs(report_item.logs)
|
||||
self._instances_model.set_report(report)
|
||||
self._plugins_model.set_report(report)
|
||||
self._logs_text_widget.set_report(report)
|
||||
self._plugin_load_report_widget.set_report(report)
|
||||
|
||||
self._ignore_selection_changes = False
|
||||
|
||||
self._instances_view.expandAll()
|
||||
self._plugins_view.expandAll()
|
||||
|
||||
def _on_instance_change(self, *_args):
|
||||
if self._ignore_selection_changes:
|
||||
return
|
||||
|
||||
valid_index = None
|
||||
instance_ids = set()
|
||||
for index in self._instances_view.selectedIndexes():
|
||||
if index.isValid():
|
||||
valid_index = index
|
||||
break
|
||||
instance_ids.add(index.data(ITEM_ID_ROLE))
|
||||
|
||||
if valid_index is None:
|
||||
return
|
||||
|
||||
if self._plugins_view.selectedIndexes():
|
||||
self._ignore_selection_changes = True
|
||||
self._plugins_view.selectionModel().clearSelection()
|
||||
self._ignore_selection_changes = False
|
||||
|
||||
plugin_id = valid_index.data(ITEM_ID_ROLE)
|
||||
instance_item = self._report_item.instance_items_by_id[plugin_id]
|
||||
self._details_widget.set_logs(instance_item.logs)
|
||||
self._logs_text_widget.set_instance_filter(instance_ids)
|
||||
|
||||
def _on_plugin_change(self, *_args):
|
||||
if self._ignore_selection_changes:
|
||||
return
|
||||
|
||||
valid_index = None
|
||||
plugin_ids = set()
|
||||
for index in self._plugins_view.selectedIndexes():
|
||||
if index.isValid():
|
||||
valid_index = index
|
||||
break
|
||||
plugin_ids.add(index.data(ITEM_ID_ROLE))
|
||||
|
||||
if valid_index is None:
|
||||
self._details_widget.set_logs(self._report_item.logs)
|
||||
return
|
||||
|
||||
if self._instances_view.selectedIndexes():
|
||||
self._ignore_selection_changes = True
|
||||
self._instances_view.selectionModel().clearSelection()
|
||||
self._ignore_selection_changes = False
|
||||
|
||||
plugin_id = valid_index.data(ITEM_ID_ROLE)
|
||||
plugin_item = self._report_item.plugins_items_by_id[plugin_id]
|
||||
self._details_widget.set_logs(plugin_item.logs)
|
||||
self._logs_text_widget.set_plugin_filter(plugin_ids)
|
||||
|
||||
def _on_skipped_plugin_check(self):
|
||||
self._plugins_proxy.set_ignore_skipped(
|
||||
|
|
@ -332,3 +463,16 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
self._instances_proxy.set_ignore_removed(
|
||||
self._removed_instances_check.isChecked()
|
||||
)
|
||||
|
||||
def _on_details_popup(self):
|
||||
self._details_widget.setVisible(False)
|
||||
self._details_popup.show()
|
||||
|
||||
def _on_popup_close(self):
|
||||
self._details_widget.setVisible(True)
|
||||
layout = self._details_widget.layout()
|
||||
layout.insertWidget(0, self._details_tab_widget)
|
||||
|
||||
def close_details_popup(self):
|
||||
if self._details_popup.isVisible():
|
||||
self._details_popup.close()
|
||||
|
|
|
|||
|
|
@ -1,29 +1,355 @@
|
|||
from Qt import QtWidgets
|
||||
import os
|
||||
import json
|
||||
import six
|
||||
import appdirs
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype import style
|
||||
from openpype.lib import JSONSettingRegistry
|
||||
from openpype.resources import get_openpype_icon_filepath
|
||||
from openpype.tools import resources
|
||||
from openpype.tools.utils import (
|
||||
IconButton,
|
||||
paint_image_with_color
|
||||
)
|
||||
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
if __package__:
|
||||
from .widgets import PublishReportViewerWidget
|
||||
from .report_items import PublishReport
|
||||
else:
|
||||
from widgets import PublishReportViewerWidget
|
||||
from report_items import PublishReport
|
||||
|
||||
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 1
|
||||
MODIFIED_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
|
||||
class PublisherReportRegistry(JSONSettingRegistry):
|
||||
"""Class handling storing publish report tool.
|
||||
|
||||
Attributes:
|
||||
vendor (str): Name used for path construction.
|
||||
product (str): Additional name used for path construction.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.vendor = "pypeclub"
|
||||
self.product = "openpype"
|
||||
name = "publish_report_viewer"
|
||||
path = appdirs.user_data_dir(self.product, self.vendor)
|
||||
super(PublisherReportRegistry, self).__init__(name, path)
|
||||
|
||||
|
||||
class LoadedFilesMopdel(QtGui.QStandardItemModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedFilesMopdel, self).__init__(*args, **kwargs)
|
||||
self.setColumnCount(2)
|
||||
self._items_by_filepath = {}
|
||||
self._reports_by_filepath = {}
|
||||
|
||||
self._registry = PublisherReportRegistry()
|
||||
|
||||
self._loading_registry = False
|
||||
self._load_registry()
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
if section == 0:
|
||||
return "Exports"
|
||||
if section == 1:
|
||||
return "Modified"
|
||||
return ""
|
||||
super(LoadedFilesMopdel, self).headerData(section, orientation, role)
|
||||
|
||||
def _load_registry(self):
|
||||
self._loading_registry = True
|
||||
try:
|
||||
filepaths = self._registry.get_item("filepaths")
|
||||
self.add_filepaths(filepaths)
|
||||
except ValueError:
|
||||
pass
|
||||
self._loading_registry = False
|
||||
|
||||
def _store_registry(self):
|
||||
if self._loading_registry:
|
||||
return
|
||||
filepaths = list(self._items_by_filepath.keys())
|
||||
self._registry.set_item("filepaths", filepaths)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
col = index.column()
|
||||
if col != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
if role == QtCore.Qt.ToolTipRole:
|
||||
if col == 0:
|
||||
role = FILEPATH_ROLE
|
||||
elif col == 1:
|
||||
return "File modified"
|
||||
return None
|
||||
|
||||
elif role == QtCore.Qt.DisplayRole:
|
||||
if col == 1:
|
||||
role = MODIFIED_ROLE
|
||||
return super(LoadedFilesMopdel, self).data(index, role)
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
if not filepaths:
|
||||
return
|
||||
|
||||
if isinstance(filepaths, six.string_types):
|
||||
filepaths = [filepaths]
|
||||
|
||||
filtered_paths = []
|
||||
for filepath in filepaths:
|
||||
normalized_path = os.path.normpath(filepath)
|
||||
if normalized_path in self._items_by_filepath:
|
||||
continue
|
||||
|
||||
if (
|
||||
os.path.exists(normalized_path)
|
||||
and normalized_path not in filtered_paths
|
||||
):
|
||||
filtered_paths.append(normalized_path)
|
||||
|
||||
if not filtered_paths:
|
||||
return
|
||||
|
||||
new_items = []
|
||||
for filepath in filtered_paths:
|
||||
try:
|
||||
with open(normalized_path, "r") as stream:
|
||||
data = json.load(stream)
|
||||
report = PublishReport(data)
|
||||
except Exception as exc:
|
||||
# TODO handle errors
|
||||
continue
|
||||
|
||||
modified = os.path.getmtime(normalized_path)
|
||||
item = QtGui.QStandardItem(os.path.basename(normalized_path))
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setData(normalized_path, FILEPATH_ROLE)
|
||||
item.setData(modified, MODIFIED_ROLE)
|
||||
new_items.append(item)
|
||||
self._items_by_filepath[normalized_path] = item
|
||||
self._reports_by_filepath[normalized_path] = report
|
||||
|
||||
if not new_items:
|
||||
return
|
||||
|
||||
parent = self.invisibleRootItem()
|
||||
parent.appendRows(new_items)
|
||||
|
||||
self._store_registry()
|
||||
|
||||
def remove_filepaths(self, filepaths):
|
||||
if not filepaths:
|
||||
return
|
||||
|
||||
if isinstance(filepaths, six.string_types):
|
||||
filepaths = [filepaths]
|
||||
|
||||
filtered_paths = []
|
||||
for filepath in filepaths:
|
||||
normalized_path = os.path.normpath(filepath)
|
||||
if normalized_path in self._items_by_filepath:
|
||||
filtered_paths.append(normalized_path)
|
||||
|
||||
if not filtered_paths:
|
||||
return
|
||||
|
||||
parent = self.invisibleRootItem()
|
||||
for filepath in filtered_paths:
|
||||
self._reports_by_filepath.pop(normalized_path)
|
||||
item = self._items_by_filepath.pop(filepath)
|
||||
parent.removeRow(item.row())
|
||||
|
||||
self._store_registry()
|
||||
|
||||
def get_report_by_filepath(self, filepath):
|
||||
return self._reports_by_filepath.get(filepath)
|
||||
|
||||
|
||||
class LoadedFilesView(QtWidgets.QTreeView):
|
||||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedFilesView, self).__init__(*args, **kwargs)
|
||||
self.setEditTriggers(self.NoEditTriggers)
|
||||
self.setIndentation(0)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
model = LoadedFilesMopdel()
|
||||
self.setModel(model)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
self.setItemDelegateForColumn(1, time_delegate)
|
||||
|
||||
remove_btn = IconButton(self)
|
||||
remove_icon_path = resources.get_icon_path("delete")
|
||||
loaded_remove_image = QtGui.QImage(remove_icon_path)
|
||||
pix = paint_image_with_color(loaded_remove_image, QtCore.Qt.white)
|
||||
icon = QtGui.QIcon(pix)
|
||||
remove_btn.setIcon(icon)
|
||||
|
||||
model.rowsInserted.connect(self._on_rows_inserted)
|
||||
remove_btn.clicked.connect(self._on_remove_clicked)
|
||||
self.selectionModel().selectionChanged.connect(
|
||||
self._on_selection_change
|
||||
)
|
||||
|
||||
self._model = model
|
||||
self._time_delegate = time_delegate
|
||||
self._remove_btn = remove_btn
|
||||
|
||||
def _update_remove_btn(self):
|
||||
viewport = self.viewport()
|
||||
height = viewport.height() + self.header().height()
|
||||
pos_x = viewport.width() - self._remove_btn.width() - 5
|
||||
pos_y = height - self._remove_btn.height() - 5
|
||||
self._remove_btn.move(max(0, pos_x), max(0, pos_y))
|
||||
|
||||
def _on_rows_inserted(self):
|
||||
header = self.header()
|
||||
header.resizeSections(header.ResizeToContents)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(LoadedFilesView, self).resizeEvent(event)
|
||||
self._update_remove_btn()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LoadedFilesView, self).showEvent(event)
|
||||
self._update_remove_btn()
|
||||
header = self.header()
|
||||
header.resizeSections(header.ResizeToContents)
|
||||
|
||||
def _on_selection_change(self):
|
||||
self.selection_changed.emit()
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
self._model.add_filepaths(filepaths)
|
||||
self._fill_selection()
|
||||
|
||||
def remove_filepaths(self, filepaths):
|
||||
self._model.remove_filepaths(filepaths)
|
||||
self._fill_selection()
|
||||
|
||||
def _on_remove_clicked(self):
|
||||
index = self.currentIndex()
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
self.remove_filepaths(filepath)
|
||||
|
||||
def _fill_selection(self):
|
||||
index = self.currentIndex()
|
||||
if index.isValid():
|
||||
return
|
||||
|
||||
index = self._model.index(0, 0)
|
||||
if index.isValid():
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def get_current_report(self):
|
||||
index = self.currentIndex()
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
return self._model.get_report_by_filepath(filepath)
|
||||
|
||||
|
||||
class LoadedFilesWidget(QtWidgets.QWidget):
|
||||
report_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(LoadedFilesWidget, self).__init__(parent)
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
view = LoadedFilesView(self)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(view, 1)
|
||||
|
||||
view.selection_changed.connect(self._on_report_change)
|
||||
|
||||
self._view = view
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
filepaths = []
|
||||
for url in mime_data.urls():
|
||||
filepath = url.toLocalFile()
|
||||
ext = os.path.splitext(filepath)[-1]
|
||||
if os.path.exists(filepath) and ext == ".json":
|
||||
filepaths.append(filepath)
|
||||
self._add_filepaths(filepaths)
|
||||
event.accept()
|
||||
|
||||
def _on_report_change(self):
|
||||
self.report_changed.emit()
|
||||
|
||||
def _add_filepaths(self, filepaths):
|
||||
self._view.add_filepaths(filepaths)
|
||||
|
||||
def get_current_report(self):
|
||||
return self._view.get_current_report()
|
||||
|
||||
|
||||
class PublishReportViewerWindow(QtWidgets.QWidget):
|
||||
# TODO add buttons to be able load report file or paste content of report
|
||||
default_width = 1200
|
||||
default_height = 600
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PublishReportViewerWindow, self).__init__(parent)
|
||||
self.setWindowTitle("Publish report viewer")
|
||||
icon = QtGui.QIcon(get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
main_widget = PublishReportViewerWidget(self)
|
||||
body = QtWidgets.QSplitter(self)
|
||||
body.setContentsMargins(0, 0, 0, 0)
|
||||
body.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Expanding
|
||||
)
|
||||
body.setOrientation(QtCore.Qt.Horizontal)
|
||||
|
||||
loaded_files_widget = LoadedFilesWidget(body)
|
||||
main_widget = PublishReportViewerWidget(body)
|
||||
|
||||
body.addWidget(loaded_files_widget)
|
||||
body.addWidget(main_widget)
|
||||
body.setStretchFactor(0, 70)
|
||||
body.setStretchFactor(1, 65)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addWidget(main_widget)
|
||||
layout.addWidget(body, 1)
|
||||
|
||||
loaded_files_widget.report_changed.connect(self._on_report_change)
|
||||
|
||||
self._loaded_files_widget = loaded_files_widget
|
||||
self._main_widget = main_widget
|
||||
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def _on_report_change(self):
|
||||
report = self._loaded_files_widget.get_current_report()
|
||||
self.set_report(report)
|
||||
|
||||
def set_report(self, report_data):
|
||||
self._main_widget.set_report(report_data)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue