mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
added few new attribute definitions and their widgets
This commit is contained in:
parent
f28b4242c8
commit
396bdfde8a
9 changed files with 919 additions and 7 deletions
|
|
@ -1,18 +1,30 @@
|
|||
from .attribute_definitions import (
|
||||
AbtractAttrDef,
|
||||
|
||||
UIDef,
|
||||
UISeparatorDef,
|
||||
UILabelDef,
|
||||
|
||||
UnknownDef,
|
||||
NumberDef,
|
||||
TextDef,
|
||||
EnumDef,
|
||||
BoolDef
|
||||
BoolDef,
|
||||
FileDef,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AbtractAttrDef",
|
||||
|
||||
"UIDef",
|
||||
"UISeparatorDef",
|
||||
"UILabelDef",
|
||||
|
||||
"UnknownDef",
|
||||
"NumberDef",
|
||||
"TextDef",
|
||||
"EnumDef",
|
||||
"BoolDef"
|
||||
"BoolDef",
|
||||
"FileDef",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,13 +38,19 @@ class AbtractAttrDef:
|
|||
key(str): Under which key will be attribute value stored.
|
||||
label(str): Attribute label.
|
||||
tooltip(str): Attribute tooltip.
|
||||
is_label_horizontal(bool): UI specific argument. Specify if label is
|
||||
next to value input or ahead.
|
||||
"""
|
||||
is_value_def = True
|
||||
|
||||
def __init__(self, key, default, label=None, tooltip=None):
|
||||
def __init__(
|
||||
self, key, default, label=None, tooltip=None, is_label_horizontal=None
|
||||
):
|
||||
self.key = key
|
||||
self.label = label
|
||||
self.tooltip = tooltip
|
||||
self.default = default
|
||||
self.is_label_horizontal = is_label_horizontal
|
||||
self._id = uuid.uuid4()
|
||||
|
||||
self.__init__class__ = AbtractAttrDef
|
||||
|
|
@ -68,8 +74,39 @@ class AbtractAttrDef:
|
|||
pass
|
||||
|
||||
|
||||
# -----------------------------------------
|
||||
# UI attribute definitoins won't hold value
|
||||
# -----------------------------------------
|
||||
|
||||
class UIDef(AbtractAttrDef):
|
||||
is_value_def = False
|
||||
|
||||
def __init__(self, key=None, default=None, *args, **kwargs):
|
||||
super(UIDef, self).__init__(key, default, *args, **kwargs)
|
||||
|
||||
def convert_value(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class UISeparatorDef(UIDef):
|
||||
pass
|
||||
|
||||
|
||||
class UILabelDef(UIDef):
|
||||
def __init__(self, label):
|
||||
super(UILabelDef, self).__init__(label=label)
|
||||
|
||||
|
||||
# ---------------------------------------
|
||||
# Attribute defintioins should hold value
|
||||
# ---------------------------------------
|
||||
|
||||
class UnknownDef(AbtractAttrDef):
|
||||
"""Definition is not known because definition is not available."""
|
||||
"""Definition is not known because definition is not available.
|
||||
|
||||
This attribute can be used to keep existing data unchanged but does not
|
||||
have known definition of type.
|
||||
"""
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
super(UnknownDef, self).__init__(key, **kwargs)
|
||||
|
|
@ -261,3 +298,87 @@ class BoolDef(AbtractAttrDef):
|
|||
if isinstance(value, bool):
|
||||
return value
|
||||
return self.default
|
||||
|
||||
|
||||
class FileDef(AbtractAttrDef):
|
||||
"""File definition.
|
||||
It is possible to define filters of allowed file extensions and if supports
|
||||
folders.
|
||||
Args:
|
||||
multipath(bool): Allow multiple path.
|
||||
folders(bool): Allow folder paths.
|
||||
extensions(list<str>): Allow files with extensions. Empty list will
|
||||
allow all extensions and None will disable files completely.
|
||||
default(str, list<str>): Defautl value.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, key, multipath=False, folders=None, extensions=None,
|
||||
default=None, **kwargs
|
||||
):
|
||||
if folders is None and extensions is None:
|
||||
folders = True
|
||||
extensions = []
|
||||
|
||||
if default is None:
|
||||
if multipath:
|
||||
default = []
|
||||
else:
|
||||
default = ""
|
||||
else:
|
||||
if multipath:
|
||||
if not isinstance(default, (tuple, list, set)):
|
||||
raise TypeError((
|
||||
"'default' argument must be 'list', 'tuple' or 'set'"
|
||||
", not '{}'"
|
||||
).format(type(default)))
|
||||
|
||||
else:
|
||||
if not isinstance(default, six.string_types):
|
||||
raise TypeError((
|
||||
"'default' argument must be 'str' not '{}'"
|
||||
).format(type(default)))
|
||||
default = default.strip()
|
||||
|
||||
# Change horizontal label
|
||||
is_label_horizontal = kwargs.get("is_label_horizontal")
|
||||
if is_label_horizontal is None and multipath:
|
||||
kwargs["is_label_horizontal"] = False
|
||||
|
||||
self.multipath = multipath
|
||||
self.folders = folders
|
||||
self.extensions = extensions
|
||||
super(FileDef, self).__init__(key, default=default, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not super(FileDef, self).__eq__(other):
|
||||
return False
|
||||
|
||||
return (
|
||||
self.multipath == other.multipath
|
||||
and self.folders == other.folders
|
||||
and self.extensions == other.extensions
|
||||
)
|
||||
|
||||
def convert_value(self, value):
|
||||
if isinstance(value, six.string_types):
|
||||
if self.multipath:
|
||||
value = [value.strip()]
|
||||
else:
|
||||
value = value.strip()
|
||||
return value
|
||||
|
||||
if isinstance(value, (tuple, list, set)):
|
||||
_value = []
|
||||
for item in value:
|
||||
if isinstance(item, six.string_types):
|
||||
_value.append(item.strip())
|
||||
|
||||
if self.multipath:
|
||||
return _value
|
||||
|
||||
if not _value:
|
||||
return self.default
|
||||
return _value[0].strip()
|
||||
|
||||
return str(value).strip()
|
||||
|
|
|
|||
45
openpype/tools/resources/__init__.py
Normal file
45
openpype/tools/resources/__init__.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import os
|
||||
|
||||
from Qt import QtGui
|
||||
|
||||
|
||||
def get_icon_path(icon_name=None, filename=None):
|
||||
"""Path to image in './images' folder."""
|
||||
if icon_name is None and filename is None:
|
||||
return None
|
||||
|
||||
if filename is None:
|
||||
filename = "{}.png".format(icon_name)
|
||||
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"images",
|
||||
filename
|
||||
)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def get_image(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QImage."""
|
||||
path = get_icon_path(icon_name, filename)
|
||||
if path:
|
||||
return QtGui.QImage(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_pixmap(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QPixmap."""
|
||||
path = get_icon_path(icon_name, filename)
|
||||
if path:
|
||||
return QtGui.QPixmap(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_icon(icon_name=None, filename=None):
|
||||
"""Load image from './images' as QICon."""
|
||||
pix = get_pixmap(icon_name, filename)
|
||||
if pix:
|
||||
return QtGui.QIcon(pix)
|
||||
return None
|
||||
BIN
openpype/tools/resources/images/delete.png
Normal file
BIN
openpype/tools/resources/images/delete.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
openpype/tools/resources/images/file.png
Normal file
BIN
openpype/tools/resources/images/file.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
openpype/tools/resources/images/files.png
Normal file
BIN
openpype/tools/resources/images/files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3 KiB |
BIN
openpype/tools/resources/images/folder.png
Normal file
BIN
openpype/tools/resources/images/folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
645
openpype/widgets/attribute_defs/files_widget.py
Normal file
645
openpype/widgets/attribute_defs/files_widget.py
Normal file
|
|
@ -0,0 +1,645 @@
|
|||
import os
|
||||
import collections
|
||||
import uuid
|
||||
import clique
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import paint_image_with_color
|
||||
# TODO change imports
|
||||
from openpype.tools.resources import (
|
||||
get_pixmap,
|
||||
get_image,
|
||||
)
|
||||
from openpype.tools.utils import (
|
||||
IconButton,
|
||||
PixmapLabel
|
||||
)
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2
|
||||
ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3
|
||||
FILENAMES_ROLE = QtCore.Qt.UserRole + 4
|
||||
DIRPATH_ROLE = QtCore.Qt.UserRole + 5
|
||||
IS_DIR_ROLE = QtCore.Qt.UserRole + 6
|
||||
EXT_ROLE = QtCore.Qt.UserRole + 7
|
||||
|
||||
|
||||
class DropEmpty(QtWidgets.QWidget):
|
||||
_drop_enabled_text = "Drag & Drop\n(drop files here)"
|
||||
|
||||
def __init__(self, parent):
|
||||
super(DropEmpty, self).__init__(parent)
|
||||
label_widget = QtWidgets.QLabel(self._drop_enabled_text, self)
|
||||
label_widget.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addSpacing(10)
|
||||
layout.addWidget(
|
||||
label_widget,
|
||||
alignment=QtCore.Qt.AlignCenter
|
||||
)
|
||||
layout.addSpacing(10)
|
||||
|
||||
self._label_widget = label_widget
|
||||
|
||||
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()
|
||||
|
||||
left_m = content_margins.left()
|
||||
top_m = content_margins.top()
|
||||
rect = QtCore.QRect(
|
||||
left_m,
|
||||
top_m,
|
||||
(
|
||||
self.rect().width()
|
||||
- (left_m + content_margins.right() + pen.width())
|
||||
),
|
||||
(
|
||||
self.rect().height()
|
||||
- (top_m + content_margins.bottom() + pen.width())
|
||||
)
|
||||
)
|
||||
painter.drawRect(rect)
|
||||
|
||||
|
||||
class FilesModel(QtGui.QStandardItemModel):
|
||||
sequence_exts = [
|
||||
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal",
|
||||
".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits",
|
||||
".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer",
|
||||
".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2",
|
||||
".jng", ".jpeg", ".jpeg-ls", ".2000", ".jpg", ".xr",
|
||||
".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd",
|
||||
".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf",
|
||||
".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras",
|
||||
".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep",
|
||||
".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf",
|
||||
".xpm", ".xwd"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(FilesModel, self).__init__()
|
||||
self._filenames_by_dirpath = collections.defaultdict(set)
|
||||
self._items_by_dirpath = collections.defaultdict(list)
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
if not filepaths:
|
||||
return
|
||||
|
||||
new_dirpaths = set()
|
||||
for filepath in filepaths:
|
||||
filename = os.path.basename(filepath)
|
||||
dirpath = os.path.dirname(filepath)
|
||||
filenames = self._filenames_by_dirpath[dirpath]
|
||||
if filename not in filenames:
|
||||
new_dirpaths.add(dirpath)
|
||||
filenames.add(filename)
|
||||
self._refresh_items(new_dirpaths)
|
||||
|
||||
def remove_item_by_ids(self, item_ids):
|
||||
if not item_ids:
|
||||
return
|
||||
|
||||
remaining_ids = set(item_ids)
|
||||
result = collections.defaultdict(list)
|
||||
for dirpath, items in self._items_by_dirpath.items():
|
||||
if not remaining_ids:
|
||||
break
|
||||
for item in items:
|
||||
if not remaining_ids:
|
||||
break
|
||||
item_id = item.data(ITEM_ID_ROLE)
|
||||
if item_id in remaining_ids:
|
||||
remaining_ids.remove(item_id)
|
||||
result[dirpath].append(item)
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
dirpaths = set(result.keys())
|
||||
for dirpath, items in result.items():
|
||||
filenames_cache = self._filenames_by_dirpath[dirpath]
|
||||
for item in items:
|
||||
filenames = item.data(FILENAMES_ROLE)
|
||||
|
||||
self._items_by_dirpath[dirpath].remove(item)
|
||||
self.removeRows(item.row(), 1)
|
||||
for filename in filenames:
|
||||
if filename in filenames_cache:
|
||||
filenames_cache.remove(filename)
|
||||
|
||||
self._refresh_items(dirpaths)
|
||||
|
||||
def _refresh_items(self, dirpaths=None):
|
||||
if dirpaths is None:
|
||||
dirpaths = set(self._items_by_dirpath.keys())
|
||||
|
||||
new_items = []
|
||||
for dirpath in dirpaths:
|
||||
items_to_remove = list(self._items_by_dirpath[dirpath])
|
||||
cols, remainders = clique.assemble(
|
||||
self._filenames_by_dirpath[dirpath]
|
||||
)
|
||||
filtered_cols = []
|
||||
for collection in cols:
|
||||
filenames = set(collection)
|
||||
valid_col = True
|
||||
for filename in filenames:
|
||||
ext = os.path.splitext(filename)[-1]
|
||||
valid_col = ext in self.sequence_exts
|
||||
break
|
||||
|
||||
if valid_col:
|
||||
filtered_cols.append(collection)
|
||||
else:
|
||||
for filename in filenames:
|
||||
remainders.append(filename)
|
||||
|
||||
for filename in remainders:
|
||||
found = False
|
||||
for item in items_to_remove:
|
||||
item_filenames = item.data(FILENAMES_ROLE)
|
||||
if filename in item_filenames and len(item_filenames) == 1:
|
||||
found = True
|
||||
items_to_remove.remove(item)
|
||||
break
|
||||
|
||||
if found:
|
||||
continue
|
||||
|
||||
fullpath = os.path.join(dirpath, filename)
|
||||
if os.path.isdir(fullpath):
|
||||
icon_pixmap = get_pixmap(filename="folder.png")
|
||||
else:
|
||||
icon_pixmap = get_pixmap(filename="file.png")
|
||||
label = filename
|
||||
filenames = [filename]
|
||||
item = self._create_item(
|
||||
label, filenames, dirpath, icon_pixmap
|
||||
)
|
||||
new_items.append(item)
|
||||
self._items_by_dirpath[dirpath].append(item)
|
||||
|
||||
for collection in filtered_cols:
|
||||
filenames = set(collection)
|
||||
found = False
|
||||
for item in items_to_remove:
|
||||
item_filenames = item.data(FILENAMES_ROLE)
|
||||
if item_filenames == filenames:
|
||||
found = True
|
||||
items_to_remove.remove(item)
|
||||
break
|
||||
|
||||
if found:
|
||||
continue
|
||||
|
||||
col_range = collection.format("{ranges}")
|
||||
label = "{}<{}>{}".format(
|
||||
collection.head, col_range, collection.tail
|
||||
)
|
||||
icon_pixmap = get_pixmap(filename="files.png")
|
||||
item = self._create_item(
|
||||
label, filenames, dirpath, icon_pixmap
|
||||
)
|
||||
new_items.append(item)
|
||||
self._items_by_dirpath[dirpath].append(item)
|
||||
|
||||
for item in items_to_remove:
|
||||
self._items_by_dirpath[dirpath].remove(item)
|
||||
self.removeRows(item.row(), 1)
|
||||
|
||||
if new_items:
|
||||
self.invisibleRootItem().appendRows(new_items)
|
||||
|
||||
def _create_item(self, label, filenames, dirpath, icon_pixmap=None):
|
||||
first_filename = None
|
||||
for filename in filenames:
|
||||
first_filename = filename
|
||||
break
|
||||
ext = os.path.splitext(first_filename)[-1]
|
||||
is_dir = False
|
||||
if len(filenames) == 1:
|
||||
filepath = os.path.join(dirpath, first_filename)
|
||||
is_dir = os.path.isdir(filepath)
|
||||
|
||||
item = QtGui.QStandardItem()
|
||||
item.setData(str(uuid.uuid4()), ITEM_ID_ROLE)
|
||||
item.setData(label, ITEM_LABEL_ROLE)
|
||||
item.setData(filenames, FILENAMES_ROLE)
|
||||
item.setData(dirpath, DIRPATH_ROLE)
|
||||
item.setData(icon_pixmap, ITEM_ICON_ROLE)
|
||||
item.setData(ext, EXT_ROLE)
|
||||
item.setData(is_dir, IS_DIR_ROLE)
|
||||
|
||||
return item
|
||||
|
||||
|
||||
class FilesProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilesProxyModel, self).__init__(*args, **kwargs)
|
||||
self._allow_folders = False
|
||||
self._allowed_extensions = None
|
||||
|
||||
def set_allow_folders(self, allow=None):
|
||||
if allow is None:
|
||||
allow = not self._allow_folders
|
||||
|
||||
if allow == self._allow_folders:
|
||||
return
|
||||
self._allow_folders = allow
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_allowed_extensions(self, extensions=None):
|
||||
if extensions is not None:
|
||||
extensions = set(extensions)
|
||||
|
||||
if self._allowed_extensions != extensions:
|
||||
self._allowed_extensions = extensions
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
model = self.sourceModel()
|
||||
index = model.index(row, self.filterKeyColumn(), parent_index)
|
||||
# First check if item is folder and if folders are enabled
|
||||
if index.data(IS_DIR_ROLE):
|
||||
if not self._allow_folders:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Check if there are any allowed extensions
|
||||
if self._allowed_extensions is None:
|
||||
return False
|
||||
|
||||
if index.data(EXT_ROLE) not in self._allowed_extensions:
|
||||
return False
|
||||
return True
|
||||
|
||||
def lessThan(self, left, right):
|
||||
left_comparison = left.data(DIRPATH_ROLE)
|
||||
right_comparison = right.data(DIRPATH_ROLE)
|
||||
if left_comparison == right_comparison:
|
||||
left_comparison = left.data(ITEM_LABEL_ROLE)
|
||||
right_comparison = right.data(ITEM_LABEL_ROLE)
|
||||
|
||||
if sorted((left_comparison, right_comparison))[0] == left_comparison:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ItemWidget(QtWidgets.QWidget):
|
||||
remove_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, item_id, label, pixmap_icon, parent=None):
|
||||
self._item_id = item_id
|
||||
|
||||
super(ItemWidget, self).__init__(parent)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
icon_widget = PixmapLabel(pixmap_icon, self)
|
||||
label_widget = QtWidgets.QLabel(label, self)
|
||||
pixmap = paint_image_with_color(
|
||||
get_image(filename="delete.png"), QtCore.Qt.white
|
||||
)
|
||||
remove_btn = IconButton(self)
|
||||
remove_btn.setIcon(QtGui.QIcon(pixmap))
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(icon_widget, 0)
|
||||
layout.addWidget(label_widget, 1)
|
||||
layout.addWidget(remove_btn, 0)
|
||||
|
||||
remove_btn.clicked.connect(self._on_remove_clicked)
|
||||
|
||||
self._icon_widget = icon_widget
|
||||
self._label_widget = label_widget
|
||||
self._remove_btn = remove_btn
|
||||
|
||||
def _on_remove_clicked(self):
|
||||
self.remove_requested.emit(self._item_id)
|
||||
|
||||
|
||||
class FilesView(QtWidgets.QListView):
|
||||
"""View showing instances and their groups."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilesView, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setEditTriggers(QtWidgets.QListView.NoEditTriggers)
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
|
||||
def get_selected_item_ids(self):
|
||||
"""Ids of selected instances."""
|
||||
selected_item_ids = set()
|
||||
for index in self.selectionModel().selectedIndexes():
|
||||
instance_id = index.data(ITEM_ID_ROLE)
|
||||
if instance_id is not None:
|
||||
selected_item_ids.add(instance_id)
|
||||
return selected_item_ids
|
||||
|
||||
def event(self, event):
|
||||
if not event.type() == QtCore.QEvent.KeyPress:
|
||||
pass
|
||||
|
||||
elif event.key() == QtCore.Qt.Key_Space:
|
||||
self.toggle_requested.emit(-1)
|
||||
return True
|
||||
|
||||
elif event.key() == QtCore.Qt.Key_Backspace:
|
||||
self.toggle_requested.emit(0)
|
||||
return True
|
||||
|
||||
elif event.key() == QtCore.Qt.Key_Return:
|
||||
self.toggle_requested.emit(1)
|
||||
return True
|
||||
|
||||
return super(FilesView, self).event(event)
|
||||
|
||||
|
||||
class MultiFilesWidget(QtWidgets.QFrame):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(MultiFilesWidget, self).__init__(parent)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
empty_widget = DropEmpty(self)
|
||||
|
||||
files_model = FilesModel()
|
||||
files_proxy_model = FilesProxyModel()
|
||||
files_proxy_model.setSourceModel(files_model)
|
||||
files_view = FilesView(self)
|
||||
files_view.setModel(files_proxy_model)
|
||||
files_view.setVisible(False)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(empty_widget, 1)
|
||||
layout.addWidget(files_view, 1)
|
||||
|
||||
files_proxy_model.rowsInserted.connect(self._on_rows_inserted)
|
||||
files_proxy_model.rowsRemoved.connect(self._on_rows_removed)
|
||||
|
||||
self._in_set_value = False
|
||||
|
||||
self._empty_widget = empty_widget
|
||||
self._files_model = files_model
|
||||
self._files_proxy_model = files_proxy_model
|
||||
self._files_view = files_view
|
||||
|
||||
self._widgets_by_id = {}
|
||||
|
||||
def set_value(self, value, multivalue):
|
||||
self._in_set_value = True
|
||||
widget_ids = set(self._widgets_by_id.keys())
|
||||
self._remove_item_by_ids(widget_ids)
|
||||
|
||||
# TODO how to display multivalue?
|
||||
all_same = True
|
||||
if multivalue:
|
||||
new_value = set()
|
||||
item_row = None
|
||||
for _value in value:
|
||||
_value_set = set(_value)
|
||||
new_value |= _value_set
|
||||
if item_row is None:
|
||||
item_row = _value_set
|
||||
elif item_row != _value_set:
|
||||
all_same = False
|
||||
value = new_value
|
||||
|
||||
if value:
|
||||
self._add_filepaths(value)
|
||||
self._in_set_value = False
|
||||
|
||||
def current_value(self):
|
||||
model = self._files_proxy_model
|
||||
filepaths = set()
|
||||
for row in range(model.rowCount()):
|
||||
index = model.index(row, 0)
|
||||
dirpath = index.data(DIRPATH_ROLE)
|
||||
filenames = index.data(FILENAMES_ROLE)
|
||||
for filename in filenames:
|
||||
filepaths.add(os.path.join(dirpath, filename))
|
||||
return filepaths
|
||||
|
||||
def set_filters(self, folders_allowed, exts_filter):
|
||||
self._files_proxy_model.set_allow_folders(folders_allowed)
|
||||
self._files_proxy_model.set_allowed_extensions(exts_filter)
|
||||
|
||||
def _on_rows_inserted(self, parent_index, start_row, end_row):
|
||||
for row in range(start_row, end_row + 1):
|
||||
index = self._files_proxy_model.index(row, 0, parent_index)
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id in self._widgets_by_id:
|
||||
continue
|
||||
label = index.data(ITEM_LABEL_ROLE)
|
||||
pixmap_icon = index.data(ITEM_ICON_ROLE)
|
||||
|
||||
widget = ItemWidget(item_id, label, pixmap_icon)
|
||||
self._files_view.setIndexWidget(index, widget)
|
||||
self._files_proxy_model.setData(
|
||||
index, widget.sizeHint(), QtCore.Qt.SizeHintRole
|
||||
)
|
||||
widget.remove_requested.connect(self._on_remove_request)
|
||||
self._widgets_by_id[item_id] = widget
|
||||
|
||||
self._files_proxy_model.sort(0)
|
||||
|
||||
if not self._in_set_value:
|
||||
self.value_changed.emit()
|
||||
|
||||
def _on_rows_removed(self, parent_index, start_row, end_row):
|
||||
available_item_ids = set()
|
||||
for row in range(self._files_proxy_model.rowCount()):
|
||||
index = self._files_proxy_model.index(row, 0)
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
available_item_ids.add(index.data(ITEM_ID_ROLE))
|
||||
|
||||
widget_ids = set(self._widgets_by_id.keys())
|
||||
for item_id in available_item_ids:
|
||||
if item_id in widget_ids:
|
||||
widget_ids.remove(item_id)
|
||||
|
||||
for item_id in widget_ids:
|
||||
widget = self._widgets_by_id.pop(item_id)
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
|
||||
if not self._in_set_value:
|
||||
self.value_changed.emit()
|
||||
|
||||
def _on_remove_request(self, item_id):
|
||||
found_index = None
|
||||
for row in range(self._files_model.rowCount()):
|
||||
index = self._files_model.index(row, 0)
|
||||
_item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id == _item_id:
|
||||
found_index = index
|
||||
break
|
||||
|
||||
if found_index is None:
|
||||
return
|
||||
|
||||
items_to_delete = self._files_view.get_selected_item_ids()
|
||||
if item_id not in items_to_delete:
|
||||
items_to_delete = [item_id]
|
||||
|
||||
self._remove_item_by_ids(items_to_delete)
|
||||
|
||||
def sizeHint(self):
|
||||
# Get size hints of widget and visible widgets
|
||||
result = super(MultiFilesWidget, self).sizeHint()
|
||||
if not self._files_view.isVisible():
|
||||
not_visible_hint = self._files_view.sizeHint()
|
||||
else:
|
||||
not_visible_hint = self._empty_widget.sizeHint()
|
||||
|
||||
# Get margins of this widget
|
||||
margins = self.layout().contentsMargins()
|
||||
|
||||
# Change size hint based on result of maximum size hint of widgets
|
||||
result.setWidth(max(
|
||||
result.width(),
|
||||
not_visible_hint.width() + margins.left() + margins.right()
|
||||
))
|
||||
result.setHeight(max(
|
||||
result.height(),
|
||||
not_visible_hint.height() + margins.top() + margins.bottom()
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
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()
|
||||
if os.path.exists(filepath):
|
||||
filepaths.append(filepath)
|
||||
if filepaths:
|
||||
self._add_filepaths(filepaths)
|
||||
event.accept()
|
||||
|
||||
def _add_filepaths(self, filepaths):
|
||||
self._files_model.add_filepaths(filepaths)
|
||||
self._update_visibility()
|
||||
|
||||
def _remove_item_by_ids(self, item_ids):
|
||||
self._files_model.remove_item_by_ids(item_ids)
|
||||
self._update_visibility()
|
||||
|
||||
def _update_visibility(self):
|
||||
files_exists = self._files_model.rowCount() > 0
|
||||
self._files_view.setVisible(files_exists)
|
||||
self._empty_widget.setVisible(not files_exists)
|
||||
|
||||
|
||||
class SingleFileWidget(QtWidgets.QWidget):
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(SingleFileWidget, self).__init__(parent)
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
filepath_input = QtWidgets.QLineEdit(self)
|
||||
|
||||
browse_btn = QtWidgets.QPushButton("Browse", self)
|
||||
browse_btn.setVisible(False)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(filepath_input, 1)
|
||||
layout.addWidget(browse_btn, 0)
|
||||
|
||||
browse_btn.clicked.connect(self._on_browse_clicked)
|
||||
filepath_input.textChanged.connect(self._on_text_change)
|
||||
|
||||
self._in_set_value = False
|
||||
|
||||
self._filepath_input = filepath_input
|
||||
self._folders_allowed = False
|
||||
self._exts_filter = []
|
||||
|
||||
def set_value(self, value, multivalue):
|
||||
self._in_set_value = True
|
||||
|
||||
if multivalue:
|
||||
set_value = set(value)
|
||||
if len(set_value) == 1:
|
||||
value = tuple(set_value)[0]
|
||||
else:
|
||||
value = "< Multiselection >"
|
||||
self._filepath_input.setText(value)
|
||||
|
||||
self._in_set_value = False
|
||||
|
||||
def current_value(self):
|
||||
return self._filepath_input.text()
|
||||
|
||||
def set_filters(self, folders_allowed, exts_filter):
|
||||
self._folders_allowed = folders_allowed
|
||||
self._exts_filter = exts_filter
|
||||
|
||||
def _on_text_change(self, text):
|
||||
if not self._in_set_value:
|
||||
self.value_changed.emit()
|
||||
|
||||
def _on_browse_clicked(self):
|
||||
# TODO implement file dialog logic in '_on_browse_clicked'
|
||||
print("_on_browse_clicked")
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if not mime_data.hasUrls():
|
||||
return
|
||||
|
||||
filepaths = []
|
||||
for url in mime_data.urls():
|
||||
filepath = url.toLocalFile()
|
||||
if os.path.exists(filepath):
|
||||
filepaths.append(filepath)
|
||||
|
||||
# TODO add folder, extensions check
|
||||
if len(filepaths) == 1:
|
||||
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()
|
||||
if os.path.exists(filepath):
|
||||
filepaths.append(filepath)
|
||||
# TODO filter check
|
||||
if len(filepaths) == 1:
|
||||
self.set_value(filepaths[0], False)
|
||||
event.accept()
|
||||
|
|
@ -1,14 +1,20 @@
|
|||
import os
|
||||
import uuid
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from openpype.pipeline.lib import (
|
||||
AbtractAttrDef,
|
||||
UnknownDef,
|
||||
NumberDef,
|
||||
TextDef,
|
||||
EnumDef,
|
||||
BoolDef
|
||||
BoolDef,
|
||||
FileDef,
|
||||
UISeparatorDef,
|
||||
UILabelDef
|
||||
)
|
||||
from openpype.widgets.nice_checkbox import NiceCheckbox
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
def create_widget_for_attr_def(attr_def, parent=None):
|
||||
|
|
@ -32,12 +38,22 @@ def create_widget_for_attr_def(attr_def, parent=None):
|
|||
if isinstance(attr_def, UnknownDef):
|
||||
return UnknownAttrWidget(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 _BaseAttrDefWidget(QtWidgets.QWidget):
|
||||
# Type 'object' may not work with older PySide versions
|
||||
value_changed = QtCore.Signal(object, uuid.UUID)
|
||||
|
||||
def __init__(self, attr_def, parent):
|
||||
|
|
@ -68,12 +84,36 @@ class _BaseAttrDefWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_value(self, value, multivalue=False):
|
||||
raise NotImplementedError(
|
||||
"Method 'current_value' is not implemented. {}".format(
|
||||
"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 NumberAttrWidget(_BaseAttrDefWidget):
|
||||
def _ui_init(self):
|
||||
decimals = self.attr_def.decimals
|
||||
|
|
@ -83,6 +123,9 @@ class NumberAttrWidget(_BaseAttrDefWidget):
|
|||
else:
|
||||
input_widget = QtWidgets.QSpinBox(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)
|
||||
|
|
@ -136,6 +179,9 @@ class TextAttrWidget(_BaseAttrDefWidget):
|
|||
):
|
||||
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)
|
||||
|
|
@ -184,6 +230,9 @@ class BoolAttrWidget(_BaseAttrDefWidget):
|
|||
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
|
||||
|
|
@ -220,6 +269,9 @@ class EnumAttrWidget(_BaseAttrDefWidget):
|
|||
combo_delegate = QtWidgets.QStyledItemDelegate(input_widget)
|
||||
input_widget.setItemDelegate(combo_delegate)
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
items = self.attr_def.items
|
||||
for key, label in items.items():
|
||||
input_widget.addItem(label, key)
|
||||
|
|
@ -281,3 +333,40 @@ class UnknownAttrWidget(_BaseAttrDefWidget):
|
|||
if str_value != self._value:
|
||||
self._value = str_value
|
||||
self._input_widget.setText(str_value)
|
||||
|
||||
|
||||
class FileAttrWidget(_BaseAttrDefWidget):
|
||||
def _ui_init(self):
|
||||
self.multipath = self.attr_def.multipath
|
||||
if self.multipath:
|
||||
from .files_widget import MultiFilesWidget
|
||||
|
||||
input_widget = MultiFilesWidget(self)
|
||||
|
||||
else:
|
||||
from .files_widget import SingleFileWidget
|
||||
|
||||
input_widget = SingleFileWidget(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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue