ayon-core/openpype/tools/attribute_defs/widgets.py
Petr Kalis 523f023033
SiteSync: implemented in Ayon Loader (#5962)
* Added new SiteSync model

Used to get information from SiteSync module to enhance Loader UI.

* Added new SiteSync method to controller

Other models will be using these to get information pertaining SiteSync

* Added missed commit

* Implemented collection of SiteSync info

* Added AvailabilityDelegate

Shows how many representations are present locally and remotely in Loader summary page.

* Added fields to store progress info

* Fix HiddenAttr to carry value

* Refactored to internal variable

Changes made after discussion

* Implemented ActionItems for upload/download/remove

Replaced old Launcher approach, now it is not necessary after refactor of Ayon launcher.

* Update openpype/tools/ayon_loader/abstract.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Hound

* Refactor better retrieval of icon

* Refactor better readability

* Refactor renamed delegate

* Refactor better retrieval of icons

* Refactor better readability

* Refactor removed unneeded explicit refresh

* Hound

* Hound

* Hound

* Fix used wrong type

* Update openpype/tools/ayon_loader/ui/products_delegates.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Refactor renamed variable name

* Refactor formatting

* Added progress for representations

* cache version availability

* cache representations sync status

* changed representations count logic and moved it to products model

* site sync enabled is cached

* active and remote site names are cached

* small tweaks in site sync model

* change methods called by controller

* hide site sync columns if site sync not enabled

* use string conversion before iteration

* smal formatting changes

* updated abstract class with abstract methods

* renamed site sync model variable

* fixed method name

* fix used method name

* rename '_sitesync_addon' to '_site_sync_addon'

* fix remote site name cache

* small formatting changes in delegate

* modify site sync delegate to be more dynamic

* fix delegate painting

* do not handle repre progress in products model

* Add comma back

* simplify delegate code

---------

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <jakub.trllo@gmail.com>
2023-12-04 11:27:43 +01:00

661 lines
20 KiB
Python

import copy
from qtpy import QtWidgets, QtCore
from openpype.lib.attribute_definitions import (
AbstractAttrDef,
UnknownDef,
HiddenDef,
NumberDef,
TextDef,
EnumDef,
BoolDef,
FileDef,
UIDef,
UISeparatorDef,
UILabelDef
)
from openpype.tools.utils import (
CustomTextComboBox,
FocusSpinBox,
FocusDoubleSpinBox,
MultiSelectionComboBox,
)
from openpype.widgets.nice_checkbox import NiceCheckbox
from .files_widget import FilesWidget
def create_widget_for_attr_def(attr_def, parent=None):
widget = _create_widget_for_attr_def(attr_def, parent)
if attr_def.hidden:
widget.setVisible(False)
if attr_def.disabled:
widget.setEnabled(False)
return widget
def _create_widget_for_attr_def(attr_def, parent=None):
if not isinstance(attr_def, AbstractAttrDef):
raise TypeError("Unexpected type \"{}\" expected \"{}\"".format(
str(type(attr_def)), AbstractAttrDef
))
if isinstance(attr_def, NumberDef):
return NumberAttrWidget(attr_def, parent)
if isinstance(attr_def, TextDef):
return TextAttrWidget(attr_def, parent)
if isinstance(attr_def, EnumDef):
return EnumAttrWidget(attr_def, parent)
if isinstance(attr_def, BoolDef):
return BoolAttrWidget(attr_def, parent)
if isinstance(attr_def, UnknownDef):
return UnknownAttrWidget(attr_def, parent)
if isinstance(attr_def, HiddenDef):
return HiddenAttrWidget(attr_def, parent)
if isinstance(attr_def, FileDef):
return FileAttrWidget(attr_def, parent)
if isinstance(attr_def, UISeparatorDef):
return SeparatorAttrWidget(attr_def, parent)
if isinstance(attr_def, UILabelDef):
return LabelAttrWidget(attr_def, parent)
raise ValueError("Unknown attribute definition \"{}\"".format(
str(type(attr_def))
))
class AttributeDefinitionsWidget(QtWidgets.QWidget):
"""Create widgets for attribute definitions in grid layout.
Widget creates input widgets for passed attribute definitions.
Widget can't handle multiselection values.
"""
def __init__(self, attr_defs=None, parent=None):
super(AttributeDefinitionsWidget, self).__init__(parent)
self._widgets = []
self._current_keys = set()
self.set_attr_defs(attr_defs)
def clear_attr_defs(self):
"""Remove all existing widgets and reset layout if needed."""
self._widgets = []
self._current_keys = set()
layout = self.layout()
if layout is not None:
if layout.count() == 0:
return
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget:
widget.setVisible(False)
widget.deleteLater()
layout.deleteLater()
new_layout = QtWidgets.QGridLayout()
new_layout.setColumnStretch(0, 0)
new_layout.setColumnStretch(1, 1)
self.setLayout(new_layout)
def set_attr_defs(self, attr_defs):
"""Replace current attribute definitions with passed."""
self.clear_attr_defs()
if attr_defs:
self.add_attr_defs(attr_defs)
def add_attr_defs(self, attr_defs):
"""Add attribute definitions to current."""
layout = self.layout()
row = 0
for attr_def in attr_defs:
if attr_def.is_value_def:
if attr_def.key in self._current_keys:
raise KeyError(
"Duplicated key \"{}\"".format(attr_def.key))
self._current_keys.add(attr_def.key)
widget = create_widget_for_attr_def(attr_def, self)
self._widgets.append(widget)
if attr_def.hidden:
continue
expand_cols = 2
if attr_def.is_value_def and attr_def.is_label_horizontal:
expand_cols = 1
col_num = 2 - expand_cols
if attr_def.is_value_def and attr_def.label:
label_widget = QtWidgets.QLabel(attr_def.label, self)
tooltip = attr_def.tooltip
if tooltip:
label_widget.setToolTip(tooltip)
if attr_def.is_label_horizontal:
label_widget.setAlignment(
QtCore.Qt.AlignRight
| QtCore.Qt.AlignVCenter
)
layout.addWidget(
label_widget, row, 0, 1, expand_cols
)
if not attr_def.is_label_horizontal:
row += 1
layout.addWidget(
widget, row, col_num, 1, expand_cols
)
row += 1
def set_value(self, value):
new_value = copy.deepcopy(value)
unused_keys = set(new_value.keys())
for widget in self._widgets:
attr_def = widget.attr_def
if attr_def.key not in new_value:
continue
unused_keys.remove(attr_def.key)
widget_value = new_value[attr_def.key]
if widget_value is None:
widget_value = copy.deepcopy(attr_def.default)
widget.set_value(widget_value)
def current_value(self):
output = {}
for widget in self._widgets:
attr_def = widget.attr_def
if not isinstance(attr_def, UIDef):
output[attr_def.key] = widget.current_value()
return output
class _BaseAttrDefWidget(QtWidgets.QWidget):
# Type 'object' may not work with older PySide versions
value_changed = QtCore.Signal(object, str)
def __init__(self, attr_def, parent):
super(_BaseAttrDefWidget, self).__init__(parent)
self.attr_def = attr_def
main_layout = QtWidgets.QHBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout = main_layout
self._ui_init()
def _ui_init(self):
raise NotImplementedError(
"Method '_ui_init' is not implemented. {}".format(
self.__class__.__name__
)
)
def current_value(self):
raise NotImplementedError(
"Method 'current_value' is not implemented. {}".format(
self.__class__.__name__
)
)
def set_value(self, value, multivalue=False):
raise NotImplementedError(
"Method 'set_value' is not implemented. {}".format(
self.__class__.__name__
)
)
class SeparatorAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
input_widget = QtWidgets.QWidget(self)
input_widget.setObjectName("Separator")
input_widget.setMinimumHeight(2)
input_widget.setMaximumHeight(2)
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
class LabelAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
input_widget = QtWidgets.QLabel(self)
label = self.attr_def.label
if label:
input_widget.setText(str(label))
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
class ClickableLineEdit(QtWidgets.QLineEdit):
clicked = QtCore.Signal()
def __init__(self, text, parent):
super(ClickableLineEdit, self).__init__(parent)
self.setText(text)
self.setReadOnly(True)
self._mouse_pressed = False
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self._mouse_pressed = True
super(ClickableLineEdit, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if self._mouse_pressed:
self._mouse_pressed = False
if self.rect().contains(event.pos()):
self.clicked.emit()
super(ClickableLineEdit, self).mouseReleaseEvent(event)
class NumberAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
decimals = self.attr_def.decimals
if decimals > 0:
input_widget = FocusDoubleSpinBox(self)
input_widget.setDecimals(decimals)
else:
input_widget = FocusSpinBox(self)
if self.attr_def.tooltip:
input_widget.setToolTip(self.attr_def.tooltip)
input_widget.setMinimum(self.attr_def.minimum)
input_widget.setMaximum(self.attr_def.maximum)
input_widget.setValue(self.attr_def.default)
input_widget.setButtonSymbols(
QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons
)
input_line_edit = input_widget.lineEdit()
input_widget.installEventFilter(self)
multisel_widget = ClickableLineEdit("< Multiselection >", self)
multisel_widget.setVisible(False)
input_widget.valueChanged.connect(self._on_value_change)
multisel_widget.clicked.connect(self._on_multi_click)
self._input_widget = input_widget
self._input_line_edit = input_line_edit
self._multisel_widget = multisel_widget
self._last_multivalue = None
self._multivalue = False
self.main_layout.addWidget(input_widget, 0)
self.main_layout.addWidget(multisel_widget, 0)
def eventFilter(self, obj, event):
if (
self._multivalue
and obj is self._input_widget
and event.type() == QtCore.QEvent.FocusOut
):
self._set_multiselection_visible(True)
return False
def current_value(self):
return self._input_widget.value()
def set_value(self, value, multivalue=False):
self._last_multivalue = None
if multivalue:
set_value = set(value)
if None in set_value:
set_value.remove(None)
set_value.add(self.attr_def.default)
if len(set_value) > 1:
self._last_multivalue = next(iter(set_value), None)
self._set_multiselection_visible(True)
self._multivalue = True
return
value = tuple(set_value)[0]
self._multivalue = False
self._set_multiselection_visible(False)
if self.current_value != value:
self._input_widget.setValue(value)
def _on_value_change(self, new_value):
self._multivalue = False
self.value_changed.emit(new_value, self.attr_def.id)
def _on_multi_click(self):
self._set_multiselection_visible(False, True)
def _set_multiselection_visible(self, visible, change_focus=False):
self._input_widget.setVisible(not visible)
self._multisel_widget.setVisible(visible)
if visible:
return
# Change value once user clicked on the input field
if self._last_multivalue is None:
value = self.attr_def.default
else:
value = self._last_multivalue
self._input_widget.blockSignals(True)
self._input_widget.setValue(value)
self._input_widget.blockSignals(False)
if not change_focus:
return
# Change focus to input field and move cursor to the end
self._input_widget.setFocus(QtCore.Qt.MouseFocusReason)
self._input_line_edit.setCursorPosition(
len(self._input_line_edit.text())
)
class TextAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
# TODO Solve how to handle regex
# self.attr_def.regex
self.multiline = self.attr_def.multiline
if self.multiline:
input_widget = QtWidgets.QPlainTextEdit(self)
else:
input_widget = QtWidgets.QLineEdit(self)
if (
self.attr_def.placeholder
and hasattr(input_widget, "setPlaceholderText")
):
input_widget.setPlaceholderText(self.attr_def.placeholder)
if self.attr_def.tooltip:
input_widget.setToolTip(self.attr_def.tooltip)
if self.attr_def.default:
if self.multiline:
input_widget.setPlainText(self.attr_def.default)
else:
input_widget.setText(self.attr_def.default)
input_widget.textChanged.connect(self._on_value_change)
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
def _on_value_change(self):
if self.multiline:
new_value = self._input_widget.toPlainText()
else:
new_value = self._input_widget.text()
self.value_changed.emit(new_value, self.attr_def.id)
def current_value(self):
if self.multiline:
return self._input_widget.toPlainText()
return self._input_widget.text()
def set_value(self, value, multivalue=False):
block_signals = False
if multivalue:
set_value = set(value)
if None in set_value:
set_value.remove(None)
set_value.add(self.attr_def.default)
if len(set_value) == 1:
value = tuple(set_value)[0]
else:
block_signals = True
value = "< Multiselection >"
if value != self.current_value():
if block_signals:
self._input_widget.blockSignals(True)
if self.multiline:
self._input_widget.setPlainText(value)
else:
self._input_widget.setText(value)
if block_signals:
self._input_widget.blockSignals(False)
class BoolAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
input_widget = NiceCheckbox(parent=self)
input_widget.setChecked(self.attr_def.default)
if self.attr_def.tooltip:
input_widget.setToolTip(self.attr_def.tooltip)
input_widget.stateChanged.connect(self._on_value_change)
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
self.main_layout.addStretch(1)
def _on_value_change(self):
new_value = self._input_widget.isChecked()
self.value_changed.emit(new_value, self.attr_def.id)
def current_value(self):
return self._input_widget.isChecked()
def set_value(self, value, multivalue=False):
if multivalue:
set_value = set(value)
if None in set_value:
set_value.remove(None)
set_value.add(self.attr_def.default)
if len(set_value) > 1:
self._input_widget.blockSignals(True)
self._input_widget.setCheckState(QtCore.Qt.PartiallyChecked)
self._input_widget.blockSignals(False)
return
value = tuple(set_value)[0]
if value != self.current_value():
self._input_widget.setChecked(value)
class EnumAttrWidget(_BaseAttrDefWidget):
def __init__(self, *args, **kwargs):
self._multivalue = False
super(EnumAttrWidget, self).__init__(*args, **kwargs)
@property
def multiselection(self):
return self.attr_def.multiselection
def _ui_init(self):
if self.multiselection:
input_widget = MultiSelectionComboBox(self)
else:
input_widget = CustomTextComboBox(self)
combo_delegate = QtWidgets.QStyledItemDelegate(input_widget)
input_widget.setItemDelegate(combo_delegate)
self._combo_delegate = combo_delegate
if self.attr_def.tooltip:
input_widget.setToolTip(self.attr_def.tooltip)
for item in self.attr_def.items:
input_widget.addItem(item["label"], item["value"])
idx = input_widget.findData(self.attr_def.default)
if idx >= 0:
input_widget.setCurrentIndex(idx)
if self.multiselection:
input_widget.value_changed.connect(self._on_value_change)
else:
input_widget.currentIndexChanged.connect(self._on_value_change)
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
def _on_value_change(self):
new_value = self.current_value()
if self._multivalue:
self._multivalue = False
self._input_widget.set_custom_text(None)
self.value_changed.emit(new_value, self.attr_def.id)
def current_value(self):
if self.multiselection:
return self._input_widget.value()
idx = self._input_widget.currentIndex()
return self._input_widget.itemData(idx)
def _multiselection_multivalue_prep(self, values):
final = None
multivalue = False
for value in values:
value = set(value)
if final is None:
final = value
elif multivalue or final != value:
final |= value
multivalue = True
return list(final), multivalue
def set_value(self, value, multivalue=False):
if multivalue:
if self.multiselection:
value, multivalue = self._multiselection_multivalue_prep(
value)
else:
set_value = set(value)
if len(set_value) == 1:
multivalue = False
value = tuple(set_value)[0]
if self.multiselection:
self._input_widget.blockSignals(True)
self._input_widget.set_value(value)
self._input_widget.blockSignals(False)
elif not multivalue:
idx = self._input_widget.findData(value)
cur_idx = self._input_widget.currentIndex()
if idx != cur_idx and idx >= 0:
self._input_widget.setCurrentIndex(idx)
custom_text = None
if multivalue:
custom_text = "< Multiselection >"
self._input_widget.set_custom_text(custom_text)
self._multivalue = multivalue
class UnknownAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
input_widget = QtWidgets.QLabel(self)
self._value = self.attr_def.default
input_widget.setText(str(self._value))
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
def current_value(self):
raise ValueError(
"{} can't hold real value.".format(self.__class__.__name__)
)
def set_value(self, value, multivalue=False):
if multivalue:
set_value = set(value)
if len(set_value) == 1:
value = tuple(set_value)[0]
else:
value = "< Multiselection >"
str_value = str(value)
if str_value != self._value:
self._value = str_value
self._input_widget.setText(str_value)
class HiddenAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
self.setVisible(False)
self._value = self.attr_def.default
self._multivalue = False
def setVisible(self, visible):
if visible:
visible = False
super(HiddenAttrWidget, self).setVisible(visible)
def current_value(self):
if self._multivalue:
raise ValueError("{} can't output for multivalue.".format(
self.__class__.__name__
))
return self._value
def set_value(self, value, multivalue=False):
self._value = copy.deepcopy(value)
self._multivalue = multivalue
class FileAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
input_widget = FilesWidget(
self.attr_def.single_item,
self.attr_def.allow_sequences,
self.attr_def.extensions_label,
self
)
if self.attr_def.tooltip:
input_widget.setToolTip(self.attr_def.tooltip)
input_widget.set_filters(
self.attr_def.folders, self.attr_def.extensions
)
input_widget.value_changed.connect(self._on_value_change)
self._input_widget = input_widget
self.main_layout.addWidget(input_widget, 0)
def _on_value_change(self):
new_value = self.current_value()
self.value_changed.emit(new_value, self.attr_def.id)
def current_value(self):
return self._input_widget.current_value()
def set_value(self, value, multivalue=False):
self._input_widget.set_value(value, multivalue)