From f69091bd8c46ef15459cb9423462ad1fd0e9ef6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Feb 2022 16:55:24 +0100 Subject: [PATCH] enhanced report viewer --- .../publish_report_viewer/__init__.py | 5 + .../publisher/publish_report_viewer/model.py | 4 + .../publish_report_viewer/report_items.py | 126 ++++++ .../publish_report_viewer/widgets.py | 394 ++++++++++++------ .../publisher/publish_report_viewer/window.py | 334 ++++++++++++++- 5 files changed, 734 insertions(+), 129 deletions(-) create mode 100644 openpype/tools/publisher/publish_report_viewer/report_items.py 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)