diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 4cf8a238e0..f4f06448a5 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -21,3 +21,10 @@ from .widget_asset_view import AssetView from .widget_asset import AssetWidget from .widget_family_desc import FamilyDescriptionWidget from .widget_family import FamilyWidget +from .widget_drop_data import DropDataWidget + +from .widget_component import ComponentWidget +from .widget_tree_components import TreeComponents +from .widget_component_item import ComponentItem + +from .widget_drop_files import DropDataFrame diff --git a/pype/tools/standalonepublish/widgets/widget_component.py b/pype/tools/standalonepublish/widgets/widget_component.py new file mode 100644 index 0000000000..f7248e31c1 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_component.py @@ -0,0 +1,189 @@ +from . import QtCore, QtGui, QtWidgets +from . import SvgButton +from . import get_resource + + +class ComponentWidget(QtWidgets.QFrame): + C_NORMAL = '#777777' + C_HOVER = '#ffffff' + C_ACTIVE = '#4BB543' + C_ACTIVE_HOVER = '#4BF543' + signal_remove = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + self.resize(290, 70) + self.setMinimumSize(QtCore.QSize(0, 70)) + self.parent_item = parent + # Font + font = QtGui.QFont() + font.setFamily("DejaVu Sans Condensed") + font.setPointSize(9) + font.setBold(True) + font.setWeight(50) + font.setKerning(True) + + # Main widgets + frame = QtWidgets.QFrame(self) + frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + frame.setFrameShadow(QtWidgets.QFrame.Raised) + + layout_main = QtWidgets.QHBoxLayout(frame) + layout_main.setSpacing(2) + layout_main.setContentsMargins(2, 2, 2, 2) + + # Image + Info + frame_image_info = QtWidgets.QFrame(frame) + + # Layout image info + layout = QtWidgets.QVBoxLayout(frame_image_info) + layout.setSpacing(2) + layout.setContentsMargins(2, 2, 2, 2) + + image = QtWidgets.QLabel(frame) + image.setMinimumSize(QtCore.QSize(22, 22)) + image.setMaximumSize(QtCore.QSize(22, 22)) + image.setText("") + image.setScaledContents(True) + pixmap = QtGui.QPixmap(get_resource('image_sequence.png')) + image.setPixmap(pixmap) + + self.info = SvgButton( + get_resource('information.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame_image_info, False + ) + + expanding_sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding + ) + expanding_sizePolicy.setHorizontalStretch(0) + expanding_sizePolicy.setVerticalStretch(0) + + layout.addWidget(image, alignment=QtCore.Qt.AlignCenter) + layout.addWidget(self.info, alignment=QtCore.Qt.AlignCenter) + + layout_main.addWidget(frame_image_info) + + # Name + representation + self.name = QtWidgets.QLabel(frame) + self.frames = QtWidgets.QLabel(frame) + self.ext = QtWidgets.QLabel(frame) + + self.name.setFont(font) + self.frames.setFont(font) + self.ext.setFont(font) + + self.frames.setStyleSheet('padding-left:3px;') + + expanding_sizePolicy.setHeightForWidth(self.name.sizePolicy().hasHeightForWidth()) + + frame_name_repre = QtWidgets.QFrame(frame) + + self.frames.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.ext.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.name.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + + layout = QtWidgets.QHBoxLayout(frame_name_repre) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.name, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.frames, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.ext, alignment=QtCore.Qt.AlignRight) + + frame_name_repre.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding + ) + + # Frames + icons + frame_repre_icons = QtWidgets.QFrame(frame) + + label_repre = QtWidgets.QLabel() + label_repre.setText('Representation:') + + self.input_repre = QtWidgets.QLineEdit() + self.input_repre.setMaximumWidth(50) + + frame_icons = QtWidgets.QFrame(frame_repre_icons) + + self.preview = SvgButton( + get_resource('preview.svg'), 64, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + self.thumbnail = SvgButton( + get_resource('thumbnail.svg'), 84, 18, + [self.C_NORMAL, self.C_HOVER, self.C_ACTIVE, self.C_ACTIVE_HOVER], + frame_icons + ) + + layout = QtWidgets.QHBoxLayout(frame_icons) + layout.setSpacing(6) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.thumbnail) + layout.addWidget(self.preview) + + layout = QtWidgets.QHBoxLayout(frame_repre_icons) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addWidget(label_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(self.input_repre, alignment=QtCore.Qt.AlignLeft) + layout.addWidget(frame_icons, alignment=QtCore.Qt.AlignRight) + + frame_middle = QtWidgets.QFrame(frame) + + layout = QtWidgets.QVBoxLayout(frame_middle) + layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.addWidget(frame_name_repre) + layout.addWidget(frame_repre_icons) + + layout.setStretchFactor(frame_name_repre, 1) + layout.setStretchFactor(frame_repre_icons, 1) + + layout_main.addWidget(frame_middle) + + self.remove = SvgButton( + get_resource('trash.svg'), 22, 22, + [self.C_NORMAL, self.C_HOVER], + frame, False + ) + + layout_main.addWidget(self.remove) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(2, 2, 2, 2) + layout.addWidget(frame) + + self.preview.setToolTip('Mark component as Preview') + self.thumbnail.setToolTip('Component will be selected as thumbnail') + + # self.frame.setStyleSheet("border: 1px solid black;") + + def set_context(self, data): + + self.remove.clicked.connect(self._remove) + name = data['name'] + representation = data['representation'] + ext = data['ext'] + file_info = data['file_info'] + thumb = data['thumb'] + prev = data['prev'] + info = data['info'] + + self.name.setText(name) + self.input_repre.setText(representation) + self.ext.setText('( {} )'.format(ext)) + if file_info is None: + self.file_info.setVisible(False) + else: + self.file_info.setText('[{}]'.format(file_info)) + + # self.thumbnail.setVisible(thumb) + # self.preview.setVisible(prev) + + def _remove(self): + self.signal_remove.emit(self.parent_item) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py new file mode 100644 index 0000000000..1236a439c0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -0,0 +1,15 @@ +from . import QtWidgets +from . import ComponentWidget + + +class ComponentItem(QtWidgets.QTreeWidgetItem): + def __init__(self, parent, data): + super().__init__(parent) + self.in_data = data + self._widget = ComponentWidget(self) + self._widget.set_context(data) + + self.treeWidget().setItemWidget(self, 0, self._widget) + + def double_clicked(*args): + pass diff --git a/pype/tools/standalonepublish/widgets/widget_drop_data.py b/pype/tools/standalonepublish/widgets/widget_drop_data.py new file mode 100644 index 0000000000..96294ea99e --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_data.py @@ -0,0 +1,41 @@ +import os +import logging +import clique +from . import QtWidgets, QtCore, QtGui + + +class DropDataWidget(QtWidgets.QWidget): + + def __init__(self, parent): + '''Initialise DataDropZone widget.''' + super().__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + + bottomCenterAlignment = QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter + topCenterAlignment = QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + + self._label = QtWidgets.QLabel('Drop files here') + layout.addWidget( + self._label, + alignment=bottomCenterAlignment + ) + + self._browseButton = QtWidgets.QPushButton('Browse') + self._browseButton.setToolTip('Browse for file(s).') + layout.addWidget( + self._browseButton, alignment=topCenterAlignment + ) + + def paintEvent(self, event): + super().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) + painter.drawRect( + 10, 10, + self.rect().width()-15, self.rect().height()-15 + ) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_files.py b/pype/tools/standalonepublish/widgets/widget_drop_files.py new file mode 100644 index 0000000000..0b2241e465 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_drop_files.py @@ -0,0 +1,273 @@ +import os +import clique +from . import QtWidgets, QtCore +from . import ComponentItem, TreeComponents, DropDataWidget + + +class DropDataFrame(QtWidgets.QFrame): + # signal_dropped = QtCore.Signal(object) + + def __init__(self, parent): + super().__init__() + + self.items = [] + + self.setAcceptDrops(True) + layout = QtWidgets.QVBoxLayout(self) + + self.tree_widget = TreeComponents(self) + layout.addWidget(self.tree_widget) + + self.drop_widget = DropDataWidget(self) + layout.addWidget(self.drop_widget) + + self._refresh_view() + + def dragEnterEvent(self, event): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def dragLeaveEvent(self, event): + event.accept() + + def dropEvent(self, event): + paths = self._processMimeData(event.mimeData()) + if paths: + self._add_components(paths) + event.accept() + + def _processMimeData(self, mimeData): + paths = [] + + if not mimeData.hasUrls(): + print('Dropped invalid file/folder') + return paths + + for path in mimeData.urls(): + local_path = path.toLocalFile() + if os.path.isfile(local_path) or os.path.isdir(local_path): + paths.append(local_path) + else: + print('Invalid input: "{}"'.format(local_path)) + + return paths + + def _add_components(self, paths): + components = self._process_paths(paths) + if not components: + return + for component in components: + self._add_item(component) + + def _add_item(self, data): + # Assign to self so garbage collector wont remove the component + # during initialization + self.new_component = ComponentItem(self.tree_widget, data) + self.new_component._widget.signal_remove.connect(self._remove_item) + self.tree_widget.addTopLevelItem(self.new_component) + self.items.append(self.new_component) + self.new_component = None + + self._refresh_view() + + def _remove_item(self, item): + root = self.tree_widget.invisibleRootItem() + (item.parent() or root).removeChild(item) + self.items.remove(item) + self._refresh_view() + + def _refresh_view(self): + _bool = len(self.items) == 0 + + self.tree_widget.setVisible(not _bool) + self.drop_widget.setVisible(_bool) + + def _process_paths(self, in_paths): + paths = self._get_all_paths(in_paths) + collections, remainders = clique.assemble(paths) + for collection in collections: + self._process_collection(collection) + for remainder in remainders: + self._process_remainder(remainder) + + def _get_all_paths(self, paths): + output_paths = [] + for path in paths: + path = os.path.normpath(path) + if os.path.isfile(path): + output_paths.append(path) + elif os.path.isdir(path): + s_paths = [] + for s_item in os.listdir(path): + s_path = os.path.sep.join([path, s_item]) + s_paths.append(s_path) + output_paths.extend(self._get_all_paths(s_paths)) + else: + print('Invalid path: "{}"'.format(path)) + return output_paths + + def _process_collection(self, collection): + file_base = os.path.basename(collection.head) + folder_path = os.path.dirname(collection.head) + if file_base[-1] in ['.']: + file_base = file_base[:-1] + file_ext = collection.tail + repr_name = file_ext.replace('.', '') + range = self._get_ranges(collection.indexes) + thumb = False + if file_ext in ['.jpeg']: + thumb = True + + prev = False + if file_ext in ['.jpeg']: + prev = True + + files = [] + for file in os.listdir(folder_path): + if file.startswith(file_base) and file.endswith(file_ext): + files.append(os.path.sep.join([folder_path, file])) + info = {} + + data = { + 'files': files, + 'name': file_base, + 'ext': file_ext, + 'file_info': range, + 'representation': repr_name, + 'folder_path': folder_path, + 'icon': 'sequence', + 'thumb': thumb, + 'prev': prev, + 'is_sequence': True, + 'info': info + } + self._process_data(data) + + def _get_ranges(self, indexes): + if len(indexes) == 1: + return str(indexes[0]) + ranges = [] + first = None + last = None + for index in indexes: + if first is None: + first = index + last = index + elif (last+1) == index: + last = index + else: + if first == last: + range = str(first) + else: + range = '{}-{}'.format(first, last) + ranges.append(range) + first = index + last = index + if first == last: + range = str(first) + else: + range = '{}-{}'.format(first, last) + ranges.append(range) + return ', '.join(ranges) + + def _process_remainder(self, remainder): + filename = os.path.basename(remainder) + folder_path = os.path.dirname(remainder) + file_base, file_ext = os.path.splitext(filename) + repr_name = file_ext.replace('.', '') + file_info = None + thumb = False + if file_ext in ['.jpeg']: + thumb = True + + prev = False + if file_ext in ['.jpeg']: + prev = True + + files = [] + files.append(remainder) + + info = {} + + data = { + 'files': files, + 'name': file_base, + 'ext': file_ext, + 'file_info': file_info, + 'representation': repr_name, + 'folder_path': folder_path, + 'icon': 'sequence', + 'thumb': thumb, + 'prev': prev, + 'is_sequence': False, + 'info': info + } + + self._process_data(data) + + def _process_data(self, data): + found = False + for item in self.items: + if data['ext'] != item.in_data['ext']: + continue + if data['folder_path'] != item.in_data['folder_path']: + continue + + new_is_seq = data['is_sequence'] + ex_is_seq = item.in_data['is_sequence'] + + # If both are single files + if not new_is_seq and not ex_is_seq: + if data['name'] != item.in_data['name']: + continue + found = True + break + # If new is sequence and ex is single file + elif new_is_seq and not ex_is_seq: + if data['name'] not in item.in_data['name']: + continue + ex_file = item.in_data['files'][0] + found = True + # If file is one of inserted sequence + if ex_file in data['files']: + self._remove_item(item) + self._add_item(data) + break + # if file is missing in inserted sequence + paths = data['files'] + paths.append(ex_file) + collections, remainders = clique.assemble(paths) + self._process_collection(collections[0]) + break + # If new is single file existing is sequence + elif not new_is_seq and ex_is_seq: + if item.in_data['name'] not in data['name']: + continue + new_file = data['files'][0] + found = True + if new_file in item.in_data['files']: + break + paths = item.in_data['files'] + paths.append(new_file) + collections, remainders = clique.assemble(paths) + self._remove_item(item) + self._process_collection(collections[0]) + + break + # If both are sequence + else: + if data['name'] != item.in_data['name']: + continue + found = True + ex_files = item.in_data['files'] + for file in data['files']: + if file not in ex_files: + ex_files.append(file) + paths = list(set(ex_files)) + collections, remainders = clique.assemble(paths) + self._remove_item(item) + self._process_collection(collections[0]) + break + + if found is False: + self._add_item(data) diff --git a/pype/tools/standalonepublish/widgets/widget_tree_components.py b/pype/tools/standalonepublish/widgets/widget_tree_components.py new file mode 100644 index 0000000000..76e5a9bce0 --- /dev/null +++ b/pype/tools/standalonepublish/widgets/widget_tree_components.py @@ -0,0 +1,14 @@ +from . import QtCore, QtGui, QtWidgets + + +class TreeComponents(QtWidgets.QTreeWidget): + def __init__(self, parent): + super().__init__(parent) + + self.invisibleRootItem().setFlags(QtCore.Qt.ItemIsEnabled) + self.setIndentation(28) + self.headerItem().setText(0, 'Components') + + self.setRootIsDecorated(False) + + self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c))