diff --git a/openpype/tools/publisher/publish_report_viewer/__init__.py b/openpype/tools/publisher/publish_report_viewer/__init__.py
index 3cfaaa5a05..ce1cc3729c 100644
--- a/openpype/tools/publisher/publish_report_viewer/__init__.py
+++ b/openpype/tools/publisher/publish_report_viewer/__init__.py
@@ -1,3 +1,6 @@
+from .report_items import (
+ PublishReport
+)
from .widgets import (
PublishReportViewerWidget
)
@@ -8,6 +11,8 @@ from .window import (
__all__ = (
+ "PublishReport",
+
"PublishReportViewerWidget",
"PublishReportViewerWindow",
diff --git a/openpype/tools/publisher/publish_report_viewer/model.py b/openpype/tools/publisher/publish_report_viewer/model.py
index 460d3e12d1..a88129a358 100644
--- a/openpype/tools/publisher/publish_report_viewer/model.py
+++ b/openpype/tools/publisher/publish_report_viewer/model.py
@@ -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()
diff --git a/openpype/tools/publisher/publish_report_viewer/report_items.py b/openpype/tools/publisher/publish_report_viewer/report_items.py
new file mode 100644
index 0000000000..b47d14da25
--- /dev/null
+++ b/openpype/tools/publisher/publish_report_viewer/report_items.py
@@ -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"]
diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py
index 24f1d33d0e..0b17efb614 100644
--- a/openpype/tools/publisher/publish_report_viewer/widgets.py
+++ b/openpype/tools/publisher/publish_report_viewer/widgets.py
@@ -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 = (
+ "Filepath:
"
+ "{}
"
+ "Traceback:
"
+ "{}"
+ ).format(filepath, traceback_txt.replace("\n", "
"))
+ 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()
diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py
index 7a0fef7d91..8ca075e4d2 100644
--- a/openpype/tools/publisher/publish_report_viewer/window.py
+++ b/openpype/tools/publisher/publish_report_viewer/window.py
@@ -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)