Merge branch 'develop' into bugfix/OP-6027_Bug-Fusion-saver-render-publishing-doesnt-work

This commit is contained in:
Jakub Ježek 2023-05-23 22:24:40 +02:00 committed by GitHub
commit 2be43ab6cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 2373 additions and 834 deletions

View file

@ -320,6 +320,14 @@ def publish_plugins_discover(paths=None):
continue
for plugin in pyblish.plugin.plugins_from_module(module):
# Ignore base plugin classes
# NOTE 'pyblish.api.discover' does not ignore them!
if (
plugin is pyblish.api.Plugin
or plugin is pyblish.api.ContextPlugin
or plugin is pyblish.api.InstancePlugin
):
continue
if not allow_duplicates and plugin.__name__ in plugin_names:
result.duplicated_plugins.append(plugin)
log.debug("Duplicate plug-in found: %s", plugin)

View file

@ -26,8 +26,8 @@
"bg": "#2C313A",
"bg-inputs": "#21252B",
"bg-buttons": "#434a56",
"bg-button-hover": "rgb(81, 86, 97)",
"bg-buttons": "rgb(67, 74, 86)",
"bg-buttons-hover": "rgb(81, 86, 97)",
"bg-inputs-disabled": "#2C313A",
"bg-buttons-disabled": "#434a56",
@ -66,7 +66,9 @@
"bg-success": "#458056",
"bg-success-hover": "#55a066",
"bg-error": "#AD2E2E",
"bg-error-hover": "#C93636"
"bg-error-hover": "#C93636",
"bg-info": "rgb(63, 98, 121)",
"bg-info-hover": "rgb(81, 146, 181)"
},
"tab-widget": {
"bg": "#21252B",
@ -94,6 +96,7 @@
"crash": "#FF6432",
"success": "#458056",
"warning": "#ffc671",
"progress": "rgb(194, 226, 236)",
"tab-bg": "#16191d",
"list-view-group": {
"bg": "#434a56",

View file

@ -136,7 +136,7 @@ QPushButton {
}
QPushButton:hover {
background: {color:bg-button-hover};
background: {color:bg-buttons-hover};
color: {color:font-hover};
}
@ -166,7 +166,7 @@ QToolButton {
}
QToolButton:hover {
background: {color:bg-button-hover};
background: {color:bg-buttons-hover};
color: {color:font-hover};
}
@ -722,6 +722,13 @@ OverlayMessageWidget[type="error"]:hover {
background: {color:overlay-messages:bg-error-hover};
}
OverlayMessageWidget[type="info"] {
background: {color:overlay-messages:bg-info};
}
OverlayMessageWidget[type="info"]:hover {
background: {color:overlay-messages:bg-info-hover};
}
OverlayMessageWidget QWidget {
background: transparent;
}
@ -749,10 +756,11 @@ OverlayMessageWidget QWidget {
}
#InfoText {
padding-left: 30px;
padding-top: 20px;
padding-left: 0px;
padding-top: 0px;
padding-right: 20px;
background: transparent;
border: 1px solid {color:border};
border: none;
}
#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor {
@ -914,7 +922,7 @@ PixmapButton{
background: {color:bg-buttons};
}
PixmapButton:hover {
background: {color:bg-button-hover};
background: {color:bg-buttons-hover};
}
PixmapButton:disabled {
background: {color:bg-buttons-disabled};
@ -925,7 +933,7 @@ PixmapButton:disabled {
background: {color:bg-view};
}
#ThumbnailPixmapHoverButton:hover {
background: {color:bg-button-hover};
background: {color:bg-buttons-hover};
}
#CreatorDetailedDescription {
@ -946,7 +954,7 @@ PixmapButton:disabled {
}
#CreateDialogHelpButton:hover {
background: {color:bg-button-hover};
background: {color:bg-buttons-hover};
}
#CreateDialogHelpButton QWidget {
background: transparent;
@ -1005,7 +1013,7 @@ PixmapButton:disabled {
border-radius: 0.2em;
}
#CardViewWidget:hover {
background: {color:bg-button-hover};
background: {color:bg-buttons-hover};
}
#CardViewWidget[state="selected"] {
background: {color:bg-view-selection};
@ -1032,7 +1040,7 @@ PixmapButton:disabled {
}
#PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] {
background: rgb(194, 226, 236);
background: {color:publisher:progress};
}
#PublishInfoFrame QLabel {
@ -1040,6 +1048,11 @@ PixmapButton:disabled {
font-style: bold;
}
#PublishReportHeader {
font-size: 14pt;
font-weight: bold;
}
#PublishInfoMainLabel {
font-size: 12pt;
}
@ -1060,7 +1073,7 @@ ValidationArtistMessage QLabel {
}
#ValidationActionButton:hover {
background: {color:bg-button-hover};
background: {color:bg-buttons-hover};
color: {color:font-hover};
}
@ -1090,6 +1103,35 @@ ValidationArtistMessage QLabel {
border-left: 1px solid {color:border};
}
#PublishInstancesDetails {
border: 1px solid {color:border};
border-radius: 0.3em;
}
#InstancesLogsView {
border: 1px solid {color:border};
background: {color:bg-view};
border-radius: 0.3em;
}
#PublishLogMessage {
font-family: "Noto Sans Mono";
}
#PublishInstanceLogsLabel {
font-weight: bold;
}
#PublishCrashMainLabel{
font-weight: bold;
font-size: 16pt;
}
#PublishCrashReportLabel {
font-weight: bold;
font-size: 13pt;
}
#AssetNameInputWidget {
background: {color:bg-inputs};
border: 1px solid {color:border};

View file

@ -198,29 +198,33 @@ class DropEmpty(QtWidgets.QWidget):
def paintEvent(self, event):
super(DropEmpty, self).paintEvent(event)
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setWidth(1)
pen.setBrush(QtCore.Qt.darkGray)
pen.setStyle(QtCore.Qt.DashLine)
painter.setPen(pen)
content_margins = self.layout().contentsMargins()
pen.setWidth(1)
left_m = content_margins.left()
top_m = content_margins.top()
rect = QtCore.QRect(
content_margins = self.layout().contentsMargins()
rect = self.rect()
left_m = content_margins.left() + pen.width()
top_m = content_margins.top() + pen.width()
new_rect = QtCore.QRect(
left_m,
top_m,
(
self.rect().width()
rect.width()
- (left_m + content_margins.right() + pen.width())
),
(
self.rect().height()
rect.height()
- (top_m + content_margins.bottom() + pen.width())
)
)
painter.drawRect(rect)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(pen)
painter.drawRect(new_rect)
class FilesModel(QtGui.QStandardItemModel):

View file

@ -35,9 +35,13 @@ ResetKeySequence = QtGui.QKeySequence(
__all__ = (
"CONTEXT_ID",
"CONTEXT_LABEL",
"VARIANT_TOOLTIP",
"INPUTS_LAYOUT_HSPACING",
"INPUTS_LAYOUT_VSPACING",
"INSTANCE_ID_ROLE",
"SORT_VALUE_ROLE",
"IS_GROUP_ROLE",
@ -47,4 +51,6 @@ __all__ = (
"FAMILY_ROLE",
"GROUP_ROLE",
"CONVERTER_IDENTIFIER_ROLE",
"ResetKeySequence",
)

View file

@ -47,6 +47,7 @@ PLUGIN_ORDER_OFFSET = 0.5
class CardMessageTypes:
standard = None
info = "info"
error = "error"
@ -220,7 +221,12 @@ class PublishReportMaker:
def _add_plugin_data_item(self, plugin):
if plugin in self._stored_plugins:
raise ValueError("Plugin is already stored")
# A plugin would be processed more than once. What can cause it:
# - there is a bug in controller
# - plugin class is imported into multiple files
# - this can happen even with base classes from 'pyblish'
raise ValueError(
"Plugin '{}' is already stored".format(str(plugin)))
self._stored_plugins.append(plugin)
@ -239,6 +245,7 @@ class PublishReportMaker:
label = plugin.label
return {
"id": plugin.id,
"name": plugin.__name__,
"label": label,
"order": plugin.order,
@ -324,7 +331,7 @@ class PublishReportMaker:
"instances": instances_details,
"context": self._extract_context_data(self._current_context),
"crashed_file_paths": crashed_file_paths,
"id": str(uuid.uuid4()),
"id": uuid.uuid4().hex,
"report_version": "1.0.0"
}
@ -342,7 +349,9 @@ class PublishReportMaker:
"label": instance.data.get("label"),
"family": instance.data["family"],
"families": instance.data.get("families") or [],
"exists": exists
"exists": exists,
"creator_identifier": instance.data.get("creator_identifier"),
"instance_id": instance.data.get("instance_id"),
}
def _extract_instance_log_items(self, result):
@ -388,8 +397,11 @@ class PublishReportMaker:
exception = result.get("error")
if exception:
fname, line_no, func, exc = exception.traceback
# Action result does not have 'is_validation_error'
is_validation_error = result.get("is_validation_error", False)
output.append({
"type": "error",
"is_validation_error": is_validation_error,
"msg": str(exception),
"filename": str(fname),
"lineno": str(line_no),
@ -426,13 +438,15 @@ class PublishPluginsProxy:
plugin_id = plugin.id
plugins_by_id[plugin_id] = plugin
action_ids = set()
action_ids = []
action_ids_by_plugin_id[plugin_id] = action_ids
actions = getattr(plugin, "actions", None) or []
for action in actions:
action_id = action.id
action_ids.add(action_id)
if action_id in actions_by_id:
continue
action_ids.append(action_id)
actions_by_id[action_id] = action
self._plugins_by_id = plugins_by_id
@ -461,7 +475,7 @@ class PublishPluginsProxy:
return plugin.id
def get_plugin_action_items(self, plugin_id):
"""Get plugin action items for plugin by it's id.
"""Get plugin action items for plugin by its id.
Args:
plugin_id (str): Publish plugin id.
@ -568,7 +582,7 @@ class ValidationErrorItem:
context_validation,
title,
description,
detail,
detail
):
self.instance_id = instance_id
self.instance_label = instance_label
@ -677,6 +691,8 @@ class PublishValidationErrorsReport:
for title in titles:
grouped_error_items.append({
"id": uuid.uuid4().hex,
"plugin_id": plugin_id,
"plugin_action_items": list(plugin_action_items),
"error_items": error_items_by_title[title],
"title": title
@ -2379,7 +2395,8 @@ class PublisherController(BasePublisherController):
yield MainThreadItem(self.stop_publish)
# Add plugin to publish report
self._publish_report.add_plugin_iter(plugin, self._publish_context)
self._publish_report.add_plugin_iter(
plugin, self._publish_context)
# WARNING This is hack fix for optional plugins
if not self._is_publish_plugin_active(plugin):
@ -2461,14 +2478,14 @@ class PublisherController(BasePublisherController):
plugin, self._publish_context, instance
)
self._publish_report.add_result(result)
exception = result.get("error")
if exception:
has_validation_error = False
if (
isinstance(exception, PublishValidationError)
and not self.publish_has_validated
):
has_validation_error = True
self._add_validation_error(result)
else:
@ -2482,6 +2499,10 @@ class PublisherController(BasePublisherController):
self.publish_error_msg = msg
self.publish_has_crashed = True
result["is_validation_error"] = has_validation_error
self._publish_report.add_result(result)
self._publish_next_process()

View file

@ -163,7 +163,11 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
super(ZoomPlainText, self).wheelEvent(event)
return
degrees = float(event.delta()) / 8
if hasattr(event, "angleDelta"):
delta = event.angleDelta().y()
else:
delta = event.delta()
degrees = float(delta) / 8
steps = int(ceil(degrees / 5))
self._scheduled_scalings += steps
if (self._scheduled_scalings * steps < 0):

View file

@ -18,7 +18,7 @@ from .help_widget import (
from .publish_frame import PublishFrame
from .tabs_widget import PublisherTabsWidget
from .overview_widget import OverviewWidget
from .validations_widget import ValidationsWidget
from .report_page import ReportPageWidget
__all__ = (
@ -40,5 +40,5 @@ __all__ = (
"PublisherTabsWidget",
"OverviewWidget",
"ValidationsWidget",
"ReportPageWidget",
)

View file

@ -93,7 +93,7 @@ class BaseGroupWidget(QtWidgets.QWidget):
return self._group
def get_widget_by_item_id(self, item_id):
"""Get instance widget by it's id."""
"""Get instance widget by its id."""
return self._widgets_by_id.get(item_id)
@ -702,8 +702,8 @@ class InstanceCardView(AbstractInstanceView):
for group_name in sorted_group_names:
group_icons = {
idenfier: self._controller.get_creator_icon(idenfier)
for idenfier in identifiers_by_group[group_name]
identifier: self._controller.get_creator_icon(identifier)
for identifier in identifiers_by_group[group_name]
}
if group_name in self._widgets_by_group:
group_widget = self._widgets_by_group[group_name]

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -468,45 +468,14 @@ class PublishFrame(QtWidgets.QWidget):
widget.setProperty("state", state)
widget.style().polish(widget)
def _copy_report(self):
logs = self._controller.get_publish_report()
logs_string = json.dumps(logs, indent=4)
mime_data = QtCore.QMimeData()
mime_data.setText(logs_string)
QtWidgets.QApplication.instance().clipboard().setMimeData(
mime_data
)
def _export_report(self):
default_filename = "publish-report-{}".format(
time.strftime("%y%m%d-%H-%M")
)
default_filepath = os.path.join(
os.path.expanduser("~"),
default_filename
)
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
self, "Save report", default_filepath, ".json"
)
if not ext or not new_filepath:
return
logs = self._controller.get_publish_report()
full_path = new_filepath + ext
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(full_path, "w") as file_stream:
json.dump(logs, file_stream)
def _on_report_triggered(self, identifier):
if identifier == "export_report":
self._export_report()
self._controller.event_system.emit(
"export_report.request", {}, "publish_frame")
elif identifier == "copy_report":
self._copy_report()
self._controller.event_system.emit(
"copy_report.request", {}, "publish_frame")
elif identifier == "go_to_report":
self.details_page_requested.emit()

File diff suppressed because it is too large Load diff

View file

@ -75,6 +75,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
painter = QtGui.QPainter()
painter.begin(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.drawPixmap(0, 0, self._cached_pix)
painter.end()
@ -183,6 +184,18 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
backgrounded_images.append(new_pix)
return backgrounded_images
def _paint_dash_line(self, painter, rect):
pen = QtGui.QPen()
pen.setWidth(1)
pen.setBrush(QtCore.Qt.darkGray)
pen.setStyle(QtCore.Qt.DashLine)
new_rect = rect.adjusted(1, 1, -1, -1)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.transparent)
# painter.drawRect(rect)
painter.drawRect(new_rect)
def _cache_pix(self):
rect = self.rect()
rect_width = rect.width()
@ -264,13 +277,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
# Draw drop enabled dashes
if used_default_pix:
pen = QtGui.QPen()
pen.setWidth(1)
pen.setBrush(QtCore.Qt.darkGray)
pen.setStyle(QtCore.Qt.DashLine)
final_painter.setPen(pen)
final_painter.setBrush(QtCore.Qt.transparent)
final_painter.drawRect(rect)
self._paint_dash_line(final_painter, rect)
final_painter.end()

View file

@ -1,715 +0,0 @@
# -*- coding: utf-8 -*-
try:
import commonmark
except Exception:
commonmark = None
from qtpy import QtWidgets, QtCore, QtGui
from openpype.tools.utils import BaseClickableFrame, ClickableFrame
from .widgets import (
IconValuePixmapLabel
)
from ..constants import (
INSTANCE_ID_ROLE
)
class ValidationErrorInstanceList(QtWidgets.QListView):
"""List of publish instances that caused a validation error.
Instances are collected per plugin's validation error title.
"""
def __init__(self, *args, **kwargs):
super(ValidationErrorInstanceList, self).__init__(*args, **kwargs)
self.setObjectName("ValidationErrorInstanceList")
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def minimumSizeHint(self):
return self.sizeHint()
def sizeHint(self):
result = super(ValidationErrorInstanceList, self).sizeHint()
row_count = self.model().rowCount()
height = 0
if row_count > 0:
height = self.sizeHintForRow(0) * row_count
result.setHeight(height)
return result
class ValidationErrorTitleWidget(QtWidgets.QWidget):
"""Title of validation error.
Widget is used as radio button so requires clickable functionality and
changing style on selection/deselection.
Has toggle button to show/hide instances on which validation error happened
if there is a list (Valdation error may happen on context).
"""
selected = QtCore.Signal(int)
instance_changed = QtCore.Signal(int)
def __init__(self, index, error_info, parent):
super(ValidationErrorTitleWidget, self).__init__(parent)
self._index = index
self._error_info = error_info
self._selected = False
title_frame = ClickableFrame(self)
title_frame.setObjectName("ValidationErrorTitleFrame")
toggle_instance_btn = QtWidgets.QToolButton(title_frame)
toggle_instance_btn.setObjectName("ArrowBtn")
toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
toggle_instance_btn.setMaximumWidth(14)
label_widget = QtWidgets.QLabel(error_info["title"], title_frame)
title_frame_layout = QtWidgets.QHBoxLayout(title_frame)
title_frame_layout.addWidget(label_widget, 1)
title_frame_layout.addWidget(toggle_instance_btn, 0)
instances_model = QtGui.QStandardItemModel()
help_text_by_instance_id = {}
items = []
context_validation = False
for error_item in error_info["error_items"]:
context_validation = error_item.context_validation
if context_validation:
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
description = self._prepare_description(error_item)
help_text_by_instance_id[None] = description
# Add fake item to have minimum size hint of view widget
items.append(QtGui.QStandardItem("Context"))
continue
label = error_item.instance_label
item = QtGui.QStandardItem(label)
item.setFlags(
QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
)
item.setData(label, QtCore.Qt.ToolTipRole)
item.setData(error_item.instance_id, INSTANCE_ID_ROLE)
items.append(item)
description = self._prepare_description(error_item)
help_text_by_instance_id[error_item.instance_id] = description
if items:
root_item = instances_model.invisibleRootItem()
root_item.appendRows(items)
instances_view = ValidationErrorInstanceList(self)
instances_view.setModel(instances_model)
self.setLayoutDirection(QtCore.Qt.LeftToRight)
view_widget = QtWidgets.QWidget(self)
view_layout = QtWidgets.QHBoxLayout(view_widget)
view_layout.setContentsMargins(0, 0, 0, 0)
view_layout.setSpacing(0)
view_layout.addSpacing(14)
view_layout.addWidget(instances_view, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(title_frame, 0)
layout.addWidget(view_widget, 0)
view_widget.setVisible(False)
if not context_validation:
toggle_instance_btn.clicked.connect(self._on_toggle_btn_click)
title_frame.clicked.connect(self._mouse_release_callback)
instances_view.selectionModel().selectionChanged.connect(
self._on_seleciton_change
)
self._title_frame = title_frame
self._toggle_instance_btn = toggle_instance_btn
self._view_widget = view_widget
self._instances_model = instances_model
self._instances_view = instances_view
self._context_validation = context_validation
self._help_text_by_instance_id = help_text_by_instance_id
self._expanded = False
def sizeHint(self):
result = super(ValidationErrorTitleWidget, self).sizeHint()
expected_width = max(
self._view_widget.minimumSizeHint().width(),
self._view_widget.sizeHint().width()
)
if expected_width < 200:
expected_width = 200
if result.width() < expected_width:
result.setWidth(expected_width)
return result
def minimumSizeHint(self):
return self.sizeHint()
def _prepare_description(self, error_item):
"""Prepare description text for detail intput.
Args:
error_item (ValidationErrorItem): Item which hold information about
validation error.
Returns:
str: Prepared detailed description.
"""
dsc = error_item.description
detail = error_item.detail
if detail:
dsc += "<br/><br/>{}".format(detail)
description = dsc
if commonmark:
description = commonmark.commonmark(dsc)
return description
def _mouse_release_callback(self):
"""Mark this widget as selected on click."""
self.set_selected(True)
def current_description_text(self):
if self._context_validation:
return self._help_text_by_instance_id[None]
index = self._instances_view.currentIndex()
# TODO make sure instance is selected
if not index.isValid():
index = self._instances_model.index(0, 0)
indence_id = index.data(INSTANCE_ID_ROLE)
return self._help_text_by_instance_id[indence_id]
@property
def is_selected(self):
"""Is widget marked a selected.
Returns:
bool: Item is selected or not.
"""
return self._selected
@property
def index(self):
"""Widget's index set by parent.
Returns:
int: Index of widget.
"""
return self._index
def set_index(self, index):
"""Set index of widget (called by parent).
Args:
int: New index of widget.
"""
self._index = index
def _change_style_property(self, selected):
"""Change style of widget based on selection."""
value = "1" if selected else ""
self._title_frame.setProperty("selected", value)
self._title_frame.style().polish(self._title_frame)
def set_selected(self, selected=None):
"""Change selected state of widget."""
if selected is None:
selected = not self._selected
# Clear instance view selection on deselect
if not selected:
self._instances_view.clearSelection()
# Skip if has same value
if selected == self._selected:
return
self._selected = selected
self._change_style_property(selected)
if selected:
self.selected.emit(self._index)
self._set_expanded(True)
def _on_toggle_btn_click(self):
"""Show/hide instances list."""
self._set_expanded()
def _set_expanded(self, expanded=None):
if expanded is None:
expanded = not self._expanded
elif expanded is self._expanded:
return
if expanded and self._context_validation:
return
self._expanded = expanded
self._view_widget.setVisible(expanded)
if expanded:
self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow)
else:
self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
def _on_seleciton_change(self):
sel_model = self._instances_view.selectionModel()
if sel_model.selectedIndexes():
self.instance_changed.emit(self._index)
class ActionButton(BaseClickableFrame):
"""Plugin's action callback button.
Action may have label or icon or both.
Args:
plugin_action_item (PublishPluginActionItem): Action item that can be
triggered by it's id.
"""
action_clicked = QtCore.Signal(str, str)
def __init__(self, plugin_action_item, parent):
super(ActionButton, self).__init__(parent)
self.setObjectName("ValidationActionButton")
self.plugin_action_item = plugin_action_item
action_label = plugin_action_item.label
action_icon = plugin_action_item.icon
label_widget = QtWidgets.QLabel(action_label, self)
icon_label = None
if action_icon:
icon_label = IconValuePixmapLabel(action_icon, self)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(5, 0, 5, 0)
layout.addWidget(label_widget, 1)
if icon_label:
layout.addWidget(icon_label, 0)
self.setSizePolicy(
QtWidgets.QSizePolicy.Minimum,
self.sizePolicy().verticalPolicy()
)
def _mouse_release_callback(self):
self.action_clicked.emit(
self.plugin_action_item.plugin_id,
self.plugin_action_item.action_id
)
class ValidateActionsWidget(QtWidgets.QFrame):
"""Wrapper widget for plugin actions.
Change actions based on selected validation error.
"""
def __init__(self, controller, parent):
super(ValidateActionsWidget, self).__init__(parent)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
content_widget = QtWidgets.QWidget(self)
content_layout = QtWidgets.QVBoxLayout(content_widget)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(content_widget)
self._controller = controller
self._content_widget = content_widget
self._content_layout = content_layout
self._actions_mapping = {}
def clear(self):
"""Remove actions from widget."""
while self._content_layout.count():
item = self._content_layout.takeAt(0)
widget = item.widget()
if widget:
widget.setVisible(False)
widget.deleteLater()
self._actions_mapping = {}
def set_error_item(self, error_item):
"""Set selected plugin and show it's actions.
Clears current actions from widget and recreate them from the plugin.
Args:
Dict[str, Any]: Object holding error items, title and possible
actions to run.
"""
self.clear()
if not error_item:
self.setVisible(False)
return
plugin_action_items = error_item["plugin_action_items"]
for plugin_action_item in plugin_action_items:
if not plugin_action_item.active:
continue
if plugin_action_item.on_filter not in ("failed", "all"):
continue
action_id = plugin_action_item.action_id
self._actions_mapping[action_id] = plugin_action_item
action_btn = ActionButton(plugin_action_item, self._content_widget)
action_btn.action_clicked.connect(self._on_action_click)
self._content_layout.addWidget(action_btn)
if self._content_layout.count() > 0:
self.setVisible(True)
self._content_layout.addStretch(1)
else:
self.setVisible(False)
def _on_action_click(self, plugin_id, action_id):
self._controller.run_action(plugin_id, action_id)
class VerticallScrollArea(QtWidgets.QScrollArea):
"""Scroll area for validation error titles.
The biggest difference is that the scroll area has scroll bar on left side
and resize of content will also resize scrollarea itself.
Resize if deferred by 100ms because at the moment of resize are not yet
propagated sizes and visibility of scroll bars.
"""
def __init__(self, *args, **kwargs):
super(VerticallScrollArea, self).__init__(*args, **kwargs)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.setLayoutDirection(QtCore.Qt.RightToLeft)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# Background of scrollbar will be transparent
scrollbar_bg = self.verticalScrollBar().parent()
if scrollbar_bg:
scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setViewportMargins(0, 0, 0, 0)
self.verticalScrollBar().installEventFilter(self)
# Timer with 100ms offset after changing size
size_changed_timer = QtCore.QTimer()
size_changed_timer.setInterval(100)
size_changed_timer.setSingleShot(True)
size_changed_timer.timeout.connect(self._on_timer_timeout)
self._size_changed_timer = size_changed_timer
def setVerticalScrollBar(self, widget):
old_widget = self.verticalScrollBar()
if old_widget:
old_widget.removeEventFilter(self)
super(VerticallScrollArea, self).setVerticalScrollBar(widget)
if widget:
widget.installEventFilter(self)
def setWidget(self, widget):
old_widget = self.widget()
if old_widget:
old_widget.removeEventFilter(self)
super(VerticallScrollArea, self).setWidget(widget)
if widget:
widget.installEventFilter(self)
def _on_timer_timeout(self):
width = self.widget().width()
if self.verticalScrollBar().isVisible():
width += self.verticalScrollBar().width()
self.setMinimumWidth(width)
def eventFilter(self, obj, event):
if (
event.type() == QtCore.QEvent.Resize
and (obj is self.widget() or obj is self.verticalScrollBar())
):
self._size_changed_timer.start()
return super(VerticallScrollArea, self).eventFilter(obj, event)
class ValidationArtistMessage(QtWidgets.QWidget):
def __init__(self, message, parent):
super(ValidationArtistMessage, self).__init__(parent)
artist_msg_label = QtWidgets.QLabel(message, self)
artist_msg_label.setAlignment(QtCore.Qt.AlignCenter)
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(
artist_msg_label, 1, QtCore.Qt.AlignCenter
)
class ValidationsWidget(QtWidgets.QFrame):
"""Widgets showing validation error.
This widget is shown if validation error/s happened during validation part.
Shows validation error titles with instances on which happened and
validation error detail with possible actions (repair).
titles actions
Error detail
"""
def __init__(self, controller, parent):
super(ValidationsWidget, self).__init__(parent)
# Before publishing
before_publish_widget = ValidationArtistMessage(
"Nothing to report until you run publish", self
)
# After success publishing
publish_started_widget = ValidationArtistMessage(
"So far so good", self
)
# After success publishing
publish_stop_ok_widget = ValidationArtistMessage(
"Publishing finished successfully", self
)
# After failed publishing (not with validation error)
publish_stop_fail_widget = ValidationArtistMessage(
"This is not your fault...", self
)
# Validation errors
validations_widget = QtWidgets.QWidget(self)
content_widget = QtWidgets.QWidget(validations_widget)
errors_scroll = VerticallScrollArea(content_widget)
errors_scroll.setWidgetResizable(True)
errors_widget = QtWidgets.QWidget(errors_scroll)
errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
errors_layout = QtWidgets.QVBoxLayout(errors_widget)
errors_layout.setContentsMargins(0, 0, 0, 0)
errors_scroll.setWidget(errors_widget)
error_details_frame = QtWidgets.QFrame(content_widget)
error_details_input = QtWidgets.QTextEdit(error_details_frame)
error_details_input.setObjectName("InfoText")
error_details_input.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
actions_widget = ValidateActionsWidget(controller, content_widget)
actions_widget.setMinimumWidth(140)
error_details_layout = QtWidgets.QHBoxLayout(error_details_frame)
error_details_layout.addWidget(error_details_input, 1)
error_details_layout.addWidget(actions_widget, 0)
content_layout = QtWidgets.QHBoxLayout(content_widget)
content_layout.setSpacing(0)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.addWidget(errors_scroll, 0)
content_layout.addWidget(error_details_frame, 1)
top_label = QtWidgets.QLabel(
"Publish validation report", content_widget
)
top_label.setObjectName("PublishInfoMainLabel")
top_label.setAlignment(QtCore.Qt.AlignCenter)
validation_layout = QtWidgets.QVBoxLayout(validations_widget)
validation_layout.setContentsMargins(0, 0, 0, 0)
validation_layout.addWidget(top_label, 0)
validation_layout.addWidget(content_widget, 1)
main_layout = QtWidgets.QStackedLayout(self)
main_layout.addWidget(before_publish_widget)
main_layout.addWidget(publish_started_widget)
main_layout.addWidget(publish_stop_ok_widget)
main_layout.addWidget(publish_stop_fail_widget)
main_layout.addWidget(validations_widget)
main_layout.setCurrentWidget(before_publish_widget)
controller.event_system.add_callback(
"publish.process.started", self._on_publish_start
)
controller.event_system.add_callback(
"publish.reset.finished", self._on_publish_reset
)
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
)
self._main_layout = main_layout
self._before_publish_widget = before_publish_widget
self._publish_started_widget = publish_started_widget
self._publish_stop_ok_widget = publish_stop_ok_widget
self._publish_stop_fail_widget = publish_stop_fail_widget
self._validations_widget = validations_widget
self._top_label = top_label
self._errors_widget = errors_widget
self._errors_layout = errors_layout
self._error_details_frame = error_details_frame
self._error_details_input = error_details_input
self._actions_widget = actions_widget
self._title_widgets = {}
self._error_info = {}
self._previous_select = None
self._controller = controller
def clear(self):
"""Delete all dynamic widgets and hide all wrappers."""
self._title_widgets = {}
self._error_info = {}
self._previous_select = None
while self._errors_layout.count():
item = self._errors_layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
self._top_label.setVisible(False)
self._error_details_frame.setVisible(False)
self._errors_widget.setVisible(False)
self._actions_widget.setVisible(False)
def _set_errors(self, validation_error_report):
"""Set errors into context and created titles.
Args:
validation_error_report (PublishValidationErrorsReport): Report
with information about validation errors and publish plugin
actions.
"""
self.clear()
if not validation_error_report:
return
self._top_label.setVisible(True)
self._error_details_frame.setVisible(True)
self._errors_widget.setVisible(True)
grouped_error_items = validation_error_report.group_items_by_title()
for idx, error_info in enumerate(grouped_error_items):
widget = ValidationErrorTitleWidget(idx, error_info, self)
widget.selected.connect(self._on_select)
widget.instance_changed.connect(self._on_instance_change)
self._errors_layout.addWidget(widget)
self._title_widgets[idx] = widget
self._error_info[idx] = error_info
self._errors_layout.addStretch(1)
if self._title_widgets:
self._title_widgets[0].set_selected(True)
self.updateGeometry()
def _set_current_widget(self, widget):
self._main_layout.setCurrentWidget(widget)
def _on_publish_start(self):
self._set_current_widget(self._publish_started_widget)
def _on_publish_reset(self):
self._set_current_widget(self._before_publish_widget)
def _on_publish_stop(self):
if self._controller.publish_has_crashed:
self._set_current_widget(self._publish_stop_fail_widget)
return
if self._controller.publish_has_validation_errors:
validation_errors = self._controller.get_validation_errors()
self._set_current_widget(self._validations_widget)
self._set_errors(validation_errors)
return
if self._controller.publish_has_finished:
self._set_current_widget(self._publish_stop_ok_widget)
return
self._set_current_widget(self._publish_started_widget)
def _on_select(self, index):
if self._previous_select:
if self._previous_select.index == index:
return
self._previous_select.set_selected(False)
self._previous_select = self._title_widgets[index]
error_item = self._error_info[index]
self._actions_widget.set_error_item(error_item)
self._update_description()
def _on_instance_change(self, index):
if self._previous_select and self._previous_select.index != index:
self._title_widgets[index].set_selected(True)
else:
self._update_description()
def _update_description(self):
description = self._previous_select.current_description_text()
if commonmark:
html = commonmark.commonmark(description)
self._error_details_input.setHtml(html)
elif hasattr(self._error_details_input, "setMarkdown"):
self._error_details_input.setMarkdown(description)
else:
self._error_details_input.setText(description)

View file

@ -40,6 +40,41 @@ from ..constants import (
INPUTS_LAYOUT_VSPACING,
)
FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."]
def parse_icon_def(
icon_def, default_width=None, default_height=None, color=None
):
if not icon_def:
return None
if isinstance(icon_def, QtGui.QPixmap):
return icon_def
color = color or "white"
default_width = default_width or 512
default_height = default_height or 512
if isinstance(icon_def, QtGui.QIcon):
return icon_def.pixmap(default_width, default_height)
try:
if os.path.exists(icon_def):
return QtGui.QPixmap(icon_def)
except Exception:
# TODO logging
pass
for prefix in FA_PREFIXES:
try:
icon_name = "{}{}".format(prefix, icon_def)
icon = qtawesome.icon(icon_name, color=color)
return icon.pixmap(default_width, default_height)
except Exception:
# TODO logging
continue
class PublishPixmapLabel(PixmapLabel):
def _get_pix_size(self):
@ -54,7 +89,6 @@ class IconValuePixmapLabel(PublishPixmapLabel):
Handle icon parsing from creators/instances. Using of QAwesome module
of path to images.
"""
fa_prefixes = ["", "fa."]
default_size = 200
def __init__(self, icon_def, parent):
@ -77,31 +111,9 @@ class IconValuePixmapLabel(PublishPixmapLabel):
return pix
def _parse_icon_def(self, icon_def):
if not icon_def:
return self._default_pixmap()
if isinstance(icon_def, QtGui.QPixmap):
return icon_def
if isinstance(icon_def, QtGui.QIcon):
return icon_def.pixmap(self.default_size, self.default_size)
try:
if os.path.exists(icon_def):
return QtGui.QPixmap(icon_def)
except Exception:
# TODO logging
pass
for prefix in self.fa_prefixes:
try:
icon_name = "{}{}".format(prefix, icon_def)
icon = qtawesome.icon(icon_name, color="white")
return icon.pixmap(self.default_size, self.default_size)
except Exception:
# TODO logging
continue
icon = parse_icon_def(icon_def, self.default_size, self.default_size)
if icon:
return icon
return self._default_pixmap()
@ -692,6 +704,7 @@ class TasksCombobox(QtWidgets.QComboBox):
style.drawControl(
QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self
)
painter.end()
def is_valid(self):
"""Are all selected items valid."""

View file

@ -1,3 +1,6 @@
import os
import json
import time
import collections
import copy
from qtpy import QtWidgets, QtCore, QtGui
@ -15,10 +18,11 @@ from openpype.tools.utils import (
from .constants import ResetKeySequence
from .publish_report_viewer import PublishReportViewerWidget
from .control import CardMessageTypes
from .control_qt import QtPublisherController
from .widgets import (
OverviewWidget,
ValidationsWidget,
ReportPageWidget,
PublishFrame,
PublisherTabsWidget,
@ -182,7 +186,7 @@ class PublisherWindow(QtWidgets.QDialog):
controller, content_stacked_widget
)
report_widget = ValidationsWidget(controller, parent)
report_widget = ReportPageWidget(controller, parent)
# Details - Publish details
publish_details_widget = PublishReportViewerWidget(
@ -313,6 +317,13 @@ class PublisherWindow(QtWidgets.QDialog):
controller.event_system.add_callback(
"convertors.find.failed", self._on_convertor_error
)
controller.event_system.add_callback(
"export_report.request", self._export_report
)
controller.event_system.add_callback(
"copy_report.request", self._copy_report
)
# Store extra header widget for TrayPublisher
# - can be used to add additional widgets to header between context
@ -825,6 +836,9 @@ class PublisherWindow(QtWidgets.QDialog):
self._validate_btn.setEnabled(validate_enabled)
self._publish_btn.setEnabled(publish_enabled)
if not publish_enabled:
self._publish_frame.set_shrunk_state(True)
self._update_publish_details_widget()
def _validate_create_instances(self):
@ -941,6 +955,46 @@ class PublisherWindow(QtWidgets.QDialog):
under_mouse = widget_x < global_pos.x()
self._create_overlay_button.set_under_mouse(under_mouse)
def _copy_report(self):
logs = self._controller.get_publish_report()
logs_string = json.dumps(logs, indent=4)
mime_data = QtCore.QMimeData()
mime_data.setText(logs_string)
QtWidgets.QApplication.instance().clipboard().setMimeData(
mime_data
)
self._controller.emit_card_message(
"Report added to clipboard",
CardMessageTypes.info)
def _export_report(self):
default_filename = "publish-report-{}".format(
time.strftime("%y%m%d-%H-%M")
)
default_filepath = os.path.join(
os.path.expanduser("~"),
default_filename
)
new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName(
self, "Save report", default_filepath, ".json"
)
if not ext or not new_filepath:
return
logs = self._controller.get_publish_report()
full_path = new_filepath + ext
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(full_path, "w") as file_stream:
json.dump(logs, file_stream)
self._controller.emit_card_message(
"Report saved",
CardMessageTypes.info)
class ErrorsMessageBox(ErrorMessageBox):
def __init__(self, error_title, failed_info, message_start, parent):

View file

@ -1,13 +1,16 @@
from .layouts import FlowLayout
from .widgets import (
FocusSpinBox,
FocusDoubleSpinBox,
ComboBox,
CustomTextComboBox,
PlaceholderLineEdit,
ExpandingTextEdit,
BaseClickableFrame,
ClickableFrame,
ClickableLabel,
ExpandBtn,
ClassicExpandBtn,
PixmapLabel,
IconButton,
PixmapButton,
@ -37,15 +40,19 @@ from .overlay_messages import (
__all__ = (
"FlowLayout",
"FocusSpinBox",
"FocusDoubleSpinBox",
"ComboBox",
"CustomTextComboBox",
"PlaceholderLineEdit",
"ExpandingTextEdit",
"BaseClickableFrame",
"ClickableFrame",
"ClickableLabel",
"ExpandBtn",
"ClassicExpandBtn",
"PixmapLabel",
"IconButton",
"PixmapButton",

View file

@ -0,0 +1,150 @@
from qtpy import QtWidgets, QtCore
class FlowLayout(QtWidgets.QLayout):
"""Layout that organize widgets by minimum size into a flow layout.
Layout is putting widget from left to right and top to bottom. When widget
can't fit a row it is added to next line. Minimum size matches widget with
biggest 'sizeHint' width and height using calculated geometry.
Content margins are part of calculations. It is possible to define
horizontal and vertical spacing.
Layout does not support stretch and spacing items.
Todos:
Unified width concept -> use width of largest item so all of them are
same. This could allow to have minimum columns option too.
"""
def __init__(self, parent=None):
super(FlowLayout, self).__init__(parent)
# spaces between each item
self._horizontal_spacing = 5
self._vertical_spacing = 5
self._items = []
def __del__(self):
while self.count():
self.takeAt(0, False)
def isEmpty(self):
for item in self._items:
if not item.isEmpty():
return False
return True
def setSpacing(self, spacing):
self._horizontal_spacing = spacing
self._vertical_spacing = spacing
self.invalidate()
def setHorizontalSpacing(self, spacing):
self._horizontal_spacing = spacing
self.invalidate()
def setVerticalSpacing(self, spacing):
self._vertical_spacing = spacing
self.invalidate()
def addItem(self, item):
self._items.append(item)
self.invalidate()
def count(self):
return len(self._items)
def itemAt(self, index):
if 0 <= index < len(self._items):
return self._items[index]
return None
def takeAt(self, index, invalidate=True):
if 0 <= index < len(self._items):
item = self._items.pop(index)
if invalidate:
self.invalidate()
return item
return None
def expandingDirections(self):
return QtCore.Qt.Orientations(QtCore.Qt.Vertical)
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
return self._setup_geometry(QtCore.QRect(0, 0, width, 0), True)
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self._setup_geometry(rect)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QtCore.QSize(0, 0)
for item in self._items:
widget = item.widget()
if widget is not None:
parent = widget.parent()
if not widget.isVisibleTo(parent):
continue
size = size.expandedTo(item.minimumSize())
if size.width() < 1 or size.height() < 1:
return size
l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()
size += QtCore.QSize(l_margin + r_margin, t_margin + b_margin)
return size
def _setup_geometry(self, rect, only_calculate=False):
h_spacing = self._horizontal_spacing
v_spacing = self._vertical_spacing
l_margin, t_margin, r_margin, b_margin = self.getContentsMargins()
left_x = rect.x() + l_margin
top_y = rect.y() + t_margin
pos_x = left_x
pos_y = top_y
row_height = 0
for item in self._items:
item_hint = item.sizeHint()
item_width = item_hint.width()
item_height = item_hint.height()
if item_width < 1 or item_height < 1:
continue
end_x = pos_x + item_width
wrap = (
row_height > 0
and (
end_x > rect.right()
or (end_x + r_margin) > rect.right()
)
)
if not wrap:
next_pos_x = end_x + h_spacing
else:
pos_x = left_x
pos_y += row_height + v_spacing
next_pos_x = pos_x + item_width + h_spacing
row_height = 0
if not only_calculate:
item.setGeometry(
QtCore.QRect(pos_x, pos_y, item_width, item_height)
)
pos_x = next_pos_x
row_height = max(row_height, item_height)
height = (pos_y - top_y) + row_height
if height > 0:
height += b_margin
return height

View file

@ -101,6 +101,46 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit):
self.setPalette(filter_palette)
class ExpandingTextEdit(QtWidgets.QTextEdit):
"""QTextEdit which does not have sroll area but expands height."""
def __init__(self, parent=None):
super(ExpandingTextEdit, self).__init__(parent)
size_policy = self.sizePolicy()
size_policy.setHeightForWidth(True)
size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred)
self.setSizePolicy(size_policy)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
doc = self.document()
doc.contentsChanged.connect(self._on_doc_change)
def _on_doc_change(self):
self.updateGeometry()
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
margins = self.contentsMargins()
document_width = 0
if width >= margins.left() + margins.right():
document_width = width - margins.left() - margins.right()
document = self.document().clone()
document.setTextWidth(document_width)
return margins.top() + document.size().height() + margins.bottom()
def sizeHint(self):
width = super(ExpandingTextEdit, self).sizeHint().width()
return QtCore.QSize(width, self.heightForWidth(width))
class BaseClickableFrame(QtWidgets.QFrame):
"""Widget that catch left mouse click and can trigger a callback.
@ -161,19 +201,34 @@ class ClickableLabel(QtWidgets.QLabel):
class ExpandBtnLabel(QtWidgets.QLabel):
"""Label showing expand icon meant for ExpandBtn."""
state_changed = QtCore.Signal()
def __init__(self, parent):
super(ExpandBtnLabel, self).__init__(parent)
self._source_collapsed_pix = QtGui.QPixmap(
get_style_image_path("branch_closed")
)
self._source_expanded_pix = QtGui.QPixmap(
get_style_image_path("branch_open")
)
self._source_collapsed_pix = self._create_collapsed_pixmap()
self._source_expanded_pix = self._create_expanded_pixmap()
self._current_image = self._source_collapsed_pix
self._collapsed = True
def set_collapsed(self, collapsed):
def _create_collapsed_pixmap(self):
return QtGui.QPixmap(
get_style_image_path("branch_closed")
)
def _create_expanded_pixmap(self):
return QtGui.QPixmap(
get_style_image_path("branch_open")
)
@property
def collapsed(self):
return self._collapsed
def set_collapsed(self, collapsed=None):
if collapsed is None:
collapsed = not self._collapsed
if self._collapsed == collapsed:
return
self._collapsed = collapsed
@ -182,6 +237,7 @@ class ExpandBtnLabel(QtWidgets.QLabel):
else:
self._current_image = self._source_expanded_pix
self._set_resized_pix()
self.state_changed.emit()
def resizeEvent(self, event):
self._set_resized_pix()
@ -203,21 +259,55 @@ class ExpandBtnLabel(QtWidgets.QLabel):
class ExpandBtn(ClickableFrame):
state_changed = QtCore.Signal()
def __init__(self, parent=None):
super(ExpandBtn, self).__init__(parent)
pixmap_label = ExpandBtnLabel(self)
pixmap_label = self._create_pix_widget(self)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(pixmap_label)
pixmap_label.state_changed.connect(self.state_changed)
self._pixmap_label = pixmap_label
def set_collapsed(self, collapsed):
def _create_pix_widget(self, parent=None):
if parent is None:
parent = self
return ExpandBtnLabel(parent)
@property
def collapsed(self):
return self._pixmap_label.collapsed
def set_collapsed(self, collapsed=None):
self._pixmap_label.set_collapsed(collapsed)
class ClassicExpandBtnLabel(ExpandBtnLabel):
def _create_collapsed_pixmap(self):
return QtGui.QPixmap(
get_style_image_path("right_arrow")
)
def _create_expanded_pixmap(self):
return QtGui.QPixmap(
get_style_image_path("down_arrow")
)
class ClassicExpandBtn(ExpandBtn):
"""Same as 'ExpandBtn' but with arrow images."""
def _create_pix_widget(self, parent=None):
if parent is None:
parent = self
return ClassicExpandBtnLabel(parent)
class ImageButton(QtWidgets.QPushButton):
"""PushButton with icon and size of font.