From d0f8d145ab2bc759484188b73fe5b252865847e7 Mon Sep 17 00:00:00 2001 From: simonebarbieri Date: Wed, 14 Apr 2021 16:31:44 +0000 Subject: [PATCH 001/311] Create draft PR for #1346 From 602ca108f19f7a46469afb7aab8ca9cf0fa3111e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 21 Apr 2021 17:27:17 +0100 Subject: [PATCH 002/311] Added support to Blender for JSON layout loading and publishing --- .../hosts/blender/plugins/load/load_layout.py | 8 +- .../hosts/blender/plugins/load/load_model.py | 14 +-- .../hosts/blender/plugins/load/load_rig.py | 14 +-- .../blender/plugins/publish/extract_layout.py | 93 +++++++++++++++++++ .../unreal/plugins/publish/extract_layout.py | 6 +- 5 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/extract_layout.py diff --git a/openpype/hosts/blender/plugins/load/load_layout.py b/openpype/hosts/blender/plugins/load/load_layout.py index f1f2fdcddd..87ef9670a6 100644 --- a/openpype/hosts/blender/plugins/load/load_layout.py +++ b/openpype/hosts/blender/plugins/load/load_layout.py @@ -367,13 +367,13 @@ class UnrealLayoutLoader(plugin.AssetLoader): # Y axis mirrored obj.location = ( location.get('x'), - -location.get('y'), + location.get('y'), location.get('z') ) obj.rotation_euler = ( - rotation.get('x') + math.pi / 2, - -rotation.get('y'), - -rotation.get('z') + rotation.get('x'), + rotation.get('y'), + rotation.get('z') ) obj.scale = ( scale.get('x'), diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index 7297e459a6..ed0f2faf17 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -108,19 +108,21 @@ class BlendModelLoader(plugin.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( - blender.pipeline.AVALON_PROPERTY) + metadata = container.get(blender.pipeline.AVALON_PROPERTY) - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container + metadata["libpath"] = libpath + metadata["lib_container"] = lib_container obj_container = self._process( libpath, lib_container, container_name, None) - container_metadata["obj_container"] = obj_container + metadata["obj_container"] = obj_container # Save the list of objects in the metadata container - container_metadata["objects"] = obj_container.all_objects + metadata["objects"] = obj_container.all_objects + + metadata["parent"] = str(context["representation"]["parent"]) + metadata["family"] = context["representation"]["context"]["family"] nodes = list(container.objects) nodes.append(container) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index c5690a6ab8..9035458c12 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -155,18 +155,20 @@ class BlendRigLoader(plugin.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( - blender.pipeline.AVALON_PROPERTY) + metadata = container.get(blender.pipeline.AVALON_PROPERTY) - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container + metadata["libpath"] = libpath + metadata["lib_container"] = lib_container obj_container = self._process( libpath, lib_container, collection_name, None, None) - container_metadata["obj_container"] = obj_container + metadata["obj_container"] = obj_container # Save the list of objects in the metadata container - container_metadata["objects"] = obj_container.all_objects + metadata["objects"] = obj_container.all_objects + + metadata["parent"] = str(context["representation"]["parent"]) + metadata["family"] = context["representation"]["context"]["family"] nodes = list(container.objects) nodes.append(container) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py new file mode 100644 index 0000000000..09fae2fc12 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -0,0 +1,93 @@ +import os +import json +import math + +import bpy + +from avalon import blender, io +import openpype.api + + +class ExtractLayout(openpype.api.Extractor): + """Extract a layout.""" + + label = "Extract Layout" + hosts = ["blender"] + families = ["layout"] + optional = True + + def process(self, instance): + # Define extract output file path + stagingdir = self.staging_dir(instance) + + # Perform extraction + self.log.info("Performing extraction..") + + json_data = [] + + for collection in instance: + for asset in collection.children: + collection = bpy.data.collections[asset.name] + container = bpy.data.collections[asset.name + '_CON'] + metadata = container.get(blender.pipeline.AVALON_PROPERTY) + + parent = metadata["parent"] + family = metadata["family"] + + self.log.debug("Parent: {}".format(parent)) + blend = io.find_one( + { + "type": "representation", + "parent": io.ObjectId(parent), + "name": "blend" + }, + projection={"_id": True}) + blend_id = blend["_id"] + + json_element = {} + json_element["reference"] = str(blend_id) + json_element["family"] = family + json_element["instance_name"] = asset.name + json_element["asset_name"] = metadata["lib_container"] + json_element["file_path"] = metadata["libpath"] + + obj = collection.objects[0] + + json_element["transform"] = { + "translation": { + "x": obj.location.x, + "y": obj.location.y, + "z": obj.location.z + }, + "rotation": { + "x": obj.rotation_euler.x, + "y": obj.rotation_euler.y, + "z": obj.rotation_euler.z, + }, + "scale": { + "x": obj.scale.x, + "y": obj.scale.y, + "z": obj.scale.z + } + } + json_data.append(json_element) + + json_filename = "{}.json".format(instance.name) + json_path = os.path.join(stagingdir, json_filename) + + with open(json_path, "w+") as file: + json.dump(json_data, fp=file, indent=2) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'json', + 'ext': 'json', + 'files': json_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/openpype/hosts/unreal/plugins/publish/extract_layout.py b/openpype/hosts/unreal/plugins/publish/extract_layout.py index 5924221f51..2d9f6eb3d1 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_layout.py +++ b/openpype/hosts/unreal/plugins/publish/extract_layout.py @@ -78,14 +78,14 @@ class ExtractLayout(openpype.api.Extractor): json_element["transform"] = { "translation": { - "x": transform.translation.x, + "x": -transform.translation.x, "y": transform.translation.y, "z": transform.translation.z }, "rotation": { - "x": math.radians(transform.rotation.euler().x), + "x": math.radians(transform.rotation.euler().x + 90.0), "y": math.radians(transform.rotation.euler().y), - "z": math.radians(transform.rotation.euler().z), + "z": math.radians(180.0 - transform.rotation.euler().z) }, "scale": { "x": transform.scale3d.x, From bb658c3e028d41ef53b298d24e4b43cab6f4f3b6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 21 Apr 2021 17:29:53 +0100 Subject: [PATCH 003/311] Hound fix --- openpype/hosts/blender/plugins/publish/extract_layout.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 09fae2fc12..c6c9bf67f5 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -1,6 +1,5 @@ import os import json -import math import bpy From 528fa8db596260011997385eb9865af4c7be1e19 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 10:03:44 +0200 Subject: [PATCH 004/311] initial commit --- openpype/tools/project_manager/__init__.py | 6 + openpype/tools/project_manager/__main__.py | 30 + .../project_manager/__init__.py | 30 + .../project_manager/constants.py | 5 + .../project_manager/delegates.py | 23 + .../project_manager/project_manager/model.py | 562 ++++++++++++++++++ .../project_manager/project_manager/view.py | 180 ++++++ .../project_manager/project_manager/window.py | 56 ++ 8 files changed, 892 insertions(+) create mode 100644 openpype/tools/project_manager/__init__.py create mode 100644 openpype/tools/project_manager/__main__.py create mode 100644 openpype/tools/project_manager/project_manager/__init__.py create mode 100644 openpype/tools/project_manager/project_manager/constants.py create mode 100644 openpype/tools/project_manager/project_manager/delegates.py create mode 100644 openpype/tools/project_manager/project_manager/model.py create mode 100644 openpype/tools/project_manager/project_manager/view.py create mode 100644 openpype/tools/project_manager/project_manager/window.py diff --git a/openpype/tools/project_manager/__init__.py b/openpype/tools/project_manager/__init__.py new file mode 100644 index 0000000000..7d8f8bf432 --- /dev/null +++ b/openpype/tools/project_manager/__init__.py @@ -0,0 +1,6 @@ +from .project_manager import Window + + +__all__ = ( + "Window", +) diff --git a/openpype/tools/project_manager/__main__.py b/openpype/tools/project_manager/__main__.py new file mode 100644 index 0000000000..3f29eb5a21 --- /dev/null +++ b/openpype/tools/project_manager/__main__.py @@ -0,0 +1,30 @@ +import os +import sys +paths = [ + r"C:\Users\iLLiCiT\PycharmProjects\pype3\.venv\Lib\site-packages", + r"C:\Users\iLLiCiT\PycharmProjects\pype3", + r"C:\Users\iLLiCiT\PycharmProjects\pype3\repos\avalon-core" +] +for path in paths: + sys.path.append(path) + +os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" +os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:2707" +os.environ["AVALON_TIMEOUT"] = "1000" + +from project_manager import Window + +from Qt import QtWidgets + + +def main(): + app = QtWidgets.QApplication([]) + + window = Window() + window.show() + + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() diff --git a/openpype/tools/project_manager/project_manager/__init__.py b/openpype/tools/project_manager/project_manager/__init__.py new file mode 100644 index 0000000000..1a538baa20 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/__init__.py @@ -0,0 +1,30 @@ +from .constants import ( + IDENTIFIER_ROLE +) +from .view import HierarchyView +from .model import ( + HierarchyModel, + HierarchySelectionModel, + BaseItem, + RootItem, + ProjectItem, + AssetItem, + TaskItem +) +from .window import Window + +__all__ = ( + "IDENTIFIER_ROLE", + + "HierarchyView", + + "HierarchyModel", + "HierarchySelectionModel", + "BaseItem", + "RootItem", + "ProjectItem", + "AssetItem", + "TaskItem", + + "Window" +) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py new file mode 100644 index 0000000000..6c84ee6635 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -0,0 +1,5 @@ +from Qt import QtWidgets, QtCore + + +IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 +COLUMNS_ROLE = QtCore.Qt.UserRole + 2 diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py new file mode 100644 index 0000000000..605d0cbbab --- /dev/null +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -0,0 +1,23 @@ +from Qt import QtWidgets, QtCore + + +class NumberDelegate(QtWidgets.QStyledItemDelegate): + def createEditor(self, parent, option, index): + editor = QtWidgets.QSpinBox(parent) + value = index.data(QtCore.Qt.DisplayRole) + if value is not None: + editor.setValue(value) + return editor + + # def updateEditorGeometry(self, editor, options, index): + # print(editor) + # return super().updateEditorGeometry(editor, options, index) + + +class StringDelegate(QtWidgets.QStyledItemDelegate): + def createEditor(self, parent, option, index): + editor = QtWidgets.QLineEdit(parent) + value = index.data(QtCore.Qt.DisplayRole) + if value is not None: + editor.setText(str(value)) + return editor diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py new file mode 100644 index 0000000000..d8341df8c1 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/model.py @@ -0,0 +1,562 @@ +import collections +from queue import Queue +from uuid import uuid4 + +from .constants import ( + IDENTIFIER_ROLE, + COLUMNS_ROLE +) + +from avalon.api import AvalonMongoDB + +from Qt import QtWidgets, QtCore + + +class HierarchySelectionModel(QtCore.QItemSelectionModel): + def setCurrentIndex(self, index, command): + # v = "" + # if command == QtCore.QItemSelectionModel.NoUpdate: + # v += "NoUpdate" + # if command & QtCore.QItemSelectionModel.Clear: + # v += "Clear" + # if command & QtCore.QItemSelectionModel.Select: + # v += "Select" + # if command & QtCore.QItemSelectionModel.Deselect: + # v += "Deselect" + # if command & QtCore.QItemSelectionModel.Toggle: + # v += "Toggle" + # if command & QtCore.QItemSelectionModel.Current: + # v += "Current" + # if command & QtCore.QItemSelectionModel.Rows: + # v += "Rows" + # if command & QtCore.QItemSelectionModel.Columns: + # v += "Columns" + if index.column() > 0: + if ( + command & QtCore.QItemSelectionModel.Clear + and command & QtCore.QItemSelectionModel.Select + ): + command = QtCore.QItemSelectionModel.NoUpdate + super(HierarchySelectionModel, self).setCurrentIndex(index, command) + + +class HierarchyModel(QtCore.QAbstractItemModel): + columns = [ + "name", + "type", + "frameStart", + "frameEnd", + "fps", + "resolutionWidth", + "resolutionHeight" + ] + + def __init__(self, parent=None): + super(HierarchyModel, self).__init__(parent) + self._root_item = None + self._items_by_id = {} + self.dbcon = AvalonMongoDB() + + self._hierarchy_mode = True + self._reset_root_item() + + def change_edit_mode(self, hiearchy_mode): + self._hierarchy_mode = hiearchy_mode + + @property + def items_by_id(self): + return self._items_by_id + + def _reset_root_item(self): + self._root_item = RootItem(self) + + def set_project(self, project_doc): + self.clear() + + item = ProjectItem(project_doc) + self.add_item(item) + + def rowCount(self, parent=None): + if parent is None or not parent.isValid(): + parent_item = self._root_item + else: + parent_item = parent.internalPointer() + return parent_item.childCount() + + def columnCount(self, *args, **kwargs): + return len(self.columns) + + def data(self, index, role): + if not index.isValid(): + return None + + column = index.column() + key = self.columns[column] + + item = index.internalPointer() + return item.data(key, role) + + def setData(self, index, value, role=QtCore.Qt.EditRole): + if not index.isValid(): + return False + + item = index.internalPointer() + column = index.column() + key = self.columns[column] + result = item.setData(key, value, role) + if result: + self.dataChanged.emit(index, index, [role]) + + return result + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if section < len(self.columns): + return self.columns[section] + + super(HierarchyModel, self).headerData(section, orientation, role) + + def flags(self, index): + item = index.internalPointer() + column = index.column() + key = self.columns[column] + return item.flags(key) + + def parent(self, index): + item = index.internalPointer() + parent_item = item.parent() + + # If it has no parents we return invalid + if not parent_item or parent_item is self._root_item: + return QtCore.QModelIndex() + + return self.createIndex(parent_item.row(), 0, parent_item) + + def index(self, row, column, parent=None): + """Return index for row/column under parent""" + parent_item = None + if parent is not None and parent.isValid(): + parent_item = parent.internalPointer() + + return self.index_from_item(row, column, parent_item) + + def index_for_item(self, item, column=0): + return self.index_from_item( + item.row(), column, item.parent() + ) + + def index_from_item(self, row, column, parent=None): + if parent is None: + parent = self._root_item + + child_item = parent.child(row) + if child_item: + return self.createIndex(row, column, child_item) + + return QtCore.QModelIndex() + + def add_new_asset(self, source_index): + item_id = source_index.data(IDENTIFIER_ROLE) + item = self.items_by_id[item_id] + + if isinstance(item, (RootItem, ProjectItem)): + name = "eq" + parent = item + else: + name = source_index.data(QtCore.Qt.DisplayRole) + parent = item.parent() + + data = {"name": name} + new_child = AssetItem(data) + return self.add_item(new_child, parent) + + def add_new_task(self, parent): + pass + + def add_new_item(self, parent): + data = {"name": "Test {}".format(parent.childCount())} + new_child = AssetItem(data) + + return self.add_item(new_child, parent) + + def add_item(self, item, parent=None): + if parent is None: + parent = self._root_item + + idx = parent.childCount() + + parent_index = self.index_from_item(parent.row(), 0, parent.parent()) + self.beginInsertRows(parent_index, idx, idx) + + if item.parent() is not parent: + item.set_parent(parent) + + parent.add_child(item) + + if item.id not in self._items_by_id: + self._items_by_id[item.id] = item + + self.endInsertRows() + + self.rowsInserted.emit(parent_index, idx, idx) + + return self.index_from_item(idx, 0, parent) + + def remove_index(self, index): + if not index.isValid(): + return + + item_id = index.data(IDENTIFIER_ROLE) + item = self._items_by_id[item_id] + if isinstance(item, (RootItem, ProjectItem)): + return + + parent = item.parent() + all_descendants = collections.defaultdict(dict) + all_descendants[parent.id][item.id] = item + + row = item.row() + self.beginRemoveRows(self.index_for_item(parent), row, row) + + todo_queue = Queue() + todo_queue.put(item) + while not todo_queue.empty(): + _item = todo_queue.get() + for row in range(_item.childCount()): + child_item = _item.child(row) + all_descendants[_item.id][child_item.id] = child_item + todo_queue.put(child_item) + + while all_descendants: + for parent_id in tuple(all_descendants.keys()): + children = all_descendants[parent_id] + if not children: + all_descendants.pop(parent_id) + continue + + for child_id in tuple(children.keys()): + child_item = children[child_id] + if child_id in all_descendants: + continue + + children.pop(child_id) + child_item.set_parent(None) + self._items_by_id.pop(child_id) + + self.endRemoveRows() + + def move_vertical(self, index, direction): + if not index.isValid(): + return + + item_id = index.data(IDENTIFIER_ROLE) + if item_id is None: + return + + item = self._items_by_id[item_id] + if isinstance(item, (RootItem, ProjectItem)): + return + + if abs(direction) != 1: + return + + # Move under parent of parent + src_row = item.row() + src_parent = item.parent() + src_parent_index = self.index_from_item( + src_parent.row(), 0, src_parent.parent() + ) + + dst_row = None + dst_parent = None + dst_parent_index = None + + if direction == -1: + if isinstance(src_parent, (RootItem, ProjectItem)): + return + dst_parent = src_parent.parent() + dst_row = src_parent.row() + 1 + + # Move under parent before or after if before is None + elif direction == 1: + if src_parent.childCount() == 1: + return + + if item.row() == 0: + parent_row = item.row() + 1 + else: + parent_row = item.row() - 1 + dst_parent = src_parent.child(parent_row) + dst_row = dst_parent.childCount() + + if src_parent is dst_parent: + return + + if dst_parent_index is None: + dst_parent_index = self.index_from_item( + dst_parent.row(), 0, dst_parent.parent() + ) + + self.beginMoveRows( + src_parent_index, + src_row, + src_row, + dst_parent_index, + dst_row + ) + src_parent.remove_child(item) + dst_parent.add_child(item) + item.set_parent(dst_parent) + dst_parent.move_to(item, dst_row) + + self.endMoveRows() + + def move_horizontal(self, index, direction): + if not index.isValid(): + return + + item_id = index.data(IDENTIFIER_ROLE) + item = self._items_by_id[item_id] + if isinstance(item, (RootItem, ProjectItem)): + return + + if abs(direction) != 1: + return + + src_parent = item.parent() + src_parent_index = self.index_from_item( + src_parent.row(), 0, src_parent.parent() + ) + source_row = item.row() + + dst_parent = None + dst_parent_index = None + destination_row = None + _destination_row = None + # Down + if direction == 1: + if source_row < src_parent.childCount() - 1: + dst_parent_index = src_parent_index + dst_parent = src_parent + destination_row = source_row + 1 + # This row is not row number after moving but before moving + _destination_row = destination_row + 1 + else: + destination_row = 0 + parent_parent = src_parent.parent() + if not parent_parent: + return + + new_parent = parent_parent.child(src_parent.row() + 1) + if not new_parent: + return + dst_parent = new_parent + + # Up + elif direction == -1: + if source_row > 0: + dst_parent_index = src_parent_index + dst_parent = src_parent + destination_row = source_row - 1 + else: + parent_parent = src_parent.parent() + if not parent_parent: + return + + previous_parent = parent_parent.child(src_parent.row() - 1) + if not previous_parent: + return + dst_parent = previous_parent + destination_row = previous_parent.childCount() + + if dst_parent_index is None: + dst_parent_index = self.index_from_item( + dst_parent.row(), 0, dst_parent.parent() + ) + + if _destination_row is None: + _destination_row = destination_row + + self.beginMoveRows( + src_parent_index, + source_row, + source_row, + dst_parent_index, + _destination_row + ) + + if src_parent is dst_parent: + dst_parent.move_to(item, destination_row) + + else: + src_parent.remove_child(item) + dst_parent.add_child(item) + item.set_parent(dst_parent) + dst_parent.move_to(item, destination_row) + + self.endMoveRows() + + def child_removed(self, child): + self._items_by_id.pop(child.id, None) + + def column_name(self, column): + """Return column key by index""" + if column < len(self.columns): + return self.columns[column] + return None + + def clear(self): + self.beginResetModel() + self._reset_root_item() + self.endResetModel() + + +class BaseItem: + columns = ["name"] + + def __init__(self, data=None): + self._id = uuid4() + self._children = list() + self._parent = None + + self._data = { + key: None + for key in self.columns + } + self._source_data = data + if data: + for key, value in data.items(): + if key in self.columns: + self._data[key] = value + + def model(self): + return self._parent.model + + def move_to(self, item, row): + idx = self._children.index(item) + if idx == row: + return + + self._children.pop(idx) + self._children.insert(row, item) + + def data(self, key, role): + if role == IDENTIFIER_ROLE: + return self._id + + if role == COLUMNS_ROLE: + return self.columns + + if key not in self.columns: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + value = self._data[key] + if value is None: + value = self.parent().data(key, role) + return value + + return None + + def setData(self, key, value, role): + if key not in self.columns: + return False + + if role == QtCore.Qt.EditRole: + self._data[key] = value + + # must return true if successful + return True + + return False + + @property + def id(self): + return self._id + + def childCount(self): + return len(self._children) + + def child(self, row): + if -1 < row < self.childCount(): + return self._children[row] + return None + + def children(self): + return self._children + + def child_row(self, child): + if child not in self._children: + return -1 + return self._children.index(child) + + def parent(self): + return self._parent + + def set_parent(self, parent): + if parent is self._parent: + return + + if self._parent: + self._parent.remove_child(self) + self._parent = parent + + def row(self): + if self._parent is not None: + return self._parent.child_row(self) + return -1 + + def add_child(self, item): + if item not in self._children: + self._children.append(item) + + def remove_child(self, item): + if item in self._children: + self._children.remove(item) + + def flags(self, key): + flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + if key in self.columns: + flags |= QtCore.Qt.ItemIsEditable + return flags + + +class RootItem(BaseItem): + def __init__(self, model): + super(RootItem, self).__init__() + self._model = model + + def model(self): + return self._model + + def flags(self, *args, **kwargs): + return QtCore.Qt.NoItemFlags + + +class ProjectItem(BaseItem): + def __init__(self, data=None): + super(ProjectItem, self).__init__(data) + self._data["name"] = "project" + + def flags(self, *args, **kwargs): + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + +class AssetItem(BaseItem): + columns = [ + "name", + "type", + "frameStart", + "frameEnd", + "fps", + "resolutionWidth", + "resolutionHeight" + ] + + def __init__(self, data=None): + super(AssetItem, self).__init__(data) + + +class TaskItem(BaseItem): + def __init__(self, data=None): + super(TaskItem, self).__init__(data) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py new file mode 100644 index 0000000000..d49b5e3b52 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/view.py @@ -0,0 +1,180 @@ +from Qt import QtWidgets, QtCore + +from .constants import ( + IDENTIFIER_ROLE, + COLUMNS_ROLE +) +from .delegates import NumberDelegate, StringDelegate + + +class HierarchyView(QtWidgets.QTreeView): + """A tree view that deselects on clicking on an empty area in the view""" + column_delegate_defs = { + "name": StringDelegate, + "frameStart": NumberDelegate, + "frameEnd": NumberDelegate, + "fps": NumberDelegate, + "resolutionWidth": NumberDelegate, + "resolutionHeight": NumberDelegate + } + persistent_columns = [ + "frameStart", "frameEnd", "fps", "resolutionWidth", "resolutionHeight" + ] + + def __init__(self, source_model, *args, **kwargs): + super(HierarchyView, self).__init__(*args, **kwargs) + self._source_model = source_model + + main_delegate = QtWidgets.QStyledItemDelegate() + self.setItemDelegate(main_delegate) + self.setAlternatingRowColors(True) + self.setSelectionMode(HierarchyView.ContiguousSelection) + + column_delegates = {} + column_key_to_index = {} + for key, delegate_klass in self.column_delegate_defs.items(): + delegate = delegate_klass() + column = self._source_model.columns.index(key) + self.setItemDelegateForColumn(column, delegate) + column_delegates[key] = delegate + column_key_to_index[key] = column + + self._delegate = main_delegate + self._column_delegates = column_delegates + self._column_key_to_index = column_key_to_index + + def commitData(self, editor): + super(HierarchyView, self).commitData(editor) + current_index = self.currentIndex() + column = current_index.column() + row = current_index.row() + skipped_index = None + if column > 0: + indexes = [] + for index in self.selectedIndexes(): + if index.column() == column: + if index.row() == row: + skipped_index = index + else: + indexes.append(index) + + if skipped_index is not None: + value = current_index.data(QtCore.Qt.EditRole) + for index in indexes: + index.model().setData(index, value, QtCore.Qt.EditRole) + + # Update children data + self.updateEditorData() + + def _deselect_editor(self, editor): + if editor: + if isinstance(editor, QtWidgets.QSpinBox): + line_edit = editor.findChild(QtWidgets.QLineEdit) + line_edit.deselect() + + elif isinstance(editor, QtWidgets.QLineEdit): + editor.deselect() + + def edit(self, index, *args, **kwargs): + result = super(HierarchyView, self).edit(index, *args, **kwargs) + self._deselect_editor(self.indexWidget(index)) + return result + + def openPersistentEditor(self, index): + super(HierarchyView, self).openPersistentEditor(index) + self._deselect_editor(self.indexWidget(index)) + + def rowsInserted(self, parent_index, start, end): + super(HierarchyView, self).rowsInserted(parent_index, start, end) + + for row in range(start, end + 1): + index = self._source_model.index(row, 0, parent_index) + columns = index.data(COLUMNS_ROLE) or [] + for key, column in self._column_key_to_index.items(): + if key not in self.persistent_columns: + continue + col_index = self._source_model.index(row, column, parent_index) + self.openPersistentEditor(col_index) + + # Expand parent on insert + if not self.isExpanded(parent_index): + self.expand(parent_index) + + def mousePressEvent(self, event): + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + + super(HierarchyView, self).mousePressEvent(event) + + def keyPressEvent(self, event): + call_super = False + if event.key() == QtCore.Qt.Key_Delete: + self._delete_item() + + elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + if event.modifiers() == QtCore.Qt.ShiftModifier: + self._on_shift_enter_pressed() + else: + if self.state() == HierarchyView.NoState: + self._on_enter_pressed() + + elif event.modifiers() == QtCore.Qt.ControlModifier: + if event.key() == QtCore.Qt.Key_Left: + self._on_left_ctrl_pressed() + elif event.key() == QtCore.Qt.Key_Right: + self._on_right_ctrl_pressed() + elif event.key() == QtCore.Qt.Key_Up: + self._on_up_ctrl_pressed() + elif event.key() == QtCore.Qt.Key_Down: + self._on_down_ctrl_pressed() + else: + call_super = True + + if call_super: + super(HierarchyView, self).keyPressEvent(event) + else: + event.accept() + + def _delete_item(self): + index = self.currentIndex() + self._source_model.remove_index(index) + + def _on_shift_enter_pressed(self): + index = self.currentIndex() + if not index.isValid(): + return + + # Stop editing + self.setState(HierarchyView.NoState) + QtWidgets.QApplication.processEvents() + + new_index = self._source_model.add_new_asset(index) + + # Change current index + self.setCurrentIndex(new_index) + # Start editing + self.edit(new_index) + + def _on_up_ctrl_pressed(self): + self._source_model.move_horizontal(self.currentIndex(), -1) + + def _on_down_ctrl_pressed(self): + self._source_model.move_horizontal(self.currentIndex(), 1) + + def _on_left_ctrl_pressed(self): + self._source_model.move_vertical(self.currentIndex(), -1) + + def _on_right_ctrl_pressed(self): + self._source_model.move_vertical(self.currentIndex(), 1) + + def _on_enter_pressed(self): + index = self.currentIndex() + if ( + index.isValid() + and index.flags() & QtCore.Qt.ItemIsEditable + ): + self.edit(index) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py new file mode 100644 index 0000000000..db5527b4ac --- /dev/null +++ b/openpype/tools/project_manager/project_manager/window.py @@ -0,0 +1,56 @@ +from Qt import QtWidgets, QtCore + +from . import ( + HierarchyModel, + HierarchySelectionModel, + HierarchyView +) + + +class Window(QtWidgets.QWidget): + def __init__(self, parent=None): + super(Window, self).__init__(parent) + + model = HierarchyModel() + view = HierarchyView(model, self) + view.setModel(model) + _selection_model = HierarchySelectionModel() + _selection_model.setModel(view.model()) + view.setSelectionModel(_selection_model) + + header = view.header() + header.setStretchLastSection(False) + header.setSectionResizeMode( + header.logicalIndex(0), QtWidgets.QHeaderView.Stretch + ) + checkbox = QtWidgets.QCheckBox(self) + # btn = QtWidgets.QPushButton("Refresh") + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(view) + main_layout.addWidget(checkbox) + # main_layout.addWidget(btn) + # btn.clicked.connect(self._on_refresh) + + checkbox.toggled.connect(self._on_checkbox) + + self.view = view + self.model = model + # self.btn = btn + self.checkbox = checkbox + + self.change_edit_mode() + + self.resize(1200, 600) + model.set_project({"name": "test"}) + + def change_edit_mode(self, value=None): + if value is None: + value = self.checkbox.isChecked() + self.model.change_edit_mode(value) + + def _on_checkbox(self, value): + self.change_edit_mode(value) + + def _on_refresh(self): + self.model.clear() From f6f779e127033cb147d9605a108ca10b012ec307 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 10:55:24 +0200 Subject: [PATCH 005/311] removed develop code --- openpype/tools/project_manager/__main__.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/tools/project_manager/__main__.py b/openpype/tools/project_manager/__main__.py index 3f29eb5a21..0855a0fc71 100644 --- a/openpype/tools/project_manager/__main__.py +++ b/openpype/tools/project_manager/__main__.py @@ -1,16 +1,4 @@ -import os import sys -paths = [ - r"C:\Users\iLLiCiT\PycharmProjects\pype3\.venv\Lib\site-packages", - r"C:\Users\iLLiCiT\PycharmProjects\pype3", - r"C:\Users\iLLiCiT\PycharmProjects\pype3\repos\avalon-core" -] -for path in paths: - sys.path.append(path) - -os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" -os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:2707" -os.environ["AVALON_TIMEOUT"] = "1000" from project_manager import Window From 3a139256acc1f18c5fe08f86519742afd7196ccc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 19:02:53 +0200 Subject: [PATCH 006/311] task item has basic implementation --- .../project_manager/project_manager/model.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index d8341df8c1..aa3380b87f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -170,8 +170,19 @@ class HierarchyModel(QtCore.QAbstractItemModel): new_child = AssetItem(data) return self.add_item(new_child, parent) - def add_new_task(self, parent): - pass + def add_new_task(self, source_index): + item_id = source_index.data(IDENTIFIER_ROLE) + item = self.items_by_id[item_id] + + if not isinstance(item, AssetItem): + return None + + name = "task" + parent = item.parent() + + data = {"name": name} + new_child = TaskItem(data) + return self.add_item(new_child, parent) def add_new_item(self, parent): data = {"name": "Test {}".format(parent.childCount())} From 0ba22be07d380ebee17e681557eaa94078974369 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 19:03:06 +0200 Subject: [PATCH 007/311] remove not needed debug --- .../project_manager/project_manager/model.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index aa3380b87f..63b833910e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -14,23 +14,6 @@ from Qt import QtWidgets, QtCore class HierarchySelectionModel(QtCore.QItemSelectionModel): def setCurrentIndex(self, index, command): - # v = "" - # if command == QtCore.QItemSelectionModel.NoUpdate: - # v += "NoUpdate" - # if command & QtCore.QItemSelectionModel.Clear: - # v += "Clear" - # if command & QtCore.QItemSelectionModel.Select: - # v += "Select" - # if command & QtCore.QItemSelectionModel.Deselect: - # v += "Deselect" - # if command & QtCore.QItemSelectionModel.Toggle: - # v += "Toggle" - # if command & QtCore.QItemSelectionModel.Current: - # v += "Current" - # if command & QtCore.QItemSelectionModel.Rows: - # v += "Rows" - # if command & QtCore.QItemSelectionModel.Columns: - # v += "Columns" if index.column() > 0: if ( command & QtCore.QItemSelectionModel.Clear From 81958d7461ef45373c35722cde132ff59f82b92c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 19:03:30 +0200 Subject: [PATCH 008/311] added ability to create task with ctrl+shift+enter --- .../project_manager/project_manager/view.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index d49b5e3b52..62a2f22506 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -116,7 +116,10 @@ class HierarchyView(QtWidgets.QTreeView): self._delete_item() elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): - if event.modifiers() == QtCore.Qt.ShiftModifier: + mdfs = event.modifiers() + if mdfs == (QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier): + self._on_ctrl_shift_enter_pressed() + elif mdfs == QtCore.Qt.ShiftModifier: self._on_shift_enter_pressed() else: if self.state() == HierarchyView.NoState: @@ -143,6 +146,24 @@ class HierarchyView(QtWidgets.QTreeView): index = self.currentIndex() self._source_model.remove_index(index) + def _on_ctrl_shift_enter_pressed(self): + index = self.currentIndex() + if not index.isValid(): + return + + new_index = self._source_model.add_new_task(index) + if new_index is None: + return + + # Stop editing + self.setState(HierarchyView.NoState) + QtWidgets.QApplication.processEvents() + + # Change current index + self.setCurrentIndex(new_index) + # Start editing + self.edit(new_index) + def _on_shift_enter_pressed(self): index = self.currentIndex() if not index.isValid(): From dcb51bfaf3a3ea891b78092b79c7258bc8b4c2fd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Apr 2021 19:04:02 +0200 Subject: [PATCH 009/311] columns definitions per line --- openpype/tools/project_manager/project_manager/view.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 62a2f22506..e35663c24f 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -18,7 +18,11 @@ class HierarchyView(QtWidgets.QTreeView): "resolutionHeight": NumberDelegate } persistent_columns = [ - "frameStart", "frameEnd", "fps", "resolutionWidth", "resolutionHeight" + "frameStart", + "frameEnd", + "fps", + "resolutionWidth", + "resolutionHeight" ] def __init__(self, source_model, *args, **kwargs): From 57ba2a547d528204da3dc9d6acf826acb0d60e22 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 09:55:24 +0200 Subject: [PATCH 010/311] replaced chidCount with rowCount --- .../project_manager/project_manager/model.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 63b833910e..bdb0da6492 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -9,7 +9,7 @@ from .constants import ( from avalon.api import AvalonMongoDB -from Qt import QtWidgets, QtCore +from Qt import QtCore class HierarchySelectionModel(QtCore.QItemSelectionModel): @@ -64,7 +64,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): parent_item = self._root_item else: parent_item = parent.internalPointer() - return parent_item.childCount() + return parent_item.rowCount() def columnCount(self, *args, **kwargs): return len(self.columns) @@ -168,7 +168,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): return self.add_item(new_child, parent) def add_new_item(self, parent): - data = {"name": "Test {}".format(parent.childCount())} + data = {"name": "Test {}".format(parent.rowCount())} new_child = AssetItem(data) return self.add_item(new_child, parent) @@ -177,7 +177,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if parent is None: parent = self._root_item - idx = parent.childCount() + idx = parent.rowCount() parent_index = self.index_from_item(parent.row(), 0, parent.parent()) self.beginInsertRows(parent_index, idx, idx) @@ -216,7 +216,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): todo_queue.put(item) while not todo_queue.empty(): _item = todo_queue.get() - for row in range(_item.childCount()): + for row in range(_item.rowCount()): child_item = _item.child(row) all_descendants[_item.id][child_item.id] = child_item todo_queue.put(child_item) @@ -273,7 +273,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): # Move under parent before or after if before is None elif direction == 1: - if src_parent.childCount() == 1: + if src_parent.rowCount() == 1: return if item.row() == 0: @@ -281,7 +281,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): else: parent_row = item.row() - 1 dst_parent = src_parent.child(parent_row) - dst_row = dst_parent.childCount() + dst_row = dst_parent.rowCount() if src_parent is dst_parent: return @@ -329,7 +329,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): _destination_row = None # Down if direction == 1: - if source_row < src_parent.childCount() - 1: + if source_row < src_parent.rowCount() - 1: dst_parent_index = src_parent_index dst_parent = src_parent destination_row = source_row + 1 @@ -361,7 +361,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not previous_parent: return dst_parent = previous_parent - destination_row = previous_parent.childCount() + destination_row = previous_parent.rowCount() if dst_parent_index is None: dst_parent_index = self.index_from_item( @@ -468,11 +468,11 @@ class BaseItem: def id(self): return self._id - def childCount(self): + def rowCount(self): return len(self._children) def child(self, row): - if -1 < row < self.childCount(): + if -1 < row < self.rowCount(): return self._children[row] return None From e4282babb510a71225b25468d27cbe9b21f02047 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 13:23:05 +0200 Subject: [PATCH 011/311] cleanup unused parts --- openpype/tools/project_manager/project_manager/constants.py | 2 +- openpype/tools/project_manager/project_manager/view.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 6c84ee6635..21db80e5f7 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -1,4 +1,4 @@ -from Qt import QtWidgets, QtCore +from Qt import QtCore IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index e35663c24f..606edbda99 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -1,9 +1,5 @@ from Qt import QtWidgets, QtCore -from .constants import ( - IDENTIFIER_ROLE, - COLUMNS_ROLE -) from .delegates import NumberDelegate, StringDelegate @@ -92,8 +88,6 @@ class HierarchyView(QtWidgets.QTreeView): super(HierarchyView, self).rowsInserted(parent_index, start, end) for row in range(start, end + 1): - index = self._source_model.index(row, 0, parent_index) - columns = index.data(COLUMNS_ROLE) or [] for key, column in self._column_key_to_index.items(): if key not in self.persistent_columns: continue From 5e48a87531650562829e1f369405120f1bd78c36 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 13:23:30 +0200 Subject: [PATCH 012/311] added basic icons --- .../project_manager/project_manager/model.py | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index bdb0da6492..721a1c88e4 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -8,6 +8,7 @@ from .constants import ( ) from avalon.api import AvalonMongoDB +from avalon.vendor import qtawesome from Qt import QtCore @@ -407,6 +408,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): class BaseItem: columns = ["name"] + _name_icon = None def __init__(self, data=None): self._id = uuid4() @@ -423,6 +425,10 @@ class BaseItem: if key in self.columns: self._data[key] = value + @classmethod + def name_icon(cls): + return cls._name_icon + def model(self): return self._parent.model @@ -450,6 +456,8 @@ class BaseItem: value = self.parent().data(key, role) return value + if role == QtCore.Qt.DecorationRole and key == "name": + return self.name_icon() return None def setData(self, key, value, role): @@ -500,9 +508,23 @@ class BaseItem: return self._parent.child_row(self) return -1 - def add_child(self, item): - if item not in self._children: + def add_child(self, item, row=None): + if item in self._children: + return + + row_count = self.rowCount() + if row is None or row == row_count: self._children.append(item) + return + + if row > row_count or row < 0: + raise ValueError( + "Invalid row number {} expected range 0 - {}".format( + row, row_count + ) + ) + + self._children.insert(row, item) def remove_child(self, item): if item in self._children: @@ -547,10 +569,16 @@ class AssetItem(BaseItem): "resolutionHeight" ] - def __init__(self, data=None): - super(AssetItem, self).__init__(data) + @classmethod + def name_icon(cls): + if cls._name_icon is None: + cls._name_icon = qtawesome.icon("fa.folder", color="#333333") + return cls._name_icon class TaskItem(BaseItem): - def __init__(self, data=None): - super(TaskItem, self).__init__(data) + @classmethod + def name_icon(cls): + if cls._name_icon is None: + cls._name_icon = qtawesome.icon("fa.file-o", color="#333333") + return cls._name_icon From b224216cf55d76c34e0dd566835ad7a7cf38c958 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 13:23:43 +0200 Subject: [PATCH 013/311] it is possible to add asset directly under current selection --- .../project_manager/project_manager/model.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 721a1c88e4..7b54f2682f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -143,28 +143,33 @@ class HierarchyModel(QtCore.QAbstractItemModel): item_id = source_index.data(IDENTIFIER_ROLE) item = self.items_by_id[item_id] + new_row = None if isinstance(item, (RootItem, ProjectItem)): name = "eq" parent = item else: name = source_index.data(QtCore.Qt.DisplayRole) parent = item.parent() + new_row = item.row() + 1 data = {"name": name} new_child = AssetItem(data) - return self.add_item(new_child, parent) - def add_new_task(self, source_index): - item_id = source_index.data(IDENTIFIER_ROLE) + return self.add_item(new_child, parent, new_row) + + def add_new_task(self, parent_index): + item_id = parent_index.data(IDENTIFIER_ROLE) item = self.items_by_id[item_id] - if not isinstance(item, AssetItem): + if isinstance(item, TaskItem): + parent = item.parent() + else: + parent = item + + if not isinstance(parent, AssetItem): return None - name = "task" - parent = item.parent() - - data = {"name": name} + data = {"name": "task"} new_child = TaskItem(data) return self.add_item(new_child, parent) @@ -174,28 +179,29 @@ class HierarchyModel(QtCore.QAbstractItemModel): return self.add_item(new_child, parent) - def add_item(self, item, parent=None): + def add_item(self, item, parent=None, row=None): if parent is None: parent = self._root_item - idx = parent.rowCount() + if row is None: + row = parent.rowCount() parent_index = self.index_from_item(parent.row(), 0, parent.parent()) - self.beginInsertRows(parent_index, idx, idx) + self.beginInsertRows(parent_index, row, row) if item.parent() is not parent: item.set_parent(parent) - parent.add_child(item) + parent.add_child(item, row) if item.id not in self._items_by_id: self._items_by_id[item.id] = item self.endInsertRows() - self.rowsInserted.emit(parent_index, idx, idx) + self.rowsInserted.emit(parent_index, row, row) - return self.index_from_item(idx, 0, parent) + return self.index_from_item(row, 0, parent) def remove_index(self, index): if not index.isValid(): From e88f7c3e333d4eb809516df1ef4b71cfb69d4e49 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 13:26:19 +0200 Subject: [PATCH 014/311] avoid moving task under project --- openpype/tools/project_manager/project_manager/model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 7b54f2682f..8c233564ff 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -293,6 +293,12 @@ class HierarchyModel(QtCore.QAbstractItemModel): if src_parent is dst_parent: return + if ( + isinstance(item, TaskItem) + and not isinstance(dst_parent, AssetItem) + ): + return + if dst_parent_index is None: dst_parent_index = self.index_from_item( dst_parent.row(), 0, dst_parent.parent() From f32cd5ced5fa11b94ea677f35ae6fe7ca3d21e82 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 13:41:38 +0200 Subject: [PATCH 015/311] added some kind of check of editability --- openpype/tools/project_manager/project_manager/model.py | 4 ++++ openpype/tools/project_manager/project_manager/view.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 8c233564ff..867adbaef3 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -589,6 +589,10 @@ class AssetItem(BaseItem): class TaskItem(BaseItem): + columns = [ + "name", + "type" + ] @classmethod def name_icon(cls): if cls._name_icon is None: diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 606edbda99..7843254f87 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -92,7 +92,11 @@ class HierarchyView(QtWidgets.QTreeView): if key not in self.persistent_columns: continue col_index = self._source_model.index(row, column, parent_index) - self.openPersistentEditor(col_index) + if bool( + self._source_model.flags(col_index) + & QtCore.Qt.ItemIsEditable + ): + self.openPersistentEditor(col_index) # Expand parent on insert if not self.isExpanded(parent_index): From c45af6caebd6ad7463f97d9db3a56b1a2458dd64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 23 Apr 2021 17:23:43 +0200 Subject: [PATCH 016/311] init commit asset name duplicity validation --- .../project_manager/constants.py | 2 +- .../project_manager/project_manager/model.py | 59 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 21db80e5f7..61d1944979 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -2,4 +2,4 @@ from Qt import QtCore IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 -COLUMNS_ROLE = QtCore.Qt.UserRole + 2 +DUPLICATED_ROLE = QtCore.Qt.UserRole + 2 diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 867adbaef3..47fc608112 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -4,7 +4,7 @@ from uuid import uuid4 from .constants import ( IDENTIFIER_ROLE, - COLUMNS_ROLE + DUPLICATED_ROLE ) from avalon.api import AvalonMongoDB @@ -39,6 +39,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): super(HierarchyModel, self).__init__(parent) self._root_item = None self._items_by_id = {} + self._asset_items_by_name = collections.defaultdict(list) self.dbcon = AvalonMongoDB() self._hierarchy_mode = True @@ -87,6 +88,12 @@ class HierarchyModel(QtCore.QAbstractItemModel): item = index.internalPointer() column = index.column() key = self.columns[column] + if ( + key == "name" + and role in (QtCore.Qt.EditRole, QtCore.Qt.DisplayRole) + ): + self._rename_asset(item, value) + result = item.setData(key, value, role) if result: self.dataChanged.emit(index, index, [role]) @@ -154,8 +161,13 @@ class HierarchyModel(QtCore.QAbstractItemModel): data = {"name": name} new_child = AssetItem(data) + self._asset_items_by_name[name].append(new_child) - return self.add_item(new_child, parent, new_row) + result = self.add_item(new_child, parent, new_row) + + self._validate_asset_duplicity(name) + + return result def add_new_task(self, parent_index): item_id = parent_index.data(IDENTIFIER_ROLE) @@ -173,12 +185,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): new_child = TaskItem(data) return self.add_item(new_child, parent) - def add_new_item(self, parent): - data = {"name": "Test {}".format(parent.rowCount())} - new_child = AssetItem(data) - - return self.add_item(new_child, parent) - def add_item(self, item, parent=None, row=None): if parent is None: parent = self._root_item @@ -240,12 +246,46 @@ class HierarchyModel(QtCore.QAbstractItemModel): if child_id in all_descendants: continue + if isinstance(child_item, AssetItem): + self._rename_asset(child_item, None) children.pop(child_id) child_item.set_parent(None) self._items_by_id.pop(child_id) self.endRemoveRows() + def _rename_asset(self, asset_item, new_name): + if not isinstance(asset_item, AssetItem): + return + + prev_name = asset_item.data("name", QtCore.Qt.DisplayRole) + print(prev_name) + self._asset_items_by_name[prev_name].remove(asset_item) + + self._validate_asset_duplicity(prev_name) + + if new_name is None: + return + self._asset_items_by_name[new_name].append(asset_item) + + self._validate_asset_duplicity(new_name) + + def _validate_asset_duplicity(self, name): + if name not in self._asset_items_by_name: + return + + items = self._asset_items_by_name[name] + if not items: + self._asset_items_by_name.pop(name) + + elif len(items) == 1: + index = self.index_for_item(items[0]) + self.setData(index, False, DUPLICATED_ROLE) + else: + for item in items: + index = self.index_for_item(item) + self.setData(index, True, DUPLICATED_ROLE) + def move_vertical(self, index, direction): if not index.isValid(): return @@ -456,9 +496,6 @@ class BaseItem: if role == IDENTIFIER_ROLE: return self._id - if role == COLUMNS_ROLE: - return self.columns - if key not in self.columns: return None From 7ea8a6222e78ba202da3bcdb366cd58463d8b9b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Apr 2021 09:10:36 +0200 Subject: [PATCH 017/311] asset names has duplicated information --- .../project_manager/project_manager/model.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 47fc608112..5e497b755d 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -10,7 +10,7 @@ from .constants import ( from avalon.api import AvalonMongoDB from avalon.vendor import qtawesome -from Qt import QtCore +from Qt import QtCore, QtGui class HierarchySelectionModel(QtCore.QItemSelectionModel): @@ -259,7 +259,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): return prev_name = asset_item.data("name", QtCore.Qt.DisplayRole) - print(prev_name) self._asset_items_by_name[prev_name].remove(asset_item) self._validate_asset_duplicity(prev_name) @@ -461,6 +460,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): class BaseItem: columns = ["name"] _name_icon = None + _is_duplicated = False def __init__(self, data=None): self._id = uuid4() @@ -496,6 +496,19 @@ class BaseItem: if role == IDENTIFIER_ROLE: return self._id + if role == DUPLICATED_ROLE: + return self._is_duplicated + + if role == QtCore.Qt.ForegroundRole: + if self._is_duplicated: + return QtGui.QColor(255, 0, 0) + + if role == QtCore.Qt.ToolTipRole: + if self._is_duplicated: + return "Asset with name \"{}\" already exists.".format( + self._data["name"] + ) + if key not in self.columns: return None @@ -510,6 +523,13 @@ class BaseItem: return None def setData(self, key, value, role): + if role == DUPLICATED_ROLE: + if value == self._is_duplicated: + return False + + self._is_duplicated = value + return True + if key not in self.columns: return False From eb6ece64222561bf9a0eaab2e5b56d268b6e0380 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Apr 2021 11:54:17 +0200 Subject: [PATCH 018/311] avoid adding children to TaskItem --- .../project_manager/project_manager/model.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5e497b755d..88ae8f841b 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -319,14 +319,30 @@ class HierarchyModel(QtCore.QAbstractItemModel): # Move under parent before or after if before is None elif direction == 1: - if src_parent.rowCount() == 1: + src_row_count = src_parent.rowCount() + if src_row_count == 1: return - if item.row() == 0: - parent_row = item.row() + 1 - else: - parent_row = item.row() - 1 - dst_parent = src_parent.child(parent_row) + item_row = item.row() + dst_parent = None + for row in reversed(range(item_row)): + if row == item_row: + continue + _item = src_parent.child(row) + if not isinstance(_item, TaskItem): + dst_parent = _item + break + + if dst_parent is None: + for row in range(item_row + 1, src_row_count + 2): + _item = src_parent.child(row) + if not isinstance(_item, TaskItem): + dst_parent = _item + break + + if dst_parent is None: + return + dst_row = dst_parent.rowCount() if src_parent is dst_parent: @@ -655,3 +671,6 @@ class TaskItem(BaseItem): if cls._name_icon is None: cls._name_icon = qtawesome.icon("fa.file-o", color="#333333") return cls._name_icon + + def add_child(self, item, row=None): + raise AssertionError("BUG: Can't add children to Task") From 7c78a9a584457252d7a201cda24c0a09d315121d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Apr 2021 11:59:36 +0200 Subject: [PATCH 019/311] always expand parent of moved item --- .../project_manager/project_manager/view.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 7843254f87..78536d556d 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -183,16 +183,29 @@ class HierarchyView(QtWidgets.QTreeView): self.edit(new_index) def _on_up_ctrl_pressed(self): - self._source_model.move_horizontal(self.currentIndex(), -1) + index = self.currentIndex() + self._source_model.move_horizontal(index, -1) + parent_index = index.parent() + if not self.isExpanded(parent_index): + self.expand(parent_index) def _on_down_ctrl_pressed(self): - self._source_model.move_horizontal(self.currentIndex(), 1) + index = self.currentIndex() + self._source_model.move_horizontal(index, 1) + parent_index = index.parent() + if not self.isExpanded(parent_index): + self.expand(parent_index) def _on_left_ctrl_pressed(self): self._source_model.move_vertical(self.currentIndex(), -1) def _on_right_ctrl_pressed(self): - self._source_model.move_vertical(self.currentIndex(), 1) + index = self.currentIndex() + self._source_model.move_vertical(index, 1) + + parent_index = index.parent() + if not self.isExpanded(parent_index): + self.expand(parent_index) def _on_enter_pressed(self): index = self.currentIndex() From 31145de994857f23e7b8b4e5db1fa0cd20218ba8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Apr 2021 12:36:05 +0200 Subject: [PATCH 020/311] added index_moved signal to model --- openpype/tools/project_manager/project_manager/model.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 88ae8f841b..d426c15a03 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -34,6 +34,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): "resolutionWidth", "resolutionHeight" ] + index_moved = QtCore.Signal(QtCore.QModelIndex) def __init__(self, parent=None): super(HierarchyModel, self).__init__(parent) @@ -205,8 +206,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endInsertRows() - self.rowsInserted.emit(parent_index, row, row) - return self.index_from_item(row, 0, parent) def remove_index(self, index): @@ -373,6 +372,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endMoveRows() + self.index_moved.emit(index) + def move_horizontal(self, index, direction): if not index.isValid(): return @@ -458,6 +459,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endMoveRows() + self.index_moved.emit(index) + def child_removed(self, child): self._items_by_id.pop(child.id, None) From cdaa06706de32e47e705aa1cd43b96839bb6cd9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Apr 2021 12:36:25 +0200 Subject: [PATCH 021/311] added row moved callback --- .../project_manager/project_manager/view.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 78536d556d..1f064ee17d 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -39,10 +39,17 @@ class HierarchyView(QtWidgets.QTreeView): column_delegates[key] = delegate column_key_to_index[key] = column + source_model.index_moved.connect(self._on_rows_moved) + self._delegate = main_delegate self._column_delegates = column_delegates self._column_key_to_index = column_key_to_index + def _on_rows_moved(self, index): + parent_index = index.parent() + if not self.isExpanded(parent_index): + self.expand(parent_index) + def commitData(self, editor): super(HierarchyView, self).commitData(editor) current_index = self.currentIndex() @@ -183,29 +190,16 @@ class HierarchyView(QtWidgets.QTreeView): self.edit(new_index) def _on_up_ctrl_pressed(self): - index = self.currentIndex() - self._source_model.move_horizontal(index, -1) - parent_index = index.parent() - if not self.isExpanded(parent_index): - self.expand(parent_index) + self._source_model.move_horizontal(self.currentIndex(), -1) def _on_down_ctrl_pressed(self): - index = self.currentIndex() - self._source_model.move_horizontal(index, 1) - parent_index = index.parent() - if not self.isExpanded(parent_index): - self.expand(parent_index) + self._source_model.move_horizontal(self.currentIndex(), 1) def _on_left_ctrl_pressed(self): self._source_model.move_vertical(self.currentIndex(), -1) def _on_right_ctrl_pressed(self): - index = self.currentIndex() - self._source_model.move_vertical(index, 1) - - parent_index = index.parent() - if not self.isExpanded(parent_index): - self.expand(parent_index) + self._source_model.move_vertical(self.currentIndex(), 1) def _on_enter_pressed(self): index = self.currentIndex() From a36048ca1340881b384b118df8ae0308aac10019 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Apr 2021 12:57:17 +0200 Subject: [PATCH 022/311] move up/dowm works for multiselection --- .../project_manager/project_manager/model.py | 42 ++++++++++++++++++- .../project_manager/project_manager/view.py | 6 ++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index d426c15a03..fc6442830a 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -374,7 +374,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.index_moved.emit(index) - def move_horizontal(self, index, direction): + def _move_horizontal_single(self, index, direction): if not index.isValid(): return @@ -461,6 +461,46 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.index_moved.emit(index) + def move_horizontal(self, indexes, direction): + if not indexes: + return + + if isinstance(indexes, QtCore.QModelIndex): + indexes = [indexes] + + if len(indexes) == 1: + self._move_horizontal_single(indexes[0], direction) + return + + items_by_id = {} + for index in indexes: + item_id = index.data(IDENTIFIER_ROLE) + items_by_id[item_id] = self._items_by_id[item_id] + + skip_ids = set(items_by_id.keys()) + for item_id, item in tuple(items_by_id.items()): + parent = item.parent() + parent_ids = set() + skip_item = False + while parent is not None: + if parent.id in skip_ids: + skip_item = True + skip_ids |= parent_ids + break + parent_ids.add(parent.id) + parent = parent.parent() + + if skip_item: + items_by_id.pop(item_id) + + items = tuple(items_by_id.values()) + if direction == 1: + items = reversed(items) + + for item in items: + index = self.index_for_item(item) + self._move_horizontal_single(index, direction) + def child_removed(self, child): self._items_by_id.pop(child.id, None) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 1f064ee17d..59a380d6b6 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -190,10 +190,12 @@ class HierarchyView(QtWidgets.QTreeView): self.edit(new_index) def _on_up_ctrl_pressed(self): - self._source_model.move_horizontal(self.currentIndex(), -1) + indexes = self.selectedIndexes() + self._source_model.move_horizontal(indexes, -1) def _on_down_ctrl_pressed(self): - self._source_model.move_horizontal(self.currentIndex(), 1) + indexes = self.selectedIndexes() + self._source_model.move_horizontal(indexes, 1) def _on_left_ctrl_pressed(self): self._source_model.move_vertical(self.currentIndex(), -1) From 33c86aef37e281c4dd9c27b001926941c8540f6b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 09:58:24 +0200 Subject: [PATCH 023/311] use extended selection --- openpype/tools/project_manager/project_manager/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 59a380d6b6..59eb968423 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -28,7 +28,7 @@ class HierarchyView(QtWidgets.QTreeView): main_delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(main_delegate) self.setAlternatingRowColors(True) - self.setSelectionMode(HierarchyView.ContiguousSelection) + self.setSelectionMode(HierarchyView.ExtendedSelection) column_delegates = {} column_key_to_index = {} From 77fa75ec17544064d05a0fbc9adc3e45f39c7d67 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 10:16:38 +0200 Subject: [PATCH 024/311] move vertically multiple items --- .../project_manager/project_manager/model.py | 69 ++++++++++++++++++- .../project_manager/project_manager/view.py | 6 +- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index fc6442830a..04399c637e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -284,7 +284,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): index = self.index_for_item(item) self.setData(index, True, DUPLICATED_ROLE) - def move_vertical(self, index, direction): + def _move_vertical_single(self, index, direction): if not index.isValid(): return @@ -374,6 +374,73 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.index_moved.emit(index) + def move_vertical(self, indexes, direction): + if not indexes: + return + + if isinstance(indexes, QtCore.QModelIndex): + indexes = [indexes] + + if len(indexes) == 1: + self._move_vertical_single(indexes[0], direction) + return + + items_by_id = {} + for index in indexes: + item_id = index.data(IDENTIFIER_ROLE) + item = self._items_by_id[item_id] + if isinstance(item, (RootItem, ProjectItem)): + continue + + if ( + direction == -1 + and isinstance(item.parent(), (RootItem, ProjectItem)) + ): + continue + + items_by_id[item_id] = item + + if not items_by_id: + return + + parents_by_id = {} + items_ids_by_parent_id = collections.defaultdict(set) + skip_ids = set(items_by_id.keys()) + for item_id, item in tuple(items_by_id.items()): + item_parent = item.parent() + + parent_ids = set() + skip_item = False + parent = item_parent + while parent is not None: + if parent.id in skip_ids: + skip_item = True + skip_ids |= parent_ids + break + parent_ids.add(parent.id) + parent = parent.parent() + + if skip_item: + items_by_id.pop(item_id) + else: + parents_by_id[item_parent.id] = item_parent + items_ids_by_parent_id[item_parent.id].add(item_id) + + if direction == 1: + for parent_id, parent in parents_by_id.items(): + items_ids = items_ids_by_parent_id[parent_id] + if len(items_ids) == parent.rowCount(): + for item_id in items_ids: + items_by_id.pop(item_id) + + items = tuple(items_by_id.values()) + if direction == -1: + items = reversed(items) + + for item in items: + index = self.index_for_item(item) + self._move_vertical_single(index, direction) + def _move_horizontal_single(self, index, direction): if not index.isValid(): return diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 59eb968423..b17736d7b0 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -198,10 +198,12 @@ class HierarchyView(QtWidgets.QTreeView): self._source_model.move_horizontal(indexes, 1) def _on_left_ctrl_pressed(self): - self._source_model.move_vertical(self.currentIndex(), -1) + indexes = self.selectedIndexes() + self._source_model.move_vertical(indexes, -1) def _on_right_ctrl_pressed(self): - self._source_model.move_vertical(self.currentIndex(), 1) + indexes = self.selectedIndexes() + self._source_model.move_vertical(indexes, 1) def _on_enter_pressed(self): index = self.currentIndex() From 77488bbd974a40cf39218ad342127e5c41f361f4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 10:21:07 +0200 Subject: [PATCH 025/311] items have editable columns definition --- .../project_manager/project_manager/model.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 04399c637e..6a899b3519 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -585,6 +585,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): class BaseItem: columns = ["name"] + # Use `set` for faster result + editable_columns = {"name"} + _name_icon = None _is_duplicated = False @@ -727,7 +730,7 @@ class BaseItem: def flags(self, key): flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - if key in self.columns: + if key in self.editable_columns: flags |= QtCore.Qt.ItemIsEditable return flags @@ -763,6 +766,14 @@ class AssetItem(BaseItem): "resolutionWidth", "resolutionHeight" ] + editable_columns = { + "name", + "frameStart", + "frameEnd", + "fps", + "resolutionWidth", + "resolutionHeight" + } @classmethod def name_icon(cls): @@ -776,6 +787,11 @@ class TaskItem(BaseItem): "name", "type" ] + editable_columns = { + "name", + "type" + } + @classmethod def name_icon(cls): if cls._name_icon is None: From 21620f715fa094e6923a134156ede71c27b76063 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 10:23:49 +0200 Subject: [PATCH 026/311] small tweaks of columns and editable columns --- .../project_manager/project_manager/model.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 6a899b3519..e53f3fe405 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -160,7 +160,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): parent = item.parent() new_row = item.row() + 1 - data = {"name": name} + data = { + "name": name, + "type": "asset" + } new_child = AssetItem(data) self._asset_items_by_name[name].append(new_child) @@ -584,9 +587,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): class BaseItem: - columns = ["name"] + columns = [] # Use `set` for faster result - editable_columns = {"name"} + editable_columns = set() _name_icon = None _is_duplicated = False @@ -748,9 +751,15 @@ class RootItem(BaseItem): class ProjectItem(BaseItem): + columns = [ + "name", + "type" + ] + def __init__(self, data=None): super(ProjectItem, self).__init__(data) self._data["name"] = "project" + self._data["type"] = "project" def flags(self, *args, **kwargs): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable From f79d497313b134d3308428f80c0f5551de4dea90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 10:24:01 +0200 Subject: [PATCH 027/311] red color only for name column --- openpype/tools/project_manager/project_manager/model.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index e53f3fe405..14950e09c6 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -631,10 +631,6 @@ class BaseItem: if role == DUPLICATED_ROLE: return self._is_duplicated - if role == QtCore.Qt.ForegroundRole: - if self._is_duplicated: - return QtGui.QColor(255, 0, 0) - if role == QtCore.Qt.ToolTipRole: if self._is_duplicated: return "Asset with name \"{}\" already exists.".format( @@ -644,6 +640,11 @@ class BaseItem: if key not in self.columns: return None + if role == QtCore.Qt.ForegroundRole: + if self._is_duplicated and key == "name": + return QtGui.QColor(255, 0, 0) + return None + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): value = self._data[key] if value is None: From 8ebf7f9fb32553cfd955f6c2f7692cafc3c26910 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:07:15 +0200 Subject: [PATCH 028/311] added project model --- .../project_manager/project_manager/model.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 14950e09c6..08621075c0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -13,6 +13,43 @@ from avalon.vendor import qtawesome from Qt import QtCore, QtGui +class ProjectModel(QtGui.QStandardItemModel): + project_changed = QtCore.Signal() + + def __init__(self, dbcon, *args, **kwargs): + self.dbcon = dbcon + + self._project_names = set() + + super(ProjectModel, self).__init__(*args, **kwargs) + + def refresh(self): + self.dbcon.Session["AVALON_PROJECT"] = None + + project_items = [] + database = self.dbcon.database + project_names = set() + for project_name in database.collection_names(): + # Each collection will have exactly one project document + project_doc = database[project_name].find_one( + {"type": "project"}, + {"name": 1} + ) + if not project_doc: + continue + + project_name = project_doc.get("name") + if project_name: + project_names.add(project_name) + project_items.append(QtGui.QStandardItem(project_name)) + + self.clear() + + self._project_names = project_names + + self.invisibleRootItem().appendRows(project_items) + + class HierarchySelectionModel(QtCore.QItemSelectionModel): def setCurrentIndex(self, index, command): if index.column() > 0: From f13d8c7cd9fe0032fdb0533fc526540de1f83853 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:09:15 +0200 Subject: [PATCH 029/311] dbconection is passed from window --- openpype/tools/project_manager/project_manager/model.py | 6 ++---- openpype/tools/project_manager/project_manager/window.py | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 08621075c0..a0f75b5121 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -7,9 +7,7 @@ from .constants import ( DUPLICATED_ROLE ) -from avalon.api import AvalonMongoDB from avalon.vendor import qtawesome - from Qt import QtCore, QtGui @@ -73,12 +71,12 @@ class HierarchyModel(QtCore.QAbstractItemModel): ] index_moved = QtCore.Signal(QtCore.QModelIndex) - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(HierarchyModel, self).__init__(parent) self._root_item = None self._items_by_id = {} self._asset_items_by_name = collections.defaultdict(list) - self.dbcon = AvalonMongoDB() + self.dbcon = dbcon self._hierarchy_mode = True self._reset_root_item() diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index db5527b4ac..257850c014 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -6,12 +6,16 @@ from . import ( HierarchyView ) +from avalon.api import AvalonMongoDB + class Window(QtWidgets.QWidget): def __init__(self, parent=None): super(Window, self).__init__(parent) - model = HierarchyModel() + dbcon = AvalonMongoDB() + + model = HierarchyModel(dbcon) view = HierarchyView(model, self) view.setModel(model) _selection_model = HierarchySelectionModel() From 00b1f35526da3784f691cc75a09b41239321636b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:11:08 +0200 Subject: [PATCH 030/311] renmamed variables in window --- .../project_manager/project_manager/window.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 257850c014..77cff6759c 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -15,14 +15,15 @@ class Window(QtWidgets.QWidget): dbcon = AvalonMongoDB() - model = HierarchyModel(dbcon) - view = HierarchyView(model, self) - view.setModel(model) - _selection_model = HierarchySelectionModel() - _selection_model.setModel(view.model()) - view.setSelectionModel(_selection_model) + hierarchy_model = HierarchyModel(dbcon) - header = view.header() + hierarchy_view = HierarchyView(hierarchy_model, self) + hierarchy_view.setModel(hierarchy_model) + _selection_model = HierarchySelectionModel() + _selection_model.setModel(hierarchy_view.model()) + hierarchy_view.setSelectionModel(_selection_model) + + header = hierarchy_view.header() header.setStretchLastSection(False) header.setSectionResizeMode( header.logicalIndex(0), QtWidgets.QHeaderView.Stretch @@ -31,27 +32,26 @@ class Window(QtWidgets.QWidget): # btn = QtWidgets.QPushButton("Refresh") main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(view) + main_layout.addWidget(hierarchy_view) main_layout.addWidget(checkbox) # main_layout.addWidget(btn) # btn.clicked.connect(self._on_refresh) checkbox.toggled.connect(self._on_checkbox) - self.view = view - self.model = model # self.btn = btn + self.hierarchy_view = hierarchy_view + self.hierarchy_model = hierarchy_model self.checkbox = checkbox self.change_edit_mode() self.resize(1200, 600) - model.set_project({"name": "test"}) def change_edit_mode(self, value=None): if value is None: value = self.checkbox.isChecked() - self.model.change_edit_mode(value) + self.hierarchy_model.change_edit_mode(value) def _on_checkbox(self, value): self.change_edit_mode(value) From 64e4d3be4af122aaa1822d0b32298d4b943dbdef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:11:40 +0200 Subject: [PATCH 031/311] added project combobox to window --- .../tools/project_manager/project_manager/__init__.py | 4 ++++ openpype/tools/project_manager/project_manager/window.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/__init__.py b/openpype/tools/project_manager/project_manager/__init__.py index 1a538baa20..a652e950c4 100644 --- a/openpype/tools/project_manager/project_manager/__init__.py +++ b/openpype/tools/project_manager/project_manager/__init__.py @@ -3,6 +3,8 @@ from .constants import ( ) from .view import HierarchyView from .model import ( + ProjectModel, + HierarchyModel, HierarchySelectionModel, BaseItem, @@ -18,6 +20,8 @@ __all__ = ( "HierarchyView", + "ProjectModel", + "HierarchyModel", "HierarchySelectionModel", "BaseItem", diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 77cff6759c..61db70a6d7 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -1,6 +1,8 @@ from Qt import QtWidgets, QtCore from . import ( + ProjectModel, + HierarchyModel, HierarchySelectionModel, HierarchyView @@ -15,6 +17,12 @@ class Window(QtWidgets.QWidget): dbcon = AvalonMongoDB() + project_model = ProjectModel(dbcon) + + project_combobox = QtWidgets.QComboBox() + project_combobox.setModel(project_model) + project_combobox.setRootModelIndex(QtCore.QModelIndex()) + hierarchy_model = HierarchyModel(dbcon) hierarchy_view = HierarchyView(hierarchy_model, self) @@ -32,6 +40,7 @@ class Window(QtWidgets.QWidget): # btn = QtWidgets.QPushButton("Refresh") main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(project_combobox) main_layout.addWidget(hierarchy_view) main_layout.addWidget(checkbox) # main_layout.addWidget(btn) From 9250939bdd36b3ac531c6c695debeb7871f4d7d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:12:46 +0200 Subject: [PATCH 032/311] project item has more columns and has convertor from project document --- .../project_manager/project_manager/model.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index a0f75b5121..5f1f1d5d0a 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -789,13 +789,33 @@ class RootItem(BaseItem): class ProjectItem(BaseItem): columns = [ "name", - "type" + "type", + "frameStart", + "frameEnd", + "fps", + "resolutionWidth", + "resolutionHeight" ] - def __init__(self, data=None): + def __init__(self, project_doc): + + data = self.data_from_doc(project_doc) super(ProjectItem, self).__init__(data) - self._data["name"] = "project" - self._data["type"] = "project" + + @classmethod + def data_from_doc(cls, project_doc): + data = { + "name": project_doc["name"], + "type": project_doc["type"] + } + doc_data = project_doc.get("data") or {} + for key in cls.columns: + if key in data: + continue + + data[key] = doc_data.get(key) + + return data def flags(self, *args, **kwargs): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable From eb8380aeb25ee6db25a3198c0a232193bf113e23 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:13:21 +0200 Subject: [PATCH 033/311] model expect project name in set_project method --- .../project_manager/project_manager/model.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5f1f1d5d0a..9c307063c0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -73,6 +73,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): def __init__(self, dbcon, parent=None): super(HierarchyModel, self).__init__(parent) + self._current_project = None self._root_item = None self._items_by_id = {} self._asset_items_by_name = collections.defaultdict(list) @@ -91,11 +92,22 @@ class HierarchyModel(QtCore.QAbstractItemModel): def _reset_root_item(self): self._root_item = RootItem(self) - def set_project(self, project_doc): + def set_project(self, project_name): + if self._current_project == project_name: + return + self.clear() - item = ProjectItem(project_doc) - self.add_item(item) + self._current_project = project_name + if not project_name: + return + + project_doc = self.dbcon.database[project_name].find_one({ + "type": "project" + }) + if project_doc: + item = ProjectItem(project_doc) + self.add_item(item) def rowCount(self, parent=None): if parent is None or not parent.isValid(): From 135d811654874e0becf5c369392a75f35bd66626 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:13:35 +0200 Subject: [PATCH 034/311] window can load and change projects --- .../project_manager/project_manager/window.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 61db70a6d7..fa47bfd1bb 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -57,6 +57,31 @@ class Window(QtWidgets.QWidget): self.resize(1200, 600) + self.refresh_projects() + + def refresh_projects(self): + current_project = None + if self.project_combobox.count() > 0: + current_project = self.project_combobox.currentText() + + self.project_model.refresh() + + if self.project_combobox.count() == 0: + return self._set_project() + + if current_project: + row = self.project_combobox.findText(current_project) + if row >= 0: + self._set_project(current_project) + index = self.project_combobox.model().index(row, 0) + self.project_combobox.setCurrentIndex(index) + return + + self._set_project(self.project_combobox.currentText()) + + def _set_project(self, project_name=None): + self.hierarchy_model.set_project(project_name) + def change_edit_mode(self, value=None): if value is None: value = self.checkbox.isChecked() From f8ef837a0cbdfe8552c98172c449896e3df2b6c7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:19:55 +0200 Subject: [PATCH 035/311] added simple refresh button for projects --- .../tools/project_manager/project_manager/window.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index fa47bfd1bb..0dee861f59 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -17,12 +17,23 @@ class Window(QtWidgets.QWidget): dbcon = AvalonMongoDB() + # TOP Project selection + project_widget = QtWidgets.QWidget(self) + project_model = ProjectModel(dbcon) - project_combobox = QtWidgets.QComboBox() + project_combobox = QtWidgets.QComboBox(project_widget) project_combobox.setModel(project_model) project_combobox.setRootModelIndex(QtCore.QModelIndex()) + refresh_projects_btn = QtWidgets.QPushButton("Refresh", project_widget) + + project_layout = QtWidgets.QHBoxLayout(project_widget) + project_layout.setContentsMargins(0, 0, 0, 0) + project_layout.addWidget(refresh_projects_btn, 0) + project_layout.addWidget(project_combobox, 0) + project_layout.addStretch(1) + hierarchy_model = HierarchyModel(dbcon) hierarchy_view = HierarchyView(hierarchy_model, self) From 533adaf44db4c22f1a9c71ac29ee914772758778 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:20:49 +0200 Subject: [PATCH 036/311] removed unused parts --- openpype/tools/project_manager/project_manager/window.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 0dee861f59..2f7d4d472e 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -48,20 +48,17 @@ class Window(QtWidgets.QWidget): header.logicalIndex(0), QtWidgets.QHeaderView.Stretch ) checkbox = QtWidgets.QCheckBox(self) - # btn = QtWidgets.QPushButton("Refresh") main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(project_combobox) main_layout.addWidget(hierarchy_view) main_layout.addWidget(checkbox) - # main_layout.addWidget(btn) - # btn.clicked.connect(self._on_refresh) checkbox.toggled.connect(self._on_checkbox) - # self.btn = btn self.hierarchy_view = hierarchy_view self.hierarchy_model = hierarchy_model + self.checkbox = checkbox self.change_edit_mode() @@ -100,6 +97,3 @@ class Window(QtWidgets.QWidget): def _on_checkbox(self, value): self.change_edit_mode(value) - - def _on_refresh(self): - self.model.clear() From 27eb318274c3f274eb198c12a27e93bf9654704e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:21:09 +0200 Subject: [PATCH 037/311] project is changed on combobox change --- .../project_manager/project_manager/window.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 2f7d4d472e..3f9a54442b 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -50,11 +50,16 @@ class Window(QtWidgets.QWidget): checkbox = QtWidgets.QCheckBox(self) main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(project_combobox) + main_layout.addWidget(project_widget) main_layout.addWidget(hierarchy_view) main_layout.addWidget(checkbox) + refresh_projects_btn.clicked.connect(self._on_project_refresh) checkbox.toggled.connect(self._on_checkbox) + project_combobox.currentIndexChanged.connect(self._on_project_change) + + self.project_model = project_model + self.project_combobox = project_combobox self.hierarchy_view = hierarchy_view self.hierarchy_model = hierarchy_model @@ -67,6 +72,14 @@ class Window(QtWidgets.QWidget): self.refresh_projects() + def _change_edit_mode(self, value=None): + if value is None: + value = self.checkbox.isChecked() + self.hierarchy_model.change_edit_mode(value) + + def _set_project(self, project_name=None): + self.hierarchy_model.set_project(project_name) + def refresh_projects(self): current_project = None if self.project_combobox.count() > 0: @@ -87,13 +100,11 @@ class Window(QtWidgets.QWidget): self._set_project(self.project_combobox.currentText()) - def _set_project(self, project_name=None): - self.hierarchy_model.set_project(project_name) + def _on_project_change(self): + self._set_project(self.project_combobox.currentText()) - def change_edit_mode(self, value=None): - if value is None: - value = self.checkbox.isChecked() - self.hierarchy_model.change_edit_mode(value) + def _on_project_refresh(self): + self.refresh_projects() def _on_checkbox(self, value): - self.change_edit_mode(value) + self._change_edit_mode(value) From b7efe9b9f61a21082113918fd23bd4bb5b6e6993 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 11:24:13 +0200 Subject: [PATCH 038/311] removed checkbox without logic --- .../project_manager/project_manager/model.py | 4 ---- .../project_manager/project_manager/window.py | 15 --------------- 2 files changed, 19 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9c307063c0..f7973563ad 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -79,12 +79,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._asset_items_by_name = collections.defaultdict(list) self.dbcon = dbcon - self._hierarchy_mode = True self._reset_root_item() - def change_edit_mode(self, hiearchy_mode): - self._hierarchy_mode = hiearchy_mode - @property def items_by_id(self): return self._items_by_id diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 3f9a54442b..7f9b4e7331 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -47,15 +47,12 @@ class Window(QtWidgets.QWidget): header.setSectionResizeMode( header.logicalIndex(0), QtWidgets.QHeaderView.Stretch ) - checkbox = QtWidgets.QCheckBox(self) main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(project_widget) main_layout.addWidget(hierarchy_view) - main_layout.addWidget(checkbox) refresh_projects_btn.clicked.connect(self._on_project_refresh) - checkbox.toggled.connect(self._on_checkbox) project_combobox.currentIndexChanged.connect(self._on_project_change) self.project_model = project_model @@ -64,19 +61,10 @@ class Window(QtWidgets.QWidget): self.hierarchy_view = hierarchy_view self.hierarchy_model = hierarchy_model - self.checkbox = checkbox - - self.change_edit_mode() - self.resize(1200, 600) self.refresh_projects() - def _change_edit_mode(self, value=None): - if value is None: - value = self.checkbox.isChecked() - self.hierarchy_model.change_edit_mode(value) - def _set_project(self, project_name=None): self.hierarchy_model.set_project(project_name) @@ -105,6 +93,3 @@ class Window(QtWidgets.QWidget): def _on_project_refresh(self): self.refresh_projects() - - def _on_checkbox(self, value): - self._change_edit_mode(value) From 108666cd3ffd4dbc4c9c08d1043b76be10dd17f0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:05:28 +0200 Subject: [PATCH 039/311] added hardcoded min/max for number delegate --- openpype/tools/project_manager/project_manager/delegates.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 605d0cbbab..1102242199 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -4,6 +4,8 @@ from Qt import QtWidgets, QtCore class NumberDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): editor = QtWidgets.QSpinBox(parent) + editor.setMaximum(999999) + editor.setMinimum(0) value = index.data(QtCore.Qt.DisplayRole) if value is not None: editor.setValue(value) From 6e578ee63c99a2f7afb8360e2e30613567312194 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:06:12 +0200 Subject: [PATCH 040/311] items have defined query projections --- .../project_manager/project_manager/model.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index f7973563ad..dea14d021d 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -804,9 +804,18 @@ class ProjectItem(BaseItem): "resolutionWidth", "resolutionHeight" ] + query_projection = { + "_id": 1, + "name": 1, + "type": 1, + "data.frameStart": 1, + "data.frameEnd": 1, + "data.fps": 1, + "data.resolutionWidth": 1, + "data.resolutionHeight": 1 + } def __init__(self, project_doc): - data = self.data_from_doc(project_doc) super(ProjectItem, self).__init__(data) @@ -847,6 +856,17 @@ class AssetItem(BaseItem): "resolutionWidth", "resolutionHeight" } + query_projection = { + "_id": 1, + "name": 1, + "type": 1, + "data.frameStart": 1, + "data.frameEnd": 1, + "data.fps": 1, + "data.resolutionWidth": 1, + "data.resolutionHeight": 1, + "data.visualParent": 1 + } @classmethod def name_icon(cls): From 7d3132f058f89c7f3c631bee523a4a89c5e5b0db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:06:24 +0200 Subject: [PATCH 041/311] add existing asset docs to hierarchy --- .../project_manager/project_manager/model.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index dea14d021d..8cafc9019a 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -98,12 +98,37 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not project_name: return - project_doc = self.dbcon.database[project_name].find_one({ - "type": "project" - }) - if project_doc: - item = ProjectItem(project_doc) - self.add_item(item) + project_doc = self.dbcon.database[project_name].find_one( + {"type": "project"}, + ProjectItem.query_projection + ) + if not project_doc: + return + + project_item = ProjectItem(project_doc) + self.add_item(project_item) + + asset_docs = self.dbcon.database[project_name].find( + {"type": "asset"}, + AssetItem.query_projection + ) + asset_docs_by_parent_id = collections.defaultdict(list) + for asset_doc in asset_docs: + parent_id = asset_doc["data"]["visualParent"] + asset_docs_by_parent_id[parent_id].append(asset_doc) + + appending_queue = Queue() + appending_queue.put((None, project_item)) + + while not appending_queue.empty(): + parent_id, parent_item = appending_queue.get() + if parent_id not in asset_docs_by_parent_id: + continue + + for asset_doc in asset_docs_by_parent_id[parent_id]: + new_item = AssetItem(asset_doc) + self.add_item(new_item, parent_item) + appending_queue.put((asset_doc["_id"], new_item)) def rowCount(self, parent=None): if parent is None or not parent.isValid(): From df179af335a3a6dd44ce1c64fa3bc25b536c928f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:08:04 +0200 Subject: [PATCH 042/311] AssetItem has convertor from document to data --- .../project_manager/project_manager/model.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 8cafc9019a..bf00c3f865 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -893,6 +893,25 @@ class AssetItem(BaseItem): "data.visualParent": 1 } + def __init__(self, asset_doc): + data = self.data_from_doc(asset_doc) + super(AssetItem, self).__init__(data) + + @classmethod + def data_from_doc(cls, asset_doc): + data = { + "name": asset_doc["name"], + "type": asset_doc["type"] + } + doc_data = asset_doc.get("data") or {} + for key in cls.columns: + if key in data: + continue + + data[key] = doc_data.get(key) + + return data + @classmethod def name_icon(cls): if cls._name_icon is None: From d08fbcc02f0f3395137e483e23078f7273f4902d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:15:32 +0200 Subject: [PATCH 043/311] added ability to add multiple items under same parent --- .../project_manager/project_manager/model.py | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index bf00c3f865..55dd23c76a 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -125,11 +125,14 @@ class HierarchyModel(QtCore.QAbstractItemModel): if parent_id not in asset_docs_by_parent_id: continue + new_items = [] for asset_doc in asset_docs_by_parent_id[parent_id]: new_item = AssetItem(asset_doc) - self.add_item(new_item, parent_item) + new_items.append(new_item) appending_queue.put((asset_doc["_id"], new_item)) + self.add_items(new_items, parent_item) + def rowCount(self, parent=None): if parent is None or not parent.isValid(): parent_item = self._root_item @@ -257,27 +260,39 @@ class HierarchyModel(QtCore.QAbstractItemModel): new_child = TaskItem(data) return self.add_item(new_child, parent) - def add_item(self, item, parent=None, row=None): + def add_items(self, items, parent=None, start_row=None): if parent is None: parent = self._root_item - if row is None: - row = parent.rowCount() + if start_row is None: + start_row = parent.rowCount() + + end_row = start_row + len(items) - 1 parent_index = self.index_from_item(parent.row(), 0, parent.parent()) - self.beginInsertRows(parent_index, row, row) + self.beginInsertRows(parent_index, start_row, end_row) - if item.parent() is not parent: - item.set_parent(parent) + for idx, item in enumerate(items): + row = start_row + idx + if item.parent() is not parent: + item.set_parent(parent) - parent.add_child(item, row) + parent.add_child(item, row) - if item.id not in self._items_by_id: - self._items_by_id[item.id] = item + if item.id not in self._items_by_id: + self._items_by_id[item.id] = item self.endInsertRows() - return self.index_from_item(row, 0, parent) + indexes = [] + for row in range(start_row, end_row + 1): + indexes.append( + self.index_from_item(row, 0, parent) + ) + return indexes + + def add_item(self, item, parent=None, row=None): + return self.add_items([item], parent, row)[0] def remove_index(self, index): if not index.isValid(): From b8e500cb6809f831dc50f33fdff9cd7ad5efa823 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:22:05 +0200 Subject: [PATCH 044/311] fix project combobox refresh --- openpype/tools/project_manager/project_manager/window.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 7f9b4e7331..56c496346d 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -81,10 +81,7 @@ class Window(QtWidgets.QWidget): if current_project: row = self.project_combobox.findText(current_project) if row >= 0: - self._set_project(current_project) - index = self.project_combobox.model().index(row, 0) - self.project_combobox.setCurrentIndex(index) - return + self.project_combobox.setCurrentIndex(row) self._set_project(self.project_combobox.currentText()) From ab54913a435dd7a0319e94a3cf4a36eedf5e8e35 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:24:09 +0200 Subject: [PATCH 045/311] modified asset query projection --- openpype/tools/project_manager/project_manager/model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 55dd23c76a..f25b187b6c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -898,14 +898,16 @@ class AssetItem(BaseItem): } query_projection = { "_id": 1, + "data.tasks": 1, + "data.visualParent": 1, + "name": 1, "type": 1, "data.frameStart": 1, "data.frameEnd": 1, "data.fps": 1, "data.resolutionWidth": 1, - "data.resolutionHeight": 1, - "data.visualParent": 1 + "data.resolutionHeight": 1 } def __init__(self, asset_doc): From 558ca50ed18c69127729b66d95f4c61fee17da30 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:27:20 +0200 Subject: [PATCH 046/311] load also task items --- .../project_manager/project_manager/model.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index f25b187b6c..947dd5087f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1,4 +1,5 @@ import collections +import copy from queue import Queue from uuid import uuid4 @@ -112,14 +113,20 @@ class HierarchyModel(QtCore.QAbstractItemModel): {"type": "asset"}, AssetItem.query_projection ) + asset_docs_by_id = { + asset_doc["_id"]: asset_doc + for asset_doc in asset_docs + } asset_docs_by_parent_id = collections.defaultdict(list) - for asset_doc in asset_docs: + for asset_doc in asset_docs_by_id.values(): parent_id = asset_doc["data"]["visualParent"] asset_docs_by_parent_id[parent_id].append(asset_doc) appending_queue = Queue() appending_queue.put((None, project_item)) + asset_items_by_id = {} + while not appending_queue.empty(): parent_id, parent_item = appending_queue.get() if parent_id not in asset_docs_by_parent_id: @@ -127,12 +134,33 @@ class HierarchyModel(QtCore.QAbstractItemModel): new_items = [] for asset_doc in asset_docs_by_parent_id[parent_id]: + # Create new Item new_item = AssetItem(asset_doc) + # Store item to be added under parent in bulk new_items.append(new_item) - appending_queue.put((asset_doc["_id"], new_item)) + + # Store item by id for task processing + asset_id = asset_doc["_id"] + asset_items_by_id[asset_id] = new_item + # Add item to appending queue + appending_queue.put((asset_id, new_item)) self.add_items(new_items, parent_item) + for asset_id, asset_item in asset_items_by_id.items(): + asset_doc = asset_docs_by_id[asset_id] + asset_tasks = asset_doc["data"]["tasks"] + if not asset_tasks: + continue + + task_items = [] + for task_name, task_data in asset_tasks.items(): + _task_data = copy.deepcopy(task_data) + _task_data["name"] = task_name + task_item = TaskItem(_task_data) + task_items.append(task_item) + self.add_items(task_items, asset_item) + def rowCount(self, parent=None): if parent is None or not parent.isValid(): parent_item = self._root_item From e771d4b887d5657c282e70eb99e1e7b667023c29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:40:41 +0200 Subject: [PATCH 047/311] different way how to define delegates with more abilities --- .../project_manager/delegates.py | 17 +++++++-- .../project_manager/project_manager/view.py | 36 ++++++++++++++----- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 1102242199..a26fb17b29 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -2,10 +2,21 @@ from Qt import QtWidgets, QtCore class NumberDelegate(QtWidgets.QStyledItemDelegate): + def __init__(self, minimum, maximum, decimals, *args, **kwargs): + super(NumberDelegate, self).__init__(*args, **kwargs) + self.minimum = minimum + self.maximum = maximum + self.decimals = decimals + def createEditor(self, parent, option, index): - editor = QtWidgets.QSpinBox(parent) - editor.setMaximum(999999) - editor.setMinimum(0) + print(option.rect) + if self.decimals > 0: + editor = QtWidgets.QDoubleSpinBox(parent) + else: + editor = QtWidgets.QSpinBox(parent) + editor.setMinimum(self.minimum) + editor.setMaximum(self.maximum) + value = index.data(QtCore.Qt.DisplayRole) if value is not None: editor.setValue(value) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b17736d7b0..90e0e5289e 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -3,15 +3,27 @@ from Qt import QtWidgets, QtCore from .delegates import NumberDelegate, StringDelegate +class StringDef: + def __init__(self, regex=None): + self.regex = regex + + +class NumberDef: + def __init__(self, minimum=None, maximum=None, decimals=None): + self.minimum = 0 if minimum is None else minimum + self.maximum = 999999 if maximum is None else maximum + self.decimals = 0 if decimals is None else decimals + + class HierarchyView(QtWidgets.QTreeView): """A tree view that deselects on clicking on an empty area in the view""" column_delegate_defs = { - "name": StringDelegate, - "frameStart": NumberDelegate, - "frameEnd": NumberDelegate, - "fps": NumberDelegate, - "resolutionWidth": NumberDelegate, - "resolutionHeight": NumberDelegate + "name": StringDef(), + "frameStart": NumberDef(1), + "frameEnd": NumberDef(1), + "fps": NumberDef(1, decimals=2), + "resolutionWidth": NumberDef(0), + "resolutionHeight": NumberDef(0) } persistent_columns = [ "frameStart", @@ -32,8 +44,16 @@ class HierarchyView(QtWidgets.QTreeView): column_delegates = {} column_key_to_index = {} - for key, delegate_klass in self.column_delegate_defs.items(): - delegate = delegate_klass() + for key, item_type in self.column_delegate_defs.items(): + if isinstance(item_type, StringDef): + delegate = StringDelegate() + elif isinstance(item_type, NumberDef): + delegate = NumberDelegate( + item_type.minimum, + item_type.maximum, + item_type.decimals + ) + column = self._source_model.columns.index(key) self.setItemDelegateForColumn(column, delegate) column_delegates[key] = delegate From f5bbe2a3d4265e20e5e4bd249b1c73aca6b9bd42 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 12:41:24 +0200 Subject: [PATCH 048/311] remove debug print --- openpype/tools/project_manager/project_manager/delegates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index a26fb17b29..51bf6515ad 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -9,7 +9,6 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): self.decimals = decimals def createEditor(self, parent, option, index): - print(option.rect) if self.decimals > 0: editor = QtWidgets.QDoubleSpinBox(parent) else: From 550945972ad41102887bfc220803837f7801b546 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Apr 2021 13:40:29 +0200 Subject: [PATCH 049/311] added more columns --- .../project_manager/project_manager/model.py | 94 ++++++++++++++----- .../project_manager/project_manager/view.py | 19 +++- 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 947dd5087f..a6446b22bb 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -61,15 +61,30 @@ class HierarchySelectionModel(QtCore.QItemSelectionModel): class HierarchyModel(QtCore.QAbstractItemModel): - columns = [ - "name", - "type", - "frameStart", - "frameEnd", - "fps", - "resolutionWidth", - "resolutionHeight" + _columns_def = [ + ("name", "Name"), + ("type", "Type"), + ("fps", "FPS"), + ("frameStart", "Frame start"), + ("frameEnd", "Frame end"), + ("handleStart", "Handle start"), + ("handleEnd", "Handle end"), + ("resolutionWidth", "Width"), + ("resolutionHeight", "Height"), + ("clipIn", "Clip in"), + ("clipOut", "Clip out"), + ("pixelAspect", "Pixel aspect"), + ("tools_env", "Tools") ] + columns = [ + item[0] + for item in _columns_def + ] + columns_len = len(columns) + column_labels = { + idx: item[1] + for idx, item in enumerate(_columns_def) + } index_moved = QtCore.Signal(QtCore.QModelIndex) def __init__(self, dbcon, parent=None): @@ -169,7 +184,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): return parent_item.rowCount() def columnCount(self, *args, **kwargs): - return len(self.columns) + return self.columns_len def data(self, index, role): if not index.isValid(): @@ -202,8 +217,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): def headerData(self, section, orientation, role): if role == QtCore.Qt.DisplayRole: - if section < len(self.columns): - return self.columns[section] + if section < self.columnCount(): + return self.column_labels[section] super(HierarchyModel, self).headerData(section, orientation, role) @@ -252,7 +267,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): new_row = None if isinstance(item, (RootItem, ProjectItem)): - name = "eq" + name = "ep" parent = item else: name = source_index.data(QtCore.Qt.DisplayRole) @@ -863,24 +878,37 @@ class RootItem(BaseItem): class ProjectItem(BaseItem): - columns = [ + columns = { "name", "type", "frameStart", "frameEnd", "fps", "resolutionWidth", - "resolutionHeight" - ] + "resolutionHeight", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "pixelAspect", + "tools_env", + } query_projection = { "_id": 1, "name": 1, "type": 1, + "data.frameStart": 1, "data.frameEnd": 1, "data.fps": 1, "data.resolutionWidth": 1, - "data.resolutionHeight": 1 + "data.resolutionHeight": 1, + "data.handleStart": 1, + "data.handleEnd": 1, + "data.clipIn": 1, + "data.clipOut": 1, + "data.pixelAspect": 1, + "data.tools_env": 1 } def __init__(self, project_doc): @@ -907,22 +935,34 @@ class ProjectItem(BaseItem): class AssetItem(BaseItem): - columns = [ + columns = { "name", "type", + "fps", "frameStart", "frameEnd", - "fps", "resolutionWidth", - "resolutionHeight" - ] + "resolutionHeight", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "pixelAspect", + "tools_env" + } editable_columns = { "name", "frameStart", "frameEnd", "fps", "resolutionWidth", - "resolutionHeight" + "resolutionHeight", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "pixelAspect", + "tools_env" } query_projection = { "_id": 1, @@ -935,7 +975,13 @@ class AssetItem(BaseItem): "data.frameEnd": 1, "data.fps": 1, "data.resolutionWidth": 1, - "data.resolutionHeight": 1 + "data.resolutionHeight": 1, + "data.handleStart": 1, + "data.handleEnd": 1, + "data.clipIn": 1, + "data.clipOut": 1, + "data.pixelAspect": 1, + "data.tools_env": 1 } def __init__(self, asset_doc): @@ -965,10 +1011,10 @@ class AssetItem(BaseItem): class TaskItem(BaseItem): - columns = [ + columns = { "name", "type" - ] + } editable_columns = { "name", "type" diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 90e0e5289e..7131687612 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -23,14 +23,25 @@ class HierarchyView(QtWidgets.QTreeView): "frameEnd": NumberDef(1), "fps": NumberDef(1, decimals=2), "resolutionWidth": NumberDef(0), - "resolutionHeight": NumberDef(0) + "resolutionHeight": NumberDef(0), + "handleStart": NumberDef(0), + "handleEnd": NumberDef(0), + "clipIn": NumberDef(1), + "clipOut": NumberDef(1), + "pixelAspect": NumberDef(0, decimals=2), + # "tools_env": NumberDef(0) } persistent_columns = [ "frameStart", "frameEnd", "fps", "resolutionWidth", - "resolutionHeight" + "resolutionHeight", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "pixelAspect" ] def __init__(self, source_model, *args, **kwargs): @@ -95,7 +106,9 @@ class HierarchyView(QtWidgets.QTreeView): def _deselect_editor(self, editor): if editor: - if isinstance(editor, QtWidgets.QSpinBox): + if isinstance( + editor, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox) + ): line_edit = editor.findChild(QtWidgets.QLineEdit) line_edit.deselect() From 16f7bd9ec1c8438fdfeddc51c7c2c4e1348ec74a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:07:34 +0200 Subject: [PATCH 050/311] adde ProjectHelper which helps to keep track with project document --- .../project_manager/project_manager/view.py | 25 ++++++++++++++++++- .../project_manager/project_manager/window.py | 4 +-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 7131687612..7f8abd82c9 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -15,6 +15,22 @@ class NumberDef: self.decimals = 0 if decimals is None else decimals +class ProjectHelper: + def __init__(self, dbcon): + self.dbcon = dbcon + self.project_doc = None + + def set_project(self, project_name): + self.project_doc = None + + if not project_name: + return + + self.project_doc = self.dbcon.database[project_name].find_one( + {"type": "project"} + ) + + class HierarchyView(QtWidgets.QTreeView): """A tree view that deselects on clicking on an empty area in the view""" column_delegate_defs = { @@ -44,10 +60,12 @@ class HierarchyView(QtWidgets.QTreeView): "pixelAspect" ] - def __init__(self, source_model, *args, **kwargs): + def __init__(self, dbcon, source_model, *args, **kwargs): super(HierarchyView, self).__init__(*args, **kwargs) self._source_model = source_model + project_helper = ProjectHelper(dbcon) + main_delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(main_delegate) self.setAlternatingRowColors(True) @@ -72,10 +90,15 @@ class HierarchyView(QtWidgets.QTreeView): source_model.index_moved.connect(self._on_rows_moved) + self._project_helper = project_helper self._delegate = main_delegate self._column_delegates = column_delegates self._column_key_to_index = column_key_to_index + def set_project(self, project_name): + self._source_model.set_project(project_name) + self._project_helper.set_project(project_name) + def _on_rows_moved(self, index): parent_index = index.parent() if not self.isExpanded(parent_index): diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 56c496346d..790df4557e 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -36,7 +36,7 @@ class Window(QtWidgets.QWidget): hierarchy_model = HierarchyModel(dbcon) - hierarchy_view = HierarchyView(hierarchy_model, self) + hierarchy_view = HierarchyView(dbcon, hierarchy_model, self) hierarchy_view.setModel(hierarchy_model) _selection_model = HierarchySelectionModel() _selection_model.setModel(hierarchy_view.model()) @@ -66,7 +66,7 @@ class Window(QtWidgets.QWidget): self.refresh_projects() def _set_project(self, project_name=None): - self.hierarchy_model.set_project(project_name) + self.hierarchy_view.set_project(project_name) def refresh_projects(self): current_project = None From 00b7b4514ef10acf74bf35ae35a5bd538aa5f79b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:07:48 +0200 Subject: [PATCH 051/311] adde type delegate for task types --- .../project_manager/delegates.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 51bf6515ad..e660cd05f4 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -33,3 +33,20 @@ class StringDelegate(QtWidgets.QStyledItemDelegate): if value is not None: editor.setText(str(value)) return editor + + +class TypeDelegate(QtWidgets.QStyledItemDelegate): + def __init__(self, project_helper, *args, **kwargs): + self.project_helper = project_helper + super(TypeDelegate, self).__init__(*args, **kwargs) + + def createEditor(self, parent, option, index): + editor = QtWidgets.QComboBox(parent) + task_type_defs = self.project_helper.project_doc["config"]["tasks"] + items = list(task_type_defs.keys()) + + value = index.data(QtCore.Qt.DisplayRole) + + editor.addItems(items) + + return editor From 3028e61c0010d4012b539da9f15d8c3ee0b8b119 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:08:02 +0200 Subject: [PATCH 052/311] use task type delegate creating combobox --- .../tools/project_manager/project_manager/view.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 7f8abd82c9..88836aa969 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -1,6 +1,10 @@ from Qt import QtWidgets, QtCore -from .delegates import NumberDelegate, StringDelegate +from .delegates import ( + NumberDelegate, + StringDelegate, + TypeDelegate +) class StringDef: @@ -15,6 +19,10 @@ class NumberDef: self.decimals = 0 if decimals is None else decimals +class TypeDef: + pass + + class ProjectHelper: def __init__(self, dbcon): self.dbcon = dbcon @@ -35,6 +43,7 @@ class HierarchyView(QtWidgets.QTreeView): """A tree view that deselects on clicking on an empty area in the view""" column_delegate_defs = { "name": StringDef(), + "type": TypeDef(), "frameStart": NumberDef(1), "frameEnd": NumberDef(1), "fps": NumberDef(1, decimals=2), @@ -82,6 +91,8 @@ class HierarchyView(QtWidgets.QTreeView): item_type.maximum, item_type.decimals ) + elif isinstance(item_type, TypeDef): + delegate = TypeDelegate(project_helper) column = self._source_model.columns.index(key) self.setItemDelegateForColumn(column, delegate) From 72c2d66c029c61fe767099296bc2027505d24c96 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:56:48 +0200 Subject: [PATCH 053/311] fixed TypeDelegate --- .../project_manager/project_manager/delegates.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index e660cd05f4..22b8427dd1 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -36,17 +36,17 @@ class StringDelegate(QtWidgets.QStyledItemDelegate): class TypeDelegate(QtWidgets.QStyledItemDelegate): - def __init__(self, project_helper, *args, **kwargs): - self.project_helper = project_helper + def __init__(self, project_doc_cache, *args, **kwargs): + self._project_doc_cache = project_doc_cache super(TypeDelegate, self).__init__(*args, **kwargs) def createEditor(self, parent, option, index): editor = QtWidgets.QComboBox(parent) - task_type_defs = self.project_helper.project_doc["config"]["tasks"] - items = list(task_type_defs.keys()) + if not self._project_doc_cache.project_doc: + return editor - value = index.data(QtCore.Qt.DisplayRole) + task_type_defs = self._project_doc_cache.project_doc["config"]["tasks"] + editor.addItems(list(task_type_defs.keys())) - editor.addItems(items) return editor From 3e2507598f5e2780314c78c945e95488c563e450 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:57:06 +0200 Subject: [PATCH 054/311] added multiselection combobox --- .../multiselection_combobox.py | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 openpype/tools/project_manager/project_manager/multiselection_combobox.py diff --git a/openpype/tools/project_manager/project_manager/multiselection_combobox.py b/openpype/tools/project_manager/project_manager/multiselection_combobox.py new file mode 100644 index 0000000000..b26976d3c6 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/multiselection_combobox.py @@ -0,0 +1,215 @@ +from Qt import QtCore, QtGui, QtWidgets + + +class ComboItemDelegate(QtWidgets.QStyledItemDelegate): + """ + Helper styled delegate (mostly based on existing private Qt's + delegate used by the QtWidgets.QComboBox). Used to style the popup like a + list view (e.g windows style). + """ + + def paint(self, painter, option, index): + option = QtWidgets.QStyleOptionViewItem(option) + option.showDecorationSelected = True + + # option.state &= ( + # ~QtWidgets.QStyle.State_HasFocus + # & ~QtWidgets.QStyle.State_MouseOver + # ) + super(ComboItemDelegate, self).paint(painter, option, index) + + +class MultiSelectionComboBox(QtWidgets.QComboBox): + value_changed = QtCore.Signal() + ignored_keys = { + QtCore.Qt.Key_Up, + QtCore.Qt.Key_Down, + QtCore.Qt.Key_PageDown, + QtCore.Qt.Key_PageUp, + QtCore.Qt.Key_Home, + QtCore.Qt.Key_End + } + + def __init__(self, parent=None, **kwargs): + super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs) + self.setObjectName("MultiSelectionComboBox") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + self._popup_is_shown = False + self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True) + self._initial_mouse_pos = None + self._delegate = ComboItemDelegate(self) + self.setItemDelegate(self._delegate) + + def mousePressEvent(self, event): + """Reimplemented.""" + self._popup_is_shown = False + super(MultiSelectionComboBox, self).mousePressEvent(event) + if self._popup_is_shown: + self._initial_mouse_pos = self.mapToGlobal(event.pos()) + self._block_mouse_release_timer.start( + QtWidgets.QApplication.doubleClickInterval() + ) + + def showPopup(self): + """Reimplemented.""" + super(MultiSelectionComboBox, self).showPopup() + view = self.view() + view.installEventFilter(self) + view.viewport().installEventFilter(self) + self._popup_is_shown = True + + def hidePopup(self): + """Reimplemented.""" + self.view().removeEventFilter(self) + self.view().viewport().removeEventFilter(self) + self._popup_is_shown = False + self._initial_mouse_pos = None + super(MultiSelectionComboBox, self).hidePopup() + self.view().clearFocus() + + def _event_popup_shown(self, obj, event): + if not self._popup_is_shown: + return + + current_index = self.view().currentIndex() + model = self.model() + + if event.type() == QtCore.QEvent.MouseMove: + if ( + self.view().isVisible() + and self._initial_mouse_pos is not None + and self._block_mouse_release_timer.isActive() + ): + diff = obj.mapToGlobal(event.pos()) - self._initial_mouse_pos + if diff.manhattanLength() > 9: + self._block_mouse_release_timer.stop() + return + + index_flags = current_index.flags() + state = current_index.data(QtCore.Qt.CheckStateRole) + new_state = None + + if event.type() == QtCore.QEvent.MouseButtonRelease: + if ( + self._block_mouse_release_timer.isActive() + or not current_index.isValid() + or not self.view().isVisible() + or not self.view().rect().contains(event.pos()) + or not index_flags & QtCore.Qt.ItemIsSelectable + or not index_flags & QtCore.Qt.ItemIsEnabled + or not index_flags & QtCore.Qt.ItemIsUserCheckable + ): + return + + if state == QtCore.Qt.Unchecked: + new_state = QtCore.Qt.Checked + else: + new_state = QtCore.Qt.Unchecked + + elif event.type() == QtCore.QEvent.KeyPress: + # TODO: handle QtCore.Qt.Key_Enter, Key_Return? + if event.key() == QtCore.Qt.Key_Space: + # toogle the current items check state + if ( + index_flags & QtCore.Qt.ItemIsUserCheckable + and index_flags & QtCore.Qt.ItemIsTristate + ): + new_state = QtCore.Qt.CheckState((int(state) + 1) % 3) + + elif index_flags & QtCore.Qt.ItemIsUserCheckable: + if state != QtCore.Qt.Checked: + new_state = QtCore.Qt.Checked + else: + new_state = QtCore.Qt.Unchecked + + if new_state is not None: + model.setData(current_index, new_state, QtCore.Qt.CheckStateRole) + self.view().update(current_index) + self.value_changed.emit() + return True + + def eventFilter(self, obj, event): + """Reimplemented.""" + result = self._event_popup_shown(obj, event) + if result is not None: + return result + + return super(MultiSelectionComboBox, self).eventFilter(obj, event) + + def addItem(self, *args, **kwargs): + idx = self.count() + super(MultiSelectionComboBox, self).addItem(*args, **kwargs) + self.model().item(idx).setCheckable(True) + + def paintEvent(self, event): + """Reimplemented.""" + painter = QtWidgets.QStylePainter(self) + option = QtWidgets.QStyleOptionComboBox() + self.initStyleOption(option) + painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option) + + # draw the icon and text + items = self.checked_items_text() + if not items: + return + + text_rect = self.style().subControlRect( + QtWidgets.QStyle.CC_ComboBox, + option, + QtWidgets.QStyle.SC_ComboBoxEditField + ) + text = ", ".join(items) + new_text = self.fontMetrics().elidedText( + text, QtCore.Qt.ElideRight, text_rect.width() + ) + painter.drawText( + text_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, + new_text + ) + + def setItemCheckState(self, index, state): + self.setItemData(index, state, QtCore.Qt.CheckStateRole) + + def set_value(self, values): + for idx in range(self.count()): + value = self.itemData(idx, role=QtCore.Qt.UserRole) + if value in values: + check_state = QtCore.Qt.Checked + else: + check_state = QtCore.Qt.Unchecked + self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole) + + def value(self): + items = list() + for idx in range(self.count()): + state = self.itemData(idx, role=QtCore.Qt.CheckStateRole) + if state == QtCore.Qt.Checked: + items.append( + self.itemData(idx, role=QtCore.Qt.UserRole) + ) + return items + + def checked_items_text(self): + items = list() + for idx in range(self.count()): + state = self.itemData(idx, role=QtCore.Qt.CheckStateRole) + if state == QtCore.Qt.Checked: + items.append(self.itemText(idx)) + return items + + def wheelEvent(self, event): + event.ignore() + + def keyPressEvent(self, event): + if ( + event.key() == QtCore.Qt.Key_Down + and event.modifiers() & QtCore.Qt.AltModifier + ): + return self.showPopup() + + if event.key() in self.ignored_keys: + return event.ignore() + + return super(MultiSelectionComboBox, self).keyPressEvent(event) From 948212c658475ab3d9a3d61e48cc85aac3c83a63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:57:34 +0200 Subject: [PATCH 055/311] implemented ToolsDelegate --- .../project_manager/delegates.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 22b8427dd1..67e462bf4a 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -1,5 +1,7 @@ from Qt import QtWidgets, QtCore +from .multiselection_combobox import MultiSelectionComboBox + class NumberDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, minimum, maximum, decimals, *args, **kwargs): @@ -48,5 +50,20 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): task_type_defs = self._project_doc_cache.project_doc["config"]["tasks"] editor.addItems(list(task_type_defs.keys())) + return editor + + +class ToolsDelegate(QtWidgets.QStyledItemDelegate): + def __init__(self, tools_cache, *args, **kwargs): + self._tools_cache = tools_cache + super(ToolsDelegate, self).__init__(*args, **kwargs) + + def createEditor(self, parent, option, index): + editor = MultiSelectionComboBox(parent) + if not self._tools_cache.tools_data: + return editor + + for key, label in self._tools_cache.tools_data: + editor.addItem(label, key) return editor From e802fd240c5a72c7c4dd391afa6d2c65df84bd8a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:58:36 +0200 Subject: [PATCH 056/311] changed variable name --- .../tools/project_manager/project_manager/view.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 88836aa969..2543d061ea 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -23,7 +23,7 @@ class TypeDef: pass -class ProjectHelper: +class ProjectDocCache: def __init__(self, dbcon): self.dbcon = dbcon self.project_doc = None @@ -73,7 +73,7 @@ class HierarchyView(QtWidgets.QTreeView): super(HierarchyView, self).__init__(*args, **kwargs) self._source_model = source_model - project_helper = ProjectHelper(dbcon) + project_doc_cache = ProjectDocCache(dbcon) main_delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(main_delegate) @@ -101,14 +101,17 @@ class HierarchyView(QtWidgets.QTreeView): source_model.index_moved.connect(self._on_rows_moved) - self._project_helper = project_helper + self._project_doc_cache = project_doc_cache self._delegate = main_delegate self._column_delegates = column_delegates self._column_key_to_index = column_key_to_index def set_project(self, project_name): + # Trigger helpers first + self._project_doc_cache.set_project(project_name) + + # Trigger update of model after all data for delegates are filled self._source_model.set_project(project_name) - self._project_helper.set_project(project_name) def _on_rows_moved(self, index): parent_index = index.parent() From 09a92474f8fd1cc54c3d24cfc4b3f24f8710b302 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 14:58:56 +0200 Subject: [PATCH 057/311] added tools delegate --- .../project_manager/project_manager/view.py | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 2543d061ea..82275a4557 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -3,9 +3,12 @@ from Qt import QtWidgets, QtCore from .delegates import ( NumberDelegate, StringDelegate, - TypeDelegate + TypeDelegate, + ToolsDelegate ) +from openpype.lib import ApplicationManager + class StringDef: def __init__(self, regex=None): @@ -23,6 +26,10 @@ class TypeDef: pass +class ToolsDef: + pass + + class ProjectDocCache: def __init__(self, dbcon): self.dbcon = dbcon @@ -39,6 +46,20 @@ class ProjectDocCache: ) +class ToolsCache: + def __init__(self): + self.tools_data = [] + + def refresh(self): + app_manager = ApplicationManager() + tools_data = [] + for tool_name, tool in app_manager.tools.items(): + tools_data.append( + (tool_name, tool.label) + ) + self.tools_data = tools_data + + class HierarchyView(QtWidgets.QTreeView): """A tree view that deselects on clicking on an empty area in the view""" column_delegate_defs = { @@ -54,7 +75,7 @@ class HierarchyView(QtWidgets.QTreeView): "clipIn": NumberDef(1), "clipOut": NumberDef(1), "pixelAspect": NumberDef(0, decimals=2), - # "tools_env": NumberDef(0) + "tools_env": ToolsDef() } persistent_columns = [ "frameStart", @@ -66,7 +87,8 @@ class HierarchyView(QtWidgets.QTreeView): "handleEnd", "clipIn", "clipOut", - "pixelAspect" + "pixelAspect", + "tools_env" ] def __init__(self, dbcon, source_model, *args, **kwargs): @@ -74,6 +96,7 @@ class HierarchyView(QtWidgets.QTreeView): self._source_model = source_model project_doc_cache = ProjectDocCache(dbcon) + tools_cache = ToolsCache() main_delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(main_delegate) @@ -85,14 +108,19 @@ class HierarchyView(QtWidgets.QTreeView): for key, item_type in self.column_delegate_defs.items(): if isinstance(item_type, StringDef): delegate = StringDelegate() + elif isinstance(item_type, NumberDef): delegate = NumberDelegate( item_type.minimum, item_type.maximum, item_type.decimals ) + elif isinstance(item_type, TypeDef): - delegate = TypeDelegate(project_helper) + delegate = TypeDelegate(project_doc_cache) + + elif isinstance(item_type, ToolsDef): + delegate = ToolsDelegate(tools_cache) column = self._source_model.columns.index(key) self.setItemDelegateForColumn(column, delegate) @@ -102,6 +130,8 @@ class HierarchyView(QtWidgets.QTreeView): source_model.index_moved.connect(self._on_rows_moved) self._project_doc_cache = project_doc_cache + self._tools_cache = tools_cache + self._delegate = main_delegate self._column_delegates = column_delegates self._column_key_to_index = column_key_to_index @@ -109,6 +139,7 @@ class HierarchyView(QtWidgets.QTreeView): def set_project(self, project_name): # Trigger helpers first self._project_doc_cache.set_project(project_name) + self._tools_cache.refresh() # Trigger update of model after all data for delegates are filled self._source_model.set_project(project_name) From 4d737e47d00fd4e4a1ef68944d2b57384ad18d5d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 15:19:13 +0200 Subject: [PATCH 058/311] separated global data getter --- .../project_manager/project_manager/model.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index a6446b22bb..7907966951 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -720,6 +720,8 @@ class BaseItem: _name_icon = None _is_duplicated = False + _None = object() + def __init__(self, data=None): self._id = uuid4() self._children = list() @@ -750,18 +752,19 @@ class BaseItem: self._children.pop(idx) self._children.insert(row, item) - def data(self, key, role): + def _global_data(self, role): if role == IDENTIFIER_ROLE: return self._id if role == DUPLICATED_ROLE: return self._is_duplicated - if role == QtCore.Qt.ToolTipRole: - if self._is_duplicated: - return "Asset with name \"{}\" already exists.".format( - self._data["name"] - ) + return self._None + + def data(self, key, role): + value = self._global_data(role) + if value is not self._None: + return value if key not in self.columns: return None @@ -1009,6 +1012,13 @@ class AssetItem(BaseItem): cls._name_icon = qtawesome.icon("fa.folder", color="#333333") return cls._name_icon + def _global_data(self, role): + if role == QtCore.Qt.ToolTipRole and self._is_duplicated: + return "Asset with name \"{}\" already exists.".format( + self._data["name"] + ) + return super(AssetItem, self)._global_data(role) + class TaskItem(BaseItem): columns = { @@ -1028,3 +1038,10 @@ class TaskItem(BaseItem): def add_child(self, item, row=None): raise AssertionError("BUG: Can't add children to Task") + + def _global_data(self, role): + if role == QtCore.Qt.ToolTipRole and self._is_duplicated: + return "Duplicated Task name \"{}\".".format( + self._data["name"] + ) + return super(TaskItem, self)._global_data(role) From 1d0058dc6145deef13e2ba552093911dee1ef4b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 16:09:14 +0200 Subject: [PATCH 059/311] fix asset name duplications --- openpype/tools/project_manager/project_manager/model.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 7907966951..e6344fc5dc 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -279,7 +279,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): "type": "asset" } new_child = AssetItem(data) - self._asset_items_by_name[name].append(new_child) result = self.add_item(new_child, parent, new_row) @@ -322,6 +321,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): parent.add_child(item, row) + if isinstance(item, AssetItem): + name = item.data("name", QtCore.Qt.DisplayRole) + self._asset_items_by_name[name].append(item) + if item.id not in self._items_by_id: self._items_by_id[item.id] = item @@ -387,6 +390,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): return prev_name = asset_item.data("name", QtCore.Qt.DisplayRole) + if prev_name == new_name: + return + self._asset_items_by_name[prev_name].remove(asset_item) self._validate_asset_duplicity(prev_name) From b5bd9e57131271380e63243f852a1d3d51ce9494 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 16:09:32 +0200 Subject: [PATCH 060/311] tools_env data can be changed --- .../project_manager/project_manager/delegates.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 67e462bf4a..3d8451bde6 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -18,7 +18,7 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): editor.setMinimum(self.minimum) editor.setMaximum(self.maximum) - value = index.data(QtCore.Qt.DisplayRole) + value = index.data(QtCore.Qt.EditRole) if value is not None: editor.setValue(value) return editor @@ -31,7 +31,7 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): class StringDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): editor = QtWidgets.QLineEdit(parent) - value = index.data(QtCore.Qt.DisplayRole) + value = index.data(QtCore.Qt.EditRole) if value is not None: editor.setText(str(value)) return editor @@ -67,3 +67,10 @@ class ToolsDelegate(QtWidgets.QStyledItemDelegate): editor.addItem(label, key) return editor + + def setEditorData(self, editor, index): + value = index.data(QtCore.Qt.EditRole) + editor.set_value(value) + + def setModelData(self, editor, model, index): + model.setData(index, editor.value(), QtCore.Qt.EditRole) From 9813597feb4ecf76e4405ea9e5ff8edba07eda84 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 20:45:00 +0200 Subject: [PATCH 061/311] added base of save mechanism --- .../tools/project_manager/project_manager/model.py | 3 +++ .../project_manager/project_manager/window.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index e6344fc5dc..db016b38c9 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -717,6 +717,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._reset_root_item() self.endResetModel() + def save(self): + print("Saving (They said)") + class BaseItem: columns = [] diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 790df4557e..0be69c29c8 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -38,6 +38,7 @@ class Window(QtWidgets.QWidget): hierarchy_view = HierarchyView(dbcon, hierarchy_model, self) hierarchy_view.setModel(hierarchy_model) + _selection_model = HierarchySelectionModel() _selection_model.setModel(hierarchy_view.model()) hierarchy_view.setSelectionModel(_selection_model) @@ -47,13 +48,23 @@ class Window(QtWidgets.QWidget): header.setSectionResizeMode( header.logicalIndex(0), QtWidgets.QHeaderView.Stretch ) + buttons_widget = QtWidgets.QWidget(self) + + save_btn = QtWidgets.QPushButton("Save", buttons_widget) + + buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addStretch(1) + buttons_layout.addWidget(save_btn) main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(project_widget) main_layout.addWidget(hierarchy_view) + main_layout.addWidget(buttons_widget) refresh_projects_btn.clicked.connect(self._on_project_refresh) project_combobox.currentIndexChanged.connect(self._on_project_change) + save_btn.clicked.connect(self._on_save_click) self.project_model = project_model self.project_combobox = project_combobox @@ -90,3 +101,6 @@ class Window(QtWidgets.QWidget): def _on_project_refresh(self): self.refresh_projects() + + def _on_save_click(self): + self.hierarchy_model.save() From ce95ee27de1262d527e67c645af9f954932112c4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 20:49:01 +0200 Subject: [PATCH 062/311] implemented few necessary properties --- .../project_manager/project_manager/model.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index db016b38c9..d7abca47f0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -924,9 +924,22 @@ class ProjectItem(BaseItem): } def __init__(self, project_doc): + self._mongo_id = project_doc["_id"] + data = self.data_from_doc(project_doc) super(ProjectItem, self).__init__(data) + @property + def project_id(self): + return self._mongo_id + + @property + def asset_id(self): + return None + + @property + def name(self): + return self._data["name"] @classmethod def data_from_doc(cls, project_doc): data = { @@ -997,9 +1010,25 @@ class AssetItem(BaseItem): } def __init__(self, asset_doc): + self.mongo_id = asset_doc.get("_id") + self._project_id = None + data = self.data_from_doc(asset_doc) super(AssetItem, self).__init__(data) + @property + def project_id(self): + if self._project_id is None: + self._project_id = self.parent().project_id + return self._project_id + + @property + def asset_id(self): + return self.mongo_id + + @property + def name(self): + return self._data["name"] @classmethod def data_from_doc(cls, asset_doc): data = { From c2e6c974b979f6c5d3992286a78a1a1044c975d2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 20:51:13 +0200 Subject: [PATCH 063/311] task can be converted to data --- openpype/tools/project_manager/project_manager/model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index d7abca47f0..b346dbba07 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1083,3 +1083,10 @@ class TaskItem(BaseItem): self._data["name"] ) return super(TaskItem, self)._global_data(role) + + def to_doc_data(self): + data = copy.deepcopy(self._data) + name = data.pop("name") + return { + name: data + } From df75e05e7d4dd835dbca2072d756f02afe4c7328 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 20:59:41 +0200 Subject: [PATCH 064/311] added conversion of asset item to doc data --- .../project_manager/project_manager/model.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b346dbba07..5ed09bc677 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1029,6 +1029,35 @@ class AssetItem(BaseItem): @property def name(self): return self._data["name"] + + def to_doc(self): + tasks = {} + for item in self.children(): + if isinstance(item, TaskItem): + tasks.update(item.to_doc_data()) + + doc_data = { + "parents": self.parent().asset_parents(), + "visualParent": self.parent().asset_id, + "tasks": tasks + } + schema_name = ( + self._origin_asset_doc.get("schema") or "openpype:asset-3.0" + ) + + doc = { + "name": self._data["name"], + "type": self._data["type"], + "schema": schema_name, + "data": doc_data, + "parent": self.project_id + } + for key, value in self._data.items(): + if key in doc: + continue + doc_data[key] = value + + return doc @classmethod def data_from_doc(cls, asset_doc): data = { From 7d56929ffff8655a673dc35bd6332b13e6b616a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 21:00:05 +0200 Subject: [PATCH 065/311] it is possible to get update changes for mongo from asset item --- .../project_manager/project_manager/model.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5ed09bc677..9d26a1c96e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1058,6 +1058,42 @@ class AssetItem(BaseItem): doc_data[key] = value return doc + + def update_data(self): + if not self.mongo_id: + return {} + + document = self.to_doc() + + changes = {} + + for key, value in document.items(): + if key in ("data", "_id"): + continue + + if ( + key in self._origin_asset_doc + and self._origin_asset_doc[key] == value + ): + continue + + changes[key] = value + + if "data" not in self._origin_asset_doc: + changes["data"] = document["data"] + else: + origin_data = self._origin_asset_doc["data"] + + for key, value in document["data"].items(): + if key in origin_data and origin_data[key] == value: + continue + _key = "data.{}".format(key) + changes[_key] = value + + if changes: + return {"$set": changes} + return {} + @classmethod def data_from_doc(cls, asset_doc): data = { From feb64e776e4fff1cdf6ccc879dc94763b2a17b13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 21:03:28 +0200 Subject: [PATCH 066/311] added child_parents method --- .../tools/project_manager/project_manager/model.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9d26a1c96e..cc52e67c47 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -940,6 +940,10 @@ class ProjectItem(BaseItem): @property def name(self): return self._data["name"] + + def child_parents(self): + return [] + @classmethod def data_from_doc(cls, project_doc): data = { @@ -1030,6 +1034,11 @@ class AssetItem(BaseItem): def name(self): return self._data["name"] + def child_parents(self): + parents = self.parent().child_parents() + parents.append(self.name) + return parents + def to_doc(self): tasks = {} for item in self.children(): @@ -1037,7 +1046,7 @@ class AssetItem(BaseItem): tasks.update(item.to_doc_data()) doc_data = { - "parents": self.parent().asset_parents(), + "parents": self.parent().child_parents(), "visualParent": self.parent().asset_id, "tasks": tasks } From e60aa9e80de3d03e69ead12e3c9f9a63b56f5f45 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 21:08:01 +0200 Subject: [PATCH 067/311] modified how AssetItem is created --- .../project_manager/project_manager/model.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index cc52e67c47..9690f52ed5 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -274,11 +274,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): parent = item.parent() new_row = item.row() + 1 - data = { - "name": name, - "type": "asset" - } - new_child = AssetItem(data) + new_child = AssetItem() result = self.add_item(new_child, parent, new_row) @@ -1013,10 +1009,14 @@ class AssetItem(BaseItem): "data.tools_env": 1 } - def __init__(self, asset_doc): + def __init__(self, asset_doc=None): + if not asset_doc: + asset_doc = {} self.mongo_id = asset_doc.get("_id") self._project_id = None + self._origin_asset_doc = copy.deepcopy(asset_doc) + data = self.data_from_doc(asset_doc) super(AssetItem, self).__init__(data) @@ -1106,9 +1106,14 @@ class AssetItem(BaseItem): @classmethod def data_from_doc(cls, asset_doc): data = { - "name": asset_doc["name"], - "type": asset_doc["type"] + "name": None, + "type": "asset" } + if asset_doc: + for key in data.keys(): + if key in asset_doc: + data[key] = asset_doc[key] + doc_data = asset_doc.get("data") or {} for key in cls.columns: if key in data: From dc76e91f41c3b87c96abc950e059d6864777d349 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 21:08:32 +0200 Subject: [PATCH 068/311] add id to doc data --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9690f52ed5..9eb7a7656c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1061,6 +1061,9 @@ class AssetItem(BaseItem): "data": doc_data, "parent": self.project_id } + if self.mongo_id: + doc["_id"] = self.mongo_id + for key, value in self._data.items(): if key in doc: continue From bc532fa6cb330907d1ee578c2efb306e157090e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 21:10:15 +0200 Subject: [PATCH 069/311] AssetItem requires at least empty dictionary --- .../tools/project_manager/project_manager/model.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9eb7a7656c..dd9bfdc561 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -266,15 +266,19 @@ class HierarchyModel(QtCore.QAbstractItemModel): item = self.items_by_id[item_id] new_row = None + name = None + asset_data = {} if isinstance(item, (RootItem, ProjectItem)): name = "ep" parent = item else: - name = source_index.data(QtCore.Qt.DisplayRole) parent = item.parent() new_row = item.row() + 1 - new_child = AssetItem() + if name: + asset_data["name"] = name + + new_child = AssetItem(asset_data) result = self.add_item(new_child, parent, new_row) @@ -1009,7 +1013,7 @@ class AssetItem(BaseItem): "data.tools_env": 1 } - def __init__(self, asset_doc=None): + def __init__(self, asset_doc): if not asset_doc: asset_doc = {} self.mongo_id = asset_doc.get("_id") From 1c1c11625aef236fe6250b3c1123159fecb43e15 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 21:11:09 +0200 Subject: [PATCH 070/311] added schema to query projection --- openpype/tools/project_manager/project_manager/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index dd9bfdc561..12ad49086b 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -997,6 +997,7 @@ class AssetItem(BaseItem): "_id": 1, "data.tasks": 1, "data.visualParent": 1, + "schema": 1, "name": 1, "type": 1, From e124d084aa2795ac9e13c4feba2654b127d4c7c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 May 2021 21:11:20 +0200 Subject: [PATCH 071/311] implemented save method --- .../project_manager/project_manager/model.py | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 12ad49086b..83a8b2d7f8 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -7,7 +7,7 @@ from .constants import ( IDENTIFIER_ROLE, DUPLICATED_ROLE ) - +from pymongo import UpdateOne from avalon.vendor import qtawesome from Qt import QtCore, QtGui @@ -718,7 +718,51 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endResetModel() def save(self): - print("Saving (They said)") + project_item = None + for _project_item in self._root_item.children(): + project_item = _project_item + + if not project_item: + return + + project_name = project_item.name + project_col = self.dbcon.database[project_name] + + to_process = Queue() + to_process.put(project_item) + + update_list = [] + while not to_process.empty(): + parent = to_process.get() + insert_list = [] + for item in parent.children(): + if not isinstance(item, AssetItem): + continue + + to_process.put(item) + + if item.asset_id is None: + insert_list.append(item) + continue + + update_data = item.update_data() + if update_data: + update_list.append(UpdateOne( + {"_id": item.asset_id}, + update_data + )) + + if insert_list: + new_docs = [] + for item in insert_list: + new_docs.append(item.to_doc()) + + result = project_col.insert_many(new_docs) + for idx, mongo_id in enumerate(result.inserted_ids): + insert_list[idx].mongo_id = mongo_id + + if update_list: + project_col.bulk_write(update_list) class BaseItem: From 5eb439b3b32653b8f24bdb667e100b8cdf5285e4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 May 2021 14:05:11 +0200 Subject: [PATCH 072/311] changed imports and main definition location --- openpype/tools/project_manager/__init__.py | 6 ++- openpype/tools/project_manager/__main__.py | 15 +------ .../project_manager/__init__.py | 40 +++++++++++++------ 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/openpype/tools/project_manager/__init__.py b/openpype/tools/project_manager/__init__.py index 7d8f8bf432..880fc253cf 100644 --- a/openpype/tools/project_manager/__init__.py +++ b/openpype/tools/project_manager/__init__.py @@ -1,6 +1,10 @@ -from .project_manager import Window +from .project_manager import ( + Window, + main +) __all__ = ( "Window", + "main" ) diff --git a/openpype/tools/project_manager/__main__.py b/openpype/tools/project_manager/__main__.py index 0855a0fc71..2e57af5f11 100644 --- a/openpype/tools/project_manager/__main__.py +++ b/openpype/tools/project_manager/__main__.py @@ -1,17 +1,4 @@ -import sys - -from project_manager import Window - -from Qt import QtWidgets - - -def main(): - app = QtWidgets.QApplication([]) - - window = Window() - window.show() - - sys.exit(app.exec_()) +from project_manager import main if __name__ == "__main__": diff --git a/openpype/tools/project_manager/project_manager/__init__.py b/openpype/tools/project_manager/project_manager/__init__.py index a652e950c4..dccc46f771 100644 --- a/openpype/tools/project_manager/project_manager/__init__.py +++ b/openpype/tools/project_manager/project_manager/__init__.py @@ -1,3 +1,23 @@ +__all__ = ( + "IDENTIFIER_ROLE", + + "HierarchyView", + + "ProjectModel", + + "HierarchyModel", + "HierarchySelectionModel", + "BaseItem", + "RootItem", + "ProjectItem", + "AssetItem", + "TaskItem", + + "Window", + "main" +) + + from .constants import ( IDENTIFIER_ROLE ) @@ -15,20 +35,14 @@ from .model import ( ) from .window import Window -__all__ = ( - "IDENTIFIER_ROLE", - "HierarchyView", +def main(): + import sys + from Qt import QtWidgets - "ProjectModel", + app = QtWidgets.QApplication([]) - "HierarchyModel", - "HierarchySelectionModel", - "BaseItem", - "RootItem", - "ProjectItem", - "AssetItem", - "TaskItem", + window = Window() + window.show() - "Window" -) + sys.exit(app.exec_()) From 70ed7abeb5d4201965570bbc88ee53e68587c4d2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 May 2021 15:13:50 +0200 Subject: [PATCH 073/311] added projectmanager to openpype cli commands --- openpype/cli.py | 5 +++++ openpype/pype_commands.py | 6 ++++++ tools/run_project_manager.ps1 | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 tools/run_project_manager.ps1 diff --git a/openpype/cli.py b/openpype/cli.py index 9c49825721..df38c74a21 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -224,6 +224,11 @@ def launch(app, project, asset, task, PypeCommands().run_application(app, project, asset, task, tools, arguments) +@main.command(context_settings={"ignore_unknown_options": True}) +def projectmanager(): + PypeCommands().launch_project_manager() + + @main.command( context_settings=dict( ignore_unknown_options=True, diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 981cca82dc..326ca8349a 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -110,6 +110,12 @@ class PypeCommands: with open(output_json_path, "w") as file_stream: json.dump(env, file_stream, indent=4) + @staticmethod + def launch_project_manager(): + from openpype.tools import project_manager + + project_manager.main() + def texture_copy(self, project, asset, path): pass diff --git a/tools/run_project_manager.ps1 b/tools/run_project_manager.ps1 new file mode 100644 index 0000000000..78dce19df1 --- /dev/null +++ b/tools/run_project_manager.ps1 @@ -0,0 +1,18 @@ +<# +.SYNOPSIS + Helper script OpenPype Tray. + +.DESCRIPTION + + +.EXAMPLE + +PS> .\run_tray.ps1 + +#> +$current_dir = Get-Location +$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$openpype_root = (Get-Item $script_dir).parent.FullName +Set-Location -Path $openpype_root +& poetry run python "$($openpype_root)\start.py" projectmanager +Set-Location -Path $current_dir From f561e1a864f04d5d1389403fd0a0668d61c3b8b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 May 2021 15:14:05 +0200 Subject: [PATCH 074/311] added validation of number values in number delegate --- .../tools/project_manager/project_manager/delegates.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 3d8451bde6..88b366a7d0 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -20,7 +20,14 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): value = index.data(QtCore.Qt.EditRole) if value is not None: - editor.setValue(value) + try: + if isinstance(value, str): + value = float(value) + editor.setValue(value) + + except Exception: + print("Couldn't set invalid value \"{}\"".format(str(value))) + return editor # def updateEditorGeometry(self, editor, options, index): From 0a273b23049c7bef207c4b539c9ef4da1e7a3a41 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:03:39 +0200 Subject: [PATCH 075/311] added _add_task to hierarchy view --- .../tools/project_manager/project_manager/view.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 82275a4557..30a9ec6eb1 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -257,11 +257,16 @@ class HierarchyView(QtWidgets.QTreeView): self._source_model.remove_index(index) def _on_ctrl_shift_enter_pressed(self): - index = self.currentIndex() - if not index.isValid(): + self._add_task() + + def _add_task(self, parent_index=None): + if parent_index is None: + parent_index = self.currentIndex() + + if not parent_index.isValid(): return - new_index = self._source_model.add_new_task(index) + new_index = self._source_model.add_new_task(parent_index) if new_index is None: return From 96d8079a54431a83c9c4063fc7f0c7055debb01f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:03:57 +0200 Subject: [PATCH 076/311] type is edited as first thing on task creation --- openpype/tools/project_manager/project_manager/view.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 30a9ec6eb1..66051fe030 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -274,10 +274,14 @@ class HierarchyView(QtWidgets.QTreeView): self.setState(HierarchyView.NoState) QtWidgets.QApplication.processEvents() + # TODO change hardcoded column index to coded + task_type_index = self._source_model.index( + new_index.row(), 1, new_index.parent() + ) # Change current index - self.setCurrentIndex(new_index) + self.setCurrentIndex(task_type_index) # Start editing - self.edit(new_index) + self.edit(task_type_index) def _on_shift_enter_pressed(self): index = self.currentIndex() From 1e79141b24d946fc9e0e42efde0275b0eb903777 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:09:51 +0200 Subject: [PATCH 077/311] added type to persisten columns --- openpype/tools/project_manager/project_manager/view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 66051fe030..b2ca27e77c 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -78,6 +78,7 @@ class HierarchyView(QtWidgets.QTreeView): "tools_env": ToolsDef() } persistent_columns = [ + "type", "frameStart", "frameEnd", "fps", From acb7ea347a37f9a4510c0ad3160bb22497672121 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:10:29 +0200 Subject: [PATCH 078/311] TaskItem does not have default values prefilled --- .../project_manager/project_manager/model.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 83a8b2d7f8..b70426640b 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -298,8 +298,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not isinstance(parent, AssetItem): return None - data = {"name": "task"} - new_child = TaskItem(data) + new_child = TaskItem() return self.add_item(new_child, parent) def add_items(self, items, parent=None, start_row=None): @@ -1199,6 +1198,11 @@ class TaskItem(BaseItem): "type" } + def __init__(self, data=None): + if data is None: + data = {} + super(TaskItem, self).__init__(data) + @classmethod def name_icon(cls): if cls._name_icon is None: @@ -1217,7 +1221,16 @@ class TaskItem(BaseItem): def to_doc_data(self): data = copy.deepcopy(self._data) - name = data.pop("name") + data.pop("name") + name = self.data("name", QtCore.Qt.DisplayRole) return { name: data } + + def data(self, key, role): + if ( + role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) + and key == "name" + ): + return self._data[key] or self._data["type"] or "< Select Type >" + return super(TaskItem, self).data(key, role) From 048f856f6317496b23aec92c1da4de91e72ac068 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:12:13 +0200 Subject: [PATCH 079/311] do not set keys that are not in editable keys --- openpype/tools/project_manager/project_manager/model.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b70426640b..3f400fc8ee 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -844,12 +844,11 @@ class BaseItem: self._is_duplicated = value return True - if key not in self.columns: - return False - if role == QtCore.Qt.EditRole: - self._data[key] = value + if key not in self.editable_columns: + return False + self._data[key] = value # must return true if successful return True From 35e7cb7b46e18c41ac0543e1c3f749906fd1aedd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:14:02 +0200 Subject: [PATCH 080/311] defined new role defying if asset item is modifiable --- openpype/tools/project_manager/project_manager/constants.py | 1 + openpype/tools/project_manager/project_manager/model.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 61d1944979..49ae2f7883 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -3,3 +3,4 @@ from Qt import QtCore IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 DUPLICATED_ROLE = QtCore.Qt.UserRole + 2 +HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3 diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 3f400fc8ee..c397906e71 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -5,7 +5,8 @@ from uuid import uuid4 from .constants import ( IDENTIFIER_ROLE, - DUPLICATED_ROLE + DUPLICATED_ROLE, + HIERARCHY_CHANGE_ABLE_ROLE ) from pymongo import UpdateOne from avalon.vendor import qtawesome From c9202e2c029044b0df60d4d7a238c5164ecb47a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:14:48 +0200 Subject: [PATCH 081/311] asset item can handle new role defying if is editable --- .../project_manager/project_manager/model.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c397906e71..6744d51113 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1062,6 +1062,7 @@ class AssetItem(BaseItem): asset_doc = {} self.mongo_id = asset_doc.get("_id") self._project_id = None + self._hierarchy_changes_enabled = True self._origin_asset_doc = copy.deepcopy(asset_doc) @@ -1181,12 +1182,38 @@ class AssetItem(BaseItem): return cls._name_icon def _global_data(self, role): + if role == HIERARCHY_CHANGE_ABLE_ROLE: + return self._hierarchy_changes_enabled + if role == QtCore.Qt.ToolTipRole and self._is_duplicated: return "Asset with name \"{}\" already exists.".format( self._data["name"] ) return super(AssetItem, self)._global_data(role) + def setData(self, key, value, role): + if role == HIERARCHY_CHANGE_ABLE_ROLE: + if self._hierarchy_changes_enabled == value: + return False + self._hierarchy_changes_enabled = value + return True + + if ( + role == QtCore.Qt.EditRole + and key == "name" + and not self._hierarchy_changes_enabled + ): + return False + return super(AssetItem, self).setData(key, value, role) + + def flags(self, key): + if key == "name": + flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + if self._hierarchy_changes_enabled: + flags |= QtCore.Qt.ItemIsEditable + return flags + return super(AssetItem, self).flags(key) + class TaskItem(BaseItem): columns = { From 9a0203bd9e45ca6820030bb805a49186231c7e9d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 18:15:15 +0200 Subject: [PATCH 082/311] store information about loaded asset documents form database if can be modified --- .../project_manager/project_manager/model.py | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 6744d51113..90a45b9f48 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -133,6 +133,36 @@ class HierarchyModel(QtCore.QAbstractItemModel): asset_doc["_id"]: asset_doc for asset_doc in asset_docs } + + # Prepare booleans if asset item can be modified (name or hierarchy) + # - the same must be applied to all it's parents + asset_ids = list(asset_docs_by_id.keys()) + result = [] + if asset_ids: + result = self.dbcon.database[project_name].aggregate([ + { + "$match": { + "type": "subset", + "parent": {"$in": asset_ids} + } + }, + { + "$group": { + "_id": "$parent", + "count": {"$sum": 1} + } + } + ]) + + asset_modifiable = { + asset_id: True + for asset_id in asset_docs_by_id.keys() + } + for item in result: + asset_id = item["_id"] + count = item["count"] + asset_modifiable[asset_id] = count < 1 + asset_docs_by_parent_id = collections.defaultdict(list) for asset_doc in asset_docs_by_id.values(): parent_id = asset_doc["data"]["visualParent"] @@ -142,7 +172,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): appending_queue.put((None, project_item)) asset_items_by_id = {} - + non_modifiable_items = set() while not appending_queue.empty(): parent_id, parent_item = appending_queue.get() if parent_id not in asset_docs_by_parent_id: @@ -157,12 +187,35 @@ class HierarchyModel(QtCore.QAbstractItemModel): # Store item by id for task processing asset_id = asset_doc["_id"] + if not asset_modifiable[asset_id]: + non_modifiable_items.add(new_item.id) + asset_items_by_id[asset_id] = new_item # Add item to appending queue appending_queue.put((asset_id, new_item)) self.add_items(new_items, parent_item) + # Handle Asset's that are not modifiable + # - pass the information to all it's parents + non_modifiable_queue = Queue() + for item_id in non_modifiable_items: + non_modifiable_queue.put(item_id) + + while not non_modifiable_queue.empty(): + item_id = non_modifiable_queue.get() + item = self._items_by_id[item_id] + item.setData(None, False, HIERARCHY_CHANGE_ABLE_ROLE) + + parent = item.parent() + if ( + isinstance(parent, AssetItem) + and parent.id not in non_modifiable_items + ): + non_modifiable_items.add(parent.id) + non_modifiable_queue.put(parent.id) + + # Add task items for asset_id, asset_item in asset_items_by_id.items(): asset_doc = asset_docs_by_id[asset_id] asset_tasks = asset_doc["data"]["tasks"] From 6b940ae6bff1a1036deb4efef9a28ed1a8a6d313 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:24:23 +0200 Subject: [PATCH 083/311] added few checks --- openpype/tools/project_manager/project_manager/view.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b2ca27e77c..dee694a59c 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -253,8 +253,9 @@ class HierarchyView(QtWidgets.QTreeView): else: event.accept() - def _delete_item(self): - index = self.currentIndex() + def _delete_item(self, index=None): + if index is None: + index = self.currentIndex() self._source_model.remove_index(index) def _on_ctrl_shift_enter_pressed(self): @@ -294,6 +295,8 @@ class HierarchyView(QtWidgets.QTreeView): QtWidgets.QApplication.processEvents() new_index = self._source_model.add_new_asset(index) + if new_index is None: + return # Change current index self.setCurrentIndex(new_index) From 41f21bc0669394b22c2d1105ec7f2e546e8707f1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:24:33 +0200 Subject: [PATCH 084/311] added REMOVED_ROLE --- openpype/tools/project_manager/project_manager/constants.py | 1 + openpype/tools/project_manager/project_manager/model.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 49ae2f7883..7f9a859ac1 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -4,3 +4,4 @@ from Qt import QtCore IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 DUPLICATED_ROLE = QtCore.Qt.UserRole + 2 HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3 +REMOVED_ROLE = QtCore.Qt.UserRole + 4 diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 90a45b9f48..c06be9e219 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -6,7 +6,8 @@ from uuid import uuid4 from .constants import ( IDENTIFIER_ROLE, DUPLICATED_ROLE, - HIERARCHY_CHANGE_ABLE_ROLE + HIERARCHY_CHANGE_ABLE_ROLE, + REMOVED_ROLE ) from pymongo import UpdateOne from avalon.vendor import qtawesome From b8775392f190c4958a06ca6eec7a7da80f7d9fb5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:26:13 +0200 Subject: [PATCH 085/311] minor changes and fixes --- .../tools/project_manager/project_manager/model.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c06be9e219..750cb52661 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -91,6 +91,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): def __init__(self, dbcon, parent=None): super(HierarchyModel, self).__init__(parent) + # TODO Reset them on project change self._current_project = None self._root_item = None self._items_by_id = {} @@ -275,7 +276,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): if section < self.columnCount(): return self.column_labels[section] - super(HierarchyModel, self).headerData(section, orientation, role) + return super(HierarchyModel, self).headerData( + section, orientation, role + ) def flags(self, index): item = index.internalPointer() @@ -283,7 +286,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): key = self.columns[column] return item.flags(key) - def parent(self, index): + def parent(self, index=None): + if not index.isValid(): + return QtCore.QModelIndex() + item = index.internalPointer() parent_item = item.parent() From 72508c738f151a251f5b9816c8d1dd439d045675 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:26:43 +0200 Subject: [PATCH 086/311] added validations on add item which may not be successfull --- openpype/tools/project_manager/project_manager/model.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 750cb52661..05e7b1831b 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -342,8 +342,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): new_child = AssetItem(asset_data) result = self.add_item(new_child, parent, new_row) - - self._validate_asset_duplicity(name) + if result is not None: + self._validate_asset_duplicity(name) return result @@ -398,7 +398,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): return indexes def add_item(self, item, parent=None, row=None): - return self.add_items([item], parent, row)[0] + result = self.add_items([item], parent, row) + if result: + return result[0] + return None def remove_index(self, index): if not index.isValid(): From 0a764aca055c22cfb176d4b3881e1cd1b7585427 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:27:22 +0200 Subject: [PATCH 087/311] items have is_new properties --- .../tools/project_manager/project_manager/model.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 05e7b1831b..ae47fc3168 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -922,6 +922,10 @@ class BaseItem: def id(self): return self._id + @property + def is_new(self): + return False + def rowCount(self): return len(self._children) @@ -1142,6 +1146,10 @@ class AssetItem(BaseItem): def asset_id(self): return self.mongo_id + @property + def is_new(self): + return self.asset_id is None + @property def name(self): return self._data["name"] @@ -1293,6 +1301,10 @@ class TaskItem(BaseItem): data = {} super(TaskItem, self).__init__(data) + @property + def is_new(self): + return self._origin_data is None + @classmethod def name_icon(cls): if cls._name_icon is None: From 146a99bd8658849bc8e0bdb81655ce15b701fda0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:29:05 +0200 Subject: [PATCH 088/311] items can be tagged as removed --- .../project_manager/project_manager/model.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index ae47fc3168..c060a49037 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1130,6 +1130,7 @@ class AssetItem(BaseItem): self.mongo_id = asset_doc.get("_id") self._project_id = None self._hierarchy_changes_enabled = True + self._removed = False self._origin_asset_doc = copy.deepcopy(asset_doc) @@ -1256,6 +1257,9 @@ class AssetItem(BaseItem): if role == HIERARCHY_CHANGE_ABLE_ROLE: return self._hierarchy_changes_enabled + if role == REMOVED_ROLE: + return self._removed + if role == QtCore.Qt.ToolTipRole and self._is_duplicated: return "Asset with name \"{}\" already exists.".format( self._data["name"] @@ -1263,6 +1267,10 @@ class AssetItem(BaseItem): return super(AssetItem, self)._global_data(role) def setData(self, key, value, role): + if role == REMOVED_ROLE: + self._removed = value + return True + if role == HIERARCHY_CHANGE_ABLE_ROLE: if self._hierarchy_changes_enabled == value: return False @@ -1297,6 +1305,7 @@ class TaskItem(BaseItem): } def __init__(self, data=None): + self._removed = False if data is None: data = {} super(TaskItem, self).__init__(data) @@ -1315,6 +1324,9 @@ class TaskItem(BaseItem): raise AssertionError("BUG: Can't add children to Task") def _global_data(self, role): + if role == REMOVED_ROLE: + return self._removed + if role == QtCore.Qt.ToolTipRole and self._is_duplicated: return "Duplicated Task name \"{}\".".format( self._data["name"] @@ -1336,3 +1348,9 @@ class TaskItem(BaseItem): ): return self._data[key] or self._data["type"] or "< Select Type >" return super(TaskItem, self).data(key, role) + + def setData(self, key, value, role): + if role == REMOVED_ROLE: + self._removed = value + return True + return super(TaskItem, self).setData(key, value, role) From 656c0f87c910ea77d2e7671d563540d57488d500 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:29:25 +0200 Subject: [PATCH 089/311] set red background on removed items --- openpype/tools/project_manager/project_manager/model.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c060a49037..d8dfe5287e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1266,6 +1266,12 @@ class AssetItem(BaseItem): ) return super(AssetItem, self)._global_data(role) + def data(self, key, role): + if self._removed and role == QtCore.Qt.BackgroundRole: + return QtGui.QColor(255, 0, 0, 127) + + return super(AssetItem, self).data(key, role) + def setData(self, key, value, role): if role == REMOVED_ROLE: self._removed = value @@ -1342,6 +1348,9 @@ class TaskItem(BaseItem): } def data(self, key, role): + if self._removed and role == QtCore.Qt.BackgroundRole: + return QtGui.QColor(255, 0, 0, 127) + if ( role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and key == "name" From 15c5361733b780dd420ed17900bcff2c907ec0aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:30:04 +0200 Subject: [PATCH 090/311] use is_new attribute --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index d8dfe5287e..aa429d9fbe 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -804,7 +804,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): to_process.put(item) - if item.asset_id is None: + if item.is_new: insert_list.append(item) continue From 7d225a2ab461b5b5bbd7cfce911bdc96df255384 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:33:31 +0200 Subject: [PATCH 091/311] added default value for removed role --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index aa429d9fbe..0c999974f6 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -875,6 +875,9 @@ class BaseItem: if role == DUPLICATED_ROLE: return self._is_duplicated + if role == REMOVED_ROLE: + return False + return self._None def data(self, key, role): From 84887088f908d05e94c78a0fe1d11e9740ecd2b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:33:48 +0200 Subject: [PATCH 092/311] don't allow adding items under removed items --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 0c999974f6..ea3fd85496 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -366,6 +366,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): if parent is None: parent = self._root_item + if parent.data(None, REMOVED_ROLE): + return [] + if start_row is None: start_row = parent.rowCount() From c84e37c4bb7c009bd22c9876a5dc9791ba153d31 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:34:05 +0200 Subject: [PATCH 093/311] store origin data of task item --- openpype/tools/project_manager/project_manager/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index ea3fd85496..20cce4fd24 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1317,6 +1317,7 @@ class TaskItem(BaseItem): } def __init__(self, data=None): + self._origin_data = copy.deepcopy(data) self._removed = False if data is None: data = {} From 335c1073dffd6374434abfeacfaef73fab705d84 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:34:27 +0200 Subject: [PATCH 094/311] modified delete index method --- .../project_manager/project_manager/model.py | 94 +++++++++++++++---- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 20cce4fd24..cdc46b4383 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -410,27 +410,38 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not index.isValid(): return - item_id = index.data(IDENTIFIER_ROLE) - item = self._items_by_id[item_id] + item = index.internalPointer() + if isinstance(item, (RootItem, ProjectItem)): return parent = item.parent() + all_descendants = collections.defaultdict(dict) all_descendants[parent.id][item.id] = item - row = item.row() - self.beginRemoveRows(self.index_for_item(parent), row, row) + def _fill_children(_all_descendants, cur_item, parent_item=None): + if parent_item is not None: + _all_descendants[parent_item.id][cur_item.id] = cur_item - todo_queue = Queue() - todo_queue.put(item) - while not todo_queue.empty(): - _item = todo_queue.get() - for row in range(_item.rowCount()): - child_item = _item.child(row) - all_descendants[_item.id][child_item.id] = child_item - todo_queue.put(child_item) + if isinstance(cur_item, TaskItem): + task_removed = True + cur_item.setData(None, task_removed, REMOVED_ROLE) + return task_removed + remove_item = cur_item.data(None, HIERARCHY_CHANGE_ABLE_ROLE) + for row in range(cur_item.rowCount()): + child_item = cur_item.child(row) + if not _fill_children(_all_descendants, child_item, cur_item): + remove_item = False + + if remove_item: + cur_item.setData(None, True, REMOVED_ROLE) + return remove_item + + _fill_children(all_descendants, item) + + modified_children = [] while all_descendants: for parent_id in tuple(all_descendants.keys()): children = all_descendants[parent_id] @@ -438,18 +449,63 @@ class HierarchyModel(QtCore.QAbstractItemModel): all_descendants.pop(parent_id) continue + parent_children = {} + all_without_children = True for child_id in tuple(children.keys()): - child_item = children[child_id] if child_id in all_descendants: + all_without_children = False + break + parent_children[child_id] = children[child_id] + + if not all_without_children: + continue + + parent_item = self._items_by_id[parent_id] + row_ranges = [] + start_row = end_row = None + chilren_by_row = {} + for row in range(parent_item.rowCount()): + child_item = parent_item.child(row) + child_id = child_item.id + if child_id not in children: continue - if isinstance(child_item, AssetItem): - self._rename_asset(child_item, None) - children.pop(child_id) - child_item.set_parent(None) - self._items_by_id.pop(child_id) + chilren_by_row[row] = child_item + children.pop(child_item.id) - self.endRemoveRows() + remove_item = child_item.data(None, REMOVED_ROLE) + if not remove_item or not child_item.is_new: + modified_children.append(child_item) + if end_row is not None: + row_ranges.append((start_row, end_row)) + start_row = end_row = None + continue + + end_row = row + if start_row is None: + start_row = row + + if end_row is not None: + row_ranges.append((start_row, end_row)) + + parent_index = None + for start, end in row_ranges: + if parent_index is None: + parent_index = self.index_for_item(parent_item) + + self.beginRemoveRows(parent_index, start, end) + + for idx in range(start, end + 1): + child_item = chilren_by_row[idx] + child_item.set_parent(None) + self._items_by_id.pop(child_item.id) + + self.endRemoveRows() + + for item in modified_children: + s_index = self.index_for_item(item) + e_index = self.index_for_item(item, column=self.columns_len - 1) + self.dataChanged.emit(s_index, e_index, [QtCore.Qt.BackgroundRole]) def _rename_asset(self, asset_item, new_name): if not isinstance(asset_item, AssetItem): From a000c51dd4a882b3a6e44b7c277c98d78a688693 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:36:44 +0200 Subject: [PATCH 095/311] force name validation on remove --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index cdc46b4383..986bc7f38a 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -497,6 +497,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): for idx in range(start, end + 1): child_item = chilren_by_row[idx] + # Force name validation + if isinstance(child_item, AssetItem): + self._rename_asset(child_item, None) child_item.set_parent(None) self._items_by_id.pop(child_item.id) From b27116102bd609eab54607f0ab96f3d2381b4830 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 May 2021 22:38:28 +0200 Subject: [PATCH 096/311] added bg color for new items --- .../project_manager/project_manager/model.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 986bc7f38a..baea086d87 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1332,8 +1332,11 @@ class AssetItem(BaseItem): return super(AssetItem, self)._global_data(role) def data(self, key, role): - if self._removed and role == QtCore.Qt.BackgroundRole: - return QtGui.QColor(255, 0, 0, 127) + if role == QtCore.Qt.BackgroundRole: + if self._removed: + return QtGui.QColor(255, 0, 0, 127) + elif self.is_new: + return QtGui.QColor(0, 255, 0, 127) return super(AssetItem, self).data(key, role) @@ -1414,8 +1417,12 @@ class TaskItem(BaseItem): } def data(self, key, role): - if self._removed and role == QtCore.Qt.BackgroundRole: - return QtGui.QColor(255, 0, 0, 127) + if role == QtCore.Qt.BackgroundRole: + if self._removed: + return QtGui.QColor(255, 0, 0, 127) + + elif self.is_new: + return QtGui.QColor(0, 255, 0, 127) if ( role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) From d76fae0096aeada27b2eb46dec21b1cf1376780e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 09:29:33 +0200 Subject: [PATCH 097/311] added filtrable combobox widget --- .../project_manager/widgets.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 openpype/tools/project_manager/project_manager/widgets.py diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py new file mode 100644 index 0000000000..03f43fa489 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -0,0 +1,46 @@ +from Qt import QtWidgets, QtCore + + +class FilterComboBox(QtWidgets.QComboBox): + def __init__(self, parent=None): + super(FilterComboBox, self).__init__(parent) + + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setEditable(True) + + filter_proxy_model = QtCore.QSortFilterProxyModel(self) + filter_proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + filter_proxy_model.setSourceModel(self.model()) + + completer = QtWidgets.QCompleter(filter_proxy_model, self) + completer.setCompletionMode( + QtWidgets.QCompleter.UnfilteredPopupCompletion + ) + self.setCompleter(completer) + + self.lineEdit().textEdited.connect( + filter_proxy_model.setFilterFixedString + ) + completer.activated.connect(self.on_completer_activated) + + self._completer = completer + self._filter_proxy_model = filter_proxy_model + + def focusInEvent(self, event): + super(FilterComboBox, self).focusInEvent(event) + self.lineEdit().selectAll() + + def on_completer_activated(self, text): + if text: + index = self.findText(text) + self.setCurrentIndex(index) + + def setModel(self, model): + super(FilterComboBox, self).setModel(model) + self._filter_proxy_model.setSourceModel(model) + self._completer.setModel(self._filter_proxy_model) + + def setModelColumn(self, column): + self._completer.setCompletionColumn(column) + self._filter_proxy_model.setFilterKeyColumn(column) + super(FilterComboBox, self).setModelColumn(column) From 342fd59eb28a34e0c75a4d87fd6c1da00c8f4964 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 09:30:29 +0200 Subject: [PATCH 098/311] use filter combobox for type editor --- .../tools/project_manager/project_manager/delegates.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 88b366a7d0..c00cd37956 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -1,5 +1,6 @@ from Qt import QtWidgets, QtCore +from .widgets import FilterComboBox from .multiselection_combobox import MultiSelectionComboBox @@ -50,7 +51,7 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): super(TypeDelegate, self).__init__(*args, **kwargs) def createEditor(self, parent, option, index): - editor = QtWidgets.QComboBox(parent) + editor = FilterComboBox(parent) if not self._project_doc_cache.project_doc: return editor @@ -59,6 +60,12 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): return editor + def setEditorData(self, editor, index): + value = index.data(QtCore.Qt.EditRole) + index = editor.findText(value) + if index >= 0: + editor.setCurrentIndex(index) + class ToolsDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, tools_cache, *args, **kwargs): From 6b8002f4a0d354b01d6c6459c9e9ff3bb973a38a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 09:45:12 +0200 Subject: [PATCH 099/311] Text input may have set text regex --- .../project_manager/project_manager/delegates.py | 14 ++++++++++++-- .../tools/project_manager/project_manager/view.py | 2 +- .../project_manager/project_manager/widgets.py | 11 +++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index c00cd37956..bfd6cfaabb 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -1,6 +1,9 @@ from Qt import QtWidgets, QtCore -from .widgets import FilterComboBox +from .widgets import ( + RegexTextEdit, + FilterComboBox +) from .multiselection_combobox import MultiSelectionComboBox @@ -37,8 +40,15 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): class StringDelegate(QtWidgets.QStyledItemDelegate): + def __init__(self, regex, *args, **kwargs): + super(StringDelegate, self).__init__(*args, **kwargs) + self._regex = regex + def createEditor(self, parent, option, index): - editor = QtWidgets.QLineEdit(parent) + if self._regex: + editor = RegexTextEdit(self._regex, parent) + else: + editor = QtWidgets.QLineEdit(parent) value = index.data(QtCore.Qt.EditRole) if value is not None: editor.setText(str(value)) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index dee694a59c..be7513de25 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -108,7 +108,7 @@ class HierarchyView(QtWidgets.QTreeView): column_key_to_index = {} for key, item_type in self.column_delegate_defs.items(): if isinstance(item_type, StringDef): - delegate = StringDelegate() + delegate = StringDelegate(item_type.regex) elif isinstance(item_type, NumberDef): delegate = NumberDelegate( diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 03f43fa489..9ef5dfaf85 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -1,6 +1,17 @@ from Qt import QtWidgets, QtCore +class RegexTextEdit(QtWidgets.QLineEdit): + def __init__(self, regex, *args, **kwargs): + super(RegexTextEdit, self).__init__(*args, **kwargs) + self._regex = regex + + self.textChanged.connect(self._on_text_change) + + def _on_text_change(self, text): + print(text) + + class FilterComboBox(QtWidgets.QComboBox): def __init__(self, parent=None): super(FilterComboBox, self).__init__(parent) From 586f8faa1047b176009c2c4b2f29b012f2a73de3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:08:14 +0200 Subject: [PATCH 100/311] changed string delegate to name delegate --- .../project_manager/project_manager/constants.py | 4 ++++ .../project_manager/project_manager/delegates.py | 13 +++---------- .../tools/project_manager/project_manager/view.py | 13 ++++++------- .../project_manager/project_manager/widgets.py | 12 ++++++++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 7f9a859ac1..76d3d3cda1 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -1,3 +1,4 @@ +import re from Qt import QtCore @@ -5,3 +6,6 @@ IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 DUPLICATED_ROLE = QtCore.Qt.UserRole + 2 HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3 REMOVED_ROLE = QtCore.Qt.UserRole + 4 + +NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" +NAME_REGEX = re.compile("^[" + NAME_ALLOWED_SYMBOLS + "]*$") diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index bfd6cfaabb..18e3e2d81b 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -1,7 +1,7 @@ from Qt import QtWidgets, QtCore from .widgets import ( - RegexTextEdit, + NameTextEdit, FilterComboBox ) from .multiselection_combobox import MultiSelectionComboBox @@ -39,16 +39,9 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): # return super().updateEditorGeometry(editor, options, index) -class StringDelegate(QtWidgets.QStyledItemDelegate): - def __init__(self, regex, *args, **kwargs): - super(StringDelegate, self).__init__(*args, **kwargs) - self._regex = regex - +class NameDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): - if self._regex: - editor = RegexTextEdit(self._regex, parent) - else: - editor = QtWidgets.QLineEdit(parent) + editor = NameTextEdit(parent) value = index.data(QtCore.Qt.EditRole) if value is not None: editor.setText(str(value)) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index be7513de25..b782d27d47 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -2,7 +2,7 @@ from Qt import QtWidgets, QtCore from .delegates import ( NumberDelegate, - StringDelegate, + NameDelegate, TypeDelegate, ToolsDelegate ) @@ -10,9 +10,8 @@ from .delegates import ( from openpype.lib import ApplicationManager -class StringDef: - def __init__(self, regex=None): - self.regex = regex +class NameDef: + pass class NumberDef: @@ -63,7 +62,7 @@ class ToolsCache: class HierarchyView(QtWidgets.QTreeView): """A tree view that deselects on clicking on an empty area in the view""" column_delegate_defs = { - "name": StringDef(), + "name": NameDef(), "type": TypeDef(), "frameStart": NumberDef(1), "frameEnd": NumberDef(1), @@ -107,8 +106,8 @@ class HierarchyView(QtWidgets.QTreeView): column_delegates = {} column_key_to_index = {} for key, item_type in self.column_delegate_defs.items(): - if isinstance(item_type, StringDef): - delegate = StringDelegate(item_type.regex) + if isinstance(item_type, NameDef): + delegate = NameDelegate() elif isinstance(item_type, NumberDef): delegate = NumberDelegate( diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 9ef5dfaf85..878033de27 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -1,10 +1,14 @@ +import re +from .constants import ( + NAME_ALLOWED_SYMBOLS, + NAME_REGEX +) from Qt import QtWidgets, QtCore -class RegexTextEdit(QtWidgets.QLineEdit): - def __init__(self, regex, *args, **kwargs): - super(RegexTextEdit, self).__init__(*args, **kwargs) - self._regex = regex +class NameTextEdit(QtWidgets.QLineEdit): + def __init__(self, *args, **kwargs): + super(NameTextEdit, self).__init__(*args, **kwargs) self.textChanged.connect(self._on_text_change) From 6daec25886aa7fe6585ff538eb6fb3e1e4915af2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:08:29 +0200 Subject: [PATCH 101/311] skip not allowed symbols and keep position --- .../project_manager/project_manager/widgets.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 878033de27..1da4da3c24 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -13,7 +13,19 @@ class NameTextEdit(QtWidgets.QLineEdit): self.textChanged.connect(self._on_text_change) def _on_text_change(self, text): - print(text) + if NAME_REGEX.match(text): + return + + idx = self.cursorPosition() + before_text = text[0:idx] + after_text = text[idx:len(text)] + sub_regex = "[^{}]+".format(NAME_ALLOWED_SYMBOLS) + new_before_text = re.sub(sub_regex, "", before_text) + new_after_text = re.sub(sub_regex, "", after_text) + idx -= (len(before_text) - len(new_before_text)) + + self.setText(new_before_text + new_after_text) + self.setCursorPosition(idx) class FilterComboBox(QtWidgets.QComboBox): From fb09eb8ef75ffa454a09cb0a5f600069a51ac46b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:35:09 +0200 Subject: [PATCH 102/311] AssetItem cares about children task duplications --- .../project_manager/project_manager/model.py | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index baea086d87..2ed9f04e4f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1197,6 +1197,9 @@ class AssetItem(BaseItem): self._hierarchy_changes_enabled = True self._removed = False + self._task_items_by_name = collections.defaultdict(list) + self._task_name_by_item_id = {} + self._origin_asset_doc = copy.deepcopy(asset_doc) data = self.data_from_doc(asset_doc) @@ -1367,6 +1370,76 @@ class AssetItem(BaseItem): return flags return super(AssetItem, self).flags(key) + def _add_task(self, item): + name = item.data("name", QtCore.Qt.DisplayRole).lower() + item_id = item.data(None, IDENTIFIER_ROLE) + + self._task_name_by_item_id[item_id] = name + self._task_items_by_name[name].append(item) + if len(self._task_items_by_name[name]) > 1: + for _item in self._task_items_by_name[name]: + _item.setData(None, True, DUPLICATED_ROLE) + + def _remove_task(self, item): + item_id = item.data(None, IDENTIFIER_ROLE) + name = self._task_name_by_item_id[item_id] + + self._task_name_by_item_id.pop(item_id) + self._task_items_by_name[name].append(item) + if not self._task_items_by_name[name]: + self._task_items_by_name.pop(name) + + elif len(self._task_items_by_name[name]) == 1: + for _item in self._task_items_by_name[name]: + _item.setData(None, False, DUPLICATED_ROLE) + + def _rename_task(self, item): + new_name = item.data("name", QtCore.Qt.DisplayRole).lower() + item_id = item.data(None, IDENTIFIER_ROLE) + prev_name = self._task_name_by_item_id[item_id] + if new_name == prev_name: + return + + # Remove from previous name mapping + self._task_items_by_name[prev_name].remove(item) + if not self._task_items_by_name[prev_name]: + self._task_items_by_name.pop(prev_name) + + elif len(self._task_items_by_name[prev_name]) == 1: + for _item in self._task_items_by_name[prev_name]: + _item.setData(None, False, DUPLICATED_ROLE) + + # Add to new name mapping + self._task_items_by_name[new_name].append(item) + if len(self._task_items_by_name[new_name]) > 1: + for _item in self._task_items_by_name[new_name]: + _item.setData(None, True, DUPLICATED_ROLE) + else: + item.setData(None, False, DUPLICATED_ROLE) + + self._task_name_by_item_id[item_id] = new_name + + def on_task_name_change(self, task_item): + self._rename_task(task_item) + + def add_child(self, item, row=None): + if item in self._children: + return + + super(AssetItem, self).add_child(item, row) + + if isinstance(item, TaskItem): + self._add_task(item) + + def remove_child(self, item): + if item not in self._children: + return + + if isinstance(item, TaskItem): + self._remove_task(item) + + super(AssetItem).remove_child(item) + class TaskItem(BaseItem): columns = { @@ -1435,4 +1508,10 @@ class TaskItem(BaseItem): if role == REMOVED_ROLE: self._removed = value return True - return super(TaskItem, self).setData(key, value, role) + + result = super(TaskItem, self).setData(key, value, role) + + if key == "name": + self.parent().on_task_name_change(self) + + return result From 94dedde1dc865ca711d1f9a95902ce04be2c4bb8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:41:06 +0200 Subject: [PATCH 103/311] check for type changes if name is not set --- openpype/tools/project_manager/project_manager/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 2ed9f04e4f..e26d213297 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1511,7 +1511,10 @@ class TaskItem(BaseItem): result = super(TaskItem, self).setData(key, value, role) - if key == "name": + if ( + key == "name" + or (key == "type" and self._data["name"] is None) + ): self.parent().on_task_name_change(self) return result From 964f7b2b290e6876559921726235f4b2dc685624 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:41:21 +0200 Subject: [PATCH 104/311] store duplicated task names --- openpype/tools/project_manager/project_manager/model.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index e26d213297..8f7e86dbb0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1194,12 +1194,17 @@ class AssetItem(BaseItem): asset_doc = {} self.mongo_id = asset_doc.get("_id") self._project_id = None + + # Item data self._hierarchy_changes_enabled = True self._removed = False + # Task children duplication variables self._task_items_by_name = collections.defaultdict(list) self._task_name_by_item_id = {} + self._duplicated_task_names = set() + # Copy of original document self._origin_asset_doc = copy.deepcopy(asset_doc) data = self.data_from_doc(asset_doc) @@ -1377,6 +1382,7 @@ class AssetItem(BaseItem): self._task_name_by_item_id[item_id] = name self._task_items_by_name[name].append(item) if len(self._task_items_by_name[name]) > 1: + self._duplicated_task_names.add(name) for _item in self._task_items_by_name[name]: _item.setData(None, True, DUPLICATED_ROLE) @@ -1390,6 +1396,7 @@ class AssetItem(BaseItem): self._task_items_by_name.pop(name) elif len(self._task_items_by_name[name]) == 1: + self._duplicated_task_names.remove(name) for _item in self._task_items_by_name[name]: _item.setData(None, False, DUPLICATED_ROLE) @@ -1406,12 +1413,14 @@ class AssetItem(BaseItem): self._task_items_by_name.pop(prev_name) elif len(self._task_items_by_name[prev_name]) == 1: + self._duplicated_task_names.remove(prev_name) for _item in self._task_items_by_name[prev_name]: _item.setData(None, False, DUPLICATED_ROLE) # Add to new name mapping self._task_items_by_name[new_name].append(item) if len(self._task_items_by_name[new_name]) > 1: + self._duplicated_task_names.add(new_name) for _item in self._task_items_by_name[new_name]: _item.setData(None, True, DUPLICATED_ROLE) else: From ad50d5b38d09b73db6edda66ceb8112a67d85486 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:46:58 +0200 Subject: [PATCH 105/311] added base of item validations --- .../tools/project_manager/project_manager/model.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 8f7e86dbb0..e6df0ea5ab 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -843,6 +843,15 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endResetModel() def save(self): + all_valid = True + for item in self._items_by_id.values(): + if not item.is_valid: + all_valid = False + break + + if not all_valid: + return + project_item = None for _project_item in self._root_item.children(): project_item = _project_item @@ -919,6 +928,10 @@ class BaseItem: def name_icon(cls): return cls._name_icon + @property + def is_valid(self): + return not self._is_duplicated + def model(self): return self._parent.model From 5fa0bb524aa2766961b8705b71a63b887a7dc8b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:47:12 +0200 Subject: [PATCH 106/311] use data method to get asset values --- openpype/tools/project_manager/project_manager/model.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index e6df0ea5ab..bf1f0eda49 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1262,8 +1262,8 @@ class AssetItem(BaseItem): ) doc = { - "name": self._data["name"], - "type": self._data["type"], + "name": self.data("name", QtCore.Qt.DisplayRole), + "type": self.data("type", QtCore.Qt.DisplayRole), "schema": schema_name, "data": doc_data, "parent": self.project_id @@ -1271,10 +1271,11 @@ class AssetItem(BaseItem): if self.mongo_id: doc["_id"] = self.mongo_id - for key, value in self._data.items(): + for key in self._data.keys(): if key in doc: continue - doc_data[key] = value + # Use `data` method to get inherited values + doc_data[key] = self.data(key, QtCore.Qt.DisplayRole) return doc From c26c141343685ec8815d01df03247fa19664e637 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 10:49:31 +0200 Subject: [PATCH 107/311] fix super call --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index bf1f0eda49..86c7f79aab 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1461,7 +1461,7 @@ class AssetItem(BaseItem): if isinstance(item, TaskItem): self._remove_task(item) - super(AssetItem).remove_child(item) + super(AssetItem, self).remove_child(item) class TaskItem(BaseItem): From 53027f6916392ca851bf730397d99443cdf8ec14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 11:01:08 +0200 Subject: [PATCH 108/311] store origin data of asset and task --- openpype/tools/project_manager/project_manager/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 86c7f79aab..9c8021f974 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1221,6 +1221,9 @@ class AssetItem(BaseItem): self._origin_asset_doc = copy.deepcopy(asset_doc) data = self.data_from_doc(asset_doc) + + self._origin_data = copy.deepcopy(data) + super(AssetItem, self).__init__(data) @property @@ -1475,10 +1478,11 @@ class TaskItem(BaseItem): } def __init__(self, data=None): - self._origin_data = copy.deepcopy(data) self._removed = False if data is None: data = {} + self._origin_data = copy.deepcopy(data) + super(TaskItem, self).__init__(data) @property From 2bc795197e07f53a15078435e888aa859695030b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 11:25:49 +0200 Subject: [PATCH 109/311] implemented delegate that can resize editor to required sizes --- .../project_manager/delegates.py | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 18e3e2d81b..4292224b09 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -7,6 +7,65 @@ from .widgets import ( from .multiselection_combobox import MultiSelectionComboBox +class ResizeEditorDelegate(QtWidgets.QStyledItemDelegate): + @staticmethod + def _q_smart_min_size(editor): + min_size_hint = editor.minimumSizeHint() + size_policy = editor.sizePolicy() + width = 0 + height = 0 + if size_policy.horizontalPolicy() != QtWidgets.QSizePolicy.Ignored: + if ( + size_policy.horizontalPolicy() + & QtWidgets.QSizePolicy.ShrinkFlag + ): + width = min_size_hint.width() + else: + width = max( + editor.sizeHint().width(), + min_size_hint.width() + ) + + if size_policy.verticalPolicy() != QtWidgets.QSizePolicy.Ignored: + if size_policy.verticalPolicy() & QtWidgets.QSizePolicy.ShrinkFlag: + height = min_size_hint.height() + else: + height = max( + editor.sizeHint().height(), + min_size_hint.height() + ) + + output = QtCore.QSize(width, height).boundedTo(editor.maximumSize()) + min_size = editor.minimumSize() + if min_size.width() > 0: + output.setWidth(min_size.width()) + if min_size.height() > 0: + output.setHeight(min_size.height()) + + return output.expandedTo(QtCore.QSize(0, 0)) + + def updateEditorGeometry(self, editor, option, index): + self.initStyleOption(option, index) + + option.showDecorationSelected = editor.style().styleHint( + QtWidgets.QStyle.SH_ItemView_ShowDecorationSelected, None, editor + ) + + widget = option.widget + + style = widget.style() if widget else QtWidgets.QApplication.style() + geo = style.subElementRect( + QtWidgets.QStyle.SE_ItemViewItemText, option, widget + ) + delta = self._q_smart_min_size(editor).width() - geo.width() + if delta > 0: + if editor.layoutDirection() == QtCore.Qt.RightToLeft: + geo.adjust(-delta, 0, 0, 0) + else: + geo.adjust(0, 0, delta, 0) + editor.setGeometry(geo) + + class NumberDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, minimum, maximum, decimals, *args, **kwargs): super(NumberDelegate, self).__init__(*args, **kwargs) @@ -34,10 +93,6 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): return editor - # def updateEditorGeometry(self, editor, options, index): - # print(editor) - # return super().updateEditorGeometry(editor, options, index) - class NameDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): From 1c9c6164a62d6bd9ec9c2b6558ca1ac63a49ba3e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 11:38:44 +0200 Subject: [PATCH 110/311] fix TaskItem new item detection --- openpype/tools/project_manager/project_manager/model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9c8021f974..cf5f3cf9f1 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1479,15 +1479,16 @@ class TaskItem(BaseItem): def __init__(self, data=None): self._removed = False + self._is_new = data is None if data is None: data = {} - self._origin_data = copy.deepcopy(data) + self._origin_data = copy.deepcopy(data) super(TaskItem, self).__init__(data) @property def is_new(self): - return self._origin_data is None + return self._is_new @classmethod def name_icon(cls): From 675aac19322b345094a2864f18c568c96d7b9707 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 12:15:52 +0200 Subject: [PATCH 111/311] model has defined multiselection columns --- .../project_manager/project_manager/model.py | 26 ++++++++++++++++++- .../project_manager/project_manager/view.py | 4 +-- .../project_manager/project_manager/window.py | 4 ++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index cf5f3cf9f1..f34b9eb39c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -52,8 +52,12 @@ class ProjectModel(QtGui.QStandardItemModel): class HierarchySelectionModel(QtCore.QItemSelectionModel): + def __init__(self, multiselection_columns, *args, **kwargs): + super(HierarchySelectionModel, self).__init__(*args, **kwargs) + self.multiselection_columns = multiselection_columns + def setCurrentIndex(self, index, command): - if index.column() > 0: + if index.column() in self.multiselection_columns: if ( command & QtCore.QItemSelectionModel.Clear and command & QtCore.QItemSelectionModel.Select @@ -78,6 +82,19 @@ class HierarchyModel(QtCore.QAbstractItemModel): ("pixelAspect", "Pixel aspect"), ("tools_env", "Tools") ] + multiselection_columns = { + "frameStart", + "frameEnd", + "fps", + "resolutionWidth", + "resolutionHeight", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "pixelAspect", + "tools_env" + } columns = [ item[0] for item in _columns_def @@ -87,10 +104,17 @@ class HierarchyModel(QtCore.QAbstractItemModel): idx: item[1] for idx, item in enumerate(_columns_def) } + index_moved = QtCore.Signal(QtCore.QModelIndex) def __init__(self, dbcon, parent=None): super(HierarchyModel, self).__init__(parent) + + self.multiselection_column_indexes = { + self.columns.index(key) + for key in self.multiselection_columns + } + # TODO Reset them on project change self._current_project = None self._root_item = None diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b782d27d47..04c0e6b08b 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -76,7 +76,7 @@ class HierarchyView(QtWidgets.QTreeView): "pixelAspect": NumberDef(0, decimals=2), "tools_env": ToolsDef() } - persistent_columns = [ + persistent_columns = { "type", "frameStart", "frameEnd", @@ -89,7 +89,7 @@ class HierarchyView(QtWidgets.QTreeView): "clipOut", "pixelAspect", "tools_env" - ] + } def __init__(self, dbcon, source_model, *args, **kwargs): super(HierarchyView, self).__init__(*args, **kwargs) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 0be69c29c8..f2ad399ab5 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -39,7 +39,9 @@ class Window(QtWidgets.QWidget): hierarchy_view = HierarchyView(dbcon, hierarchy_model, self) hierarchy_view.setModel(hierarchy_model) - _selection_model = HierarchySelectionModel() + _selection_model = HierarchySelectionModel( + hierarchy_model.multiselection_column_indexes + ) _selection_model.setModel(hierarchy_view.model()) hierarchy_view.setSelectionModel(_selection_model) From db10512a305f66b61e97c87b6047b920601d56b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 12:16:14 +0200 Subject: [PATCH 112/311] clear selection on adding new item --- .../tools/project_manager/project_manager/view.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 04c0e6b08b..4d3557eb9e 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -280,7 +280,11 @@ class HierarchyView(QtWidgets.QTreeView): new_index.row(), 1, new_index.parent() ) # Change current index - self.setCurrentIndex(task_type_index) + self.selectionModel().setCurrentIndex( + task_type_index, + QtCore.QItemSelectionModel.Clear + | QtCore.QItemSelectionModel.Select + ) # Start editing self.edit(task_type_index) @@ -298,7 +302,11 @@ class HierarchyView(QtWidgets.QTreeView): return # Change current index - self.setCurrentIndex(new_index) + self.selectionModel().setCurrentIndex( + new_index, + QtCore.QItemSelectionModel.Clear + | QtCore.QItemSelectionModel.Select + ) # Start editing self.edit(new_index) From 8b30c7a5edcc170c469704b7fb9f3118acaca1d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 May 2021 12:16:23 +0200 Subject: [PATCH 113/311] added `_add_asset` method --- openpype/tools/project_manager/project_manager/view.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 4d3557eb9e..1db8a12c23 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -289,7 +289,12 @@ class HierarchyView(QtWidgets.QTreeView): self.edit(task_type_index) def _on_shift_enter_pressed(self): - index = self.currentIndex() + self._add_asset() + + def _add_asset(self, index=None): + if index is None: + index = self.currentIndex() + if not index.isValid(): return From 54e7a479c3096f73010332b1cc2f57874686382e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 13:06:02 +0200 Subject: [PATCH 114/311] dont remove tasks if it's parent was not removed and task is not selected --- openpype/tools/project_manager/project_manager/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index f34b9eb39c..03e28717af 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -449,8 +449,12 @@ class HierarchyModel(QtCore.QAbstractItemModel): _all_descendants[parent_item.id][cur_item.id] = cur_item if isinstance(cur_item, TaskItem): + was_removed = cur_item.data(None, REMOVED_ROLE) task_removed = True - cur_item.setData(None, task_removed, REMOVED_ROLE) + if not was_removed and parent_item is not None: + task_removed = parent_item.data(None, REMOVED_ROLE) + if not was_removed: + cur_item.setData(None, task_removed, REMOVED_ROLE) return task_removed remove_item = cur_item.data(None, HIERARCHY_CHANGE_ABLE_ROLE) From e31a2687c88801dbd1db85d384700e0357df3494 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 13:08:01 +0200 Subject: [PATCH 115/311] added method remove multiple indexes at once --- .../project_manager/project_manager/model.py | 108 +----------------- 1 file changed, 3 insertions(+), 105 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 03e28717af..c0a96058e3 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -431,112 +431,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): return None def remove_index(self, index): - if not index.isValid(): - return + return self.remove_indexes([index]) - item = index.internalPointer() - - if isinstance(item, (RootItem, ProjectItem)): - return - - parent = item.parent() - - all_descendants = collections.defaultdict(dict) - all_descendants[parent.id][item.id] = item - - def _fill_children(_all_descendants, cur_item, parent_item=None): - if parent_item is not None: - _all_descendants[parent_item.id][cur_item.id] = cur_item - - if isinstance(cur_item, TaskItem): - was_removed = cur_item.data(None, REMOVED_ROLE) - task_removed = True - if not was_removed and parent_item is not None: - task_removed = parent_item.data(None, REMOVED_ROLE) - if not was_removed: - cur_item.setData(None, task_removed, REMOVED_ROLE) - return task_removed - - remove_item = cur_item.data(None, HIERARCHY_CHANGE_ABLE_ROLE) - for row in range(cur_item.rowCount()): - child_item = cur_item.child(row) - if not _fill_children(_all_descendants, child_item, cur_item): - remove_item = False - - if remove_item: - cur_item.setData(None, True, REMOVED_ROLE) - return remove_item - - _fill_children(all_descendants, item) - - modified_children = [] - while all_descendants: - for parent_id in tuple(all_descendants.keys()): - children = all_descendants[parent_id] - if not children: - all_descendants.pop(parent_id) - continue - - parent_children = {} - all_without_children = True - for child_id in tuple(children.keys()): - if child_id in all_descendants: - all_without_children = False - break - parent_children[child_id] = children[child_id] - - if not all_without_children: - continue - - parent_item = self._items_by_id[parent_id] - row_ranges = [] - start_row = end_row = None - chilren_by_row = {} - for row in range(parent_item.rowCount()): - child_item = parent_item.child(row) - child_id = child_item.id - if child_id not in children: - continue - - chilren_by_row[row] = child_item - children.pop(child_item.id) - - remove_item = child_item.data(None, REMOVED_ROLE) - if not remove_item or not child_item.is_new: - modified_children.append(child_item) - if end_row is not None: - row_ranges.append((start_row, end_row)) - start_row = end_row = None - continue - - end_row = row - if start_row is None: - start_row = row - - if end_row is not None: - row_ranges.append((start_row, end_row)) - - parent_index = None - for start, end in row_ranges: - if parent_index is None: - parent_index = self.index_for_item(parent_item) - - self.beginRemoveRows(parent_index, start, end) - - for idx in range(start, end + 1): - child_item = chilren_by_row[idx] - # Force name validation - if isinstance(child_item, AssetItem): - self._rename_asset(child_item, None) - child_item.set_parent(None) - self._items_by_id.pop(child_item.id) - - self.endRemoveRows() - - for item in modified_children: - s_index = self.index_for_item(item) - e_index = self.index_for_item(item, column=self.columns_len - 1) - self.dataChanged.emit(s_index, e_index, [QtCore.Qt.BackgroundRole]) + def remove_indexes(self, indexes): + items_by_id = {} def _rename_asset(self, asset_item, new_name): if not isinstance(asset_item, AssetItem): From 44cc3d40113d2110d0f274e9b2f1da61fe7db018 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 13:08:32 +0200 Subject: [PATCH 116/311] implemeted method that cares about removing single item --- .../project_manager/project_manager/model.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c0a96058e3..cb51323975 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -436,6 +436,110 @@ class HierarchyModel(QtCore.QAbstractItemModel): def remove_indexes(self, indexes): items_by_id = {} + def _remove_item(self, item): + is_removed = item.data(None, REMOVED_ROLE) + if is_removed: + return + + parent = item.parent() + + all_descendants = collections.defaultdict(dict) + all_descendants[parent.id][item.id] = item + + def _fill_children(_all_descendants, cur_item, parent_item=None): + if parent_item is not None: + _all_descendants[parent_item.id][cur_item.id] = cur_item + + if isinstance(cur_item, TaskItem): + was_removed = cur_item.data(None, REMOVED_ROLE) + task_removed = True + if not was_removed and parent_item is not None: + task_removed = parent_item.data(None, REMOVED_ROLE) + if not was_removed: + cur_item.setData(None, task_removed, REMOVED_ROLE) + return task_removed + + remove_item = cur_item.data(None, HIERARCHY_CHANGE_ABLE_ROLE) + for row in range(cur_item.rowCount()): + child_item = cur_item.child(row) + if not _fill_children(_all_descendants, child_item, cur_item): + remove_item = False + + if remove_item: + cur_item.setData(None, True, REMOVED_ROLE) + return remove_item + + _fill_children(all_descendants, item) + + modified_children = [] + while all_descendants: + for parent_id in tuple(all_descendants.keys()): + children = all_descendants[parent_id] + if not children: + all_descendants.pop(parent_id) + continue + + parent_children = {} + all_without_children = True + for child_id in tuple(children.keys()): + if child_id in all_descendants: + all_without_children = False + break + parent_children[child_id] = children[child_id] + + if not all_without_children: + continue + + parent_item = self._items_by_id[parent_id] + row_ranges = [] + start_row = end_row = None + chilren_by_row = {} + for row in range(parent_item.rowCount()): + child_item = parent_item.child(row) + child_id = child_item.id + if child_id not in children: + continue + + chilren_by_row[row] = child_item + children.pop(child_item.id) + + remove_item = child_item.data(None, REMOVED_ROLE) + if not remove_item or not child_item.is_new: + modified_children.append(child_item) + if end_row is not None: + row_ranges.append((start_row, end_row)) + start_row = end_row = None + continue + + end_row = row + if start_row is None: + start_row = row + + if end_row is not None: + row_ranges.append((start_row, end_row)) + + parent_index = None + for start, end in row_ranges: + if parent_index is None: + parent_index = self.index_for_item(parent_item) + + self.beginRemoveRows(parent_index, start, end) + + for idx in range(start, end + 1): + child_item = chilren_by_row[idx] + # Force name validation + if isinstance(child_item, AssetItem): + self._rename_asset(child_item, None) + child_item.set_parent(None) + self._items_by_id.pop(child_item.id) + + self.endRemoveRows() + + for item in modified_children: + s_index = self.index_for_item(item) + e_index = self.index_for_item(item, column=self.columns_len - 1) + self.dataChanged.emit(s_index, e_index, [QtCore.Qt.BackgroundRole]) + def _rename_asset(self, asset_item, new_name): if not isinstance(asset_item, AssetItem): return From 188cf1e57a37d70d228f433d8a183132f35da271 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 13:10:45 +0200 Subject: [PATCH 117/311] remove indexes implemented remove_indexes content --- .../project_manager/project_manager/model.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index cb51323975..0b3bf2fa9e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -435,6 +435,25 @@ class HierarchyModel(QtCore.QAbstractItemModel): def remove_indexes(self, indexes): items_by_id = {} + processed_ids = set() + for index in indexes: + if not index.isValid(): + continue + + item_id = index.data(IDENTIFIER_ROLE) + # There may be indexes for multiple columns + if item_id not in processed_ids: + processed_ids.add(item_id) + + item = self._items_by_id[item_id] + if isinstance(item, (TaskItem, AssetItem)): + items_by_id[item_id] = item + + if not items_by_id: + return + + for item in items_by_id.values(): + self._remove_item(item) def _remove_item(self, item): is_removed = item.data(None, REMOVED_ROLE) From c1a8d59ad125c7eff5517d934569a68e9785383e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 13:17:10 +0200 Subject: [PATCH 118/311] view send all selected indexes to model on remove --- openpype/tools/project_manager/project_manager/view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 1db8a12c23..680601587b 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -252,10 +252,10 @@ class HierarchyView(QtWidgets.QTreeView): else: event.accept() - def _delete_item(self, index=None): - if index is None: - index = self.currentIndex() - self._source_model.remove_index(index) + def _delete_item(self, indexes=None): + if indexes is None: + indexes = self.selectedIndexes() + self._source_model.remove_indexes(indexes) def _on_ctrl_shift_enter_pressed(self): self._add_task() From 15a15336b6659daf1d78035692a97b0eea2849d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 14:56:27 +0200 Subject: [PATCH 119/311] added ability to unset delete flags by item ids --- .../project_manager/project_manager/model.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 0b3bf2fa9e..c672cdc053 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -430,6 +430,33 @@ class HierarchyModel(QtCore.QAbstractItemModel): return result[0] return None + def remove_delete_flag(self, item_ids): + remove_tag_items_by_id = {} + for item_id in item_ids: + item = self.items_by_id[item_id] + if not isinstance(item, (AssetItem, TaskItem)): + continue + + if item.data(None, REMOVED_ROLE): + remove_tag_items_by_id[item_id] = item + + for item in remove_tag_items_by_id.values(): + parent = item.parent() + while True: + if not isinstance(parent, (AssetItem, TaskItem)): + break + + if parent.id in remove_tag_items_by_id: + continue + + if parent.data(None, REMOVED_ROLE): + remove_tag_items_by_id[parent.id] = parent + + parent = parent.parent() + + for item in remove_tag_items_by_id.values(): + item.setData(None, False, REMOVED_ROLE) + def remove_index(self, index): return self.remove_indexes([index]) From 439876d64b76c80cd7b2c5901c196dc7653e1e5f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 14:57:04 +0200 Subject: [PATCH 120/311] hierarchy view has ability to show context menu --- .../project_manager/project_manager/view.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 680601587b..598d55270a 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -102,6 +102,7 @@ class HierarchyView(QtWidgets.QTreeView): self.setItemDelegate(main_delegate) self.setAlternatingRowColors(True) self.setSelectionMode(HierarchyView.ExtendedSelection) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) column_delegates = {} column_key_to_index = {} @@ -128,6 +129,7 @@ class HierarchyView(QtWidgets.QTreeView): column_key_to_index[key] = column source_model.index_moved.connect(self._on_rows_moved) + self.customContextMenuRequested.connect(self._on_context_menu) self._project_doc_cache = project_doc_cache self._tools_cache = tools_cache @@ -338,3 +340,21 @@ class HierarchyView(QtWidgets.QTreeView): and index.flags() & QtCore.Qt.ItemIsEditable ): self.edit(index) + + def _on_context_menu(self, point): + index = self.indexAt(point) + if index.column() != 0: + return + + actions = [] + + context_menu = QtWidgets.QMenu(self) + if not actions: + return + + for action in actions: + context_menu.addAction(action) + + global_point = self.viewport().mapToGlobal(point) + context_menu.exec_(global_point) + From 196c2bc175cc1cf723fbadf36b80268654fc32e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 14:57:16 +0200 Subject: [PATCH 121/311] added action to unsed remove tag --- .../project_manager/project_manager/view.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 598d55270a..a91f02a590 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -8,6 +8,10 @@ from .delegates import ( ) from openpype.lib import ApplicationManager +from .constants import ( + REMOVED_ROLE, + IDENTIFIER_ROLE +) class NameDef: @@ -349,6 +353,28 @@ class HierarchyView(QtWidgets.QTreeView): actions = [] context_menu = QtWidgets.QMenu(self) + + indexes = self.selectedIndexes() + + items_by_id = {} + for index in indexes: + item_id = index.data(IDENTIFIER_ROLE) + if item_id in items_by_id: + continue + items_by_id[item_id] = self._source_model.items_by_id[item_id] + + removed_item_ids = [] + for item_id, item in items_by_id.items(): + if item.data(None, REMOVED_ROLE): + removed_item_ids.append(item_id) + + if removed_item_ids: + action = QtWidgets.QAction("Keep items", context_menu) + action.triggered.connect( + lambda: self._remove_delete_flag(removed_item_ids) + ) + actions.append(action) + if not actions: return @@ -358,3 +384,5 @@ class HierarchyView(QtWidgets.QTreeView): global_point = self.viewport().mapToGlobal(point) context_menu.exec_(global_point) + def _remove_delete_flag(self, item_ids): + self._source_model.remove_delete_flag(item_ids) From 40ac5afb28bb0275cd0de89a0fc71f5842d9387c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:08:58 +0200 Subject: [PATCH 122/311] fix task removement validation order --- openpype/tools/project_manager/project_manager/model.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c672cdc053..dd514369ea 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -506,13 +506,21 @@ class HierarchyModel(QtCore.QAbstractItemModel): return task_removed remove_item = cur_item.data(None, HIERARCHY_CHANGE_ABLE_ROLE) + task_children = [] for row in range(cur_item.rowCount()): child_item = cur_item.child(row) + if isinstance(child_item, TaskItem): + task_children.append(child_item) + continue + if not _fill_children(_all_descendants, child_item, cur_item): remove_item = False if remove_item: cur_item.setData(None, True, REMOVED_ROLE) + + for task_item in task_children: + _fill_children(_all_descendants, task_item, cur_item) return remove_item _fill_children(all_descendants, item) From 85f0117bec9540b6ff85d74d706c4ea8253780ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:20:55 +0200 Subject: [PATCH 123/311] fix remove_delete_flag method --- .../project_manager/project_manager/model.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index dd514369ea..ed84ab631b 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -430,7 +430,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): return result[0] return None - def remove_delete_flag(self, item_ids): + def remove_delete_flag(self, item_ids, with_children=True): remove_tag_items_by_id = {} for item_id in item_ids: item = self.items_by_id[item_id] @@ -440,7 +440,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if item.data(None, REMOVED_ROLE): remove_tag_items_by_id[item_id] = item - for item in remove_tag_items_by_id.values(): + for item in tuple(remove_tag_items_by_id.values()): parent = item.parent() while True: if not isinstance(parent, (AssetItem, TaskItem)): @@ -454,6 +454,18 @@ class HierarchyModel(QtCore.QAbstractItemModel): parent = parent.parent() + if not with_children: + continue + + def _children_recursion(_item, store_obj): + if isinstance(_item, AssetItem): + for row in range(_item.rowCount()): + _child_item = _item.child(row) + if _child_item.id not in store_obj: + store_obj[_child_item.id] = _child_item + _children_recursion(_child_item, store_obj) + _children_recursion(item, remove_tag_items_by_id) + for item in remove_tag_items_by_id.values(): item.setData(None, False, REMOVED_ROLE) From 0739ee5a3a98f85ee761ba81541687f3738fbc32 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:29:25 +0200 Subject: [PATCH 124/311] save can remove tasks and change type of asset to archived_asset --- .../project_manager/project_manager/model.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index ed84ab631b..e9f517b7f1 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -961,7 +961,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): to_process = Queue() to_process.put(project_item) - update_list = [] + bulk_writes = [] while not to_process.empty(): parent = to_process.get() insert_list = [] @@ -973,15 +973,21 @@ class HierarchyModel(QtCore.QAbstractItemModel): if item.is_new: insert_list.append(item) - continue - update_data = item.update_data() - if update_data: - update_list.append(UpdateOne( + elif item.data(None, REMOVED_ROLE): + bulk_writes.append(UpdateOne( {"_id": item.asset_id}, - update_data + {"$set": {"type": "archived_asset"}} )) + else: + update_data = item.update_data() + if update_data: + bulk_writes.append(UpdateOne( + {"_id": item.asset_id}, + update_data + )) + if insert_list: new_docs = [] for item in insert_list: @@ -991,8 +997,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): for idx, mongo_id in enumerate(result.inserted_ids): insert_list[idx].mongo_id = mongo_id - if update_list: - project_col.bulk_write(update_list) + if bulk_writes: + project_col.bulk_write(bulk_writes) class BaseItem: @@ -1606,6 +1612,8 @@ class TaskItem(BaseItem): return super(TaskItem, self)._global_data(role) def to_doc_data(self): + if self._removed: + return {} data = copy.deepcopy(self._data) data.pop("name") name = self.data("name", QtCore.Qt.DisplayRole) From 1d4dd107d30c73355caacd7a72666a1a02e2240f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:30:22 +0200 Subject: [PATCH 125/311] refresh project after save --- openpype/tools/project_manager/project_manager/model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index e9f517b7f1..1cca1d1544 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -131,6 +131,11 @@ class HierarchyModel(QtCore.QAbstractItemModel): def _reset_root_item(self): self._root_item = RootItem(self) + def refresh_project(self): + project_name = self._current_project + self._current_project = None + self.set_project(project_name) + def set_project(self, project_name): if self._current_project == project_name: return @@ -1000,6 +1005,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): if bulk_writes: project_col.bulk_write(bulk_writes) + self.refresh_project() + class BaseItem: columns = [] From 30bc0c8fabc28e1b94de4015e20dc42b528c5c3e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:46:28 +0200 Subject: [PATCH 126/311] items have item_type --- .../project_manager/constants.py | 1 + .../project_manager/project_manager/model.py | 44 +++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 76d3d3cda1..703cc0d003 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -6,6 +6,7 @@ IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 DUPLICATED_ROLE = QtCore.Qt.UserRole + 2 HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3 REMOVED_ROLE = QtCore.Qt.UserRole + 4 +ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5 NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" NAME_REGEX = re.compile("^[" + NAME_ALLOWED_SYMBOLS + "]*$") diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 1cca1d1544..145dacd740 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -5,6 +5,7 @@ from uuid import uuid4 from .constants import ( IDENTIFIER_ROLE, + ITEM_TYPE_ROLE, DUPLICATED_ROLE, HIERARCHY_CHANGE_ABLE_ROLE, REMOVED_ROLE @@ -1015,6 +1016,7 @@ class BaseItem: _name_icon = None _is_duplicated = False + item_type = "base" _None = object() @@ -1027,6 +1029,7 @@ class BaseItem: key: None for key in self.columns } + self._global_data = {} self._source_data = data if data: for key, value in data.items(): @@ -1052,7 +1055,10 @@ class BaseItem: self._children.pop(idx) self._children.insert(row, item) - def _global_data(self, role): + def _get_global_data(self, role): + if role == ITEM_TYPE_ROLE: + return self.item_type + if role == IDENTIFIER_ROLE: return self._id @@ -1062,10 +1068,14 @@ class BaseItem: if role == REMOVED_ROLE: return False - return self._None + return self._global_data.get(role, self._None) + + def _set_global_data(self, value, role): + self._global_data[role] = value + return True def data(self, key, role): - value = self._global_data(role) + value = self._get_global_data(role) if value is not self._None: return value @@ -1096,14 +1106,12 @@ class BaseItem: return True if role == QtCore.Qt.EditRole: - if key not in self.editable_columns: - return False + if key in self.editable_columns: + self._data[key] = value + # must return true if successful + return True - self._data[key] = value - # must return true if successful - return True - - return False + return self._set_global_data(value, role) @property def id(self): @@ -1175,6 +1183,8 @@ class BaseItem: class RootItem(BaseItem): + item_type = "root" + def __init__(self, model): super(RootItem, self).__init__() self._model = model @@ -1187,6 +1197,8 @@ class RootItem(BaseItem): class ProjectItem(BaseItem): + item_type = "project" + columns = { "name", "type", @@ -1261,6 +1273,8 @@ class ProjectItem(BaseItem): class AssetItem(BaseItem): + item_type = "asset" + columns = { "name", "type", @@ -1452,7 +1466,7 @@ class AssetItem(BaseItem): cls._name_icon = qtawesome.icon("fa.folder", color="#333333") return cls._name_icon - def _global_data(self, role): + def _get_global_data(self, role): if role == HIERARCHY_CHANGE_ABLE_ROLE: return self._hierarchy_changes_enabled @@ -1463,7 +1477,7 @@ class AssetItem(BaseItem): return "Asset with name \"{}\" already exists.".format( self._data["name"] ) - return super(AssetItem, self)._global_data(role) + return super(AssetItem, self)._get_global_data(role) def data(self, key, role): if role == QtCore.Qt.BackgroundRole: @@ -1577,6 +1591,8 @@ class AssetItem(BaseItem): class TaskItem(BaseItem): + item_type = "task" + columns = { "name", "type" @@ -1608,7 +1624,7 @@ class TaskItem(BaseItem): def add_child(self, item, row=None): raise AssertionError("BUG: Can't add children to Task") - def _global_data(self, role): + def _get_global_data(self, role): if role == REMOVED_ROLE: return self._removed @@ -1616,7 +1632,7 @@ class TaskItem(BaseItem): return "Duplicated Task name \"{}\".".format( self._data["name"] ) - return super(TaskItem, self)._global_data(role) + return super(TaskItem, self)._get_global_data(role) def to_doc_data(self): if self._removed: From 6f8a57daaf26e170075cf12a27bc7ed4428eb386 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:48:21 +0200 Subject: [PATCH 127/311] context menu has add task and add asset actions --- .../project_manager/project_manager/view.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index a91f02a590..c8ce7219ca 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -10,7 +10,8 @@ from .delegates import ( from openpype.lib import ApplicationManager from .constants import ( REMOVED_ROLE, - IDENTIFIER_ROLE + IDENTIFIER_ROLE, + ITEM_TYPE_ROLE ) @@ -347,7 +348,8 @@ class HierarchyView(QtWidgets.QTreeView): def _on_context_menu(self, point): index = self.indexAt(point) - if index.column() != 0: + column = index.column() + if column != 0: return actions = [] @@ -358,11 +360,31 @@ class HierarchyView(QtWidgets.QTreeView): items_by_id = {} for index in indexes: - item_id = index.data(IDENTIFIER_ROLE) - if item_id in items_by_id: + if index.column() != column: continue + + item_id = index.data(IDENTIFIER_ROLE) items_by_id[item_id] = self._source_model.items_by_id[item_id] + item_ids = tuple(items_by_id.keys()) + if len(item_ids) == 1: + item = items_by_id[item_ids[0]] + item_type = item.data(None, ITEM_TYPE_ROLE) + if item_type in ("asset", "project"): + add_asset_action = QtWidgets.QAction("Add Asset", context_menu) + add_asset_action.triggered.connect( + lambda: self._add_asset() + ) + actions.append(add_asset_action) + + if item_type in ("asset", "task"): + add_task_action = QtWidgets.QAction("Add Task", context_menu) + add_task_action.triggered.connect( + lambda: self._add_task() + ) + actions.append(add_task_action) + + # Remove delete tag on items removed_item_ids = [] for item_id, item in items_by_id.items(): if item.data(None, REMOVED_ROLE): From 4319136dd8bb3ae9a4fdc2d421a3a8d6a3aa6f46 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:51:18 +0200 Subject: [PATCH 128/311] assets and task are loaded sorted by name --- openpype/tools/project_manager/project_manager/model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 145dacd740..e0af188ec6 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -211,7 +211,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): continue new_items = [] - for asset_doc in asset_docs_by_parent_id[parent_id]: + asset_docs = asset_docs_by_parent_id[parent_id] + for asset_doc in sorted(asset_docs, key=lambda item: item["name"]): # Create new Item new_item = AssetItem(asset_doc) # Store item to be added under parent in bulk @@ -255,8 +256,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): continue task_items = [] - for task_name, task_data in asset_tasks.items(): - _task_data = copy.deepcopy(task_data) + for task_name in sorted(asset_tasks.keys()): + _task_data = copy.deepcopy(asset_tasks[task_name]) _task_data["name"] = task_name task_item = TaskItem(_task_data) task_items.append(task_item) From 6e16479d22d2f2baaf858787388eb295b71833d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 15:58:15 +0200 Subject: [PATCH 129/311] auto refill selected value of filter combobox on focus out --- .../tools/project_manager/project_manager/widgets.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 1da4da3c24..566e17ea86 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -57,6 +57,15 @@ class FilterComboBox(QtWidgets.QComboBox): super(FilterComboBox, self).focusInEvent(event) self.lineEdit().selectAll() + def focusOutEvent(self, event): + idx = self.currentIndex() + if idx > -1: + index = self.model().index(idx, 0) + text = index.data(QtCore.Qt.DisplayRole) + if text != self.lineEdit().text(): + self.lineEdit().setText(text) + super(FilterComboBox, self).focusOutEvent(event) + def on_completer_activated(self, text): if text: index = self.findText(text) From 9b65ef84eb7b6f4925928c56f4b7eb44273ac154 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 16:00:07 +0200 Subject: [PATCH 130/311] shuffle methods order --- .../tools/project_manager/project_manager/view.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index c8ce7219ca..a2a9fc1666 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -295,9 +295,6 @@ class HierarchyView(QtWidgets.QTreeView): # Start editing self.edit(task_type_index) - def _on_shift_enter_pressed(self): - self._add_asset() - def _add_asset(self, index=None): if index is None: index = self.currentIndex() @@ -322,6 +319,9 @@ class HierarchyView(QtWidgets.QTreeView): # Start editing self.edit(new_index) + def _on_shift_enter_pressed(self): + self._add_asset() + def _on_up_ctrl_pressed(self): indexes = self.selectedIndexes() self._source_model.move_horizontal(indexes, -1) @@ -346,6 +346,9 @@ class HierarchyView(QtWidgets.QTreeView): ): self.edit(index) + def _remove_delete_flag(self, item_ids): + self._source_model.remove_delete_flag(item_ids) + def _on_context_menu(self, point): index = self.indexAt(point) column = index.column() @@ -405,6 +408,3 @@ class HierarchyView(QtWidgets.QTreeView): global_point = self.viewport().mapToGlobal(point) context_menu.exec_(global_point) - - def _remove_delete_flag(self, item_ids): - self._source_model.remove_delete_flag(item_ids) From 434db48bc8e27a1c6e306cdb1accab5884cd96d8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 16:52:41 +0200 Subject: [PATCH 131/311] fix horizontal/vertical naming --- .../project_manager/project_manager/model.py | 16 ++++++++-------- .../project_manager/project_manager/view.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index e0af188ec6..435b6e2538 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -647,7 +647,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): index = self.index_for_item(item) self.setData(index, True, DUPLICATED_ROLE) - def _move_vertical_single(self, index, direction): + def _move_horizontal_single(self, index, direction): if not index.isValid(): return @@ -737,7 +737,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.index_moved.emit(index) - def move_vertical(self, indexes, direction): + def move_horizontal(self, indexes, direction): if not indexes: return @@ -745,7 +745,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): indexes = [indexes] if len(indexes) == 1: - self._move_vertical_single(indexes[0], direction) + self._move_horizontal_single(indexes[0], direction) return items_by_id = {} @@ -802,9 +802,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): for item in items: index = self.index_for_item(item) - self._move_vertical_single(index, direction) + self._move_horizontal_single(index, direction) - def _move_horizontal_single(self, index, direction): + def _move_vertical_single(self, index, direction): if not index.isValid(): return @@ -891,7 +891,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.index_moved.emit(index) - def move_horizontal(self, indexes, direction): + def move_vertical(self, indexes, direction): if not indexes: return @@ -899,7 +899,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): indexes = [indexes] if len(indexes) == 1: - self._move_horizontal_single(indexes[0], direction) + self._move_vertical_single(indexes[0], direction) return items_by_id = {} @@ -929,7 +929,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): for item in items: index = self.index_for_item(item) - self._move_horizontal_single(index, direction) + self._move_vertical_single(index, direction) def child_removed(self, child): self._items_by_id.pop(child.id, None) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index a2a9fc1666..4a1f4998a8 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -324,19 +324,19 @@ class HierarchyView(QtWidgets.QTreeView): def _on_up_ctrl_pressed(self): indexes = self.selectedIndexes() - self._source_model.move_horizontal(indexes, -1) + self._source_model.move_vertical(indexes, -1) def _on_down_ctrl_pressed(self): indexes = self.selectedIndexes() - self._source_model.move_horizontal(indexes, 1) + self._source_model.move_vertical(indexes, 1) def _on_left_ctrl_pressed(self): indexes = self.selectedIndexes() - self._source_model.move_vertical(indexes, -1) + self._source_model.move_horizontal(indexes, -1) def _on_right_ctrl_pressed(self): indexes = self.selectedIndexes() - self._source_model.move_vertical(indexes, 1) + self._source_model.move_horizontal(indexes, 1) def _on_enter_pressed(self): index = self.currentIndex() From d01cd88a1be997c8a52a1ea029aa74f443b93cc1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 17:00:48 +0200 Subject: [PATCH 132/311] change `key` argument order for `data` and `setData` methods --- .../project_manager/project_manager/model.py | 84 +++++++++---------- .../project_manager/project_manager/view.py | 4 +- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 435b6e2538..b44e8786aa 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -238,7 +238,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): while not non_modifiable_queue.empty(): item_id = non_modifiable_queue.get() item = self._items_by_id[item_id] - item.setData(None, False, HIERARCHY_CHANGE_ABLE_ROLE) + item.setData(False, HIERARCHY_CHANGE_ABLE_ROLE) parent = item.parent() if ( @@ -281,7 +281,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): key = self.columns[column] item = index.internalPointer() - return item.data(key, role) + return item.data(role, key) def setData(self, index, value, role=QtCore.Qt.EditRole): if not index.isValid(): @@ -296,7 +296,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): ): self._rename_asset(item, value) - result = item.setData(key, value, role) + result = item.setData(value, role, key) if result: self.dataChanged.emit(index, index, [role]) @@ -397,7 +397,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if parent is None: parent = self._root_item - if parent.data(None, REMOVED_ROLE): + if parent.data(REMOVED_ROLE): return [] if start_row is None: @@ -416,7 +416,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): parent.add_child(item, row) if isinstance(item, AssetItem): - name = item.data("name", QtCore.Qt.DisplayRole) + name = item.data(QtCore.Qt.DisplayRole, "name") self._asset_items_by_name[name].append(item) if item.id not in self._items_by_id: @@ -444,7 +444,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not isinstance(item, (AssetItem, TaskItem)): continue - if item.data(None, REMOVED_ROLE): + if item.data(REMOVED_ROLE): remove_tag_items_by_id[item_id] = item for item in tuple(remove_tag_items_by_id.values()): @@ -456,7 +456,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if parent.id in remove_tag_items_by_id: continue - if parent.data(None, REMOVED_ROLE): + if parent.data(REMOVED_ROLE): remove_tag_items_by_id[parent.id] = parent parent = parent.parent() @@ -474,7 +474,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): _children_recursion(item, remove_tag_items_by_id) for item in remove_tag_items_by_id.values(): - item.setData(None, False, REMOVED_ROLE) + item.setData(False, REMOVED_ROLE) def remove_index(self, index): return self.remove_indexes([index]) @@ -502,7 +502,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._remove_item(item) def _remove_item(self, item): - is_removed = item.data(None, REMOVED_ROLE) + is_removed = item.data(REMOVED_ROLE) if is_removed: return @@ -516,15 +516,15 @@ class HierarchyModel(QtCore.QAbstractItemModel): _all_descendants[parent_item.id][cur_item.id] = cur_item if isinstance(cur_item, TaskItem): - was_removed = cur_item.data(None, REMOVED_ROLE) + was_removed = cur_item.data(REMOVED_ROLE) task_removed = True if not was_removed and parent_item is not None: - task_removed = parent_item.data(None, REMOVED_ROLE) + task_removed = parent_item.data(REMOVED_ROLE) if not was_removed: - cur_item.setData(None, task_removed, REMOVED_ROLE) + cur_item.setData(task_removed, REMOVED_ROLE) return task_removed - remove_item = cur_item.data(None, HIERARCHY_CHANGE_ABLE_ROLE) + remove_item = cur_item.data(HIERARCHY_CHANGE_ABLE_ROLE) task_children = [] for row in range(cur_item.rowCount()): child_item = cur_item.child(row) @@ -536,7 +536,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): remove_item = False if remove_item: - cur_item.setData(None, True, REMOVED_ROLE) + cur_item.setData(True, REMOVED_ROLE) for task_item in task_children: _fill_children(_all_descendants, task_item, cur_item) @@ -576,7 +576,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): chilren_by_row[row] = child_item children.pop(child_item.id) - remove_item = child_item.data(None, REMOVED_ROLE) + remove_item = child_item.data(REMOVED_ROLE) if not remove_item or not child_item.is_new: modified_children.append(child_item) if end_row is not None: @@ -617,7 +617,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not isinstance(asset_item, AssetItem): return - prev_name = asset_item.data("name", QtCore.Qt.DisplayRole) + prev_name = asset_item.data(QtCore.Qt.DisplayRole, "name") if prev_name == new_name: return @@ -981,7 +981,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if item.is_new: insert_list.append(item) - elif item.data(None, REMOVED_ROLE): + elif item.data(REMOVED_ROLE): bulk_writes.append(UpdateOne( {"_id": item.asset_id}, {"$set": {"type": "archived_asset"}} @@ -1075,7 +1075,7 @@ class BaseItem: self._global_data[role] = value return True - def data(self, key, role): + def data(self, role, key=None): value = self._get_global_data(role) if value is not self._None: return value @@ -1091,14 +1091,14 @@ class BaseItem: if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): value = self._data[key] if value is None: - value = self.parent().data(key, role) + value = self.parent().data(role, key) return value if role == QtCore.Qt.DecorationRole and key == "name": return self.name_icon() return None - def setData(self, key, value, role): + def setData(self, value, role, key=None): if role == DUPLICATED_ROLE: if value == self._is_duplicated: return False @@ -1389,8 +1389,8 @@ class AssetItem(BaseItem): ) doc = { - "name": self.data("name", QtCore.Qt.DisplayRole), - "type": self.data("type", QtCore.Qt.DisplayRole), + "name": self.data(QtCore.Qt.DisplayRole, "name"), + "type": self.data(QtCore.Qt.DisplayRole, "type"), "schema": schema_name, "data": doc_data, "parent": self.project_id @@ -1402,7 +1402,7 @@ class AssetItem(BaseItem): if key in doc: continue # Use `data` method to get inherited values - doc_data[key] = self.data(key, QtCore.Qt.DisplayRole) + doc_data[key] = self.data(QtCore.Qt.DisplayRole, key) return doc @@ -1480,16 +1480,16 @@ class AssetItem(BaseItem): ) return super(AssetItem, self)._get_global_data(role) - def data(self, key, role): + def data(self, role, key=None): if role == QtCore.Qt.BackgroundRole: if self._removed: return QtGui.QColor(255, 0, 0, 127) elif self.is_new: return QtGui.QColor(0, 255, 0, 127) - return super(AssetItem, self).data(key, role) + return super(AssetItem, self).data(role, key) - def setData(self, key, value, role): + def setData(self, value, role, key=None): if role == REMOVED_ROLE: self._removed = value return True @@ -1506,7 +1506,7 @@ class AssetItem(BaseItem): and not self._hierarchy_changes_enabled ): return False - return super(AssetItem, self).setData(key, value, role) + return super(AssetItem, self).setData(value, role, key) def flags(self, key): if key == "name": @@ -1517,18 +1517,18 @@ class AssetItem(BaseItem): return super(AssetItem, self).flags(key) def _add_task(self, item): - name = item.data("name", QtCore.Qt.DisplayRole).lower() - item_id = item.data(None, IDENTIFIER_ROLE) + name = item.data(QtCore.Qt.DisplayRole, "name").lower() + item_id = item.data(IDENTIFIER_ROLE) self._task_name_by_item_id[item_id] = name self._task_items_by_name[name].append(item) if len(self._task_items_by_name[name]) > 1: self._duplicated_task_names.add(name) for _item in self._task_items_by_name[name]: - _item.setData(None, True, DUPLICATED_ROLE) + _item.setData(True, DUPLICATED_ROLE) def _remove_task(self, item): - item_id = item.data(None, IDENTIFIER_ROLE) + item_id = item.data(IDENTIFIER_ROLE) name = self._task_name_by_item_id[item_id] self._task_name_by_item_id.pop(item_id) @@ -1539,11 +1539,11 @@ class AssetItem(BaseItem): elif len(self._task_items_by_name[name]) == 1: self._duplicated_task_names.remove(name) for _item in self._task_items_by_name[name]: - _item.setData(None, False, DUPLICATED_ROLE) + _item.setData(False, DUPLICATED_ROLE) def _rename_task(self, item): - new_name = item.data("name", QtCore.Qt.DisplayRole).lower() - item_id = item.data(None, IDENTIFIER_ROLE) + new_name = item.data(QtCore.Qt.DisplayRole, "name").lower() + item_id = item.data(IDENTIFIER_ROLE) prev_name = self._task_name_by_item_id[item_id] if new_name == prev_name: return @@ -1556,16 +1556,16 @@ class AssetItem(BaseItem): elif len(self._task_items_by_name[prev_name]) == 1: self._duplicated_task_names.remove(prev_name) for _item in self._task_items_by_name[prev_name]: - _item.setData(None, False, DUPLICATED_ROLE) + _item.setData(False, DUPLICATED_ROLE) # Add to new name mapping self._task_items_by_name[new_name].append(item) if len(self._task_items_by_name[new_name]) > 1: self._duplicated_task_names.add(new_name) for _item in self._task_items_by_name[new_name]: - _item.setData(None, True, DUPLICATED_ROLE) + _item.setData(True, DUPLICATED_ROLE) else: - item.setData(None, False, DUPLICATED_ROLE) + item.setData(False, DUPLICATED_ROLE) self._task_name_by_item_id[item_id] = new_name @@ -1640,12 +1640,12 @@ class TaskItem(BaseItem): return {} data = copy.deepcopy(self._data) data.pop("name") - name = self.data("name", QtCore.Qt.DisplayRole) + name = self.data(QtCore.Qt.DisplayRole, "name") return { name: data } - def data(self, key, role): + def data(self, role, key=None): if role == QtCore.Qt.BackgroundRole: if self._removed: return QtGui.QColor(255, 0, 0, 127) @@ -1658,14 +1658,14 @@ class TaskItem(BaseItem): and key == "name" ): return self._data[key] or self._data["type"] or "< Select Type >" - return super(TaskItem, self).data(key, role) + return super(TaskItem, self).data(role, key) - def setData(self, key, value, role): + def setData(self, value, role, key=None): if role == REMOVED_ROLE: self._removed = value return True - result = super(TaskItem, self).setData(key, value, role) + result = super(TaskItem, self).setData(value, role, key) if ( key == "name" diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 4a1f4998a8..32eff23209 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -372,7 +372,7 @@ class HierarchyView(QtWidgets.QTreeView): item_ids = tuple(items_by_id.keys()) if len(item_ids) == 1: item = items_by_id[item_ids[0]] - item_type = item.data(None, ITEM_TYPE_ROLE) + item_type = item.data(ITEM_TYPE_ROLE) if item_type in ("asset", "project"): add_asset_action = QtWidgets.QAction("Add Asset", context_menu) add_asset_action.triggered.connect( @@ -390,7 +390,7 @@ class HierarchyView(QtWidgets.QTreeView): # Remove delete tag on items removed_item_ids = [] for item_id, item in items_by_id.items(): - if item.data(None, REMOVED_ROLE): + if item.data(REMOVED_ROLE): removed_item_ids.append(item_id) if removed_item_ids: From 0c779b7ed2e068cfc91ae073a4162ac7212fa157 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 17:28:48 +0200 Subject: [PATCH 133/311] not possible to move with deleted or not hierachy changeable items --- .../project_manager/project_manager/model.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b44e8786aa..0ca958f83f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -659,6 +659,15 @@ class HierarchyModel(QtCore.QAbstractItemModel): if isinstance(item, (RootItem, ProjectItem)): return + if item.data(REMOVED_ROLE): + return + + if ( + isinstance(item, AssetItem) + and not item.data(HIERARCHY_CHANGE_ABLE_ROLE) + ): + return + if abs(direction) != 1: return @@ -813,6 +822,15 @@ class HierarchyModel(QtCore.QAbstractItemModel): if isinstance(item, (RootItem, ProjectItem)): return + if item.data(REMOVED_ROLE): + return + + if ( + isinstance(item, AssetItem) + and not item.data(HIERARCHY_CHANGE_ABLE_ROLE) + ): + return + if abs(direction) != 1: return From 31077bdc5a63cfd8ba6b303dbb91163baa4fddda Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 May 2021 17:29:29 +0200 Subject: [PATCH 134/311] added restrictions to vertical movement into removed items --- .../project_manager/project_manager/model.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 0ca958f83f..b375bba22f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -697,22 +697,31 @@ class HierarchyModel(QtCore.QAbstractItemModel): item_row = item.row() dst_parent = None for row in reversed(range(item_row)): - if row == item_row: - continue _item = src_parent.child(row) - if not isinstance(_item, TaskItem): + if not isinstance(_item, AssetItem): + continue + + if _item.data(REMOVED_ROLE): + continue + + dst_parent = _item + break + + _next_row = item_row + 1 + if dst_parent is None and _next_row < src_row_count: + for row in range(_next_row, src_row_count): + _item = src_parent.child(row) + if not isinstance(_item, AssetItem): + continue + + if _item.data(REMOVED_ROLE): + continue + dst_parent = _item break if dst_parent is None: - for row in range(item_row + 1, src_row_count + 2): - _item = src_parent.child(row) - if not isinstance(_item, TaskItem): - dst_parent = _item - break - - if dst_parent is None: - return + return dst_row = dst_parent.rowCount() From dab7a53fb48c97fc42894935c723ff23fbc7e286 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 11:42:41 +0200 Subject: [PATCH 135/311] vertical movement can go to next parents across the board --- .../project_manager/project_manager/model.py | 89 ++++++++++++++++--- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b375bba22f..7626ef4199 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -844,11 +844,22 @@ class HierarchyModel(QtCore.QAbstractItemModel): return src_parent = item.parent() + if not isinstance(src_parent, AssetItem): + return + src_parent_index = self.index_from_item( src_parent.row(), 0, src_parent.parent() ) source_row = item.row() + parent_items = [] + parent = src_parent + while True: + parent = parent.parent() + parent_items.insert(0, parent) + if isinstance(parent, ProjectItem): + break + dst_parent = None dst_parent_index = None destination_row = None @@ -874,20 +885,71 @@ class HierarchyModel(QtCore.QAbstractItemModel): # Up elif direction == -1: - if source_row > 0: - dst_parent_index = src_parent_index - dst_parent = src_parent - destination_row = source_row - 1 - else: - parent_parent = src_parent.parent() - if not parent_parent: + current_idxs = [] + for parent_item in parent_items: + if not isinstance(parent_item, ProjectItem): + current_idxs.append(parent_item.row()) + current_idxs.append(src_parent.row()) + + max_idxs = [0 for _ in current_idxs] + indexes_len = len(current_idxs) + + while True: + if current_idxs == max_idxs: return - previous_parent = parent_parent.child(src_parent.row() - 1) - if not previous_parent: + def _update_parents( + _current_idx, _parent_items, _current_idxs, top=True + ): + if _current_idx < 0: + return False + + if _current_idxs[_current_idx] == 0: + if not _update_parents( + _current_idx - 1, _parent_items, _current_idxs, False + ): + return False + + parent = _parent_items[_current_idx] + row_count = 0 + if parent is not None: + row_count = parent.rowCount() + _current_idxs[_current_idx] = row_count + return True + if top: + return True + + _current_idxs[_current_idx] -= 1 + parent_item = _parent_items[_current_idx] + new_item = parent_item.child(_current_idxs[_current_idx]) + _parent_items[_current_idx + 1] = new_item + + return True + + updated = _update_parents( + indexes_len - 1, parent_items, current_idxs + ) + if not updated: return - dst_parent = previous_parent - destination_row = previous_parent.rowCount() + + parent_item = parent_items[-1] + row_count = current_idxs[-1] + current_idxs[-1] = 0 + for row in reversed(range(row_count)): + child_item = parent_item.child(row) + if ( + child_item is src_parent + or child_item.data(REMOVED_ROLE) + or not isinstance(child_item, AssetItem) + ): + continue + + dst_parent = child_item + destination_row = dst_parent.rowCount() + break + + if dst_parent is not None: + break if dst_parent_index is None: dst_parent_index = self.index_from_item( @@ -916,7 +978,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endMoveRows() - self.index_moved.emit(index) + new_index = self.index( + _destination_row, index.column(), dst_parent_index + ) + self.index_moved.emit(new_index) def move_vertical(self, indexes, direction): if not indexes: From 1bdae76a96a40b5199cd220cf5389c1d98945f97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 11:50:42 +0200 Subject: [PATCH 136/311] simplified parent resolving function --- .../project_manager/project_manager/model.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 7626ef4199..4e0139a8d1 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -898,37 +898,31 @@ class HierarchyModel(QtCore.QAbstractItemModel): if current_idxs == max_idxs: return - def _update_parents( - _current_idx, _parent_items, _current_idxs, top=True - ): + def _update_parents(_current_idx, top=True): if _current_idx < 0: return False - if _current_idxs[_current_idx] == 0: - if not _update_parents( - _current_idx - 1, _parent_items, _current_idxs, False - ): + if current_idxs[_current_idx] == 0: + if not _update_parents(_current_idx - 1, False): return False - parent = _parent_items[_current_idx] + parent = parent_items[_current_idx] row_count = 0 if parent is not None: row_count = parent.rowCount() - _current_idxs[_current_idx] = row_count + current_idxs[_current_idx] = row_count return True if top: return True - _current_idxs[_current_idx] -= 1 - parent_item = _parent_items[_current_idx] - new_item = parent_item.child(_current_idxs[_current_idx]) - _parent_items[_current_idx + 1] = new_item + current_idxs[_current_idx] -= 1 + parent_item = parent_items[_current_idx] + new_item = parent_item.child(current_idxs[_current_idx]) + parent_items[_current_idx + 1] = new_item return True - updated = _update_parents( - indexes_len - 1, parent_items, current_idxs - ) + updated = _update_parents(indexes_len - 1) if not updated: return From 1e59f2f91903fa5a61c86848f1ba2c59d940c47a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 12:10:07 +0200 Subject: [PATCH 137/311] it is possible to jump with items in both ways --- .../project_manager/project_manager/model.py | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 4e0139a8d1..86f889ae95 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -861,27 +861,67 @@ class HierarchyModel(QtCore.QAbstractItemModel): break dst_parent = None - dst_parent_index = None - destination_row = None - _destination_row = None # Down if direction == 1: - if source_row < src_parent.rowCount() - 1: - dst_parent_index = src_parent_index - dst_parent = src_parent - destination_row = source_row + 1 - # This row is not row number after moving but before moving - _destination_row = destination_row + 1 - else: - destination_row = 0 - parent_parent = src_parent.parent() - if not parent_parent: + current_idxs = [] + current_max_idxs = [] + for parent_item in parent_items: + current_max_idxs.append(parent_item.rowCount()) + if not isinstance(parent_item, ProjectItem): + current_idxs.append(parent_item.row()) + current_idxs.append(src_parent.row()) + indexes_len = len(current_idxs) + + while True: + def _update_parents(idx, top=True): + if idx < 0: + return False + + if current_max_idxs[idx] == current_idxs[idx]: + if not _update_parents(idx - 1, False): + return False + + parent = parent_items[idx] + row_count = 0 + if parent is not None: + row_count = parent.rowCount() + current_max_idxs[idx] = row_count + current_idxs[idx] = 0 + return True + + if top: + return True + + current_idxs[idx] += 1 + parent_item = parent_items[idx] + new_item = parent_item.child(current_idxs[idx]) + parent_items[idx + 1] = new_item + + return True + + updated = _update_parents(indexes_len - 1) + if not updated: return - new_parent = parent_parent.child(src_parent.row() + 1) - if not new_parent: - return - dst_parent = new_parent + start = current_idxs[-1] + end = current_max_idxs[-1] + current_idxs[-1] = current_max_idxs[-1] + parent = parent_items[-1] + for row in range(start, end): + child_item = parent.child(row) + if ( + child_item is src_parent + or child_item.data(REMOVED_ROLE) + or not isinstance(child_item, AssetItem) + ): + continue + + dst_parent = child_item + destination_row = 0 + break + + if dst_parent is not None: + break # Up elif direction == -1: @@ -945,20 +985,19 @@ class HierarchyModel(QtCore.QAbstractItemModel): if dst_parent is not None: break - if dst_parent_index is None: - dst_parent_index = self.index_from_item( - dst_parent.row(), 0, dst_parent.parent() - ) + if dst_parent is None: + return - if _destination_row is None: - _destination_row = destination_row + dst_parent_index = self.index_from_item( + dst_parent.row(), 0, dst_parent.parent() + ) self.beginMoveRows( src_parent_index, source_row, source_row, dst_parent_index, - _destination_row + destination_row ) if src_parent is dst_parent: @@ -973,7 +1012,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endMoveRows() new_index = self.index( - _destination_row, index.column(), dst_parent_index + destination_row, index.column(), dst_parent_index ) self.index_moved.emit(new_index) From d3f66d28d6b352347d809c801c86b8e1a9315067 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 12:10:24 +0200 Subject: [PATCH 138/311] easier horizontal movement --- .../tools/project_manager/project_manager/model.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 86f889ae95..98952c8b73 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -680,7 +680,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): dst_row = None dst_parent = None - dst_parent_index = None if direction == -1: if isinstance(src_parent, (RootItem, ProjectItem)): @@ -734,10 +733,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): ): return - if dst_parent_index is None: - dst_parent_index = self.index_from_item( - dst_parent.row(), 0, dst_parent.parent() - ) + dst_parent_index = self.index_from_item( + dst_parent.row(), 0, dst_parent.parent() + ) self.beginMoveRows( src_parent_index, @@ -753,7 +751,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.endMoveRows() - self.index_moved.emit(index) + new_index = self.index(dst_row, index.column(), dst_parent_index) + self.index_moved.emit(new_index) def move_horizontal(self, indexes, direction): if not indexes: From e999b1a8c4c2e854f6f0a382aef5e25f69c93542 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 12:10:37 +0200 Subject: [PATCH 139/311] fix task remove and name validations --- openpype/tools/project_manager/project_manager/model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 98952c8b73..8c70c4e9fe 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1650,13 +1650,14 @@ class AssetItem(BaseItem): self._duplicated_task_names.add(name) for _item in self._task_items_by_name[name]: _item.setData(True, DUPLICATED_ROLE) + elif item.data(DUPLICATED_ROLE): + item.setData(False, DUPLICATED_ROLE) def _remove_task(self, item): item_id = item.data(IDENTIFIER_ROLE) - name = self._task_name_by_item_id[item_id] - self._task_name_by_item_id.pop(item_id) - self._task_items_by_name[name].append(item) + name = self._task_name_by_item_id.pop(item_id) + self._task_items_by_name[name].remove(item) if not self._task_items_by_name[name]: self._task_items_by_name.pop(name) From 8a1028912bfdcbc02c13e2890b71ddaca8880618 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 12:43:29 +0200 Subject: [PATCH 140/311] return no flags if item is None --- openpype/tools/project_manager/project_manager/model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 8c70c4e9fe..20162847f2 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -313,6 +313,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): def flags(self, index): item = index.internalPointer() + if item is None: + return QtCore.Qt.NoItemFlags column = index.column() key = self.columns[column] return item.flags(key) From 9268f5b8e05565411dfd06d2acd1f8f0572e8462 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 12:56:02 +0200 Subject: [PATCH 141/311] jump from type to name index on type commit --- openpype/tools/project_manager/project_manager/view.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 32eff23209..a1763d3067 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -162,7 +162,15 @@ class HierarchyView(QtWidgets.QTreeView): column = current_index.column() row = current_index.row() skipped_index = None - if column > 0: + # Change column from "type" to "name" + if column == 1: + new_index = self._source_model.index( + current_index.row(), + 0, + current_index.parent() + ) + self.setCurrentIndex(new_index) + elif column > 0: indexes = [] for index in self.selectedIndexes(): if index.column() == column: From 9042e6689437f187ae4e0755196e4531856ae417 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 12:56:36 +0200 Subject: [PATCH 142/311] change method name to match plural --- openpype/tools/project_manager/project_manager/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index a1763d3067..c1f416a484 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -238,7 +238,7 @@ class HierarchyView(QtWidgets.QTreeView): def keyPressEvent(self, event): call_super = False if event.key() == QtCore.Qt.Key_Delete: - self._delete_item() + self._delete_items() elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): mdfs = event.modifiers() @@ -267,7 +267,7 @@ class HierarchyView(QtWidgets.QTreeView): else: event.accept() - def _delete_item(self, indexes=None): + def _delete_items(self, indexes=None): if indexes is None: indexes = self.selectedIndexes() self._source_model.remove_indexes(indexes) From f6019b2bb14ecdeccc95793d3bfddd35136981e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 13:53:10 +0200 Subject: [PATCH 143/311] added collapse and expand actions --- .../project_manager/project_manager/view.py | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index c1f416a484..042fae6074 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -1,4 +1,6 @@ -from Qt import QtWidgets, QtCore +from queue import Queue + +from Qt import QtWidgets, QtCore, QtGui from .delegates import ( NumberDelegate, @@ -357,6 +359,52 @@ class HierarchyView(QtWidgets.QTreeView): def _remove_delete_flag(self, item_ids): self._source_model.remove_delete_flag(item_ids) + def _expand_items(self, indexes): + item_ids = set() + process_queue = Queue() + for index in indexes: + if index.column() == 0: + process_queue.put(index) + + while not process_queue.empty(): + index = process_queue.get() + item_id = index.data(IDENTIFIER_ROLE) + if item_id in item_ids: + continue + item_ids.add(item_id) + + item = self._source_model._items_by_id[item_id] + if not self.isExpanded(index): + self.expand(index) + + for row in range(item.rowCount()): + process_queue.put(self._source_model.index( + row, 0, index + )) + + def _collapse_items(self, indexes): + item_ids = set() + process_queue = Queue() + for index in indexes: + if index.column() == 0: + process_queue.put(index) + + while not process_queue.empty(): + index = process_queue.get() + item_id = index.data(IDENTIFIER_ROLE) + if item_id in item_ids: + continue + item_ids.add(item_id) + + item = self._source_model._items_by_id[item_id] + if self.isExpanded(index): + self.collapse(index) + + for row in range(item.rowCount()): + process_queue.put(self._source_model.index( + row, 0, index + )) + def _on_context_menu(self, point): index = self.indexAt(point) column = index.column() @@ -408,6 +456,27 @@ class HierarchyView(QtWidgets.QTreeView): ) actions.append(action) + # Collapse/Expand action + show_collapse_expand_action = False + for item_id in item_ids: + item = items_by_id[item_ids[0]] + item_type = item.data(ITEM_TYPE_ROLE) + if item_type != "task": + show_collapse_expand_action = True + break + + if show_collapse_expand_action: + expand_action = QtWidgets.QAction("Expand all", context_menu) + collapse_action = QtWidgets.QAction("Collapse all", context_menu) + expand_action.triggered.connect( + lambda: self._expand_items(indexes) + ) + collapse_action.triggered.connect( + lambda: self._collapse_items(indexes) + ) + actions.append(expand_action) + actions.append(collapse_action) + if not actions: return From 64c6414367c14fe7b39cceff56e8547b590a478c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 14:17:40 +0200 Subject: [PATCH 144/311] implemented copy/paste of tasks --- .../project_manager/project_manager/model.py | 83 ++++++++++++++++++- .../project_manager/project_manager/view.py | 18 ++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 20162847f2..4cb8ec6b90 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1,5 +1,6 @@ import collections import copy +import json from queue import Queue from uuid import uuid4 @@ -1135,6 +1136,75 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.refresh_project() + def copy_mime_data(self, indexes): + items = [] + processed_ids = set() + for index in indexes: + if not index.isValid(): + continue + item_id = index.data(IDENTIFIER_ROLE) + if item_id in processed_ids: + continue + processed_ids.add(item_id) + item = self._items_by_id[item_id] + items.append(item) + + parent_item = None + for item in items: + if not isinstance(item, TaskItem): + raise ValueError("Can copy only tasks") + + if parent_item is None: + parent_item = item.parent() + elif item.parent() is not parent_item: + raise ValueError("Can copy only tasks from same parent") + + data = [] + for task_item in items: + data.append(task_item.to_json_data()) + + encoded_data = QtCore.QByteArray() + stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.WriteOnly) + stream.writeQString(json.dumps(data)) + mimedata = QtCore.QMimeData() + mimedata.setData("application/copy_task", encoded_data) + return mimedata + + def paste_mime_data(self, index, mime_data): + if not index.isValid(): + return + + item_id = index.data(IDENTIFIER_ROLE) + item = self._items_by_id[item_id] + if not isinstance(item, (AssetItem, TaskItem)): + return + + raw_data = mime_data.data("application/copy_task") + encoded_data = QtCore.QByteArray.fromRawData(raw_data) + stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.ReadOnly) + text = stream.readQString() + try: + data = json.loads(text) + except Exception: + data = [] + + if not data: + return + + if isinstance(item, TaskItem): + parent = item.parent() + else: + parent = item + + for task_item_data in data: + task_data = {} + for name, data in task_item_data.items(): + task_data = data + task_data["name"] = name + + task_item = TaskItem(task_data, True) + self.add_item(task_item, parent) + class BaseItem: columns = [] @@ -1730,9 +1800,11 @@ class TaskItem(BaseItem): "type" } - def __init__(self, data=None): + def __init__(self, data=None, is_new=None): self._removed = False - self._is_new = data is None + if is_new is None: + is_new = data is None + self._is_new = is_new if data is None: data = {} @@ -1801,3 +1873,10 @@ class TaskItem(BaseItem): self.parent().on_task_name_change(self) return result + + def to_json_data(self): + """Convert json data without parent reference. + + Method used for mime data on copy/paste + """ + return self.to_doc_data() diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 042fae6074..236a62425b 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -242,6 +242,12 @@ class HierarchyView(QtWidgets.QTreeView): if event.key() == QtCore.Qt.Key_Delete: self._delete_items() + elif event.matches(QtGui.QKeySequence.Copy): + self._copy_items() + + elif event.matches(QtGui.QKeySequence.Paste): + self._paste_items() + elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): mdfs = event.modifiers() if mdfs == (QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier): @@ -269,6 +275,18 @@ class HierarchyView(QtWidgets.QTreeView): else: event.accept() + def _copy_items(self, indexes=None): + if indexes is None: + indexes = self.selectedIndexes() + mime_data = self._source_model.copy_mime_data(indexes) + + QtWidgets.QApplication.clipboard().setMimeData(mime_data) + + def _paste_items(self): + index = self.currentIndex() + mime_data = QtWidgets.QApplication.clipboard().mimeData() + self._source_model.paste_mime_data(index, mime_data) + def _delete_items(self, indexes=None): if indexes is None: indexes = self.selectedIndexes() From 6f68b9599121260d15dffdb39bc4fb5cb9a75ad4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 14:25:21 +0200 Subject: [PATCH 145/311] added basic message showing --- .../project_manager/project_manager/view.py | 23 ++++++++++++++----- .../project_manager/project_manager/window.py | 8 +++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 236a62425b..37c3423de5 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -98,9 +98,12 @@ class HierarchyView(QtWidgets.QTreeView): "tools_env" } - def __init__(self, dbcon, source_model, *args, **kwargs): - super(HierarchyView, self).__init__(*args, **kwargs) + def __init__(self, dbcon, source_model, parent): + super(HierarchyView, self).__init__(parent) + # Direct access to model self._source_model = source_model + # Access to parent because of `show_message` method + self._parent = parent project_doc_cache = ProjectDocCache(dbcon) tools_cache = ToolsCache() @@ -276,11 +279,15 @@ class HierarchyView(QtWidgets.QTreeView): event.accept() def _copy_items(self, indexes=None): - if indexes is None: - indexes = self.selectedIndexes() - mime_data = self._source_model.copy_mime_data(indexes) + try: + if indexes is None: + indexes = self.selectedIndexes() + mime_data = self._source_model.copy_mime_data(indexes) - QtWidgets.QApplication.clipboard().setMimeData(mime_data) + QtWidgets.QApplication.clipboard().setMimeData(mime_data) + self._show_message("Tasks copied") + except ValueError as exc: + self._show_message(str(exc)) def _paste_items(self): index = self.currentIndex() @@ -423,6 +430,10 @@ class HierarchyView(QtWidgets.QTreeView): row, 0, index )) + def _show_message(self, message): + """Show message to user.""" + self._parent.show_message(message) + def _on_context_menu(self, point): index = self.indexAt(point) column = index.column() diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index f2ad399ab5..d98cc0f801 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -52,10 +52,12 @@ class Window(QtWidgets.QWidget): ) buttons_widget = QtWidgets.QWidget(self) + message_label = QtWidgets.QLabel(buttons_widget) save_btn = QtWidgets.QPushButton("Save", buttons_widget) buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addWidget(message_label) buttons_layout.addStretch(1) buttons_layout.addWidget(save_btn) @@ -74,6 +76,8 @@ class Window(QtWidgets.QWidget): self.hierarchy_view = hierarchy_view self.hierarchy_model = hierarchy_model + self.message_label = message_label + self.resize(1200, 600) self.refresh_projects() @@ -106,3 +110,7 @@ class Window(QtWidgets.QWidget): def _on_save_click(self): self.hierarchy_model.save() + + def show_message(self, message): + # TODO add nicer message pop + self.message_label.setText(message) From 2d4a2188f21759ef2d2ecf216f7017acf4b3d4e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 14:42:57 +0200 Subject: [PATCH 146/311] fix variable usage --- openpype/tools/project_manager/project_manager/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 37c3423de5..cd6ee005dc 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -488,7 +488,7 @@ class HierarchyView(QtWidgets.QTreeView): # Collapse/Expand action show_collapse_expand_action = False for item_id in item_ids: - item = items_by_id[item_ids[0]] + item = items_by_id[item_id] item_type = item.data(ITEM_TYPE_ROLE) if item_type != "task": show_collapse_expand_action = True From 275bf78be28dc381051461c1ddfe077e3288c88e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 14:43:14 +0200 Subject: [PATCH 147/311] faster remove deletion tag --- .../project_manager/project_manager/model.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 4cb8ec6b90..530fee3528 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -441,43 +441,46 @@ class HierarchyModel(QtCore.QAbstractItemModel): return None def remove_delete_flag(self, item_ids, with_children=True): - remove_tag_items_by_id = {} + items_by_id = {} for item_id in item_ids: - item = self.items_by_id[item_id] - if not isinstance(item, (AssetItem, TaskItem)): + if item_id in items_by_id: continue - if item.data(REMOVED_ROLE): - remove_tag_items_by_id[item_id] = item + item = self.items_by_id[item_id] + if isinstance(item, (AssetItem, TaskItem)): + items_by_id[item_id] = item - for item in tuple(remove_tag_items_by_id.values()): + for item in tuple(items_by_id.values()): parent = item.parent() while True: if not isinstance(parent, (AssetItem, TaskItem)): break - if parent.id in remove_tag_items_by_id: - continue - - if parent.data(REMOVED_ROLE): - remove_tag_items_by_id[parent.id] = parent + if parent.id not in items_by_id: + items_by_id[parent.id] = parent parent = parent.parent() if not with_children: continue - def _children_recursion(_item, store_obj): - if isinstance(_item, AssetItem): - for row in range(_item.rowCount()): - _child_item = _item.child(row) - if _child_item.id not in store_obj: - store_obj[_child_item.id] = _child_item - _children_recursion(_child_item, store_obj) - _children_recursion(item, remove_tag_items_by_id) + def _children_recursion(_item): + if not isinstance(_item, AssetItem): + return - for item in remove_tag_items_by_id.values(): - item.setData(False, REMOVED_ROLE) + for row in range(_item.rowCount()): + _child_item = _item.child(row) + if _child_item.id in items_by_id: + continue + + items_by_id[_child_item.id] = _child_item + _children_recursion(_child_item) + + _children_recursion(item) + + for item in items_by_id.values(): + if item.data(REMOVED_ROLE): + item.setData(False, REMOVED_ROLE) def remove_index(self, index): return self.remove_indexes([index]) From bc3c392ba59ef2d2bfa95fb055c03fd414807c2b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 15:36:53 +0200 Subject: [PATCH 148/311] renamed remove_index to delete_index --- openpype/tools/project_manager/project_manager/model.py | 6 +++--- openpype/tools/project_manager/project_manager/view.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 530fee3528..9525d808f8 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -482,10 +482,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): if item.data(REMOVED_ROLE): item.setData(False, REMOVED_ROLE) - def remove_index(self, index): - return self.remove_indexes([index]) + def delete_indexe(self, index): + return self.delete_indexes([index]) - def remove_indexes(self, indexes): + def delete_indexes(self, indexes): items_by_id = {} processed_ids = set() for index in indexes: diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index cd6ee005dc..7a5cdb72da 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -297,7 +297,7 @@ class HierarchyView(QtWidgets.QTreeView): def _delete_items(self, indexes=None): if indexes is None: indexes = self.selectedIndexes() - self._source_model.remove_indexes(indexes) + self._source_model.delete_indexes(indexes) def _on_ctrl_shift_enter_pressed(self): self._add_task() From 890f612c2eb620dfbb0aad152721747b84245f9a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:14:46 +0200 Subject: [PATCH 149/311] fixed typo --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9525d808f8..00cd22c720 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -482,7 +482,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if item.data(REMOVED_ROLE): item.setData(False, REMOVED_ROLE) - def delete_indexe(self, index): + def delete_index(self, index): return self.delete_indexes([index]) def delete_indexes(self, indexes): From 6b2548bc00394109680450d5796269ac70b343b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:15:17 +0200 Subject: [PATCH 150/311] implemented new item AddAssetItem --- .../project_manager/project_manager/model.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 00cd22c720..575aba8394 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1472,6 +1472,65 @@ class ProjectItem(BaseItem): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable +class AddAssetItem(BaseItem): + item_type = "add_asset" + columns = {"name"} + editable_columns = {"name"} + + def __init__(self, parent): + super(AddAssetItem, self).__init__() + self._parent = parent + + @classmethod + def name_icon(cls): + if cls._name_icon is None: + cls._name_icon = qtawesome.icon( + "fa.plus-circle", + color="#333333" + ) + return cls._name_icon + + def data(self, role, key=None): + if role == REMOVED_ROLE: + return True + + if role == HIERARCHY_CHANGE_ABLE_ROLE: + return True + + if key == "name": + if role == QtCore.Qt.DisplayRole: + return "Add Asset" + elif role == QtCore.Qt.EditRole: + return "" + return super(AddAssetItem, self).data(role, key) + + def setData(self, value, role, key=None): + if key == "name": + if not value: + return False + index = self.model().index_for_item(self) + new_index = self.model().add_new_asset(index) + self.model().setData(new_index, value, QtCore.Qt.EditRole) + return True + return super(AddAssetItem, self).setData(value, role, key) + + def flags(self, key): + if key != "name": + return QtCore.Qt.NoItemFlags + + return ( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsEditable + ) + + def add_child(self, item, row=None): + raise AssertionError("BUG: Can't add children to AddAssetItem") + + def remove_child(self, item): + raise AssertionError("BUG: Can't remove children from AddAssetItem") + + class AssetItem(BaseItem): item_type = "asset" From c1a7a18cea14bf50d3fd35793b7a7ec72652f3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:17:24 +0200 Subject: [PATCH 151/311] each AssetItem and ProjectItem have AddAssetItem --- openpype/tools/project_manager/project_manager/model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 575aba8394..89f20311ad 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1217,6 +1217,7 @@ class BaseItem: _name_icon = None _is_duplicated = False item_type = "base" + add_asset_item_visible = False _None = object() @@ -1435,6 +1436,8 @@ class ProjectItem(BaseItem): def __init__(self, project_doc): self._mongo_id = project_doc["_id"] + self.add_asset_item = AddAssetItem(self) + data = self.data_from_doc(project_doc) super(ProjectItem, self).__init__(data) @@ -1590,6 +1593,9 @@ class AssetItem(BaseItem): self.mongo_id = asset_doc.get("_id") self._project_id = None + self.add_asset_item_visible = False + self.add_asset_item = AddAssetItem(self) + # Item data self._hierarchy_changes_enabled = True self._removed = False From c4854c02ddbbcaef3e9de9b1bc51e1296f99e503 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:18:25 +0200 Subject: [PATCH 152/311] make sure AddAsseItem is visible --- .../tools/project_manager/project_manager/model.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 89f20311ad..24e766671f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -208,11 +208,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): non_modifiable_items = set() while not appending_queue.empty(): parent_id, parent_item = appending_queue.get() - if parent_id not in asset_docs_by_parent_id: - continue + asset_docs = asset_docs_by_parent_id.get(parent_id) or [] new_items = [] - asset_docs = asset_docs_by_parent_id[parent_id] for asset_doc in sorted(asset_docs, key=lambda item: item["name"]): # Create new Item new_item = AssetItem(asset_doc) @@ -228,7 +226,11 @@ class HierarchyModel(QtCore.QAbstractItemModel): # Add item to appending queue appending_queue.put((asset_id, new_item)) - self.add_items(new_items, parent_item) + if isinstance(parent_item, (ProjectItem, AssetItem)): + new_items.append(parent_item.add_asset_item) + + if new_items: + self.add_items(new_items, parent_item) # Handle Asset's that are not modifiable # - pass the information to all it's parents @@ -262,6 +264,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): _task_data["name"] = task_name task_item = TaskItem(_task_data) task_items.append(task_item) + self.add_items(task_items, asset_item) def rowCount(self, parent=None): @@ -379,6 +382,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): if result is not None: self._validate_asset_duplicity(name) + if not new_child.add_asset_item_visible: + self.add_item(new_child.add_asset_item, new_child) + return result def add_new_task(self, parent_index): From 2e703231668acf17889e7bf572b82bb403d0891d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:19:13 +0200 Subject: [PATCH 153/311] fix model method on items --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 24e766671f..1117e505ad 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1252,7 +1252,7 @@ class BaseItem: return not self._is_duplicated def model(self): - return self._parent.model + return self._parent.model() def move_to(self, item, row): idx = self._children.index(item) From 9e40246514b2d8dbfa204a45959c1c107ea33617 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:19:42 +0200 Subject: [PATCH 154/311] AddAssetItem are skipped on remove --- openpype/tools/project_manager/project_manager/model.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 1117e505ad..fea76367a0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -504,7 +504,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): processed_ids.add(item_id) item = self._items_by_id[item_id] - if isinstance(item, (TaskItem, AssetItem)): + if isinstance(item, (TaskItem, AssetItem, AddAssetItem)): items_by_id[item_id] = item if not items_by_id: @@ -514,6 +514,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._remove_item(item) def _remove_item(self, item): + if isinstance(item, AddAssetItem): + return + is_removed = item.data(REMOVED_ROLE) if is_removed: return @@ -544,6 +547,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): task_children.append(child_item) continue + elif isinstance(child_item, AddAssetItem): + continue + if not _fill_children(_all_descendants, child_item, cur_item): remove_item = False From 8493274f5c26a2bca3638e080baf0f4b627f5977 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:20:22 +0200 Subject: [PATCH 155/311] AssetItem and ProjectItem know if it's add asset item is visible --- .../project_manager/project_manager/model.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index fea76367a0..67a3157cb2 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1486,6 +1486,18 @@ class ProjectItem(BaseItem): def flags(self, *args, **kwargs): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + def add_child(self, item, row=None): + if not self.add_asset_item_visible and item is self.add_asset_item: + self.add_asset_item_visible = True + + super(ProjectItem, self).add_child(item, row) + + def remove_child(self, item): + if self.add_asset_item_visible and item is self.add_asset_item: + self.add_asset_item_visible = False + + super(ProjectItem, self).remove_child(item) + class AddAssetItem(BaseItem): item_type = "add_asset" @@ -1855,14 +1867,20 @@ class AssetItem(BaseItem): super(AssetItem, self).add_child(item, row) - if isinstance(item, TaskItem): + if not self.add_asset_item_visible and item is self.add_asset_item: + self.add_asset_item_visible = True + + elif isinstance(item, TaskItem): self._add_task(item) def remove_child(self, item): if item not in self._children: return - if isinstance(item, TaskItem): + if self.add_asset_item_visible and item is self.add_asset_item: + self.add_asset_item_visible = False + + elif isinstance(item, TaskItem): self._remove_task(item) super(AssetItem, self).remove_child(item) From 1dd406722f963d1e44041f8aa53ae4ef32c57389 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:20:54 +0200 Subject: [PATCH 156/311] implemented delete_add_asset_item to remove AddAssetItem --- .../tools/project_manager/project_manager/model.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 67a3157cb2..9aa553b8a1 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -488,6 +488,20 @@ class HierarchyModel(QtCore.QAbstractItemModel): if item.data(REMOVED_ROLE): item.setData(False, REMOVED_ROLE) + def delete_add_asset_item(self, parent): + item = parent.add_asset_item + children = parent.children() + if item not in children: + return + parent_index = self.index_for_item(parent) + row = children.index(item) + + self.beginRemoveRows(parent_index, row, row) + + parent.remove_child(item) + + self.endRemoveRows() + def delete_index(self, index): return self.delete_indexes([index]) From a66a9245beb7ee6e08d0ca5bb93cd46afa9e79ba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:21:26 +0200 Subject: [PATCH 157/311] use one row less if AddAssetItem is visible on item --- openpype/tools/project_manager/project_manager/model.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9aa553b8a1..cc0cd1c3bd 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -412,9 +412,13 @@ class HierarchyModel(QtCore.QAbstractItemModel): if start_row is None: start_row = parent.rowCount() + if parent.add_asset_item_visible and start_row == parent.rowCount(): + start_row -= 1 + end_row = start_row + len(items) - 1 parent_index = self.index_from_item(parent.row(), 0, parent.parent()) + self.beginInsertRows(parent_index, start_row, end_row) for idx, item in enumerate(items): @@ -755,6 +759,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): return dst_row = dst_parent.rowCount() + if dst_parent.add_asset_item_visible: + dst_row -= 1 if src_parent is dst_parent: return @@ -1011,6 +1017,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): dst_parent = child_item destination_row = dst_parent.rowCount() + if dst_parent.add_asset_item_visible: + destination_row -= 1 break if dst_parent is not None: From 2e5bec225aa7f796d817f5e99df15edc5f452fd1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:22:02 +0200 Subject: [PATCH 158/311] AssetItem handle for adding removing AddAssetItem on change of removed role --- openpype/tools/project_manager/project_manager/model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index cc0cd1c3bd..79ef5e22e6 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1802,6 +1802,11 @@ class AssetItem(BaseItem): def setData(self, value, role, key=None): if role == REMOVED_ROLE: self._removed = value + if not value and not self.add_asset_item_visible: + self.model().add_item(self.add_asset_item, self) + elif value and self.add_asset_item_visible: + self.model().delete_add_asset_item(self) + return True if role == HIERARCHY_CHANGE_ABLE_ROLE: From 2a4b4ebdd24a5a38e476039bde290aebe686c667 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:26:21 +0200 Subject: [PATCH 159/311] HierarchicalModel has project_item property --- .../tools/project_manager/project_manager/model.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 79ef5e22e6..abacac0d32 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -138,6 +138,16 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._current_project = None self.set_project(project_name) + @property + def project_item(self): + output = None + for row in range(self._root_item.rowCount()): + item = self._root_item.child(row) + if isinstance(item, ProjectItem): + output = item + break + return output + def set_project(self, project_name): if self._current_project == project_name: return From 300c7a2c308f95ef0969e763004fb4575ab2c3a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:26:45 +0200 Subject: [PATCH 160/311] HierarchicalView collapse all except project on project load --- openpype/tools/project_manager/project_manager/view.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 7a5cdb72da..b2f21aaffd 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -156,6 +156,13 @@ class HierarchyView(QtWidgets.QTreeView): # Trigger update of model after all data for delegates are filled self._source_model.set_project(project_name) + self.collapseAll() + + project_item = self._source_model.project_item + if project_item: + index = self._source_model.index_for_item(project_item) + self.expand(index) + def _on_rows_moved(self, index): parent_index = index.parent() if not self.isExpanded(parent_index): From f053323d2e1368d87bca9b9c12787f8c418c3441 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 16:38:26 +0200 Subject: [PATCH 161/311] keep name resized to content --- .../project_manager/project_manager/window.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index d98cc0f801..25cb7c9975 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -47,9 +47,17 @@ class Window(QtWidgets.QWidget): header = hierarchy_view.header() header.setStretchLastSection(False) - header.setSectionResizeMode( - header.logicalIndex(0), QtWidgets.QHeaderView.Stretch - ) + for idx in range(header.count()): + logical_index = header.logicalIndex(idx) + if idx == 0: + header.setSectionResizeMode( + logical_index, QtWidgets.QHeaderView.Stretch + ) + else: + header.setSectionResizeMode( + logical_index, QtWidgets.QHeaderView.ResizeToContents + ) + buttons_widget = QtWidgets.QWidget(self) message_label = QtWidgets.QLabel(buttons_widget) From 8db22fbb9870397f48aec54ae499d62dbf1e05b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 17:56:05 +0200 Subject: [PATCH 162/311] added initial styles --- .../project_manager/style/__init__.py | 13 +++++++++++++ .../project_manager/project_manager/style/style.css | 5 +++++ .../tools/project_manager/project_manager/window.py | 2 ++ 3 files changed, 20 insertions(+) create mode 100644 openpype/tools/project_manager/project_manager/style/__init__.py create mode 100644 openpype/tools/project_manager/project_manager/style/style.css diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py new file mode 100644 index 0000000000..5a57642ee1 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -0,0 +1,13 @@ +import os +from openpype import resources + + +def load_stylesheet(): + style_path = os.path.join(os.path.dirname(__file__), "style.css") + with open(style_path, "r") as style_file: + stylesheet = style_file.read() + return stylesheet + + +def app_icon_path(): + return resources.pype_icon_filepath() diff --git a/openpype/tools/project_manager/project_manager/style/style.css b/openpype/tools/project_manager/project_manager/style/style.css new file mode 100644 index 0000000000..6730342b30 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/style/style.css @@ -0,0 +1,5 @@ +QTreeView::item { + padding-top: 3px; + padding-bottom: 3px; + padding-right: 3px; +} diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 25cb7c9975..bfc76b2bf7 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -7,6 +7,7 @@ from . import ( HierarchySelectionModel, HierarchyView ) +from .style import load_stylesheet from avalon.api import AvalonMongoDB @@ -87,6 +88,7 @@ class Window(QtWidgets.QWidget): self.message_label = message_label self.resize(1200, 600) + self.setStyleSheet(load_stylesheet()) self.refresh_projects() From 5390658044e58334570e53a4499a0c10337e9ff3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 18:13:57 +0200 Subject: [PATCH 163/311] removed AddAssetItem --- .../project_manager/project_manager/model.py | 124 +----------------- 1 file changed, 3 insertions(+), 121 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index abacac0d32..621521b68b 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -236,9 +236,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): # Add item to appending queue appending_queue.put((asset_id, new_item)) - if isinstance(parent_item, (ProjectItem, AssetItem)): - new_items.append(parent_item.add_asset_item) - if new_items: self.add_items(new_items, parent_item) @@ -392,9 +389,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): if result is not None: self._validate_asset_duplicity(name) - if not new_child.add_asset_item_visible: - self.add_item(new_child.add_asset_item, new_child) - return result def add_new_task(self, parent_index): @@ -422,9 +416,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): if start_row is None: start_row = parent.rowCount() - if parent.add_asset_item_visible and start_row == parent.rowCount(): - start_row -= 1 - end_row = start_row + len(items) - 1 parent_index = self.index_from_item(parent.row(), 0, parent.parent()) @@ -502,20 +493,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): if item.data(REMOVED_ROLE): item.setData(False, REMOVED_ROLE) - def delete_add_asset_item(self, parent): - item = parent.add_asset_item - children = parent.children() - if item not in children: - return - parent_index = self.index_for_item(parent) - row = children.index(item) - - self.beginRemoveRows(parent_index, row, row) - - parent.remove_child(item) - - self.endRemoveRows() - def delete_index(self, index): return self.delete_indexes([index]) @@ -532,7 +509,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): processed_ids.add(item_id) item = self._items_by_id[item_id] - if isinstance(item, (TaskItem, AssetItem, AddAssetItem)): + if isinstance(item, (TaskItem, AssetItem)): items_by_id[item_id] = item if not items_by_id: @@ -542,9 +519,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._remove_item(item) def _remove_item(self, item): - if isinstance(item, AddAssetItem): - return - is_removed = item.data(REMOVED_ROLE) if is_removed: return @@ -575,9 +549,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): task_children.append(child_item) continue - elif isinstance(child_item, AddAssetItem): - continue - if not _fill_children(_all_descendants, child_item, cur_item): remove_item = False @@ -769,8 +740,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): return dst_row = dst_parent.rowCount() - if dst_parent.add_asset_item_visible: - dst_row -= 1 if src_parent is dst_parent: return @@ -1027,8 +996,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): dst_parent = child_item destination_row = dst_parent.rowCount() - if dst_parent.add_asset_item_visible: - destination_row -= 1 break if dst_parent is not None: @@ -1261,7 +1228,6 @@ class BaseItem: _name_icon = None _is_duplicated = False item_type = "base" - add_asset_item_visible = False _None = object() @@ -1480,8 +1446,6 @@ class ProjectItem(BaseItem): def __init__(self, project_doc): self._mongo_id = project_doc["_id"] - self.add_asset_item = AddAssetItem(self) - data = self.data_from_doc(project_doc) super(ProjectItem, self).__init__(data) @@ -1518,76 +1482,8 @@ class ProjectItem(BaseItem): def flags(self, *args, **kwargs): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - def add_child(self, item, row=None): - if not self.add_asset_item_visible and item is self.add_asset_item: - self.add_asset_item_visible = True - - super(ProjectItem, self).add_child(item, row) - - def remove_child(self, item): - if self.add_asset_item_visible and item is self.add_asset_item: - self.add_asset_item_visible = False - - super(ProjectItem, self).remove_child(item) -class AddAssetItem(BaseItem): - item_type = "add_asset" - columns = {"name"} - editable_columns = {"name"} - - def __init__(self, parent): - super(AddAssetItem, self).__init__() - self._parent = parent - - @classmethod - def name_icon(cls): - if cls._name_icon is None: - cls._name_icon = qtawesome.icon( - "fa.plus-circle", - color="#333333" - ) - return cls._name_icon - - def data(self, role, key=None): - if role == REMOVED_ROLE: - return True - - if role == HIERARCHY_CHANGE_ABLE_ROLE: - return True - - if key == "name": - if role == QtCore.Qt.DisplayRole: - return "Add Asset" - elif role == QtCore.Qt.EditRole: - return "" - return super(AddAssetItem, self).data(role, key) - - def setData(self, value, role, key=None): - if key == "name": - if not value: - return False - index = self.model().index_for_item(self) - new_index = self.model().add_new_asset(index) - self.model().setData(new_index, value, QtCore.Qt.EditRole) - return True - return super(AddAssetItem, self).setData(value, role, key) - - def flags(self, key): - if key != "name": - return QtCore.Qt.NoItemFlags - - return ( - QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsEditable - ) - - def add_child(self, item, row=None): - raise AssertionError("BUG: Can't add children to AddAssetItem") - - def remove_child(self, item): - raise AssertionError("BUG: Can't remove children from AddAssetItem") class AssetItem(BaseItem): @@ -1649,9 +1545,6 @@ class AssetItem(BaseItem): self.mongo_id = asset_doc.get("_id") self._project_id = None - self.add_asset_item_visible = False - self.add_asset_item = AddAssetItem(self) - # Item data self._hierarchy_changes_enabled = True self._removed = False @@ -1812,11 +1705,6 @@ class AssetItem(BaseItem): def setData(self, value, role, key=None): if role == REMOVED_ROLE: self._removed = value - if not value and not self.add_asset_item_visible: - self.model().add_item(self.add_asset_item, self) - elif value and self.add_asset_item_visible: - self.model().delete_add_asset_item(self) - return True if role == HIERARCHY_CHANGE_ABLE_ROLE: @@ -1904,20 +1792,14 @@ class AssetItem(BaseItem): super(AssetItem, self).add_child(item, row) - if not self.add_asset_item_visible and item is self.add_asset_item: - self.add_asset_item_visible = True - - elif isinstance(item, TaskItem): + if isinstance(item, TaskItem): self._add_task(item) def remove_child(self, item): if item not in self._children: return - if self.add_asset_item_visible and item is self.add_asset_item: - self.add_asset_item_visible = False - - elif isinstance(item, TaskItem): + if isinstance(item, TaskItem): self._remove_task(item) super(AssetItem, self).remove_child(item) From 4f2e82aeccd5efd40a59efaa555455d5aec8cd73 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 18:28:14 +0200 Subject: [PATCH 164/311] asset is created under passed asset --- .../tools/project_manager/project_manager/model.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 621521b68b..79c0bc952f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -370,22 +370,20 @@ class HierarchyModel(QtCore.QAbstractItemModel): item_id = source_index.data(IDENTIFIER_ROLE) item = self.items_by_id[item_id] - new_row = None - name = None - asset_data = {} if isinstance(item, (RootItem, ProjectItem)): name = "ep" - parent = item + new_row = None else: - parent = item.parent() + name = None new_row = item.row() + 1 + asset_data = {} if name: asset_data["name"] = name new_child = AssetItem(asset_data) - result = self.add_item(new_child, parent, new_row) + result = self.add_item(new_child, item, new_row) if result is not None: self._validate_asset_duplicity(name) From 12a7ae0ace910e9e55fb2259b5dbb4d9e763d289 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 18:28:32 +0200 Subject: [PATCH 165/311] validate new name instead of passed name --- openpype/tools/project_manager/project_manager/model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 79c0bc952f..c365bb65f8 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -385,7 +385,9 @@ class HierarchyModel(QtCore.QAbstractItemModel): result = self.add_item(new_child, item, new_row) if result is not None: - self._validate_asset_duplicity(name) + # WARNING Expecting result is index for column 0 ("name") + new_name = result.data(QtCore.Qt.DisplayRole) + self._validate_asset_duplicity(new_name) return result From 2ce685d3e391f92d4619e8dce67659b668b1e3bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 18:29:11 +0200 Subject: [PATCH 166/311] asset name duplications are handled by item id --- .../project_manager/project_manager/model.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c365bb65f8..84b0c17e8c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -121,7 +121,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._current_project = None self._root_item = None self._items_by_id = {} - self._asset_items_by_name = collections.defaultdict(list) + self._asset_items_by_name = collections.defaultdict(set) self.dbcon = dbcon self._reset_root_item() @@ -431,7 +431,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if isinstance(item, AssetItem): name = item.data(QtCore.Qt.DisplayRole, "name") - self._asset_items_by_name[name].append(item) + self._asset_items_by_name[name].add(item.id) if item.id not in self._items_by_id: self._items_by_id[item.id] = item @@ -638,13 +638,13 @@ class HierarchyModel(QtCore.QAbstractItemModel): if prev_name == new_name: return - self._asset_items_by_name[prev_name].remove(asset_item) + self._asset_items_by_name[prev_name].remove(asset_item.id) self._validate_asset_duplicity(prev_name) if new_name is None: return - self._asset_items_by_name[new_name].append(asset_item) + self._asset_items_by_name[new_name].add(asset_item.id) self._validate_asset_duplicity(new_name) @@ -652,15 +652,19 @@ class HierarchyModel(QtCore.QAbstractItemModel): if name not in self._asset_items_by_name: return - items = self._asset_items_by_name[name] - if not items: + item_ids = self._asset_items_by_name[name] + if not item_ids: self._asset_items_by_name.pop(name) - elif len(items) == 1: - index = self.index_for_item(items[0]) + elif len(item_ids) == 1: + for item_id in item_ids: + item = self._items_by_id[item_id] + index = self.index_for_item(item) self.setData(index, False, DUPLICATED_ROLE) + else: - for item in items: + for item_id in item_ids: + item = self._items_by_id[item_id] index = self.index_for_item(item) self.setData(index, True, DUPLICATED_ROLE) From cc0cbbfade168cfab07e37b347ab334dbc325c89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 18:29:47 +0200 Subject: [PATCH 167/311] small changes --- .../project_manager/project_manager/delegates.py | 5 +++++ .../project_manager/project_manager/model.py | 3 --- .../project_manager/project_manager/view.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 4292224b09..c2b2d07b58 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -78,6 +78,8 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): editor = QtWidgets.QDoubleSpinBox(parent) else: editor = QtWidgets.QSpinBox(parent) + + editor.setObjectName("NumberEditor") editor.setMinimum(self.minimum) editor.setMaximum(self.maximum) @@ -97,6 +99,7 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): class NameDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): editor = NameTextEdit(parent) + editor.setObjectName("NameEditor") value = index.data(QtCore.Qt.EditRole) if value is not None: editor.setText(str(value)) @@ -110,6 +113,7 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): editor = FilterComboBox(parent) + editor.setObjectName("TypeEditor") if not self._project_doc_cache.project_doc: return editor @@ -132,6 +136,7 @@ class ToolsDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): editor = MultiSelectionComboBox(parent) + editor.setObjectName("ToolEditor") if not self._tools_cache.tools_data: return editor diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 84b0c17e8c..171aecef7d 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1487,9 +1487,6 @@ class ProjectItem(BaseItem): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - - - class AssetItem(BaseItem): item_type = "asset" diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b2f21aaffd..8c7902cbd7 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -389,9 +389,16 @@ class HierarchyView(QtWidgets.QTreeView): self.edit(index) def _remove_delete_flag(self, item_ids): + """Remove deletion flag on items marked for deletion.""" self._source_model.remove_delete_flag(item_ids) def _expand_items(self, indexes): + """Expand multiple items with all it's children. + + Args: + indexes (list): List of QModelIndex that should be expanded. + """ + item_ids = set() process_queue = Queue() for index in indexes: @@ -415,6 +422,11 @@ class HierarchyView(QtWidgets.QTreeView): )) def _collapse_items(self, indexes): + """Collapse multiple items with all it's children. + + Args: + indexes (list): List of QModelIndex that should be collapsed. + """ item_ids = set() process_queue = Queue() for index in indexes: @@ -442,6 +454,10 @@ class HierarchyView(QtWidgets.QTreeView): self._parent.show_message(message) def _on_context_menu(self, point): + """Context menu on right click. + + Currently is menu shown only on "name" column. + """ index = self.indexAt(point) column = index.column() if column != 0: From 03cb11d091a2e76925df30ee9f9d95ff77b97c6e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 18:34:52 +0200 Subject: [PATCH 168/311] hide spinbox buttons --- openpype/tools/project_manager/project_manager/delegates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index c2b2d07b58..4c6f7f70eb 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -82,6 +82,7 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): editor.setObjectName("NumberEditor") editor.setMinimum(self.minimum) editor.setMaximum(self.maximum) + editor.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) value = index.data(QtCore.Qt.EditRole) if value is not None: From 3e58532a4252632677194630ffeda91584aeb76d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:01:28 +0200 Subject: [PATCH 169/311] added none project to project model --- openpype/tools/project_manager/project_manager/model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 171aecef7d..9d0edef886 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -30,6 +30,11 @@ class ProjectModel(QtGui.QStandardItemModel): self.dbcon.Session["AVALON_PROJECT"] = None project_items = [] + + none_project = QtGui.QStandardItem("< Select Project >") + none_project.setData(None) + project_items.append(none_project) + database = self.dbcon.database project_names = set() for project_name in database.collection_names(): From 68aa6b2aac0c31b213015016c8a10ea8cdb61f74 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:02:05 +0200 Subject: [PATCH 170/311] added cache of resources --- .../project_manager/style/__init__.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py index 5a57642ee1..bf3a739823 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -1,5 +1,73 @@ import os from openpype import resources +from avalon.vendor import qtawesome + + +class ResourceCache: + colors = { + "standard": "#333333", + "warning": "#ff0000", + "new": "#00ff00" + } + icons = None + + @classmethod + def get_icon(cls, *keys): + output = cls.get_icons() + for key in keys: + output = output[key] + return output + + @classmethod + def get_icons(cls): + if cls.icons is None: + cls.icons = { + "asset": { + "existing": qtawesome.icon( + "fa.folder", + color=cls.colors["standard"] + ), + "new": qtawesome.icon( + "fa.folder", + color=cls.colors["new"] + ), + "duplicated": qtawesome.icon( + "fa.folder", + color=cls.colors["warning"] + ), + "removed": qtawesome.icon( + "fa.trash", + color=cls.colors["warning"] + ) + }, + "task": { + "existing": qtawesome.icon( + "fa.check-circle-o", + color=cls.colors["standard"] + ), + "new": qtawesome.icon( + "fa.check-circle", + color=cls.colors["new"] + ), + "duplicated": qtawesome.icon( + "fa.check-circle", + color=cls.colors["warning"] + ), + "removed": qtawesome.icon( + "fa.trash", + color=cls.colors["warning"] + ) + }, + "refresh": qtawesome.icon( + "fa.refresh", + color=cls.colors["standard"] + ) + } + return cls.icons + + @classmethod + def get_color(cls, color_name): + return cls.colors[color_name] def load_stylesheet(): From 93c5f27e3afa4a7b2e8dbe229d9806806cfb47f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:02:26 +0200 Subject: [PATCH 171/311] refresh button has icon --- .../tools/project_manager/project_manager/style/style.css | 4 ++++ openpype/tools/project_manager/project_manager/window.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/style/style.css b/openpype/tools/project_manager/project_manager/style/style.css index 6730342b30..c62f0fcd81 100644 --- a/openpype/tools/project_manager/project_manager/style/style.css +++ b/openpype/tools/project_manager/project_manager/style/style.css @@ -3,3 +3,7 @@ QTreeView::item { padding-bottom: 3px; padding-right: 3px; } + +#RefreshBtn { + padding: 2px; +} diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index bfc76b2bf7..cae605af4d 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -7,7 +7,7 @@ from . import ( HierarchySelectionModel, HierarchyView ) -from .style import load_stylesheet +from .style import load_stylesheet, ResourceCache from avalon.api import AvalonMongoDB @@ -27,7 +27,10 @@ class Window(QtWidgets.QWidget): project_combobox.setModel(project_model) project_combobox.setRootModelIndex(QtCore.QModelIndex()) - refresh_projects_btn = QtWidgets.QPushButton("Refresh", project_widget) + refresh_projects_btn = QtWidgets.QPushButton(project_widget) + refresh_projects_btn.setIcon(ResourceCache.get_icon("refresh")) + refresh_projects_btn.setToolTip("Refresh projects") + refresh_projects_btn.setObjectName("RefreshBtn") project_layout = QtWidgets.QHBoxLayout(project_widget) project_layout.setContentsMargins(0, 0, 0, 0) From a993504fa893c3788ae056d6cadb71495c92cabf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:02:37 +0200 Subject: [PATCH 172/311] changed order of refresh button --- openpype/tools/project_manager/project_manager/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index cae605af4d..ae1ab70c70 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -34,8 +34,8 @@ class Window(QtWidgets.QWidget): project_layout = QtWidgets.QHBoxLayout(project_widget) project_layout.setContentsMargins(0, 0, 0, 0) - project_layout.addWidget(refresh_projects_btn, 0) project_layout.addWidget(project_combobox, 0) + project_layout.addWidget(refresh_projects_btn, 0) project_layout.addStretch(1) hierarchy_model = HierarchyModel(dbcon) From fe52e249d96ef6eb8744ab1e9fce833d00d277c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:04:08 +0200 Subject: [PATCH 173/311] added delete action to context menu --- .../project_manager/project_manager/view.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 8c7902cbd7..85db6161a6 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -13,7 +13,8 @@ from openpype.lib import ApplicationManager from .constants import ( REMOVED_ROLE, IDENTIFIER_ROLE, - ITEM_TYPE_ROLE + ITEM_TYPE_ROLE, + HIERARCHY_CHANGE_ABLE_ROLE ) @@ -497,9 +498,23 @@ class HierarchyView(QtWidgets.QTreeView): # Remove delete tag on items removed_item_ids = [] + show_delete_items = False for item_id, item in items_by_id.items(): if item.data(REMOVED_ROLE): removed_item_ids.append(item_id) + elif ( + not show_delete_items + and item.data(ITEM_TYPE_ROLE) != "project" + and item.data(HIERARCHY_CHANGE_ABLE_ROLE) + ): + show_delete_items = True + + if show_delete_items: + action = QtWidgets.QAction("Delete items", context_menu) + action.triggered.connect( + lambda: self._delete_items() + ) + actions.append(action) if removed_item_ids: action = QtWidgets.QAction("Keep items", context_menu) From 924dbe408c887fd00b0a63c6a0556a0e0467bdf5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:04:49 +0200 Subject: [PATCH 174/311] use icons defined in style --- .../project_manager/project_manager/model.py | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9d0edef886..d724b91dab 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -11,6 +11,7 @@ from .constants import ( HIERARCHY_CHANGE_ABLE_ROLE, REMOVED_ROLE ) +from .style import ResourceCache from pymongo import UpdateOne from avalon.vendor import qtawesome from Qt import QtCore, QtGui @@ -1234,7 +1235,7 @@ class BaseItem: # Use `set` for faster result editable_columns = set() - _name_icon = None + _name_icons = None _is_duplicated = False item_type = "base" @@ -1256,9 +1257,8 @@ class BaseItem: if key in self.columns: self._data[key] = value - @classmethod - def name_icon(cls): - return cls._name_icon + def name_icon(self): + return None @property def is_valid(self): @@ -1680,11 +1680,19 @@ class AssetItem(BaseItem): return data - @classmethod - def name_icon(cls): - if cls._name_icon is None: - cls._name_icon = qtawesome.icon("fa.folder", color="#333333") - return cls._name_icon + def name_icon(self): + if self.__class__._name_icons is None: + self.__class__._name_icons = ResourceCache.get_icons()["asset"] + + if self._removed: + icon_type = "removed" + elif self._is_duplicated: + icon_type = "duplicated" + elif self.is_new: + icon_type = "new" + else: + icon_type = "existing" + return self.__class__._name_icons[icon_type] def _get_global_data(self, role): if role == HIERARCHY_CHANGE_ABLE_ROLE: @@ -1838,11 +1846,19 @@ class TaskItem(BaseItem): def is_new(self): return self._is_new - @classmethod - def name_icon(cls): - if cls._name_icon is None: - cls._name_icon = qtawesome.icon("fa.file-o", color="#333333") - return cls._name_icon + def name_icon(self): + if self.__class__._name_icons is None: + self.__class__._name_icons = ResourceCache.get_icons()["task"] + + if self._removed: + icon_type = "removed" + elif self._is_duplicated: + icon_type = "duplicated" + elif self.is_new: + icon_type = "new" + else: + icon_type = "existing" + return self.__class__._name_icons[icon_type] def add_child(self, item, row=None): raise AssertionError("BUG: Can't add children to Task") From b388441342c44d5dc9268bac9cfbfa56734db0a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:05:03 +0200 Subject: [PATCH 175/311] don't change background of items --- openpype/tools/project_manager/project_manager/model.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index d724b91dab..4449628824 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1707,15 +1707,6 @@ class AssetItem(BaseItem): ) return super(AssetItem, self)._get_global_data(role) - def data(self, role, key=None): - if role == QtCore.Qt.BackgroundRole: - if self._removed: - return QtGui.QColor(255, 0, 0, 127) - elif self.is_new: - return QtGui.QColor(0, 255, 0, 127) - - return super(AssetItem, self).data(role, key) - def setData(self, value, role, key=None): if role == REMOVED_ROLE: self._removed = value From 4f68e953d54d62c7747cbb0568d239af6dc0f25b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 20:38:46 +0200 Subject: [PATCH 176/311] a little bit faster expanding --- openpype/tools/project_manager/project_manager/view.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 85db6161a6..b7b984fccb 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -400,12 +400,12 @@ class HierarchyView(QtWidgets.QTreeView): indexes (list): List of QModelIndex that should be expanded. """ - item_ids = set() process_queue = Queue() for index in indexes: if index.column() == 0: process_queue.put(index) + item_ids = set() while not process_queue.empty(): index = process_queue.get() item_id = index.data(IDENTIFIER_ROLE) @@ -413,11 +413,9 @@ class HierarchyView(QtWidgets.QTreeView): continue item_ids.add(item_id) - item = self._source_model._items_by_id[item_id] - if not self.isExpanded(index): - self.expand(index) + self.expand(index) - for row in range(item.rowCount()): + for row in range(self._source_model.rowCount(index)): process_queue.put(self._source_model.index( row, 0, index )) From b0f0f94f7708282f916cc3f8cfb246c2352d5cf9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 May 2021 21:25:30 +0200 Subject: [PATCH 177/311] expand/collapse event faster --- .../tools/project_manager/project_manager/view.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b7b984fccb..96e5be9a40 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -1,3 +1,4 @@ +import collections from queue import Queue from Qt import QtWidgets, QtCore, QtGui @@ -399,13 +400,14 @@ class HierarchyView(QtWidgets.QTreeView): Args: indexes (list): List of QModelIndex that should be expanded. """ - process_queue = Queue() for index in indexes: if index.column() == 0: process_queue.put(index) item_ids = set() + # Use deque as expanding not visible items as first is faster + indexes_deque = collections.deque() while not process_queue.empty(): index = process_queue.get() item_id = index.data(IDENTIFIER_ROLE) @@ -413,13 +415,16 @@ class HierarchyView(QtWidgets.QTreeView): continue item_ids.add(item_id) - self.expand(index) + indexes_deque.append(index) for row in range(self._source_model.rowCount(index)): process_queue.put(self._source_model.index( row, 0, index )) + while indexes_deque: + self.expand(indexes_deque.pop()) + def _collapse_items(self, indexes): """Collapse multiple items with all it's children. @@ -439,11 +444,9 @@ class HierarchyView(QtWidgets.QTreeView): continue item_ids.add(item_id) - item = self._source_model._items_by_id[item_id] - if self.isExpanded(index): - self.collapse(index) + self.collapse(index) - for row in range(item.rowCount()): + for row in range(self._source_model.rowCount(index)): process_queue.put(self._source_model.index( row, 0, index )) From 4439805536f8e42712f734dff62694ef8b2cf0d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 10:38:30 +0200 Subject: [PATCH 178/311] fix adding new asset --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 4449628824..612411e60b 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -381,7 +381,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): new_row = None else: name = None - new_row = item.row() + 1 + new_row = item.rowCount() asset_data = {} if name: From 0f40eb699d0f7c83c6a6c79f17f671c45bf01838 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 10:38:51 +0200 Subject: [PATCH 179/311] all assets can be marked for deletion --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 612411e60b..9f9c081458 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -547,7 +547,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): cur_item.setData(task_removed, REMOVED_ROLE) return task_removed - remove_item = cur_item.data(HIERARCHY_CHANGE_ABLE_ROLE) + remove_item = True task_children = [] for row in range(cur_item.rowCount()): child_item = cur_item.child(row) From 5a9b45f484292d50831f32362fcdd5b5e1c80041 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 10:39:35 +0200 Subject: [PATCH 180/311] assets without published content are removed from mongo --- .../project_manager/project_manager/model.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9f9c081458..4227771854 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -12,7 +12,7 @@ from .constants import ( REMOVED_ROLE ) from .style import ResourceCache -from pymongo import UpdateOne +from pymongo import UpdateOne, DeleteOne from avalon.vendor import qtawesome from Qt import QtCore, QtGui @@ -1133,10 +1133,15 @@ class HierarchyModel(QtCore.QAbstractItemModel): insert_list.append(item) elif item.data(REMOVED_ROLE): - bulk_writes.append(UpdateOne( - {"_id": item.asset_id}, - {"$set": {"type": "archived_asset"}} - )) + if item.data(HIERARCHY_CHANGE_ABLE_ROLE): + bulk_writes.append(DeleteOne( + {"_id": item.asset_id} + )) + else: + bulk_writes.append(UpdateOne( + {"_id": item.asset_id}, + {"$set": {"type": "archived_asset"}} + )) else: update_data = item.update_data() From 80dbf6a1ce820ce7a32de67b428d7cebf6979bee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 10:58:43 +0200 Subject: [PATCH 181/311] add exclamation mark to duplicated --- .../tools/project_manager/project_manager/style/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py index bf3a739823..e7bb116843 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -32,7 +32,7 @@ class ResourceCache: color=cls.colors["new"] ), "duplicated": qtawesome.icon( - "fa.folder", + "fa.exclamation-triangle", color=cls.colors["warning"] ), "removed": qtawesome.icon( @@ -50,7 +50,7 @@ class ResourceCache: color=cls.colors["new"] ), "duplicated": qtawesome.icon( - "fa.check-circle", + "fa.exclamation-circle", color=cls.colors["warning"] ), "removed": qtawesome.icon( From f85ecfaf060e8a925876fe98f7fe3f5336f92816 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 11:03:07 +0200 Subject: [PATCH 182/311] added one more level of asset/task creation --- .../project_manager/project_manager/view.py | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 96e5be9a40..6bbae10f02 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -309,16 +309,46 @@ class HierarchyView(QtWidgets.QTreeView): self._source_model.delete_indexes(indexes) def _on_ctrl_shift_enter_pressed(self): - self._add_task() + self._add_task_and_edit() - def _add_task(self, parent_index=None): + def add_asset(self, parent_index=None): if parent_index is None: parent_index = self.currentIndex() if not parent_index.isValid(): return - new_index = self._source_model.add_new_task(parent_index) + # Stop editing + self.setState(HierarchyView.NoState) + QtWidgets.QApplication.processEvents() + + return self._source_model.add_new_asset(parent_index) + + def add_task(self, parent_index=None): + if parent_index is None: + parent_index = self.currentIndex() + + if not parent_index.isValid(): + return + + return self._source_model.add_new_task(parent_index) + + def _add_asset_and_edit(self): + new_index = self.add_asset() + if new_index is None: + return + + # Change current index + self.selectionModel().setCurrentIndex( + new_index, + QtCore.QItemSelectionModel.Clear + | QtCore.QItemSelectionModel.Select + ) + # Start editing + self.edit(new_index) + + def _add_task_and_edit(self): + new_index = self.add_task() if new_index is None: return @@ -339,32 +369,8 @@ class HierarchyView(QtWidgets.QTreeView): # Start editing self.edit(task_type_index) - def _add_asset(self, index=None): - if index is None: - index = self.currentIndex() - - if not index.isValid(): - return - - # Stop editing - self.setState(HierarchyView.NoState) - QtWidgets.QApplication.processEvents() - - new_index = self._source_model.add_new_asset(index) - if new_index is None: - return - - # Change current index - self.selectionModel().setCurrentIndex( - new_index, - QtCore.QItemSelectionModel.Clear - | QtCore.QItemSelectionModel.Select - ) - # Start editing - self.edit(new_index) - def _on_shift_enter_pressed(self): - self._add_asset() + self._add_asset_and_edit() def _on_up_ctrl_pressed(self): indexes = self.selectedIndexes() @@ -486,14 +492,14 @@ class HierarchyView(QtWidgets.QTreeView): if item_type in ("asset", "project"): add_asset_action = QtWidgets.QAction("Add Asset", context_menu) add_asset_action.triggered.connect( - lambda: self._add_asset() + self._add_asset_and_edit ) actions.append(add_asset_action) if item_type in ("asset", "task"): add_task_action = QtWidgets.QAction("Add Task", context_menu) add_task_action.triggered.connect( - lambda: self._add_task() + self._add_task_and_edit ) actions.append(add_task_action) From d0c3a3df758a0d8383dd3ee1c0e55f905877641e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 11:03:24 +0200 Subject: [PATCH 183/311] added buttons to add asset and task --- .../project_manager/project_manager/window.py | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index ae1ab70c70..df762bacce 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -18,11 +18,13 @@ class Window(QtWidgets.QWidget): dbcon = AvalonMongoDB() - # TOP Project selection - project_widget = QtWidgets.QWidget(self) + # Top part of window + top_part_widget = QtWidgets.QWidget(self) + + # Project selection + project_widget = QtWidgets.QWidget(top_part_widget) project_model = ProjectModel(dbcon) - project_combobox = QtWidgets.QComboBox(project_widget) project_combobox.setModel(project_model) project_combobox.setRootModelIndex(QtCore.QModelIndex()) @@ -38,6 +40,30 @@ class Window(QtWidgets.QWidget): project_layout.addWidget(refresh_projects_btn, 0) project_layout.addStretch(1) + # Helper buttons + helper_btns_widget = QtWidgets.QWidget(top_part_widget) + + helper_label = QtWidgets.QLabel("Add:", helper_btns_widget) + add_asset_btn = QtWidgets.QPushButton(helper_btns_widget) + add_asset_btn.setIcon(ResourceCache.get_icon("asset", "existing")) + add_asset_btn.setText("Asset") + add_task_btn = QtWidgets.QPushButton("Task", helper_btns_widget) + add_task_btn.setIcon(ResourceCache.get_icon("task", "existing")) + add_task_btn.setText("Task") + + helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget) + helper_btns_layout.setContentsMargins(0, 0, 0, 0) + helper_btns_layout.addWidget(helper_label) + helper_btns_layout.addWidget(add_asset_btn) + helper_btns_layout.addWidget(add_task_btn) + helper_btns_layout.addStretch(1) + + # Add widgets to top widget layout + top_part_layout = QtWidgets.QVBoxLayout(top_part_widget) + top_part_layout.setContentsMargins(0, 0, 0, 0) + top_part_layout.addWidget(project_widget) + top_part_layout.addWidget(helper_btns_widget) + hierarchy_model = HierarchyModel(dbcon) hierarchy_view = HierarchyView(dbcon, hierarchy_model, self) @@ -74,13 +100,15 @@ class Window(QtWidgets.QWidget): buttons_layout.addWidget(save_btn) main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(project_widget) + main_layout.addWidget(top_part_widget) main_layout.addWidget(hierarchy_view) main_layout.addWidget(buttons_widget) refresh_projects_btn.clicked.connect(self._on_project_refresh) project_combobox.currentIndexChanged.connect(self._on_project_change) save_btn.clicked.connect(self._on_save_click) + add_asset_btn.clicked.connect(self._on_add_asset) + add_task_btn.clicked.connect(self._on_add_task) self.project_model = project_model self.project_combobox = project_combobox @@ -124,6 +152,12 @@ class Window(QtWidgets.QWidget): def _on_save_click(self): self.hierarchy_model.save() + def _on_add_asset(self): + self.hierarchy_view.add_asset() + + def _on_add_task(self): + self.hierarchy_view.add_task() + def show_message(self, message): # TODO add nicer message pop self.message_label.setText(message) From b382d5eea8a4a55ceb1d4d5c0237dce1f9d790a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 15:56:29 +0200 Subject: [PATCH 184/311] TaskItem and AssetItem have custom is_valid property --- .../tools/project_manager/project_manager/model.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 4227771854..1f11874ba7 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1588,6 +1588,12 @@ class AssetItem(BaseItem): def is_new(self): return self.asset_id is None + @property + def is_valid(self): + if self._is_duplicated or not self._data["name"]: + return False + return True + @property def name(self): return self._data["name"] @@ -1842,6 +1848,14 @@ class TaskItem(BaseItem): def is_new(self): return self._is_new + @property + def is_valid(self): + if self._is_duplicated or not self._data["type"]: + return False + if not self.data(QtCore.Qt.EditRole, "name"): + return False + return True + def name_icon(self): if self.__class__._name_icons is None: self.__class__._name_icons = ResourceCache.get_icons()["task"] From c2d5afb18bb6e50151599eb89bc8fbec7d2c5cda Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 16:04:44 +0200 Subject: [PATCH 185/311] use is_valid property in code --- openpype/tools/project_manager/project_manager/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 1f11874ba7..82e4b7f3e0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1308,7 +1308,7 @@ class BaseItem: return None if role == QtCore.Qt.ForegroundRole: - if self._is_duplicated and key == "name": + if key == "name" and not self.is_valid: return QtGui.QColor(255, 0, 0) return None @@ -1697,7 +1697,7 @@ class AssetItem(BaseItem): if self._removed: icon_type = "removed" - elif self._is_duplicated: + elif not self.is_valid: icon_type = "duplicated" elif self.is_new: icon_type = "new" @@ -1862,7 +1862,7 @@ class TaskItem(BaseItem): if self._removed: icon_type = "removed" - elif self._is_duplicated: + elif not self.is_valid: icon_type = "duplicated" elif self.is_new: icon_type = "new" From 3e51be7e3b62c403f719d7f9bb9b0fa08be7b6f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 16:05:34 +0200 Subject: [PATCH 186/311] change invalid tooltips based on issue --- .../project_manager/project_manager/model.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 82e4b7f3e0..d33360c413 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1712,10 +1712,15 @@ class AssetItem(BaseItem): if role == REMOVED_ROLE: return self._removed - if role == QtCore.Qt.ToolTipRole and self._is_duplicated: - return "Asset with name \"{}\" already exists.".format( - self._data["name"] - ) + if role == QtCore.Qt.ToolTipRole: + name = self.data(QtCore.Qt.EditRole, "name") + if not name: + return "Name is not set" + + elif self._is_duplicated: + return "Duplicated asset name \"{}\"".format(name) + return None + return super(AssetItem, self)._get_global_data(role) def setData(self, value, role, key=None): @@ -1877,10 +1882,18 @@ class TaskItem(BaseItem): if role == REMOVED_ROLE: return self._removed - if role == QtCore.Qt.ToolTipRole and self._is_duplicated: - return "Duplicated Task name \"{}\".".format( - self._data["name"] - ) + if role == QtCore.Qt.ToolTipRole: + if not self._data["type"]: + return "Type is not set" + + name = self.data(QtCore.Qt.EditRole, "name") + if not name: + return "Name is not set" + + elif self._is_duplicated: + return "Duplicated task name \"{}".format(name) + return None + return super(TaskItem, self)._get_global_data(role) def to_doc_data(self): From cda522d6541823c019ab0579ff3a90357f00ba66 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 16:06:11 +0200 Subject: [PATCH 187/311] task may have different edit and display role value --- .../project_manager/project_manager/model.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index d33360c413..0471e863d7 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1907,18 +1907,19 @@ class TaskItem(BaseItem): } def data(self, role, key=None): - if role == QtCore.Qt.BackgroundRole: - if self._removed: - return QtGui.QColor(255, 0, 0, 127) + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if key == "type": + return self._data["type"] - elif self.is_new: - return QtGui.QColor(0, 255, 0, 127) + if key == "name": + if not self._data["type"]: + if role == QtCore.Qt.DisplayRole: + return "< Select Type >" + if role == QtCore.Qt.EditRole: + return "" + else: + return self._data[key] or self._data["type"] - if ( - role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) - and key == "name" - ): - return self._data[key] or self._data["type"] or "< Select Type >" return super(TaskItem, self).data(role, key) def setData(self, value, role, key=None): From b86ab862a956f1014dd0e9cb42566ff205a7c373 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 16:06:32 +0200 Subject: [PATCH 188/311] task will store empty name as None --- .../project_manager/project_manager/model.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 0471e863d7..b33c4431f0 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1927,13 +1927,21 @@ class TaskItem(BaseItem): self._removed = value return True + if ( + role == QtCore.Qt.EditRole + and key == "name" + and not value + ): + value = None + result = super(TaskItem, self).setData(value, role, key) - if ( - key == "name" - or (key == "type" and self._data["name"] is None) - ): - self.parent().on_task_name_change(self) + if role == QtCore.Qt.EditRole: + if ( + key == "name" + or (key == "type" and not self._data["name"]) + ): + self.parent().on_task_name_change(self) return result From c2aef84f2e853ec797577c11e86f6dfeec6b16f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 16:13:25 +0200 Subject: [PATCH 189/311] use EditRole to get value of name and type --- .../tools/project_manager/project_manager/model.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b33c4431f0..5ce45d2b99 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -640,7 +640,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): if not isinstance(asset_item, AssetItem): return - prev_name = asset_item.data(QtCore.Qt.DisplayRole, "name") + prev_name = asset_item.data(QtCore.Qt.EditRole, "name") if prev_name == new_name: return @@ -1619,8 +1619,8 @@ class AssetItem(BaseItem): ) doc = { - "name": self.data(QtCore.Qt.DisplayRole, "name"), - "type": self.data(QtCore.Qt.DisplayRole, "type"), + "name": self.data(QtCore.Qt.EditRole, "name"), + "type": self.data(QtCore.Qt.EditRole, "type"), "schema": schema_name, "data": doc_data, "parent": self.project_id @@ -1632,7 +1632,7 @@ class AssetItem(BaseItem): if key in doc: continue # Use `data` method to get inherited values - doc_data[key] = self.data(QtCore.Qt.DisplayRole, key) + doc_data[key] = self.data(QtCore.Qt.EditRole, key) return doc @@ -1751,7 +1751,7 @@ class AssetItem(BaseItem): return super(AssetItem, self).flags(key) def _add_task(self, item): - name = item.data(QtCore.Qt.DisplayRole, "name").lower() + name = item.data(QtCore.Qt.EditRole, "name").lower() item_id = item.data(IDENTIFIER_ROLE) self._task_name_by_item_id[item_id] = name @@ -1901,7 +1901,7 @@ class TaskItem(BaseItem): return {} data = copy.deepcopy(self._data) data.pop("name") - name = self.data(QtCore.Qt.DisplayRole, "name") + name = self.data(QtCore.Qt.EditRole, "name") return { name: data } From cf9cf6278a36afd86e19dff36582e969c8184c7b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 17:22:21 +0200 Subject: [PATCH 190/311] asset and task items know if should return display role --- .../project_manager/constants.py | 1 + .../project_manager/project_manager/model.py | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 703cc0d003..6fb4b991ed 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -7,6 +7,7 @@ DUPLICATED_ROLE = QtCore.Qt.UserRole + 2 HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3 REMOVED_ROLE = QtCore.Qt.UserRole + 4 ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5 +EDITOR_OPENED_ROLE = QtCore.Qt.UserRole + 6 NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" NAME_REGEX = re.compile("^[" + NAME_ALLOWED_SYMBOLS + "]*$") diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5ce45d2b99..2c4e1838a6 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -9,7 +9,8 @@ from .constants import ( ITEM_TYPE_ROLE, DUPLICATED_ROLE, HIERARCHY_CHANGE_ABLE_ROLE, - REMOVED_ROLE + REMOVED_ROLE, + EDITOR_OPENED_ROLE ) from .style import ResourceCache from pymongo import UpdateOne, DeleteOne @@ -1555,6 +1556,10 @@ class AssetItem(BaseItem): asset_doc = {} self.mongo_id = asset_doc.get("_id") self._project_id = None + self._edited_columns = { + column_name: False + for column_name in self.editable_columns + } # Item data self._hierarchy_changes_enabled = True @@ -1723,7 +1728,24 @@ class AssetItem(BaseItem): return super(AssetItem, self)._get_global_data(role) + def data(self, role, key=None): + if role == EDITOR_OPENED_ROLE: + if key not in self._edited_columns: + return False + return self._edited_columns[key] + + if role == QtCore.Qt.DisplayRole and self._edited_columns.get(key): + return "" + + return super(AssetItem, self).data(role, key) + def setData(self, value, role, key=None): + if role == EDITOR_OPENED_ROLE: + if key not in self._edited_columns: + return False + self._edited_columns[key] = value + return True + if role == REMOVED_ROLE: self._removed = value return True @@ -1846,6 +1868,10 @@ class TaskItem(BaseItem): if data is None: data = {} + self._edited_columns = { + column_name: False + for column_name in self.editable_columns + } self._origin_data = copy.deepcopy(data) super(TaskItem, self).__init__(data) @@ -1907,6 +1933,14 @@ class TaskItem(BaseItem): } def data(self, role, key=None): + if role == EDITOR_OPENED_ROLE: + if key not in self._edited_columns: + return False + return self._edited_columns[key] + + if role == QtCore.Qt.DisplayRole and self._edited_columns.get(key): + return "" + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): if key == "type": return self._data["type"] @@ -1923,6 +1957,12 @@ class TaskItem(BaseItem): return super(TaskItem, self).data(role, key) def setData(self, value, role, key=None): + if role == EDITOR_OPENED_ROLE: + if key not in self._edited_columns: + return False + self._edited_columns[key] = value + return True + if role == REMOVED_ROLE: self._removed = value return True From 0699d8d843c548319d8de81166e6db22f95f07ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 17:23:09 +0200 Subject: [PATCH 191/311] view gives model items info if should return display role --- .../project_manager/project_manager/view.py | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 6bbae10f02..0131687541 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -15,7 +15,8 @@ from .constants import ( REMOVED_ROLE, IDENTIFIER_ROLE, ITEM_TYPE_ROLE, - HIERARCHY_CHANGE_ABLE_ROLE + HIERARCHY_CHANGE_ABLE_ROLE, + EDITOR_OPENED_ROLE ) @@ -104,6 +105,8 @@ class HierarchyView(QtWidgets.QTreeView): super(HierarchyView, self).__init__(parent) # Direct access to model self._source_model = source_model + self._editors_mapping = {} + self._persisten_editors = set() # Access to parent because of `show_message` method self._parent = parent @@ -214,12 +217,41 @@ class HierarchyView(QtWidgets.QTreeView): def edit(self, index, *args, **kwargs): result = super(HierarchyView, self).edit(index, *args, **kwargs) - self._deselect_editor(self.indexWidget(index)) + if result: + # Mark index to not return text for DisplayRole + editor = self.indexWidget(index) + if ( + editor not in self._persisten_editors + and editor not in self._editors_mapping + ): + self._editors_mapping[editor] = index + self._source_model.setData(index, True, EDITOR_OPENED_ROLE) + # Deselect content of editor + # QUESTION not sure if we want do this all the time + self._deselect_editor(editor) return result + def closeEditor(self, editor, hint): + if ( + editor not in self._persisten_editors + and editor in self._editors_mapping + ): + index = self._editors_mapping.pop(editor) + self._source_model.setData(index, False, EDITOR_OPENED_ROLE) + super(HierarchyView, self).closeEditor(editor, hint) + def openPersistentEditor(self, index): + self._source_model.setData(index, True, EDITOR_OPENED_ROLE) super(HierarchyView, self).openPersistentEditor(index) - self._deselect_editor(self.indexWidget(index)) + editor = self.indexWidget(index) + self._persisten_editors.add(editor) + self._deselect_editor(editor) + + def closePersistentEditor(self, index): + self._source_model.setData(index, False, EDITOR_OPENED_ROLE) + editor = self.indexWidget(index) + self._persisten_editors.remove(editor) + super(HierarchyView, self).closePersistentEditor(index) def rowsInserted(self, parent_index, start, end): super(HierarchyView, self).rowsInserted(parent_index, start, end) From b318387c3cb81e9b8d64cacaa7b2a6e0f9a8f91e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 17:23:47 +0200 Subject: [PATCH 192/311] added some styles to qtreeview --- .../project_manager/project_manager/delegates.py | 1 + .../project_manager/project_manager/style/style.css | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 4c6f7f70eb..f53e0442f1 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -115,6 +115,7 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): def createEditor(self, parent, option, index): editor = FilterComboBox(parent) editor.setObjectName("TypeEditor") + editor.style().polish(editor) if not self._project_doc_cache.project_doc: return editor diff --git a/openpype/tools/project_manager/project_manager/style/style.css b/openpype/tools/project_manager/project_manager/style/style.css index c62f0fcd81..31196b7cc6 100644 --- a/openpype/tools/project_manager/project_manager/style/style.css +++ b/openpype/tools/project_manager/project_manager/style/style.css @@ -4,6 +4,18 @@ QTreeView::item { padding-right: 3px; } + +QTreeView::item:selected, QTreeView::item:selected:!active { + background: rgba(0, 122, 204, 127); + color: black; +} + #RefreshBtn { padding: 2px; } + +#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { + background: transparent; + border: 1px solid #005c99; + border-radius: 0.3em; +} From 382febe03fb2081fd16fdf1ca82f48dd1c564be1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 17:28:13 +0200 Subject: [PATCH 193/311] renamed icon keys --- openpype/tools/project_manager/project_manager/model.py | 8 ++++---- .../project_manager/project_manager/style/__init__.py | 8 ++++---- openpype/tools/project_manager/project_manager/window.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 2c4e1838a6..b371242388 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1703,11 +1703,11 @@ class AssetItem(BaseItem): if self._removed: icon_type = "removed" elif not self.is_valid: - icon_type = "duplicated" + icon_type = "invalid" elif self.is_new: icon_type = "new" else: - icon_type = "existing" + icon_type = "default" return self.__class__._name_icons[icon_type] def _get_global_data(self, role): @@ -1894,11 +1894,11 @@ class TaskItem(BaseItem): if self._removed: icon_type = "removed" elif not self.is_valid: - icon_type = "duplicated" + icon_type = "invalid" elif self.is_new: icon_type = "new" else: - icon_type = "existing" + icon_type = "default" return self.__class__._name_icons[icon_type] def add_child(self, item, row=None): diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py index e7bb116843..6b9d708f76 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -23,7 +23,7 @@ class ResourceCache: if cls.icons is None: cls.icons = { "asset": { - "existing": qtawesome.icon( + "default": qtawesome.icon( "fa.folder", color=cls.colors["standard"] ), @@ -31,7 +31,7 @@ class ResourceCache: "fa.folder", color=cls.colors["new"] ), - "duplicated": qtawesome.icon( + "invalid": qtawesome.icon( "fa.exclamation-triangle", color=cls.colors["warning"] ), @@ -41,7 +41,7 @@ class ResourceCache: ) }, "task": { - "existing": qtawesome.icon( + "default": qtawesome.icon( "fa.check-circle-o", color=cls.colors["standard"] ), @@ -49,7 +49,7 @@ class ResourceCache: "fa.check-circle", color=cls.colors["new"] ), - "duplicated": qtawesome.icon( + "invalid": qtawesome.icon( "fa.exclamation-circle", color=cls.colors["warning"] ), diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index df762bacce..c4243c3cc3 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -45,10 +45,10 @@ class Window(QtWidgets.QWidget): helper_label = QtWidgets.QLabel("Add:", helper_btns_widget) add_asset_btn = QtWidgets.QPushButton(helper_btns_widget) - add_asset_btn.setIcon(ResourceCache.get_icon("asset", "existing")) + add_asset_btn.setIcon(ResourceCache.get_icon("asset", "default")) add_asset_btn.setText("Asset") add_task_btn = QtWidgets.QPushButton("Task", helper_btns_widget) - add_task_btn.setIcon(ResourceCache.get_icon("task", "existing")) + add_task_btn.setIcon(ResourceCache.get_icon("task", "default")) add_task_btn.setText("Task") helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget) From 354ea1d5818f71a95b1feb94c96e2d2cc74f17b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 17:30:13 +0200 Subject: [PATCH 194/311] unified color source --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index b371242388..9de4301e8d 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1310,7 +1310,7 @@ class BaseItem: if role == QtCore.Qt.ForegroundRole: if key == "name" and not self.is_valid: - return QtGui.QColor(255, 0, 0) + return ResourceCache.colors["warning"] return None if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): From d5201b718cce9b7fe2363d9c0438c41e9802ae89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 17:32:27 +0200 Subject: [PATCH 195/311] modified color a little bit --- .../tools/project_manager/project_manager/style/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py index 6b9d708f76..f3b0e0f9c0 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -6,8 +6,8 @@ from avalon.vendor import qtawesome class ResourceCache: colors = { "standard": "#333333", - "warning": "#ff0000", - "new": "#00ff00" + "new": "#2d9a4c", + "warning": "#c83232" } icons = None From 1c1b077fe437fbe1b43ca0193a80b107cda77257 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 17:42:01 +0200 Subject: [PATCH 196/311] properly handle duplicated asset names on remove and un-remove --- openpype/tools/project_manager/project_manager/model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 9de4301e8d..1d48e78a19 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -499,6 +499,10 @@ class HierarchyModel(QtCore.QAbstractItemModel): for item in items_by_id.values(): if item.data(REMOVED_ROLE): item.setData(False, REMOVED_ROLE) + if isinstance(item, AssetItem): + name = item.data(QtCore.Qt.EditRole, "name") + self._asset_items_by_name[name].add(item.id) + self._validate_asset_duplicity(name) def delete_index(self, index): return self.delete_indexes([index]) @@ -606,6 +610,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): if end_row is not None: row_ranges.append((start_row, end_row)) start_row = end_row = None + if isinstance(child_item, AssetItem): + self._rename_asset(child_item, None) continue end_row = row From 57dc929bd2e8128cdc4ba312487498662d8964a9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 18:09:03 +0200 Subject: [PATCH 197/311] moved header definition to HierarchicalView --- .../project_manager/project_manager/model.py | 3 +++ .../project_manager/project_manager/view.py | 18 ++++++++++++++++++ .../project_manager/project_manager/window.py | 13 ------------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 1d48e78a19..7617275b91 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -115,6 +115,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): } index_moved = QtCore.Signal(QtCore.QModelIndex) + project_changed = QtCore.Signal() def __init__(self, dbcon, parent=None): super(HierarchyModel, self).__init__(parent) @@ -281,6 +282,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.add_items(task_items, asset_item) + self.project_changed.emit() + def rowCount(self, parent=None): if parent is None or not parent.isValid(): parent_item = self._root_item diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 0131687541..02d5b40fd6 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -145,6 +145,7 @@ class HierarchyView(QtWidgets.QTreeView): source_model.index_moved.connect(self._on_rows_moved) self.customContextMenuRequested.connect(self._on_context_menu) + self._source_model.project_changed.connect(self._on_project_reset) self._project_doc_cache = project_doc_cache self._tools_cache = tools_cache @@ -153,6 +154,20 @@ class HierarchyView(QtWidgets.QTreeView): self._column_delegates = column_delegates self._column_key_to_index = column_key_to_index + def header_init(self): + header = self.header() + header.setStretchLastSection(False) + for idx in range(header.count()): + logical_index = header.logicalIndex(idx) + if idx == 0: + header.setSectionResizeMode( + logical_index, QtWidgets.QHeaderView.Stretch + ) + else: + header.setSectionResizeMode( + logical_index, QtWidgets.QHeaderView.ResizeToContents + ) + def set_project(self, project_name): # Trigger helpers first self._project_doc_cache.set_project(project_name) @@ -161,6 +176,9 @@ class HierarchyView(QtWidgets.QTreeView): # Trigger update of model after all data for delegates are filled self._source_model.set_project(project_name) + def _on_project_reset(self): + self.header_init() + self.collapseAll() project_item = self._source_model.project_item diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index c4243c3cc3..49c48912cf 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -75,19 +75,6 @@ class Window(QtWidgets.QWidget): _selection_model.setModel(hierarchy_view.model()) hierarchy_view.setSelectionModel(_selection_model) - header = hierarchy_view.header() - header.setStretchLastSection(False) - for idx in range(header.count()): - logical_index = header.logicalIndex(idx) - if idx == 0: - header.setSectionResizeMode( - logical_index, QtWidgets.QHeaderView.Stretch - ) - else: - header.setSectionResizeMode( - logical_index, QtWidgets.QHeaderView.ResizeToContents - ) - buttons_widget = QtWidgets.QWidget(self) message_label = QtWidgets.QLabel(buttons_widget) From 3ddefade0dd1d6fe3d1dc3b28a7452c5a9d0cd52 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 18:09:35 +0200 Subject: [PATCH 198/311] it is possible to force project change even if current project name is same --- openpype/tools/project_manager/project_manager/model.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 7617275b91..a597550b36 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -142,9 +142,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): self._root_item = RootItem(self) def refresh_project(self): - project_name = self._current_project - self._current_project = None - self.set_project(project_name) + self.set_project(self._current_project, True) @property def project_item(self): @@ -156,8 +154,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): break return output - def set_project(self, project_name): - if self._current_project == project_name: + def set_project(self, project_name, force=False): + if self._current_project == project_name and not force: return self.clear() From b3aa86bb54eb7d54e7be264633516f6b4a69becc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 18:10:03 +0200 Subject: [PATCH 199/311] view has defined some default widths of columns --- .../project_manager/project_manager/view.py | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 02d5b40fd6..e96176debd 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -86,6 +86,27 @@ class HierarchyView(QtWidgets.QTreeView): "pixelAspect": NumberDef(0, decimals=2), "tools_env": ToolsDef() } + + columns_sizes = { + "default": { + "stretch": QtWidgets.QHeaderView.ResizeToContents + }, + "name": { + "stretch": QtWidgets.QHeaderView.Stretch + }, + "type": { + "stretch": QtWidgets.QHeaderView.Interactive, + "width": 100 + }, + "tools_env": { + "stretch": QtWidgets.QHeaderView.Interactive, + "width": 140 + }, + "pixelAspect": { + "stretch": QtWidgets.QHeaderView.Interactive, + "width": 80 + } + } persistent_columns = { "type", "frameStart", @@ -157,16 +178,21 @@ class HierarchyView(QtWidgets.QTreeView): def header_init(self): header = self.header() header.setStretchLastSection(False) + + default_behavior = self.columns_sizes["default"] + widths_by_idx = {} for idx in range(header.count()): + key = self._source_model.columns[idx] + behavior = self.columns_sizes.get(key, default_behavior) logical_index = header.logicalIndex(idx) - if idx == 0: - header.setSectionResizeMode( - logical_index, QtWidgets.QHeaderView.Stretch - ) - else: - header.setSectionResizeMode( - logical_index, QtWidgets.QHeaderView.ResizeToContents - ) + stretch = behavior["stretch"] + header.setSectionResizeMode(logical_index, stretch) + width = behavior.get("width") + if width is not None: + widths_by_idx[idx] = width + + for idx, width in widths_by_idx.items(): + self.setColumnWidth(idx, width) def set_project(self, project_name): # Trigger helpers first From d2bb8e2836c91a9218bae7fa876812375153704f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 18:27:55 +0200 Subject: [PATCH 200/311] fixed removing of asset items --- openpype/tools/project_manager/project_manager/model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index a597550b36..874e1b3a9c 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -566,6 +566,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): if remove_item: cur_item.setData(True, REMOVED_ROLE) + if isinstance(cur_item, AssetItem): + self._rename_asset(cur_item, None) for task_item in task_children: _fill_children(_all_descendants, task_item, cur_item) @@ -611,8 +613,6 @@ class HierarchyModel(QtCore.QAbstractItemModel): if end_row is not None: row_ranges.append((start_row, end_row)) start_row = end_row = None - if isinstance(child_item, AssetItem): - self._rename_asset(child_item, None) continue end_row = row @@ -652,7 +652,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): if prev_name == new_name: return - self._asset_items_by_name[prev_name].remove(asset_item.id) + if asset_item.id in self._asset_items_by_name[prev_name]: + self._asset_items_by_name[prev_name].remove(asset_item.id) self._validate_asset_duplicity(prev_name) From ab35d00183bacc40e097c577b82e7c29af8fea90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 18:32:56 +0200 Subject: [PATCH 201/311] added name and icon to window --- openpype/tools/project_manager/project_manager/window.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 49c48912cf..52046fb94b 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -1,4 +1,4 @@ -from Qt import QtWidgets, QtCore +from Qt import QtWidgets, QtCore, QtGui from . import ( ProjectModel, @@ -9,6 +9,7 @@ from . import ( ) from .style import load_stylesheet, ResourceCache +from openpype import resources from avalon.api import AvalonMongoDB @@ -16,7 +17,8 @@ class Window(QtWidgets.QWidget): def __init__(self, parent=None): super(Window, self).__init__(parent) - dbcon = AvalonMongoDB() + self.setWindowTitle("OpenPype Project Manager") + self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) # Top part of window top_part_widget = QtWidgets.QWidget(self) @@ -24,6 +26,8 @@ class Window(QtWidgets.QWidget): # Project selection project_widget = QtWidgets.QWidget(top_part_widget) + dbcon = AvalonMongoDB() + project_model = ProjectModel(dbcon) project_combobox = QtWidgets.QComboBox(project_widget) project_combobox.setModel(project_model) From 1477dadd341b0e23fd3cfaefab9a2169945f5f8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 18:34:13 +0200 Subject: [PATCH 202/311] renamed class from Window to ProjectManagerWindow --- openpype/tools/project_manager/__init__.py | 4 ++-- openpype/tools/project_manager/project_manager/__init__.py | 6 +++--- openpype/tools/project_manager/project_manager/window.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/project_manager/__init__.py b/openpype/tools/project_manager/__init__.py index 880fc253cf..62fa8af8aa 100644 --- a/openpype/tools/project_manager/__init__.py +++ b/openpype/tools/project_manager/__init__.py @@ -1,10 +1,10 @@ from .project_manager import ( - Window, + ProjectManagerWindow, main ) __all__ = ( - "Window", + "ProjectManagerWindow", "main" ) diff --git a/openpype/tools/project_manager/project_manager/__init__.py b/openpype/tools/project_manager/project_manager/__init__.py index dccc46f771..34beec331d 100644 --- a/openpype/tools/project_manager/project_manager/__init__.py +++ b/openpype/tools/project_manager/project_manager/__init__.py @@ -13,7 +13,7 @@ __all__ = ( "AssetItem", "TaskItem", - "Window", + "ProjectManagerWindow", "main" ) @@ -33,7 +33,7 @@ from .model import ( AssetItem, TaskItem ) -from .window import Window +from .window import ProjectManagerWindow def main(): @@ -42,7 +42,7 @@ def main(): app = QtWidgets.QApplication([]) - window = Window() + window = ProjectManagerWindow() window.show() sys.exit(app.exec_()) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 52046fb94b..81f49d3c26 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -13,9 +13,9 @@ from openpype import resources from avalon.api import AvalonMongoDB -class Window(QtWidgets.QWidget): +class ProjectManagerWindow(QtWidgets.QWidget): def __init__(self, parent=None): - super(Window, self).__init__(parent) + super(ProjectManagerWindow, self).__init__(parent) self.setWindowTitle("OpenPype Project Manager") self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) From ced76863671af5bff75cc51c6e90c6e418e1b332 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 May 2021 19:45:48 +0200 Subject: [PATCH 203/311] create add asset/task buttons in one step --- .../project_manager/project_manager/window.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 81f49d3c26..61000464ea 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -48,12 +48,16 @@ class ProjectManagerWindow(QtWidgets.QWidget): helper_btns_widget = QtWidgets.QWidget(top_part_widget) helper_label = QtWidgets.QLabel("Add:", helper_btns_widget) - add_asset_btn = QtWidgets.QPushButton(helper_btns_widget) - add_asset_btn.setIcon(ResourceCache.get_icon("asset", "default")) - add_asset_btn.setText("Asset") - add_task_btn = QtWidgets.QPushButton("Task", helper_btns_widget) - add_task_btn.setIcon(ResourceCache.get_icon("task", "default")) - add_task_btn.setText("Task") + add_asset_btn = QtWidgets.QPushButton( + ResourceCache.get_icon("asset", "default"), + "Asset", + helper_btns_widget + ) + add_task_btn = QtWidgets.QPushButton( + ResourceCache.get_icon("task", "default"), + "Task", + helper_btns_widget + ) helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget) helper_btns_layout.setContentsMargins(0, 0, 0, 0) From d17a0307ab4599d82436dbbcceec79f64c81c108 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 19:47:43 +0200 Subject: [PATCH 204/311] defined CURRENT_DOC_SCHEMAS constant in openpype.lib --- openpype/lib/__init__.py | 2 ++ openpype/lib/avalon_context.py | 7 +++++++ .../ftrack/event_handlers_server/event_sync_to_avalon.py | 6 ++---- openpype/modules/ftrack/lib/avalon_sync.py | 8 ++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 457ceb1d56..6e63b3d9a6 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -58,6 +58,7 @@ from .python_module_tools import ( ) from .avalon_context import ( + CURRENT_DOC_SCHEMAS, is_latest, any_outdated, get_asset, @@ -162,6 +163,7 @@ __all__ = [ "recursive_bases_from_class", "classes_from_module", + "CURRENT_DOC_SCHEMAS", "is_latest", "any_outdated", "get_asset", diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 2d8726352a..2d608e8279 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -17,6 +17,13 @@ avalon = None log = logging.getLogger("AvalonContext") +CURRENT_DOC_SCHEMAS = { + "project": "openpype:project-3.0", + "asset": "openpype:asset-3.0", + "config": "openpype:config-2.0" +} + + def with_avalon(func): @functools.wraps(func) def wrap_avalon(*args, **kwargs): diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 3bb01798e4..410e51e2a4 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -26,9 +26,7 @@ from openpype.modules.ftrack.lib import ( BaseEvent ) -from openpype.modules.ftrack.lib.avalon_sync import ( - EntitySchemas -) +from openpype.lib import CURRENT_DOC_SCHEMAS class SyncToAvalonEvent(BaseEvent): @@ -1128,7 +1126,7 @@ class SyncToAvalonEvent(BaseEvent): "_id": mongo_id, "name": name, "type": "asset", - "schema": EntitySchemas["asset"], + "schema": CURRENT_DOC_SCHEMAS["asset"], "parent": proj["_id"], "data": { "ftrackId": ftrack_ent["id"], diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index f58e858a5a..a3b926464e 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -34,7 +34,7 @@ log = Logger.get_logger(__name__) # Current schemas for avalon types -EntitySchemas = { +CURRENT_DOC_SCHEMAS = { "project": "openpype:project-3.0", "asset": "openpype:asset-3.0", "config": "openpype:config-2.0" @@ -1862,7 +1862,7 @@ class SyncEntitiesFactory: item["_id"] = new_id item["parent"] = self.avalon_project_id - item["schema"] = EntitySchemas["asset"] + item["schema"] = CURRENT_DOC_SCHEMAS["asset"] item["data"]["visualParent"] = avalon_parent new_id_str = str(new_id) @@ -2003,8 +2003,8 @@ class SyncEntitiesFactory: project_item["_id"] = new_id project_item["parent"] = None - project_item["schema"] = EntitySchemas["project"] - project_item["config"]["schema"] = EntitySchemas["config"] + project_item["schema"] = CURRENT_DOC_SCHEMAS["project"] + project_item["config"]["schema"] = CURRENT_DOC_SCHEMAS["config"] self.ftrack_avalon_mapper[self.ft_project_id] = new_id self.avalon_ftrack_mapper[new_id] = self.ft_project_id From 85f812bfe5b74844685f3dd5fee62bb211f8ee52 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:15:31 +0200 Subject: [PATCH 205/311] define project regex in code --- openpype/lib/__init__.py | 6 ++++++ openpype/lib/avalon_context.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 6e63b3d9a6..9fcc536a3a 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -59,6 +59,9 @@ from .python_module_tools import ( from .avalon_context import ( CURRENT_DOC_SCHEMAS, + PROJECT_NAME_ALLOWED_SYMBOLS, + PROJECT_NAME_REGEX, + create_project, is_latest, any_outdated, get_asset, @@ -164,6 +167,9 @@ __all__ = [ "classes_from_module", "CURRENT_DOC_SCHEMAS", + "PROJECT_NAME_ALLOWED_SYMBOLS", + "PROJECT_NAME_REGEX", + "create_project", "is_latest", "any_outdated", "get_asset", diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 2d608e8279..f4a58c74fd 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -22,6 +22,10 @@ CURRENT_DOC_SCHEMAS = { "asset": "openpype:asset-3.0", "config": "openpype:config-2.0" } +PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" +PROJECT_NAME_REGEX = re.compile( + "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) +) def with_avalon(func): From 29040d449f42fb68e6596cb95ea8b01c46f0eacd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:23:11 +0200 Subject: [PATCH 206/311] implemented function to create new project --- openpype/lib/avalon_context.py | 82 ++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index f4a58c74fd..c53b028a44 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -28,6 +28,88 @@ PROJECT_NAME_REGEX = re.compile( ) +def create_project( + project_name, project_code, library_project=False, dbcon=None +): + """Create project using OpenPype settings. + + This project creation function is not validating project document on + creation. It is because project document is created blindly with only + minimum required information about project which is it's name, code, type + and schema. + + Entered project name must be unique and project must not exist yet. + + Args: + project_name(str): New project name. Should be unique. + project_code(str): Project's code should be unique too. + library_project(bool): Project is library project. + dbcon(AvalonMongoDB): Object of connection to MongoDB. + + Raises: + ValueError: When project name already exists in MongoDB. + + Returns: + dict: Created project document. + """ + + from openpype.settings import ProjectSettings, SaveWarningExc + from avalon.api import AvalonMongoDB + from avalon.schema import validate + + if dbcon is None: + dbcon = AvalonMongoDB() + + if not PROJECT_NAME_REGEX.match(project_name): + raise ValueError(( + "Project name \"{}\" contain invalid characters" + ).format(project_name)) + + database = dbcon.database + project_doc = database[project_name].find_one( + {"type": "project"}, + {"name": 1} + ) + if project_doc: + raise ValueError("Project with name \"{}\" already exists".format( + project_name + )) + + project_doc = { + "type": "project", + "name": project_name, + "data": { + "code": project_code, + "library_project": library_project + }, + "schema": CURRENT_DOC_SCHEMAS["project"] + } + # Insert document with basic data + database[project_name].insert_one(project_doc) + # Load ProjectSettings for the project and save it to store all attributes + # and Anatomy + try: + project_settings_entity = ProjectSettings(project_name) + project_settings_entity.save() + except SaveWarningExc as exc: + print(str(exc)) + except Exception: + database[project_name].delete_one({"type": "project"}) + raise + + project_doc = database[project_name].find_one({"type": "project"}) + + try: + # Validate created project document + validate(project_doc) + except Exception as exc: + # Remove project if is not valid + database[project_name].delete_one({"type": "project"}) + raise + + return project_doc + + def with_avalon(func): @functools.wraps(func) def wrap_avalon(*args, **kwargs): From 7d6741f18517e5ad8c17050ede6b10f41a6264cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:23:29 +0200 Subject: [PATCH 207/311] use CURRENT_DOC_SCHEMAS in hierarchy model --- openpype/tools/project_manager/project_manager/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 874e1b3a9c..8f6fe04006 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -13,6 +13,8 @@ from .constants import ( EDITOR_OPENED_ROLE ) from .style import ResourceCache + +from openpype.lib import CURRENT_DOC_SCHEMAS from pymongo import UpdateOne, DeleteOne from avalon.vendor import qtawesome from Qt import QtCore, QtGui @@ -1628,7 +1630,8 @@ class AssetItem(BaseItem): "tasks": tasks } schema_name = ( - self._origin_asset_doc.get("schema") or "openpype:asset-3.0" + self._origin_asset_doc.get("schema") + or CURRENT_DOC_SCHEMAS["asset"] ) doc = { From cc559527e249cb39fc246012cf2ea94f607f1605 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:23:47 +0200 Subject: [PATCH 208/311] implemented CreateProjectDialog to be able create projects --- .../project_manager/widgets.py | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 566e17ea86..c650a4a8b7 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -1,8 +1,16 @@ import re + from .constants import ( NAME_ALLOWED_SYMBOLS, NAME_REGEX ) +from openpype.lib import ( + create_project, + PROJECT_NAME_ALLOWED_SYMBOLS, + PROJECT_NAME_REGEX +) +from avalon.api import AvalonMongoDB + from Qt import QtWidgets, QtCore @@ -80,3 +88,177 @@ class FilterComboBox(QtWidgets.QComboBox): self._completer.setCompletionColumn(column) self._filter_proxy_model.setFilterKeyColumn(column) super(FilterComboBox, self).setModelColumn(column) + + +class CreateProjectDialog(QtWidgets.QDialog): + def __init__(self, parent=None, dbcon=None): + super(CreateProjectDialog, self).__init__(parent) + + self.setWindowTitle("Create Project") + + self.allowed_regex = "[^{}]+".format(PROJECT_NAME_ALLOWED_SYMBOLS) + + if dbcon is None: + dbcon = AvalonMongoDB() + + self.dbcon = dbcon + self._ignore_code_change = False + self._project_name_is_valid = False + self._project_code_is_valid = False + self._project_code_value = None + + project_names, project_codes = self._get_existing_projects() + + inputs_widget = QtWidgets.QWidget(self) + project_name_input = QtWidgets.QLineEdit(inputs_widget) + project_code_input = QtWidgets.QLineEdit(inputs_widget) + library_project_input = QtWidgets.QCheckBox(inputs_widget) + + inputs_layout = QtWidgets.QFormLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.addRow("Project name:", project_name_input) + inputs_layout.addRow("Project code:", project_code_input) + inputs_layout.addRow("Library project:", library_project_input) + + project_name_label = QtWidgets.QLabel(self) + project_code_label = QtWidgets.QLabel(self) + + btns_widget = QtWidgets.QWidget(self) + ok_btn = QtWidgets.QPushButton("Ok", btns_widget) + ok_btn.setEnabled(False) + cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + btns_layout.addWidget(cancel_btn) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(inputs_widget, 0) + main_layout.addWidget(project_name_label, 1) + main_layout.addWidget(project_code_label, 1) + main_layout.addStretch(1) + main_layout.addWidget(btns_widget, 0) + + project_name_input.textChanged.connect(self._on_project_name_change) + project_code_input.textChanged.connect(self._on_project_code_change) + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + self.invalid_project_names = project_names + self.invalid_project_codes = project_codes + + self.project_name_label = project_name_label + self.project_code_label = project_code_label + + self.project_name_input = project_name_input + self.project_code_input = project_code_input + self.library_project_input = library_project_input + + self.ok_btn = ok_btn + + @property + def project_name(self): + return self.project_name_input.text() + + def _on_project_name_change(self, value): + if self._project_code_value is None: + self._ignore_code_change = True + self.project_code_input.setText(value.lower()) + self._ignore_code_change = False + + self._update_valid_project_name(value) + + def _on_project_code_change(self, value): + if not value: + value = None + + self._update_valid_project_code(value) + + if not self._ignore_code_change: + self._project_code_value = value + + def _update_valid_project_name(self, value): + message = "" + is_valid = True + if not value: + message = "Project name is empty" + is_valid = False + + elif value in self.invalid_project_names: + message = "Project name \"{}\" already exist".format(value) + is_valid = False + + elif not PROJECT_NAME_REGEX.match(value): + message = ( + "Project name \"{}\" contain not supported symbols" + ).format(value) + is_valid = False + + self._project_name_is_valid = is_valid + self.project_name_label.setText(message) + self._enable_button() + + def _update_valid_project_code(self, value): + message = "" + is_valid = True + if not value: + message = "Project code is empty" + is_valid = False + + elif value in self.invalid_project_names: + message = "Project code \"{}\" already exist".format(value) + is_valid = False + + elif not PROJECT_NAME_REGEX.match(value): + message = ( + "Project code \"{}\" contain not supported symbols" + ).format(value) + is_valid = False + + self._project_code_is_valid = is_valid + self.project_code_label.setText(message) + self._enable_button() + + def _enable_button(self): + self.ok_btn.setEnabled( + self._project_name_is_valid and self._project_code_is_valid + ) + + def _on_cancel_clicked(self): + self.done(0) + + def _on_ok_clicked(self): + if not self._project_name_is_valid or not self._project_code_is_valid: + return + + project_name = self.project_name_input.text() + project_code = self.project_code_input.text() + library_project = self.library_project_input.isChecked() + create_project(project_name, project_code, library_project, self.dbcon) + + self.done(1) + + def _get_existing_projects(self): + project_names = set() + project_codes = set() + for project_name in self.dbcon.database.collection_names(): + # Each collection will have exactly one project document + project_doc = self.dbcon.database[project_name].find_one( + {"type": "project"}, + {"name": 1, "data.code": 1} + ) + if not project_doc: + continue + + project_name = project_doc.get("name") + if not project_name: + continue + + project_names.add(project_name) + project_code = project_doc.get("data", {}).get("code") + if not project_code: + project_code = project_name + + project_codes.add(project_code) + return project_names, project_codes From 8ec20d1b7e67911b94602216ff470fe41331569e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:25:08 +0200 Subject: [PATCH 209/311] use CreateProjectDialog in project manager --- .../project_manager/project_manager/window.py | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index 61000464ea..ad377c4403 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -5,7 +5,9 @@ from . import ( HierarchyModel, HierarchySelectionModel, - HierarchyView + HierarchyView, + + CreateProjectDialog ) from .style import load_stylesheet, ResourceCache @@ -38,10 +40,15 @@ class ProjectManagerWindow(QtWidgets.QWidget): refresh_projects_btn.setToolTip("Refresh projects") refresh_projects_btn.setObjectName("RefreshBtn") + create_project_btn = QtWidgets.QPushButton( + "Create project...", project_widget + ) + project_layout = QtWidgets.QHBoxLayout(project_widget) project_layout.setContentsMargins(0, 0, 0, 0) project_layout.addWidget(project_combobox, 0) project_layout.addWidget(refresh_projects_btn, 0) + project_layout.addWidget(create_project_btn, 0) project_layout.addStretch(1) # Helper buttons @@ -100,6 +107,7 @@ class ProjectManagerWindow(QtWidgets.QWidget): main_layout.addWidget(buttons_widget) refresh_projects_btn.clicked.connect(self._on_project_refresh) + create_project_btn.clicked.connect(self._on_project_create) project_combobox.currentIndexChanged.connect(self._on_project_change) save_btn.clicked.connect(self._on_save_click) add_asset_btn.clicked.connect(self._on_add_asset) @@ -121,18 +129,18 @@ class ProjectManagerWindow(QtWidgets.QWidget): def _set_project(self, project_name=None): self.hierarchy_view.set_project(project_name) - def refresh_projects(self): - current_project = None - if self.project_combobox.count() > 0: - current_project = self.project_combobox.currentText() + def refresh_projects(self, project_name=None): + if project_name is None: + if self.project_combobox.count() > 0: + project_name = self.project_combobox.currentText() self.project_model.refresh() if self.project_combobox.count() == 0: return self._set_project() - if current_project: - row = self.project_combobox.findText(current_project) + if project_name: + row = self.project_combobox.findText(project_name) if row >= 0: self.project_combobox.setCurrentIndex(row) @@ -156,3 +164,15 @@ class ProjectManagerWindow(QtWidgets.QWidget): def show_message(self, message): # TODO add nicer message pop self.message_label.setText(message) + + def _on_project_create(self): + dialog = CreateProjectDialog(self) + dialog.exec_() + print(dialog.result()) + if dialog.result() != 1: + return + + project_name = dialog.project_name + print("Created project \"{}\"".format(project_name)) + self.show_message("Created project \"{}\"".format(project_name)) + self.refresh_projects(project_name) From 6fe08049d6730c240aeefc7dda30970b3072e281 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:26:36 +0200 Subject: [PATCH 210/311] make visualParent not required on asset doc --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 8f6fe04006..0259a75f21 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -216,7 +216,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): asset_docs_by_parent_id = collections.defaultdict(list) for asset_doc in asset_docs_by_id.values(): - parent_id = asset_doc["data"]["visualParent"] + parent_id = asset_doc["data"].get("visualParent") asset_docs_by_parent_id[parent_id].append(asset_doc) appending_queue = Queue() From 1850687160d26f85e821c502a712ac05b7813158 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:27:19 +0200 Subject: [PATCH 211/311] added CreateProjectDialog to init of project manager module --- openpype/tools/project_manager/project_manager/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/__init__.py b/openpype/tools/project_manager/project_manager/__init__.py index 34beec331d..49ade4a989 100644 --- a/openpype/tools/project_manager/project_manager/__init__.py +++ b/openpype/tools/project_manager/project_manager/__init__.py @@ -4,6 +4,7 @@ __all__ = ( "HierarchyView", "ProjectModel", + "CreateProjectDialog", "HierarchyModel", "HierarchySelectionModel", @@ -21,6 +22,7 @@ __all__ = ( from .constants import ( IDENTIFIER_ROLE ) +from .widgets import CreateProjectDialog from .view import HierarchyView from .model import ( ProjectModel, From 376c3dffed965025063d3dca59cdf386e502eb38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:35:42 +0200 Subject: [PATCH 212/311] add filling of style data to stylesheets --- .../project_manager/style/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py index f3b0e0f9c0..a268d657f1 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -69,11 +69,24 @@ class ResourceCache: def get_color(cls, color_name): return cls.colors[color_name] + @classmethod + def style_fill_data(cls): + output = {} + for color_name, color_value in cls.colors.items(): + key = "color:{}".format(color_name) + output[key] = color_value + return output + def load_stylesheet(): - style_path = os.path.join(os.path.dirname(__file__), "style.css") + current_dir = os.path.dirname(os.path.abspath(__file__)) + style_path = os.path.join(current_dir, "style.css") with open(style_path, "r") as style_file: stylesheet = style_file.read() + + for key, value in ResourceCache.style_fill_data().items(): + replacement_key = "{" + key + "}" + stylesheet = stylesheet.replace(replacement_key, value) return stylesheet From 77108c554245ac3d1b311f511fcac42b880eb43c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:44:53 +0200 Subject: [PATCH 213/311] added base of resources --- .../project_manager/style/__init__.py | 1 + .../style/images/combobox_arrow.png | Bin 0 -> 166 bytes .../style/images/combobox_arrow_disabled.png | Bin 0 -> 165 bytes .../project_manager/style/pyqt5_resources.py | 94 ++++++++++++++++++ .../style/pyside2_resources.py | 75 ++++++++++++++ .../project_manager/style/rc_resources.py | 12 +++ .../project_manager/style/resources.qrc | 6 ++ 7 files changed, 188 insertions(+) create mode 100644 openpype/tools/project_manager/project_manager/style/images/combobox_arrow.png create mode 100644 openpype/tools/project_manager/project_manager/style/images/combobox_arrow_disabled.png create mode 100644 openpype/tools/project_manager/project_manager/style/pyqt5_resources.py create mode 100644 openpype/tools/project_manager/project_manager/style/pyside2_resources.py create mode 100644 openpype/tools/project_manager/project_manager/style/rc_resources.py create mode 100644 openpype/tools/project_manager/project_manager/style/resources.qrc diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py index a268d657f1..6fd7d304cb 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -79,6 +79,7 @@ class ResourceCache: def load_stylesheet(): + from . import rc_resources current_dir = os.path.dirname(os.path.abspath(__file__)) style_path = os.path.join(current_dir, "style.css") with open(style_path, "r") as style_file: diff --git a/openpype/tools/project_manager/project_manager/style/images/combobox_arrow.png b/openpype/tools/project_manager/project_manager/style/images/combobox_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..5805d9842bb3c8bdf9ae741ebabc690a4929585a GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!2%?ApR4f$QjEnx?oJHr&dIz4a+s35-CY>| zxA&jf59DzcctjR6FmMZlFeAgPITAoY_7YEDSN1y`;vAyZcdU741BJ9aT^vI=t|uoP zVCdoDDYjGKUczBuWT3#kjKjddz-flSK@mm~;ef4+85xf4WSw z1e^Sc1@brxJR*x37`TN&n2}-D90{Nxdx@v7EBhS|ac(A-+!@lDKp{;}7sn8e>&XcR z7pulY;Wn^IBG(*7A%~?b^VC!N=hHC=sm-IdEdjT~uc)I$ztaD0e F0ssj2CNKa1 literal 0 HcmV?d00001 diff --git a/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py b/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py new file mode 100644 index 0000000000..34b6551210 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x00\xa5\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\ +\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\ +\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\ +\xae\x42\x60\x82\ +\x00\x00\x00\xa6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\ +\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\ +\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\ +\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\ +\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x08\ +\x06\xc5\x8e\xa5\ +\x00\x6f\ +\x00\x70\x00\x65\x00\x6e\x00\x70\x00\x79\x00\x70\x00\x65\ +\x00\x06\ +\x07\x03\x7d\xc3\ +\x00\x69\ +\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ +\x00\x12\ +\x01\x2e\x03\x27\ +\x00\x63\ +\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\ +\x00\x67\ +\x00\x1b\ +\x03\x5a\x32\x27\ +\x00\x63\ +\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\ +\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\ +\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\ +\x00\x00\x01\x76\x41\x9d\xa2\x35\ +" + +qt_version = [int(v) for v in QtCore.qVersion().split('.')] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) diff --git a/openpype/tools/project_manager/project_manager/style/pyside2_resources.py b/openpype/tools/project_manager/project_manager/style/pyside2_resources.py new file mode 100644 index 0000000000..ecd5e7fa52 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/style/pyside2_resources.py @@ -0,0 +1,75 @@ +# Resource object code (Python 3) +# Created by: object code +# Created by: The Resource Compiler for Qt version 5.15.2 +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore + +qt_resource_data = b"\ +\x00\x00\x00\xa5\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\ +\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\ +200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\ +\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\ +\xaeB`\x82\ +\x00\x00\x00\xa6\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\ +HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ +\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\ +;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\ +\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\ +\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\ +\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +" + +qt_resource_name = b"\ +\x00\x08\ +\x06\xc5\x8e\xa5\ +\x00o\ +\x00p\x00e\x00n\x00p\x00y\x00p\x00e\ +\x00\x06\ +\x07\x03}\xc3\ +\x00i\ +\x00m\x00a\x00g\x00e\x00s\ +\x00\x12\ +\x01.\x03'\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\ +\x00g\ +\x00\x1b\ +\x03Z2'\ +\x00c\ +\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\ +\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x16\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01vA\x9d\xa25\ +\x00\x00\x00R\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\ +\x00\x00\x01vA\x9d\xa25\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) diff --git a/openpype/tools/project_manager/project_manager/style/rc_resources.py b/openpype/tools/project_manager/project_manager/style/rc_resources.py new file mode 100644 index 0000000000..19dcda9564 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/style/rc_resources.py @@ -0,0 +1,12 @@ +import Qt + + +resources = None +if Qt.__binding__ == "PySide2": + from . import pyside2_resources as resources +elif Qt.__binding__ == "PyQt5": + from . import pyqt5_resources as resources + + +if resources is not None: + resources.qInitResources() diff --git a/openpype/tools/project_manager/project_manager/style/resources.qrc b/openpype/tools/project_manager/project_manager/style/resources.qrc new file mode 100644 index 0000000000..9281c69479 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/style/resources.qrc @@ -0,0 +1,6 @@ + + + images/combobox_arrow.png + images/combobox_arrow_disabled.png + + From 807eefee6dbdb54044c9a33b96c05ebbcbc3da44 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:47:12 +0200 Subject: [PATCH 214/311] hide project name messages without text --- openpype/tools/project_manager/project_manager/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index c650a4a8b7..263b15daeb 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -197,6 +197,7 @@ class CreateProjectDialog(QtWidgets.QDialog): self._project_name_is_valid = is_valid self.project_name_label.setText(message) + self.project_name_label.setVisible(bool(message)) self._enable_button() def _update_valid_project_code(self, value): From 0b159479facbb4e5b7cc19ca14223b610e0637b6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:48:42 +0200 Subject: [PATCH 215/311] removed debug prints --- openpype/tools/project_manager/project_manager/window.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py index ad377c4403..a800214517 100644 --- a/openpype/tools/project_manager/project_manager/window.py +++ b/openpype/tools/project_manager/project_manager/window.py @@ -168,11 +168,9 @@ class ProjectManagerWindow(QtWidgets.QWidget): def _on_project_create(self): dialog = CreateProjectDialog(self) dialog.exec_() - print(dialog.result()) if dialog.result() != 1: return project_name = dialog.project_name - print("Created project \"{}\"".format(project_name)) self.show_message("Created project \"{}\"".format(project_name)) self.refresh_projects(project_name) From 5134ae743b7d27ef6b8c2a50388cbe8ef63fb2ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:53:58 +0200 Subject: [PATCH 216/311] hound fixes --- openpype/lib/avalon_context.py | 2 +- .../project_manager/style/__init__.py | 5 +++- .../project_manager/style/pyqt5_resources.py | 7 ++++++ .../style/pyside2_resources.py | 5 ++++ .../project_manager/style/qrc_resources.py | 25 +++++++++++++++++++ .../project_manager/style/rc_resources.py | 12 --------- 6 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 openpype/tools/project_manager/project_manager/style/qrc_resources.py delete mode 100644 openpype/tools/project_manager/project_manager/style/rc_resources.py diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index c53b028a44..2a7c58c4ee 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -102,7 +102,7 @@ def create_project( try: # Validate created project document validate(project_doc) - except Exception as exc: + except Exception: # Remove project if is not valid database[project_name].delete_one({"type": "project"}) raise diff --git a/openpype/tools/project_manager/project_manager/style/__init__.py b/openpype/tools/project_manager/project_manager/style/__init__.py index 6fd7d304cb..b686967ddd 100644 --- a/openpype/tools/project_manager/project_manager/style/__init__.py +++ b/openpype/tools/project_manager/project_manager/style/__init__.py @@ -79,7 +79,10 @@ class ResourceCache: def load_stylesheet(): - from . import rc_resources + from . import qrc_resources + + qrc_resources.qInitResources() + current_dir = os.path.dirname(os.path.abspath(__file__)) style_path = os.path.join(current_dir, "style.css") with open(style_path, "r") as style_file: diff --git a/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py b/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py index 34b6551210..bb26221a46 100644 --- a/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py +++ b/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py @@ -8,6 +8,7 @@ from PyQt5 import QtCore + qt_resource_data = b"\ \x00\x00\x00\xa5\ \x89\ @@ -37,6 +38,7 @@ qt_resource_data = b"\ \x44\xae\x42\x60\x82\ " + qt_resource_name = b"\ \x00\x08\ \x06\xc5\x8e\xa5\ @@ -58,6 +60,7 @@ qt_resource_name = b"\ \x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ " + qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ @@ -66,6 +69,7 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\ " + qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -79,6 +83,7 @@ qt_resource_struct_v2 = b"\ \x00\x00\x01\x76\x41\x9d\xa2\x35\ " + qt_version = [int(v) for v in QtCore.qVersion().split('.')] if qt_version < [5, 8, 0]: rcc_version = 1 @@ -87,8 +92,10 @@ else: rcc_version = 2 qt_resource_struct = qt_resource_struct_v2 + def qInitResources(): QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + def qCleanupResources(): QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) diff --git a/openpype/tools/project_manager/project_manager/style/pyside2_resources.py b/openpype/tools/project_manager/project_manager/style/pyside2_resources.py index ecd5e7fa52..a8a368601d 100644 --- a/openpype/tools/project_manager/project_manager/style/pyside2_resources.py +++ b/openpype/tools/project_manager/project_manager/style/pyside2_resources.py @@ -5,6 +5,7 @@ from PySide2 import QtCore + qt_resource_data = b"\ \x00\x00\x00\xa5\ \x89\ @@ -34,6 +35,7 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\ D\xaeB`\x82\ " + qt_resource_name = b"\ \x00\x08\ \x06\xc5\x8e\xa5\ @@ -55,6 +57,7 @@ qt_resource_name = b"\ \x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\ " + qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -68,8 +71,10 @@ qt_resource_struct = b"\ \x00\x00\x01vA\x9d\xa25\ " + def qInitResources(): QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + def qCleanupResources(): QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) diff --git a/openpype/tools/project_manager/project_manager/style/qrc_resources.py b/openpype/tools/project_manager/project_manager/style/qrc_resources.py new file mode 100644 index 0000000000..be859cae46 --- /dev/null +++ b/openpype/tools/project_manager/project_manager/style/qrc_resources.py @@ -0,0 +1,25 @@ +import Qt + + +initialized = False +resources = None +if Qt.__binding__ == "PySide2": + from . import pyside2_resources as resources +elif Qt.__binding__ == "PyQt5": + from . import pyqt5_resources as resources + + +def qInitResources(): + global resources + global initialized + if resources is not None and not initialized: + initialized = True + resources.qInitResources() + + +def qCleanupResources(): + global resources + global initialized + if resources is not None: + initialized = False + resources.qCleanupResources() diff --git a/openpype/tools/project_manager/project_manager/style/rc_resources.py b/openpype/tools/project_manager/project_manager/style/rc_resources.py deleted file mode 100644 index 19dcda9564..0000000000 --- a/openpype/tools/project_manager/project_manager/style/rc_resources.py +++ /dev/null @@ -1,12 +0,0 @@ -import Qt - - -resources = None -if Qt.__binding__ == "PySide2": - from . import pyside2_resources as resources -elif Qt.__binding__ == "PyQt5": - from . import pyqt5_resources as resources - - -if resources is not None: - resources.qInitResources() From 3600686ab06d83ef723d9bee4b82c07d93d3a349 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 20:59:41 +0200 Subject: [PATCH 217/311] a little bit more cleanup --- .../project_manager/style/pyqt5_resources.py | 8 ++++++-- .../project_manager/style/pyside2_resources.py | 8 ++++++-- .../project_manager/style/qrc_resources.py | 7 +++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py b/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py index bb26221a46..836934019d 100644 --- a/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py +++ b/openpype/tools/project_manager/project_manager/style/pyqt5_resources.py @@ -94,8 +94,12 @@ else: def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) diff --git a/openpype/tools/project_manager/project_manager/style/pyside2_resources.py b/openpype/tools/project_manager/project_manager/style/pyside2_resources.py index a8a368601d..b73d5e334a 100644 --- a/openpype/tools/project_manager/project_manager/style/pyside2_resources.py +++ b/openpype/tools/project_manager/project_manager/style/pyside2_resources.py @@ -73,8 +73,12 @@ qt_resource_struct = b"\ def qInitResources(): - QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + 0x03, qt_resource_struct, qt_resource_name, qt_resource_data + ) def qCleanupResources(): - QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + 0x03, qt_resource_struct, qt_resource_name, qt_resource_data + ) diff --git a/openpype/tools/project_manager/project_manager/style/qrc_resources.py b/openpype/tools/project_manager/project_manager/style/qrc_resources.py index be859cae46..a9e219c9ad 100644 --- a/openpype/tools/project_manager/project_manager/style/qrc_resources.py +++ b/openpype/tools/project_manager/project_manager/style/qrc_resources.py @@ -23,3 +23,10 @@ def qCleanupResources(): if resources is not None: initialized = False resources.qCleanupResources() + + +__all__ = ( + "resources", + "qInitResources", + "qCleanupResources" +) From 42c950a033bdfa22e8b193ce6814cef3f9ed8e7b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 22:26:02 +0200 Subject: [PATCH 218/311] fix new asset name validation --- openpype/tools/project_manager/project_manager/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 0259a75f21..66693b292f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -396,7 +396,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): result = self.add_item(new_child, item, new_row) if result is not None: # WARNING Expecting result is index for column 0 ("name") - new_name = result.data(QtCore.Qt.DisplayRole) + new_name = result.data(QtCore.Qt.EditRole) self._validate_asset_duplicity(new_name) return result From 229b2a17959f37e1dc258953690b1d2e6b1888e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 May 2021 22:30:11 +0200 Subject: [PATCH 219/311] even more fixes of name getting --- openpype/tools/project_manager/project_manager/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 66693b292f..6e20dd368f 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -440,7 +440,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): parent.add_child(item, row) if isinstance(item, AssetItem): - name = item.data(QtCore.Qt.DisplayRole, "name") + name = item.data(QtCore.Qt.EditRole, "name") self._asset_items_by_name[name].add(item.id) if item.id not in self._items_by_id: @@ -1810,7 +1810,7 @@ class AssetItem(BaseItem): _item.setData(False, DUPLICATED_ROLE) def _rename_task(self, item): - new_name = item.data(QtCore.Qt.DisplayRole, "name").lower() + new_name = item.data(QtCore.Qt.EditRole, "name").lower() item_id = item.data(IDENTIFIER_ROLE) prev_name = self._task_name_by_item_id[item_id] if new_name == prev_name: From 0795df31c4c905bbff08d64644f15f7d2b56799a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 09:38:12 +0200 Subject: [PATCH 220/311] shift + enter create asset as sibling --- .../tools/project_manager/project_manager/view.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index e96176debd..70af11e68d 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -409,8 +409,8 @@ class HierarchyView(QtWidgets.QTreeView): return self._source_model.add_new_task(parent_index) - def _add_asset_and_edit(self): - new_index = self.add_asset() + def _add_asset_and_edit(self, parent_index=None): + new_index = self.add_asset(parent_index) if new_index is None: return @@ -446,7 +446,13 @@ class HierarchyView(QtWidgets.QTreeView): self.edit(task_type_index) def _on_shift_enter_pressed(self): - self._add_asset_and_edit() + parent_index = self.currentIndex() + if not parent_index.isValid(): + return + + if parent_index.data(ITEM_TYPE_ROLE) == "asset": + parent_index = parent_index.parent() + self._add_asset_and_edit(parent_index) def _on_up_ctrl_pressed(self): indexes = self.selectedIndexes() From 84b42581ff1fe671fd983c839400db6d66f9f31d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 10:36:36 +0200 Subject: [PATCH 221/311] added value cleanup to filter combobox --- .../project_manager/delegates.py | 4 +++ .../project_manager/widgets.py | 27 +++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index f53e0442f1..51edff028f 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -130,6 +130,10 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): if index >= 0: editor.setCurrentIndex(index) + def setModelData(self, editor, model, index): + editor.value_cleanup() + super(TypeDelegate, self).setModelData(editor, model, index) + class ToolsDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, tools_cache, *args, **kwargs): diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 263b15daeb..503ecbb6bc 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -65,20 +65,31 @@ class FilterComboBox(QtWidgets.QComboBox): super(FilterComboBox, self).focusInEvent(event) self.lineEdit().selectAll() - def focusOutEvent(self, event): - idx = self.currentIndex() - if idx > -1: - index = self.model().index(idx, 0) - text = index.data(QtCore.Qt.DisplayRole) - if text != self.lineEdit().text(): - self.lineEdit().setText(text) - super(FilterComboBox, self).focusOutEvent(event) + def value_cleanup(self): + text = self.lineEdit().text() + idx = self.findText(text) + if idx < 0: + count = self._completer.completionModel().rowCount() + if count > 0: + index = self._completer.completionModel().index(0, 0) + text = index.data(QtCore.Qt.DisplayRole) + idx = self.findText(text) + + if idx < 0: + idx = 0 + self.setCurrentIndex(idx) def on_completer_activated(self, text): if text: index = self.findText(text) self.setCurrentIndex(index) + def keyPressEvent(self, event): + if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + self.value_cleanup() + + super(FilterComboBox, self).keyPressEvent(event) + def setModel(self, model): super(FilterComboBox, self).setModel(model) self._filter_proxy_model.setSourceModel(model) From c5525ab6936dee33915f88d5f97aec5bd09e806c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 11:22:19 +0200 Subject: [PATCH 222/311] set last value if current is invalid --- openpype/tools/project_manager/project_manager/widgets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 503ecbb6bc..9c57febcf6 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -40,6 +40,8 @@ class FilterComboBox(QtWidgets.QComboBox): def __init__(self, parent=None): super(FilterComboBox, self).__init__(parent) + self._last_value = None + self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setEditable(True) @@ -63,6 +65,7 @@ class FilterComboBox(QtWidgets.QComboBox): def focusInEvent(self, event): super(FilterComboBox, self).focusInEvent(event) + self._last_value = self.lineEdit().text() self.lineEdit().selectAll() def value_cleanup(self): @@ -74,6 +77,8 @@ class FilterComboBox(QtWidgets.QComboBox): index = self._completer.completionModel().index(0, 0) text = index.data(QtCore.Qt.DisplayRole) idx = self.findText(text) + elif self._last_value is not None: + idx = self.findText(self._last_value) if idx < 0: idx = 0 From 370ac65e1eadfb7667ad3ae6c0ef578b0d44c2f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 09:36:41 +0000 Subject: [PATCH 223/311] Create draft PR for #1405 From 384d8a9c170a9e0a286d3931f17bd0da4e0a7514 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 12:11:27 +0200 Subject: [PATCH 224/311] host implementation may have `add_implementation_envs` function in openpype.hosts. --- openpype/lib/applications.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index c5c192f51b..29cec57c4f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1089,7 +1089,22 @@ def prepare_host_environments(data): # Merge dictionaries env_values = _merge_env(tool_env, env_values) - final_env = _merge_env(acre.compute(env_values), data["env"]) + loaded_env = _merge_env(acre.compute(env_values), data["env"]) + + final_env = None + if app.host_name: + module = __import__("openpype.hosts", fromlist=[app.host_name]) + host_module = getattr(module, app.host_name, None) + add_implementation_envs = None + if host_module: + add_implementation_envs = getattr( + host_module, "add_implementation_envs", None + ) + if add_implementation_envs: + final_env = add_implementation_envs(loaded_env) + + if final_env is None: + final_env = loaded_env # Update env data["env"].update(final_env) From a3e89486ad323c4afef3d44210ac899645a4fdf0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 12:11:38 +0200 Subject: [PATCH 225/311] small cleanup --- openpype/lib/applications.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 29cec57c4f..83f11a6b07 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -3,7 +3,6 @@ import re import copy import json import platform -import getpass import collections import inspect import subprocess @@ -362,7 +361,6 @@ class ApplicationManager: context = ApplicationLaunchContext( app, executable, **data ) - # TODO pass context through launch hooks return context.launch() @@ -626,7 +624,7 @@ class ApplicationLaunchContext: # Logger logger_name = "{}-{}".format(self.__class__.__name__, self.app_name) - self.log = PypeLogger().get_logger(logger_name) + self.log = PypeLogger.get_logger(logger_name) self.executable = executable From f8f64b5a05198ddc2c4342f23aa908b6d520baa6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 12:20:49 +0200 Subject: [PATCH 226/311] prepare_host_environments can not add implementation environments --- openpype/lib/applications.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 83f11a6b07..b95ba85fbe 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1031,7 +1031,7 @@ def _merge_env(env, current_env): return result -def prepare_host_environments(data): +def prepare_host_environments(data, implementation_envs=True): """Modify launch environments based on launched app and context. Args: @@ -1090,7 +1090,8 @@ def prepare_host_environments(data): loaded_env = _merge_env(acre.compute(env_values), data["env"]) final_env = None - if app.host_name: + # + if app.host_name and implementation_envs: module = __import__("openpype.hosts", fromlist=[app.host_name]) host_module = getattr(module, app.host_name, None) add_implementation_envs = None @@ -1099,6 +1100,7 @@ def prepare_host_environments(data): host_module, "add_implementation_envs", None ) if add_implementation_envs: + # Function may only modify passed dict without returning value final_env = add_implementation_envs(loaded_env) if final_env is None: From 2de2f20bdfc5b860a7e8d81985fece326d27c49f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 May 2021 12:27:22 +0200 Subject: [PATCH 227/311] SyncServer - fix for 'local' in Local Setting broke regular sync loop 'local' cannot be used in regular sync loop, only for LS --- openpype/modules/sync_server/sync_server_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index ed403b836d..15de4b12e9 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -501,6 +501,8 @@ class SyncServerModule(PypeModule, ITrayModule): items = self.get_configurable_items_for_site(project_name, site_name, scope) + # Local Settings need 'local' instead of real value + site_name = site_name.replace(get_local_site_id(), 'local') editable[site_name] = items return editable @@ -591,8 +593,6 @@ class SyncServerModule(PypeModule, ITrayModule): else: item["value"] = val - - editable.append(item) return editable @@ -877,7 +877,7 @@ class SyncServerModule(PypeModule, ITrayModule): } all_sites = {self.DEFAULT_SITE: studio_config} if sync_enabled: - all_sites['local'] = {'provider': 'local_drive'} + all_sites[get_local_site_id()] = {'provider': 'local_drive'} return all_sites def get_provider_for_site(self, project_name=None, site=None): From e062d960b3e3a98d48006febaabaee87cc18d717 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 May 2021 12:34:18 +0200 Subject: [PATCH 228/311] SyncServer - handle possible race condition This might throw FileNotFoundError for missing file if progress check is faster than file creation --- openpype/modules/sync_server/providers/local_drive.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/providers/local_drive.py b/openpype/modules/sync_server/providers/local_drive.py index 3b3e699d00..4b80ed44f2 100644 --- a/openpype/modules/sync_server/providers/local_drive.py +++ b/openpype/modules/sync_server/providers/local_drive.py @@ -170,7 +170,10 @@ class LocalDriveHandler(AbstractProvider): site=site, progress=status_val ) - target_file_size = os.path.getsize(target_path) + try: + target_file_size = os.path.getsize(target_path) + except FileNotFoundError: + pass time.sleep(0.5) def _normalize_site_name(self, site_name): From bf07cbf494e52be3f53dc542c84039256b36771d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 13:24:21 +0200 Subject: [PATCH 229/311] initial implementation of blender's add_implementation_envs --- openpype/hosts/blender/__init__.py | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py index e69de29bb2..1a81c3add1 100644 --- a/openpype/hosts/blender/__init__.py +++ b/openpype/hosts/blender/__init__.py @@ -0,0 +1,41 @@ +import os + + +def add_implementation_envs(env): + """Modify environments to contain all required for implementation.""" + # Prepare path to implementation script + implementation_user_script_path = os.path.join( + os.environ["OPENPYPE_REPOS_ROOT"], + "repos", + "avalon-core", + "setup", + "blender" + ) + + # Add blender implementation script path to PYTHONPATH + python_path = env.get("PYTHONPATH") or "" + python_path_parts = [ + path + for path in python_path.split(os.pathsep) + if path + ] + python_path_parts.insert(0, implementation_user_script_path) + env["PYTHONPATH"] = os.pathsep.join(python_path_parts) + + # Modify Blender user scripts path + blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or "" + previous_user_scripts = [] + for path in blender_user_scripts.split(os.pathsep): + if path and os.path.exists(path): + path = os.path.normpath(path) + if path != implementation_user_script_path: + previous_user_scripts.append(path) + + env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join( + previous_user_scripts + ) + env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path + + # Define Qt binding if not defined + if not env.get("QT_PREFERRED_BINDING"): + env["QT_PREFERRED_BINDING"] = "PySide2" From eb70dbd62ae391e3123ceb1a8f5290ad5af0f818 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 13:34:08 +0200 Subject: [PATCH 230/311] removed blender's environments --- .../settings/defaults/system_settings/applications.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 63d6da4633..d439ae35d9 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -852,14 +852,7 @@ "label": "Blender", "icon": "{}/app_icons/blender.png", "host_name": "blender", - "environment": { - "BLENDER_USER_SCRIPTS": "{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/blender", - "PYTHONPATH": [ - "{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/blender", - "{PYTHONPATH}" - ], - "QT_PREFERRED_BINDING": "PySide2" - }, + "environment": {}, "variants": { "2-83": { "use_python_2": false, From ad9087e3983fb1d9ce7082d5459be8f181a2f971 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 13:36:13 +0200 Subject: [PATCH 231/311] add maya's environments to add_implementation_envs --- openpype/hosts/maya/__init__.py | 29 +++++++++++++++++++ .../system_settings/applications.json | 15 +--------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index e69de29bb2..10a128583e 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -0,0 +1,29 @@ +import os + + +def add_implementation_envs(env): + # Add requirements to PYTHONPATH + pype_root = os.environ["OPENPYPE_REPOS_ROOT"] + new_python_path = os.pathsep.join([ + os.path.join(pype_root, "openpype", "hosts", "maya", "startup"), + os.path.join(pype_root, "repos", "avalon-core", "setup", "maya"), + os.path.join(pype_root, "tools", "mayalookassigner") + ]) + old_python_path = env.get("PYTHONPATH") + if old_python_path: + new_python_path = os.pathsep.join([new_python_path, old_python_path]) + env["PYTHONPATH"] = new_python_path + + # Set default values if are not already set via settings + defaults = { + "MAYA_DISABLE_CLIC_IPM": "Yes", + "MAYA_DISABLE_CIP": "Yes", + "MAYA_DISABLE_CER": "Yes", + "PYMEL_SKIP_MEL_INIT": "Yes", + "LC_ALL": "C", + "OPENPYPE_LOG_NO_COLORS": "Yes" + } + for key, value in defaults.items(): + if env.get(key): + continue + env[key] = value diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index d439ae35d9..2904530cad 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -4,20 +4,7 @@ "label": "Maya", "icon": "{}/app_icons/maya.png", "host_name": "maya", - "environment": { - "PYTHONPATH": [ - "{OPENPYPE_REPOS_ROOT}/openpype/hosts/maya/startup", - "{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/maya", - "{OPENPYPE_REPOS_ROOT}/repos/maya-look-assigner", - "{PYTHONPATH}" - ], - "MAYA_DISABLE_CLIC_IPM": "Yes", - "MAYA_DISABLE_CIP": "Yes", - "MAYA_DISABLE_CER": "Yes", - "PYMEL_SKIP_MEL_INIT": "Yes", - "LC_ALL": "C", - "OPENPYPE_LOG_NO_COLORS": "Yes" - }, + "environment": {}, "variants": { "2022": { "use_python_2": false, From 7c8661bc2de72d2d4f20327f3ed0fb642e723515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 18 May 2021 13:37:31 +0200 Subject: [PATCH 232/311] add progress bars --- tools/build_dependencies.py | 53 ++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/tools/build_dependencies.py b/tools/build_dependencies.py index fb52e2b5fd..de3b6da021 100644 --- a/tools/build_dependencies.py +++ b/tools/build_dependencies.py @@ -26,10 +26,12 @@ import platform from pathlib import Path import shutil import blessed +import enlighten import time term = blessed.Terminal() +manager = enlighten.get_manager() def _print(msg: str, type: int = 0) -> None: @@ -52,6 +54,24 @@ def _print(msg: str, type: int = 0) -> None: print("{}{}".format(header, msg)) +def count_folders(path: Path) -> int: + """Recursively count items inside given Path. + + Args: + path (Path): Path to count. + + Returns: + int: number of items. + + """ + cnt = 0 + for child in path.iterdir(): + if child.is_dir(): + cnt += 1 + cnt += count_folders(child) + return cnt + + _print("Starting dependency cleanup ...") start_time = time.time_ns() @@ -96,30 +116,55 @@ deps_dir = build_dir / "dependencies" # copy all files _print("Copying dependencies ...") -shutil.copytree(site_pkg.as_posix(), deps_dir.as_posix()) +total_files = count_folders(site_pkg) +progress_bar = enlighten.Counter( + total=total_files, desc="Processing Dependencies", + units="%", color="green") + + +def _progress(_base, _names): + progress_bar.update() + return [] + + +shutil.copytree(site_pkg.as_posix(), + deps_dir.as_posix(), + ignore=_progress) +progress_bar.close() # iterate over frozen libs and create list to delete libs_dir = build_dir / "lib" to_delete = [] -_print("Finding duplicates ...") +# _print("Finding duplicates ...") deps_items = list(deps_dir.iterdir()) +item_count = len(list(libs_dir.iterdir())) +find_progress_bar = enlighten.Counter( + total=item_count, desc="Finding duplicates", units="%", color="yellow") + for d in libs_dir.iterdir(): if (deps_dir / d.name) in deps_items: to_delete.append(d) - _print(f"found {d}", 3) + # _print(f"found {d}", 3) + find_progress_bar.update() +find_progress_bar.close() # add openpype and igniter in libs too to_delete.append(libs_dir / "openpype") to_delete.append(libs_dir / "igniter") # delete duplicates -_print(f"Deleting {len(to_delete)} duplicates ...") +# _print(f"Deleting {len(to_delete)} duplicates ...") +delete_progress_bar = enlighten.Counter( + total=len(to_delete), desc="Deleting duplicates", units="%", color="red") for d in to_delete: if d.is_dir(): shutil.rmtree(d) else: d.unlink() + delete_progress_bar.update() + +delete_progress_bar.close() end_time = time.time_ns() total_time = (end_time - start_time) / 1000000000 From 46c603464f1fb04d7f310dd381fc284af67e489e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 13:46:07 +0200 Subject: [PATCH 233/311] skip duplicated python paths --- openpype/hosts/maya/__init__.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index 10a128583e..43a1e6f7bf 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -4,15 +4,21 @@ import os def add_implementation_envs(env): # Add requirements to PYTHONPATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] - new_python_path = os.pathsep.join([ + new_python_paths = [ os.path.join(pype_root, "openpype", "hosts", "maya", "startup"), os.path.join(pype_root, "repos", "avalon-core", "setup", "maya"), os.path.join(pype_root, "tools", "mayalookassigner") - ]) - old_python_path = env.get("PYTHONPATH") - if old_python_path: - new_python_path = os.pathsep.join([new_python_path, old_python_path]) - env["PYTHONPATH"] = new_python_path + ] + old_python_path = env.get("PYTHONPATH") or "" + for path in old_python_path.split(os.pathsep): + if not path or not os.path.exists(path): + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_python_paths: + new_python_paths.append(norm_path) + + env["PYTHONPATH"] = os.pathsep.join(new_python_paths) # Set default values if are not already set via settings defaults = { @@ -24,6 +30,5 @@ def add_implementation_envs(env): "OPENPYPE_LOG_NO_COLORS": "Yes" } for key, value in defaults.items(): - if env.get(key): - continue - env[key] = value + if not env.get(key): + env[key] = value From 5c366f2ee97a635e5db7b48dbec1ef5d25f701e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 13:46:25 +0200 Subject: [PATCH 234/311] implemented add_implementation_envs for nuke --- openpype/hosts/nuke/__init__.py | 42 +++++++++++++++++++ .../system_settings/applications.json | 10 +---- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index e69de29bb2..901ee7c54c 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -0,0 +1,42 @@ +import os +import platform + + +def add_implementation_envs(env): + # Add requirements to NUKE_PATH + pype_root = os.environ["OPENPYPE_REPOS_ROOT"] + new_nuke_paths = [ + os.path.join(pype_root, "openpype", "hosts", "maya", "startup"), + os.path.join(pype_root, "repos", "avalon-core", "setup", "maya"), + os.path.join(pype_root, "tools", "mayalookassigner") + ] + old_nuke_path = env.get("NUKE_PATH") or "" + for path in old_nuke_path.split(os.pathsep): + if not path or not os.path.exists(path): + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_nuke_paths: + new_nuke_paths.append(norm_path) + + env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths) + + # Try to add QuickTime to PATH + quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" + if platform.system() == "windows" and os.path.exists(quick_time_path): + path_value = env.get("PATH") or "" + path_paths = [ + path + for path in path_value.split(os.pathsep) + if path + ] + path_paths.append(quick_time_path) + env["PATH"] = os.pathsep.join(path_paths) + + # Set default values if are not already set via settings + defaults = { + "LOGLEVEL": "DEBUG" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2904530cad..30c697ddeb 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -97,15 +97,7 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": [ - "{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/nuke/nuke_path", - "{OPENPYPE_REPOS_ROOT}/openpype/hosts/nuke/startup", - "{OPENPYPE_STUDIO_PLUGINS}/nuke" - ], - "PATH": { - "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}" - }, - "LOGLEVEL": "DEBUG" + "NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke" }, "variants": { "13-0": { From 7bdc937f697c660361365422902ecc27cbb1848b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 13:50:50 +0200 Subject: [PATCH 235/311] fixed default paths in nuke host and removed defaults for nuke x --- openpype/hosts/nuke/__init__.py | 7 ++++--- .../defaults/system_settings/applications.json | 10 +--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index 901ee7c54c..ca4d931e2c 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -6,9 +6,10 @@ def add_implementation_envs(env): # Add requirements to NUKE_PATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] new_nuke_paths = [ - os.path.join(pype_root, "openpype", "hosts", "maya", "startup"), - os.path.join(pype_root, "repos", "avalon-core", "setup", "maya"), - os.path.join(pype_root, "tools", "mayalookassigner") + os.path.join(pype_root, "openpype", "hosts", "nuke", "startup"), + os.path.join( + pype_root, "repos", "avalon-core", "setup", "nuke", "nuke_path" + ) ] old_nuke_path = env.get("NUKE_PATH") or "" for path in old_nuke_path.split(os.pathsep): diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 30c697ddeb..01602bf549 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -203,15 +203,7 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": [ - "{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/nuke/nuke_path", - "{OPENPYPE_REPOS_ROOT}/openpype/hosts/nuke/startup", - "{OPENPYPE_STUDIO_PLUGINS}/nuke" - ], - "PATH": { - "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}" - }, - "LOGLEVEL": "DEBUG" + "NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke" }, "variants": { "13-0": { From 243de91a8ef6a0ee5862a86e43c3a0efce7d5d11 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 13:52:14 +0200 Subject: [PATCH 236/311] converted hiero and nukestudio environments to host defaults --- openpype/hosts/hiero/__init__.py | 40 +++++++++++++++++++ .../system_settings/applications.json | 18 +-------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py index e69de29bb2..5b036224e4 100644 --- a/openpype/hosts/hiero/__init__.py +++ b/openpype/hosts/hiero/__init__.py @@ -0,0 +1,40 @@ +import os +import platform + + +def add_implementation_envs(env): + # Add requirements to HIERO_PLUGIN_PATH + pype_root = os.environ["OPENPYPE_REPOS_ROOT"] + new_hiero_paths = [ + os.path.join(pype_root, "openpype", "hosts", "hiero", "startup") + ] + old_hiero_path = env.get("HIERO_PLUGIN_PATH") or "" + for path in old_hiero_path.split(os.pathsep): + if not path or not os.path.exists(path): + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_hiero_paths: + new_hiero_paths.append(norm_path) + + env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths) + + # Try to add QuickTime to PATH + quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem" + if platform.system() == "windows" and os.path.exists(quick_time_path): + path_value = env.get("PATH") or "" + path_paths = [ + path + for path in path_value.split(os.pathsep) + if path + ] + path_paths.append(quick_time_path) + env["PATH"] = os.pathsep.join(path_paths) + + # Set default values if are not already set via settings + defaults = { + "LOGLEVEL": "DEBUG" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 01602bf549..486aa06849 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -339,15 +339,8 @@ "icon": "{}/app_icons/nuke.png", "host_name": "hiero", "environment": { - "HIERO_PLUGIN_PATH": [ - "{OPENPYPE_REPOS_ROOT}/openpype/hosts/hiero/startup" - ], - "PATH": { - "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}" - }, "WORKFILES_STARTUP": "0", - "TAG_ASSETBUILD_STARTUP": "0", - "LOGLEVEL": "DEBUG" + "TAG_ASSETBUILD_STARTUP": "0" }, "variants": { "13-0": { @@ -481,15 +474,8 @@ "icon": "{}/app_icons/hiero.png", "host_name": "hiero", "environment": { - "HIERO_PLUGIN_PATH": [ - "{OPENPYPE_REPOS_ROOT}/openpype/hosts/hiero/startup" - ], - "PATH": { - "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}" - }, "WORKFILES_STARTUP": "0", - "TAG_ASSETBUILD_STARTUP": "0", - "LOGLEVEL": "DEBUG" + "TAG_ASSETBUILD_STARTUP": "0" }, "variants": { "13-0": { From 65b308c7a54a96bf44c4e7062ff69e8e3ab64700 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 14:10:00 +0200 Subject: [PATCH 237/311] converted tvpaintconverted aftereffects harmony photoshop tvpaint and unreal to define default environments --- openpype/hosts/aftereffects/__init__.py | 9 +++++++++ openpype/hosts/harmony/__init__.py | 10 ++++++++++ openpype/hosts/photoshop/__init__.py | 9 +++++++++ openpype/hosts/tvpaint/__init__.py | 8 ++++++++ openpype/hosts/unreal/__init__.py | 18 ++++++++++++++++++ .../defaults/system_settings/applications.json | 16 +++------------- 6 files changed, 57 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/aftereffects/__init__.py b/openpype/hosts/aftereffects/__init__.py index e69de29bb2..1c2d473728 100644 --- a/openpype/hosts/aftereffects/__init__.py +++ b/openpype/hosts/aftereffects/__init__.py @@ -0,0 +1,9 @@ +def add_implementation_envs(env): + """Modify environments to contain all required for implementation.""" + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True", + "WEBSOCKET_URL": "ws://localhost:8097/ws/" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/hosts/harmony/__init__.py b/openpype/hosts/harmony/__init__.py index e69de29bb2..13b72f98d6 100644 --- a/openpype/hosts/harmony/__init__.py +++ b/openpype/hosts/harmony/__init__.py @@ -0,0 +1,10 @@ +import os + + +def add_implementation_envs(env): + """Modify environments to contain all required for implementation.""" + openharmony_path = os.path.join( + os.environ["OPENPYPE_REPOS_ROOT"], "pype", "vendor", "OpenHarmony" + ) + # TODO check if is already set? What to do if is already set? + env["LIB_OPENHARMONY_PATH"] = openharmony_path diff --git a/openpype/hosts/photoshop/__init__.py b/openpype/hosts/photoshop/__init__.py index e69de29bb2..babacba9a8 100644 --- a/openpype/hosts/photoshop/__init__.py +++ b/openpype/hosts/photoshop/__init__.py @@ -0,0 +1,9 @@ +def add_implementation_envs(env): + """Modify environments to contain all required for implementation.""" + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True", + "WEBSOCKET_URL": "ws://localhost:8099/ws/" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/hosts/tvpaint/__init__.py b/openpype/hosts/tvpaint/__init__.py index e69de29bb2..0e58e31a5b 100644 --- a/openpype/hosts/tvpaint/__init__.py +++ b/openpype/hosts/tvpaint/__init__.py @@ -0,0 +1,8 @@ +def add_implementation_envs(env): + """Modify environments to contain all required for implementation.""" + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index e69de29bb2..dd7a575995 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -0,0 +1,18 @@ +import os + + +def add_implementation_envs(env): + """Modify environments to contain all required for implementation.""" + # Set AVALON_UNREAL_PLUGIN required for Unreal implementation + unreal_plugin_path = os.path.join( + os.environ["OPENPYPE_REPOS_ROOT"], "repos", "avalon-unreal-integration" + ) + env["AVALON_UNREAL_PLUGIN"] = unreal_plugin_path + + # Set default environments if are not set via settings + defaults = { + "OPENPYPE_LOG_NO_COLORS": "True" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 486aa06849..fd0a5b8c52 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -890,8 +890,7 @@ "icon": "{}/app_icons/harmony.png", "host_name": "harmony", "environment": { - "AVALON_HARMONY_WORKFILES_ON_LAUNCH": "1", - "LIB_OPENHARMONY_PATH": "{OPENPYPE_REPOS_ROOT}/pype/vendor/OpenHarmony" + "AVALON_HARMONY_WORKFILES_ON_LAUNCH": "1" }, "variants": { "20": { @@ -935,9 +934,7 @@ "label": "TVPaint", "icon": "{}/app_icons/tvpaint.png", "host_name": "tvpaint", - "environment": { - "OPENPYPE_LOG_NO_COLORS": "True" - }, + "environment": {}, "variants": { "animation_11-64bits": { "use_python_2": false, @@ -984,8 +981,6 @@ "host_name": "photoshop", "environment": { "AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH": "1", - "OPENPYPE_LOG_NO_COLORS": "Yes", - "WEBSOCKET_URL": "ws://localhost:8099/ws/", "WORKFILES_SAVE_AS": "Yes" }, "variants": { @@ -1034,8 +1029,6 @@ "host_name": "aftereffects", "environment": { "AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH": "1", - "OPENPYPE_LOG_NO_COLORS": "Yes", - "WEBSOCKET_URL": "ws://localhost:8097/ws/", "WORKFILES_SAVE_AS": "Yes" }, "variants": { @@ -1109,10 +1102,7 @@ "label": "Unreal Editor", "icon": "{}/app_icons/ue4.png'", "host_name": "unreal", - "environment": { - "AVALON_UNREAL_PLUGIN": "{OPENPYPE_REPOS_ROOT}/repos/avalon-unreal-integration", - "OPENPYPE_LOG_NO_COLORS": "True" - }, + "environment": {}, "variants": { "4-26": { "use_python_2": false, From 6690f4a88c297f64c85ead174dcc28bc1cb77e21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 14:10:19 +0200 Subject: [PATCH 238/311] houdini can define default environments --- openpype/hosts/houdini/__init__.py | 38 +++++++++++++++++++ .../system_settings/applications.json | 13 +------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/houdini/__init__.py b/openpype/hosts/houdini/__init__.py index e69de29bb2..ab552513e8 100644 --- a/openpype/hosts/houdini/__init__.py +++ b/openpype/hosts/houdini/__init__.py @@ -0,0 +1,38 @@ +import os + + +def add_implementation_envs(env): + # Add requirements to HOUDINI_PATH and HOUDINI_MENU_PATH + pype_root = os.environ["OPENPYPE_REPOS_ROOT"] + + startup_path = os.path.join( + pype_root, "openpype", "hosts", "houdini", "startup" + ) + new_houdini_path = [startup_path] + new_houdini_menu_path = [startup_path] + + old_houdini_path = env.get("HOUDINI_PATH") or "" + old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or "" + + for path in old_houdini_path.split(os.pathsep): + if not path or not os.path.exists(path): + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_houdini_path: + new_houdini_path.append(norm_path) + + for path in old_houdini_menu_path.split(os.pathsep): + if not path or not os.path.exists(path): + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_houdini_menu_path: + new_houdini_menu_path.append(norm_path) + + # Add ampersand for unknown reason (Maybe is needed in Houdini?) + new_houdini_path.append("&") + new_houdini_menu_path.append("&") + + env["HOUDINI_PATH"] = os.pathsep.join(new_houdini_path) + env["HOUDINI_MENU_PATH"] = os.pathsep.join(new_houdini_menu_path) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index fd0a5b8c52..d1976a9776 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -740,18 +740,7 @@ "label": "Houdini", "icon": "{}/app_icons/houdini.png", "host_name": "houdini", - "environment": { - "HOUDINI_PATH": { - "darwin": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&", - "linux": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&", - "windows": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup;&" - }, - "HOUDINI_MENU_PATH": { - "darwin": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&", - "linux": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&", - "windows": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup;&" - } - }, + "environment": {}, "variants": { "18-5": { "use_python_2": true, From d43b3d1b2c0e61a67587700400a7b4d89fcd5357 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 14:16:00 +0200 Subject: [PATCH 239/311] also pass Application object to `add_implementation_envs` --- openpype/hosts/aftereffects/__init__.py | 2 +- openpype/hosts/blender/__init__.py | 2 +- openpype/hosts/harmony/__init__.py | 2 +- openpype/hosts/hiero/__init__.py | 2 +- openpype/hosts/houdini/__init__.py | 2 +- openpype/hosts/maya/__init__.py | 2 +- openpype/hosts/nuke/__init__.py | 2 +- openpype/hosts/photoshop/__init__.py | 2 +- openpype/hosts/tvpaint/__init__.py | 2 +- openpype/hosts/unreal/__init__.py | 2 +- openpype/lib/applications.py | 4 ++-- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/aftereffects/__init__.py b/openpype/hosts/aftereffects/__init__.py index 1c2d473728..deae48d122 100644 --- a/openpype/hosts/aftereffects/__init__.py +++ b/openpype/hosts/aftereffects/__init__.py @@ -1,4 +1,4 @@ -def add_implementation_envs(env): +def add_implementation_envs(env, _app): """Modify environments to contain all required for implementation.""" defaults = { "OPENPYPE_LOG_NO_COLORS": "True", diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py index 1a81c3add1..4d93233449 100644 --- a/openpype/hosts/blender/__init__.py +++ b/openpype/hosts/blender/__init__.py @@ -1,7 +1,7 @@ import os -def add_implementation_envs(env): +def add_implementation_envs(env, _app): """Modify environments to contain all required for implementation.""" # Prepare path to implementation script implementation_user_script_path = os.path.join( diff --git a/openpype/hosts/harmony/__init__.py b/openpype/hosts/harmony/__init__.py index 13b72f98d6..8560fbaf4b 100644 --- a/openpype/hosts/harmony/__init__.py +++ b/openpype/hosts/harmony/__init__.py @@ -1,7 +1,7 @@ import os -def add_implementation_envs(env): +def add_implementation_envs(env, _app): """Modify environments to contain all required for implementation.""" openharmony_path = os.path.join( os.environ["OPENPYPE_REPOS_ROOT"], "pype", "vendor", "OpenHarmony" diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py index 5b036224e4..1781f808e2 100644 --- a/openpype/hosts/hiero/__init__.py +++ b/openpype/hosts/hiero/__init__.py @@ -2,7 +2,7 @@ import os import platform -def add_implementation_envs(env): +def add_implementation_envs(env, _app): # Add requirements to HIERO_PLUGIN_PATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] new_hiero_paths = [ diff --git a/openpype/hosts/houdini/__init__.py b/openpype/hosts/houdini/__init__.py index ab552513e8..8c12d13c81 100644 --- a/openpype/hosts/houdini/__init__.py +++ b/openpype/hosts/houdini/__init__.py @@ -1,7 +1,7 @@ import os -def add_implementation_envs(env): +def add_implementation_envs(env, _app): # Add requirements to HOUDINI_PATH and HOUDINI_MENU_PATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index 43a1e6f7bf..d3562e2cdd 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,7 +1,7 @@ import os -def add_implementation_envs(env): +def add_implementation_envs(env, _app): # Add requirements to PYTHONPATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] new_python_paths = [ diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index ca4d931e2c..f1e81617e0 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -2,7 +2,7 @@ import os import platform -def add_implementation_envs(env): +def add_implementation_envs(env, _app): # Add requirements to NUKE_PATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] new_nuke_paths = [ diff --git a/openpype/hosts/photoshop/__init__.py b/openpype/hosts/photoshop/__init__.py index babacba9a8..a91e0a65ff 100644 --- a/openpype/hosts/photoshop/__init__.py +++ b/openpype/hosts/photoshop/__init__.py @@ -1,4 +1,4 @@ -def add_implementation_envs(env): +def add_implementation_envs(env, _app): """Modify environments to contain all required for implementation.""" defaults = { "OPENPYPE_LOG_NO_COLORS": "True", diff --git a/openpype/hosts/tvpaint/__init__.py b/openpype/hosts/tvpaint/__init__.py index 0e58e31a5b..0e793fcf9f 100644 --- a/openpype/hosts/tvpaint/__init__.py +++ b/openpype/hosts/tvpaint/__init__.py @@ -1,4 +1,4 @@ -def add_implementation_envs(env): +def add_implementation_envs(env, _app): """Modify environments to contain all required for implementation.""" defaults = { "OPENPYPE_LOG_NO_COLORS": "True" diff --git a/openpype/hosts/unreal/__init__.py b/openpype/hosts/unreal/__init__.py index dd7a575995..1280442916 100644 --- a/openpype/hosts/unreal/__init__.py +++ b/openpype/hosts/unreal/__init__.py @@ -1,7 +1,7 @@ import os -def add_implementation_envs(env): +def add_implementation_envs(env, _app): """Modify environments to contain all required for implementation.""" # Set AVALON_UNREAL_PLUGIN required for Unreal implementation unreal_plugin_path = os.path.join( diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index b95ba85fbe..a44c43102f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1090,7 +1090,7 @@ def prepare_host_environments(data, implementation_envs=True): loaded_env = _merge_env(acre.compute(env_values), data["env"]) final_env = None - # + # Add host specific environments if app.host_name and implementation_envs: module = __import__("openpype.hosts", fromlist=[app.host_name]) host_module = getattr(module, app.host_name, None) @@ -1101,7 +1101,7 @@ def prepare_host_environments(data, implementation_envs=True): ) if add_implementation_envs: # Function may only modify passed dict without returning value - final_env = add_implementation_envs(loaded_env) + final_env = add_implementation_envs(loaded_env, app) if final_env is None: final_env = loaded_env From 6e82f2e268c4511040f7d2b5085417a8b247569e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 18 May 2021 14:16:29 +0100 Subject: [PATCH 240/311] Create layout now works by selecting the collection --- .../blender/plugins/create/create_layout.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_layout.py b/openpype/hosts/blender/plugins/create/create_layout.py index f72e364f50..5404cec587 100644 --- a/openpype/hosts/blender/plugins/create/create_layout.py +++ b/openpype/hosts/blender/plugins/create/create_layout.py @@ -20,21 +20,9 @@ class CreateLayout(openpype.hosts.blender.api.plugin.Creator): asset = self.data["asset"] subset = self.data["subset"] name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) + collection = bpy.context.collection + collection.name = name self.data['task'] = api.Session.get('AVALON_TASK') lib.imprint(collection, self.data) - # Add the rig object and all the children meshes to - # a set and link them all at the end to avoid duplicates. - # Blender crashes if trying to link an object that is already linked. - # This links automatically the children meshes if they were not - # selected, and doesn't link them twice if they, insted, - # were manually selected by the user. - objects_to_link = set() - - if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - collection.children.link(obj.users_collection[0]) - return collection From cd76b6d1f4f20babd65136c12c0373ccc7021772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 18 May 2021 15:24:27 +0200 Subject: [PATCH 241/311] fix time benchmark in build script --- tools/build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index 5c392c355c..db94dbf1af 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -164,7 +164,7 @@ Write-Host "OK" -ForegroundColor green Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Building OpenPype ..." -$startTime = (Get-Date).Millisecond +$startTime = [int][double]::Parse((Get-Date -UFormat %s)) $out = & poetry run python setup.py build 2>&1 if ($LASTEXITCODE -ne 0) @@ -183,7 +183,7 @@ Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "restoring current directory" Set-Location -Path $current_dir -$endTime = (Get-Date).Millisecond +$endTime = [int][double]::Parse((Get-Date -UFormat %s)) Write-Host "*** " -NoNewline -ForegroundColor Cyan Write-Host "All done in $($endTime - $startTime) secs. You will find OpenPype and build log in " -NoNewLine Write-Host "'.\build'" -NoNewline -ForegroundColor Green From 1cc1f290eedd3857633aad607792ed5f3717a0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 18 May 2021 16:42:37 +0200 Subject: [PATCH 242/311] disable submodule update with `--no-submodule-update` --- tools/build.ps1 | 18 ++++++++++++++---- tools/build.sh | 25 ++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index 5c392c355c..930544a054 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -14,6 +14,12 @@ PS> .\build.ps1 #> +$arguments=$ARGS +$disable_submodule_update="" +if($arguments -eq "--no-submodule-update") { + $disable_submodule_update=$true +} + function Start-Progress { param([ScriptBlock]$code) $scroll = "/-\|/-\|" @@ -134,10 +140,14 @@ catch { Write-Host $_.Exception.Message Exit-WithCode 1 } - -Write-Host ">>> " -NoNewLine -ForegroundColor green -Write-Host "Making sure submodules are up-to-date ..." -git submodule update --init --recursive +if (-not $disable_submodule_update) { + Write-Host ">>> " -NoNewLine -ForegroundColor green + Write-Host "Making sure submodules are up-to-date ..." + git submodule update --init --recursive +} else { + Write-Host "*** " -NoNewLine -ForegroundColor yellow + Write-Host "Not updating submodules ..." +} Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "OpenPype [ " -NoNewline -ForegroundColor white diff --git a/tools/build.sh b/tools/build.sh index 953d51bd81..ccd97ea4c1 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -57,6 +57,26 @@ BIPurple='\033[1;95m' # Purple BICyan='\033[1;96m' # Cyan BIWhite='\033[1;97m' # White +args=$@ +disable_submodule_update = 0 +while :; do + case $1 in + --no-submodule-update) + disable_submodule_update=1 + ;; + --) + shift + break + ;; + *) + break + esac + + shift +done + + + ############################################################################## # Detect required version of python @@ -172,9 +192,12 @@ main () { . "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } fi +if [ "$disable_submodule_update" == 1 ]; then echo -e "${BIGreen}>>>${RST} Making sure submodules are up-to-date ..." git submodule update --init --recursive - + else + echo -e "${BIYellow}***${RST} Not updating submodules ..." + fi echo -e "${BIGreen}>>>${RST} Building ..." if [[ "$OSTYPE" == "linux-gnu"* ]]; then poetry run python "$openpype_root/setup.py" build > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } From 8e4d56a8c519b9251817248cf2578f35edea61cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 18 May 2021 17:40:33 +0200 Subject: [PATCH 243/311] fix look assignment --- openpype/tools/mayalookassigner/app.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/tools/mayalookassigner/app.py b/openpype/tools/mayalookassigner/app.py index 81aa841eb7..1ec3f6f66b 100644 --- a/openpype/tools/mayalookassigner/app.py +++ b/openpype/tools/mayalookassigner/app.py @@ -192,7 +192,7 @@ class App(QtWidgets.QWidget): for i, (asset, item) in enumerate(asset_nodes.items()): # Label prefix - prefix = "({}/{})".format(i+1, len(asset_nodes)) + prefix = "({}/{})".format(i + 1, len(asset_nodes)) # Assign the first matching look relevant for this asset # (since assigning multiple to the same nodes makes no sense) @@ -212,18 +212,19 @@ class App(QtWidgets.QWidget): self.echo("{} Assigning {} to {}\t".format(prefix, subset_name, asset)) + nodes = item["nodes"] - self.echo("Getting vray proxy nodes ...") - vray_proxies = set(cmds.ls(type="VRayProxy")) - nodes = set(item["nodes"]).difference(vray_proxies) + if cmds.pluginInfo('vrayformaya', query=True, loaded=True): + self.echo("Getting vray proxy nodes ...") + vray_proxies = set(cmds.ls(type="VRayProxy")) + nodes = [set(item["nodes"]).difference(vray_proxies)] + if vray_proxies: + for vp in vray_proxies: + vrayproxy_assign_look(vp, subset_name) - # Assign look + # Assign look if nodes: - assign_look_by_version([nodes], version_id=version["_id"]) - - if vray_proxies: - for vp in vray_proxies: - vrayproxy_assign_look(vp, subset_name) + assign_look_by_version(nodes, version_id=version["_id"]) end = time.time() From 916d3457f5f016da75bc3553b038f30e14f4158d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 May 2021 18:10:25 +0200 Subject: [PATCH 244/311] moved optional maya environments back to settings --- openpype/hosts/maya/__init__.py | 5 ----- .../settings/defaults/system_settings/applications.json | 8 +++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index d3562e2cdd..549f100007 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -22,11 +22,6 @@ def add_implementation_envs(env, _app): # Set default values if are not already set via settings defaults = { - "MAYA_DISABLE_CLIC_IPM": "Yes", - "MAYA_DISABLE_CIP": "Yes", - "MAYA_DISABLE_CER": "Yes", - "PYMEL_SKIP_MEL_INIT": "Yes", - "LC_ALL": "C", "OPENPYPE_LOG_NO_COLORS": "Yes" } for key, value in defaults.items(): diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index d1976a9776..020924db67 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -4,7 +4,13 @@ "label": "Maya", "icon": "{}/app_icons/maya.png", "host_name": "maya", - "environment": {}, + "environment": { + "MAYA_DISABLE_CLIC_IPM": "Yes", + "MAYA_DISABLE_CIP": "Yes", + "MAYA_DISABLE_CER": "Yes", + "PYMEL_SKIP_MEL_INIT": "Yes", + "LC_ALL": "C" + }, "variants": { "2022": { "use_python_2": false, From 3cde37241bb3b345b64159ab0c486cafda3109f5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 18 May 2021 18:58:09 +0200 Subject: [PATCH 245/311] fix cast --- openpype/tools/mayalookassigner/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/mayalookassigner/app.py b/openpype/tools/mayalookassigner/app.py index 1ec3f6f66b..1fa3a3868a 100644 --- a/openpype/tools/mayalookassigner/app.py +++ b/openpype/tools/mayalookassigner/app.py @@ -217,7 +217,7 @@ class App(QtWidgets.QWidget): if cmds.pluginInfo('vrayformaya', query=True, loaded=True): self.echo("Getting vray proxy nodes ...") vray_proxies = set(cmds.ls(type="VRayProxy")) - nodes = [set(item["nodes"]).difference(vray_proxies)] + nodes = list(set(item["nodes"]).difference(vray_proxies)) if vray_proxies: for vp in vray_proxies: vrayproxy_assign_look(vp, subset_name) From c76d2d4ce3271ad4c8545a591eab11f395f9eacd Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 09:57:24 +0200 Subject: [PATCH 246/311] update changelog --- website/docs/changelog.md | 95 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/website/docs/changelog.md b/website/docs/changelog.md index bec4a02173..57048b9398 100644 --- a/website/docs/changelog.md +++ b/website/docs/changelog.md @@ -4,6 +4,101 @@ title: Changelog sidebar_label: Changelog --- +## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) +_**release date:** (2021-05-18)_ + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.18.0) + +**Enhancements:** + +- Use SubsetLoader and multiple contexts for delete_old_versions [\#1484](ttps://github.com/pypeclub/OpenPype/pull/1484)) +- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) +- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) + +**Fixed bugs:** + +- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) +- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) +- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) +- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) + + +### [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) +_**release date:** (2021-05-06)_ + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3) + +**Fixed bugs:** + +- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) + +### [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) +_**release date:** (2021-05-04)_ + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2) + +**Enhancements:** + +- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) + +### [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) +_**release date:** (2021-04-30)_ + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.0...2.17.1) + +**Enhancements:** + +- Faster settings UI loading [\#1442](https://github.com/pypeclub/OpenPype/pull/1442) +- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) +- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424) +- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415) +- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383) +- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371) +- AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363) + +**Fixed bugs:** + +- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) +- Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433) +- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) + +**Merged pull requests:** + +- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) +- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) + +## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) +_**release date:** (2021-04-20)_ + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0) + +**Enhancements:** + +- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243) +- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328) +- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302) +- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299) +- TV Paint: Validate mark in and out. [\#1298](https://github.com/pypeclub/OpenPype/pull/1298) +- Validate project settings [\#1297](https://github.com/pypeclub/OpenPype/pull/1297) +- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/OpenPype/pull/1234) +- Show error message in pyblish UI [\#1206](https://github.com/pypeclub/OpenPype/pull/1206) + +**Fixed bugs:** + +- Hiero: fixing source frame from correct object [\#1362](https://github.com/pypeclub/OpenPype/pull/1362) +- Nuke: fix colourspace, prerenders and nuke panes opening [\#1308](https://github.com/pypeclub/OpenPype/pull/1308) +- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/OpenPype/pull/1282) +- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/OpenPype/pull/1194) +- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/OpenPype/pull/1312) +- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/OpenPype/pull/1303) +- After Effects: remove orphaned instances [\#1275](https://github.com/pypeclub/OpenPype/pull/1275) +- Avalon schema names [\#1242](https://github.com/pypeclub/OpenPype/pull/1242) +- Handle duplication of Task name [\#1226](https://github.com/pypeclub/OpenPype/pull/1226) +- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/OpenPype/pull/1217) +- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/OpenPype/pull/1214) +- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/OpenPype/pull/1202) +- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/OpenPype/pull/1199) + ## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) _**release date:** 2021-03-22_ From 0629338afdf3b23f80ddd9bc42abbf3e1d961890 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 09:57:39 +0200 Subject: [PATCH 247/311] small cleanup on the feature page --- website/src/pages/features.js | 230 ++++++++++++++++++++++++---------- 1 file changed, 165 insertions(+), 65 deletions(-) diff --git a/website/src/pages/features.js b/website/src/pages/features.js index 35e156a230..661f631959 100644 --- a/website/src/pages/features.js +++ b/website/src/pages/features.js @@ -37,15 +37,21 @@ const key_features = [ docs: "/docs/artist_tools#publisher", }, { - label: "Inventory", + label: "Scene manager", description: "Universal GUI for managing versions of assets loaded into your working scene.", docs: "docs/artist_tools#inventory", - }, - { + }, + { + label: "Project manager", + docs: "", + description: + "Tools for creating shots, assets and task within your project if you don't use third party project management", + }, + { label: "Library Loader", description: - "A loader GUI that allows yo to load content from dedicated cross project asset library", + "A loader GUI that allows yo to load content from dedicated cross project asset library", docs: "docs/artist_tools#library-loader", image: "", }, @@ -56,12 +62,6 @@ const key_features = [ "A standalone GUI for publishing data into pipeline without going though DCC app.", image: "", }, - { - label: "Pype Tray", - link: "", - description: - "Cross platform wrapper app, which is the central point of pype. All other tools are ran from here.", - }, { label: "App Launcher", link: "", @@ -69,14 +69,26 @@ const key_features = [ "Standalone GUI for launching application in the chosen context directly from tray", }, { - label: "Timer Manager", + label: "Configuration GUI", link: "", + description: + "All settings and configuration are done via openPype Settings tool. No need to dig around .json and .yaml", + }, + { + label: "Site Sync", + docs: "docs/module_site_sync", + description: + "Built in file synchronization between your central storage (cloud or physical) and all your freelancers", + }, + { + label: "Timers Manager", + link: "docs/admin_settings_system#timers-manager", description: "Service for monitoring the user activity to start, stop and synchronise time tracking.", }, { label: "Farm rendering", - link: "", + docs: "docs/module_deadline", description: "Integrations with Deadline and Muster render managers. Render, publish and generate reviews on the farm.", }, @@ -93,10 +105,10 @@ const key_features = [ "System for simple scene building. Loads pre-defined publishes to scene with single click, speeding up scene preparation.", }, { - label: "Configuration GUI", - link: "", + label: "Reviewables", + docs: "docs/project_settings/settings_project_global#extract-review", description: - "All settings and configuration are done via openPype Settings tool. No need to dig around .json and .yaml", + "Generate automated reviewable quicktimes and sequences in any format, with metadata burnins.", }, ]; @@ -109,6 +121,10 @@ const ftrack = [ docs: "docs/manager_ftrack#project-management", label: "Project Setup", description: "Quickly sets up project with customisable pre-defined structure and attributes." + }, { + docs: "docs/module_ftrack#update-status-on-task-action", + label: "Automate statuses", + description: "Quickly sets up project with customisable pre-defined structure and attributes." }, { docs: "docs/admin_ftrack#event-server", label: "Event Server", @@ -118,7 +134,7 @@ const ftrack = [ label: "Review publishing", description: "All reviewables from all DCC aps, including farm renders are pushed to ftrack online review." }, { - docs: "", + docs: "docs/admin_settings_system#timers-manager", label: "Auto Time Tracker", description: "Automatically starts and stops ftrack time tracker, base on artist activity." } @@ -185,8 +201,8 @@ const maya_features = [ description:"Makes all your playblasts consistent, with burnins and correct viewport settings" }, { - label: "Model > Render", - description:"We cover full project data flow from model through animation, till final render.", + label: "Renderlayers and AOVs", + description:"Full support of rendersetup layers and AOVs in all major renderers.", docs: "docs/artist_hosts_maya#working-with-pype-in-maya" }, { @@ -211,6 +227,7 @@ const maya_families = [ {label:"VDB Cache"}, {label:"Assembly"}, {label:"Camera"}, + {label:"CameraRig"}, {label:"RenderSetup"}, {label:"Render"}, {label:"Plate"}, @@ -231,7 +248,7 @@ const nuke_features = [ docs: "docs/artist_hosts_nuke#set-colorspace" }, { label: "Script Building", - description:"Automatically build first workfiles from published plates or renders", + description:"Automatically build initial workfiles from published plates or renders", docs: "docs/artist_hosts_nuke#build-first-work-file" }, { @@ -254,10 +271,8 @@ const nuke_families = [ {label: "Render"}, {label: "Plate"}, {label: "Review"}, - {label: "Group"}, {label: "Workfile"}, {label: "LUT"}, - {label: "Cache"}, {label: "Gizmo"}, {label: "Prerender"}, ] @@ -294,6 +309,26 @@ const deadline_families = [ ] const hiero_features = [ + { + label: "Project setup", + description:"Automatic colour, timeline and fps setup of you hiero project." + }, + { + label: "Create shots", + description:"Populate project with shots based on your conformed edit." + }, + { + label: "Publish plates", + description:"Publish multiple tracks with plates to you shots from a single timeline." + }, + { + label: "Retimes", + description:"Publish retime information for individual plates." + }, + { + label: "LUTS and fx", + description:"Publish soft effects from your timeline to be used on shots." + }, ] const hiero_families = [ @@ -330,7 +365,6 @@ const houdini_families = [ {label:"Point Cache"}, {label:"VDB Cache"}, {label:"Camera"}, - {label:"Review"}, {label:"Workfile"}, ] @@ -355,11 +389,29 @@ const harmony_families = [ {label: "Workfile"} ] +const tvpaint_families = [ + {label: "Render"}, + {label: "Review"}, + {label: "Image"}, + {label: "Audio"}, + {label: "Workfile"} +] + const photoshop_families = [ {label: "Render"}, {label: "Plate"}, {label: "Image"}, {label: "LayeredImage"}, + {label: "Background"}, + {label: "Workfile"} +] + +const aftereffects_families = [ + {label: "Render"}, + {label: "Plate"}, + {label: "Image"}, + {label: "Audio"}, + {label: "Background"}, {label: "Workfile"} ] @@ -512,9 +564,15 @@ function Home() {
-

Autodesk Maya

+

Autodesk Maya

versions 2017 and higher

+

+ OpenPype includes very robust Maya implementation that can handle full CG workflow from model, + through animation till final renders. Scene settings, Your artists won't need to touch file browser at all and OpenPype will + take care of all the file management. +

+ {maya_features && maya_features.length && (
{maya_features.map((props, idx) => ( @@ -537,7 +595,7 @@ function Home() {
-

Foundry Nuke | NukeX

+

Foundry Nuke | NukeX

versions 11.0 and higher

@@ -563,10 +621,18 @@ function Home() {
-

Foundry Hiero | Nuke Studio

+

Foundry Hiero | Nuke Studio

versions 11.0 and higher

+ {hiero_features && hiero_features.length && ( +
+ {hiero_features.map((props, idx) => ( + + ))} +
+ )} +

Supported Families

{hiero_families && hiero_families.length && ( @@ -579,6 +645,78 @@ function Home() {
+
+
+

After Effects

+ +

versions 2020 and higher

+ +

Supported Families

+ + {aftereffects_families && aftereffects_families.length && ( +
+ {aftereffects_families.map((props, idx) => ( + + ))} +
+ )} +
+
+ +
+
+

Photoshop

+ +

versions 2020 and higher

+ +

Supported Families

+ + {photoshop_families && photoshop_families.length && ( +
+ {photoshop_families.map((props, idx) => ( + + ))} +
+ )} +
+
+ +
+
+

Harmony

+ +

versions 17 and higher

+ +

Supported Families

+ + {harmony_families && harmony_families.length && ( +
+ {harmony_families.map((props, idx) => ( + + ))} +
+ )} +
+
+ +
+
+

TV Paint

+ +

versions 11

+ +

Supported Families

+ + {tvpaint_families && tvpaint_families.length && ( +
+ {tvpaint_families.map((props, idx) => ( + + ))} +
+ )} +
+
+

Houdini

@@ -600,9 +738,9 @@ function Home() {
-

Blender

+

Blender

-

versions 2.80 and higher

+

versions 2.83 and higher

Supported Families

@@ -636,44 +774,6 @@ function Home() {
-
-
-

Harmony

- -

versions 17 and higher

- -

Supported Families

- - {harmony_families && harmony_families.length && ( -
- {harmony_families.map((props, idx) => ( - - ))} -
- )} -
-
- - -
-
-

Photoshop

- -

versions 2020 and higher

- -

Supported Families

- - {photoshop_families && photoshop_families.length && ( -
- {photoshop_families.map((props, idx) => ( - - ))} -
- )} -
-
- - ); } From 225a645b49da1f7f7450d0ca977355183cbb24cb Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 09:58:55 +0200 Subject: [PATCH 248/311] bump version --- openpype/version.py | 2 +- website/src/pages/features.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/version.py b/openpype/version.py index 27186ad2bb..a88ae329d0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.0.0-rc4" +__version__ = "3.0.0-rc.5" diff --git a/website/src/pages/features.js b/website/src/pages/features.js index 661f631959..d5c036eb89 100644 --- a/website/src/pages/features.js +++ b/website/src/pages/features.js @@ -570,7 +570,7 @@ function Home() {

OpenPype includes very robust Maya implementation that can handle full CG workflow from model, through animation till final renders. Scene settings, Your artists won't need to touch file browser at all and OpenPype will - take care of all the file management. + take care of all the file management. Most of maya workflows are supported including gpucaches, referencing, nested references and render proxies.

{maya_features && maya_features.length && ( From c80bdefe475ab88d63e3c37e475bc7642c84c23b Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 10:01:13 +0200 Subject: [PATCH 249/311] update changelog with 2.x and bump version --- CHANGELOG.md | 80 +++++++++++++++++++++++++++----------------------- pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70f3f98f4..09882896f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.18.0) + +**Enhancements:** + +- Use SubsetLoader and multiple contexts for delete_old_versions [\#1484](ttps://github.com/pypeclub/OpenPype/pull/1484)) +- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489) +- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483) + +**Fixed bugs:** + +- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486) +- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480) +- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517) +- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502) + +## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3) + +**Fixed bugs:** + +- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479) + +## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04) + +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2) + +**Enhancements:** + +- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463) ## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30) @@ -7,28 +39,30 @@ **Enhancements:** +- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) - TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424) - PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415) -- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414) - Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383) -- AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363) -- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) +- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371) **Fixed bugs:** -- Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433) -- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) - Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417) -- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) +- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426) +**Merged pull requests:** + +- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399) +- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360) ## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20) -[Full Changelog](https://github.com/pypeclub/openpype/compare/3.0.0-beta2...2.17.0) +[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0) **Enhancements:** - Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243) +- Settings in mongo as dict [\#1221](https://github.com/pypeclub/OpenPype/pull/1221) - Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328) - TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302) - TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299) @@ -56,35 +90,6 @@ - Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178) -## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-13) - -[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1) - -**Enhancements:** - -- Nuke: comp renders mix up [\#1301](https://github.com/pypeclub/pype/pull/1301) -- Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297) -- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234) - -**Fixed bugs:** - -- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312) -- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303) -- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/pype/pull/1282) -- Avalon schema names [\#1242](https://github.com/pypeclub/pype/pull/1242) -- Handle duplication of Task name [\#1226](https://github.com/pypeclub/pype/pull/1226) -- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/pype/pull/1217) -- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/pype/pull/1214) -- Bulk mov strict task [\#1204](https://github.com/pypeclub/pype/pull/1204) -- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/pype/pull/1202) -- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/pype/pull/1199) -- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/pype/pull/1178) - -**Merged pull requests:** - -- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243) -- Error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206) -- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/pype/pull/1194) ## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22) @@ -1145,4 +1150,7 @@ A large cleanup release. Most of the change are under the hood. \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* + + \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/pyproject.toml b/pyproject.toml index f7b5dd1426..f7eeafd04f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.0.0-rc4" +version = "3.0.0-rc.5" description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" diff --git a/setup.py b/setup.py index c096befa34..5fb0b33f2a 100644 --- a/setup.py +++ b/setup.py @@ -98,7 +98,7 @@ executables = [ setup( name="OpenPype", version=__version__, - description="Ultimate pipeline", + description="OpenPype", cmdclass={"build_sphinx": BuildDoc}, options={ "build_exe": build_exe_options, From 424e9950e719a4f4a965bc9c8da4fd3bb4be9540 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:40:38 +0200 Subject: [PATCH 250/311] blender implementation has copy of blender `load_scripts` function --- openpype/hosts/blender/api/lib.py | 93 +++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 openpype/hosts/blender/api/lib.py diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py new file mode 100644 index 0000000000..38a745f6ca --- /dev/null +++ b/openpype/hosts/blender/api/lib.py @@ -0,0 +1,93 @@ +import os +import traceback +import importlib + +import bpy + + +def load_scripts(paths): + """Copy of `load_scripts` from Blender's implementation. + + It is possible that whis function will be changed in future and usage will + be based on Blender version. + """ + import bpy_types + + loaded_modules = set() + + previous_classes = [ + cls + for cls in bpy.types.bpy_struct.__subclasses__() + ] + + def register_module_call(mod): + register = getattr(mod, "register", None) + if register: + try: + register() + except: + traceback.print_exc() + else: + print("\nWarning! '%s' has no register function, " + "this is now a requirement for registerable scripts" % + mod.__file__) + + def unregister_module_call(mod): + unregister = getattr(mod, "unregister", None) + if unregister: + try: + unregister() + except: + traceback.print_exc() + + def test_reload(mod): + # reloading this causes internal errors + # because the classes from this module are stored internally + # possibly to refresh internal references too but for now, best not to. + if mod == bpy_types: + return mod + + try: + return importlib.reload(mod) + except: + traceback.print_exc() + + def test_register(mod): + if mod: + register_module_call(mod) + bpy.utils._global_loaded_modules.append(mod.__name__) + + from bpy_restrict_state import RestrictBlend + + with RestrictBlend(): + for base_path in paths: + for path_subdir in bpy.utils._script_module_dirs: + path = os.path.join(base_path, path_subdir) + if not os.path.isdir(path): + continue + + bpy.utils._sys_path_ensure_prepend(path) + + # Only add to 'sys.modules' unless this is 'startup'. + if path_subdir != "startup": + continue + for mod in bpy.utils.modules_from_path(path, loaded_modules): + test_register(mod) + + # load template (if set) + if any(bpy.utils.app_template_paths()): + import bl_app_template_utils + bl_app_template_utils.reset(reload_scripts=False) + del bl_app_template_utils + + for cls in bpy.types.bpy_struct.__subclasses__(): + if cls in previous_classes: + continue + if not getattr(cls, "is_registered", False): + continue + for subcls in cls.__subclasses__(): + if not subcls.is_registered: + print( + "Warning, unregistered class: %s(%s)" % + (subcls.__name__, cls.__name__) + ) From 5110374a1b0dcc40e1fb0f554300bb287c5fe926 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:40:58 +0200 Subject: [PATCH 251/311] implemented `append_user_scripts` using `OPENPYPE_BLENDER_USER_SCRIPTS` env to load user scripts --- openpype/hosts/blender/api/lib.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 38a745f6ca..6aa1cb46ac 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -91,3 +91,15 @@ def load_scripts(paths): "Warning, unregistered class: %s(%s)" % (subcls.__name__, cls.__name__) ) + + +def append_user_scripts(): + user_scripts = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS") + if not user_scripts: + return + + try: + load_scripts(user_scripts.split(os.pathsep)) + except Exception: + print("Couldn't load user scripts \"{}\"".format(user_scripts)) + traceback.print_exc() From 83590556c5b8adfb07303e9972a490d5fbdb9b3e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:41:10 +0200 Subject: [PATCH 252/311] run append_user_scripts on blender install --- openpype/hosts/blender/api/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/__init__.py b/openpype/hosts/blender/api/__init__.py index 66102a2ae1..ecf4fdf4da 100644 --- a/openpype/hosts/blender/api/__init__.py +++ b/openpype/hosts/blender/api/__init__.py @@ -4,6 +4,8 @@ import traceback import bpy +from .lib import append_user_scripts + from avalon import api as avalon from pyblish import api as pyblish @@ -29,7 +31,7 @@ def install(): pyblish.register_plugin_path(str(PUBLISH_PATH)) avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) - + append_user_scripts() avalon.on("new", on_new) avalon.on("open", on_open) From 8df855aa115559e3d9542c5b667cc11a52baae90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:54:12 +0200 Subject: [PATCH 253/311] modified blender's init to expect somebody will set OPENPYPE_BLENDER_USER_SCRIPTS --- openpype/hosts/blender/__init__.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py index 4d93233449..747394aad0 100644 --- a/openpype/hosts/blender/__init__.py +++ b/openpype/hosts/blender/__init__.py @@ -23,18 +23,32 @@ def add_implementation_envs(env, _app): env["PYTHONPATH"] = os.pathsep.join(python_path_parts) # Modify Blender user scripts path + previous_user_scripts = set() + # Implementation path is added to set for easier paths check inside loops + # - will be removed at the end + previous_user_scripts.add(implementation_user_script_path) + + openpype_blender_user_scripts = ( + env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or "" + ) + for path in openpype_blender_user_scripts.split(os.pathsep): + if path and os.path.exists(path): + previous_user_scripts.add(os.path.normpath(path)) + blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or "" - previous_user_scripts = [] for path in blender_user_scripts.split(os.pathsep): if path and os.path.exists(path): - path = os.path.normpath(path) - if path != implementation_user_script_path: - previous_user_scripts.append(path) + previous_user_scripts.add(os.path.normpath(path)) + # Remove implementation path from user script paths as is set to + # `BLENDER_USER_SCRIPTS` + previous_user_scripts.remove(implementation_user_script_path) + env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path + + # Set custom user scripts env env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join( previous_user_scripts ) - env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path # Define Qt binding if not defined if not env.get("QT_PREFERRED_BINDING"): From 996b93abce1d1990b23008d2e328f0f64a3cd9fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:15:09 +0200 Subject: [PATCH 254/311] removed invalid is_file values from schemas --- .../schemas/projects_schema/schema_project_deadline.json | 1 - .../schemas/projects_schema/schema_project_ftrack.json | 1 - .../projects_schema/schema_project_standalonepublisher.json | 2 -- .../schemas/projects_schema/schema_project_syncserver.json | 2 -- .../schemas/projects_schema/schema_project_tvpaint.json | 1 - .../entities/schemas/system_schema/schema_modules.json | 4 +--- 6 files changed, 1 insertion(+), 10 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 1346fb3dad..d47a6917da 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -10,7 +10,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index b1bb207578..aae2bb2539 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -603,7 +603,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 47eea3441c..28755ad268 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -11,7 +11,6 @@ "key": "create", "label": "Creator plugins", "collapsible_key": true, - "is_file": true, "object_type": { "type": "dict", "children": [ @@ -56,7 +55,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json index 9428ce2db0..cb2cc9c9d1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json @@ -4,7 +4,6 @@ "label": "Site Sync (beta testing)", "collapsible": true, "checkbox_key": "enabled", - "is_file": true, "children": [ { "type": "boolean", @@ -44,7 +43,6 @@ "key": "sites", "label": "Sites", "collapsible_key": false, - "is_file": true, "object_type": { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index ab404f03ff..2f69ea8864 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -10,7 +10,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index d1b498bb86..b643293c87 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -97,7 +97,6 @@ "key": "sites", "label": "Sites", "collapsible_key": false, - "is_file": true, "object_type": { "type": "dict", @@ -156,8 +155,7 @@ }, "is_group": true, "key": "templates_mapping", - "label": "Templates mapping", - "is_file": true + "label": "Templates mapping" } ] }, From 190e778e061ad49ed20853c4a3ecbcf2f3471a77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:15:39 +0200 Subject: [PATCH 255/311] added validation of is_file attribute --- openpype/settings/entities/base_entity.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 3e73fa8aa6..b2d0f8224d 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -846,6 +846,13 @@ class ItemEntity(BaseItemEntity): ) raise EntitySchemaError(self, reason) + if self.is_file and self.file_item is not None: + reason = ( + "Entity has set `is_file` to true but" + " it's parent is already marked as file item." + ) + raise EntitySchemaError(self, reason) + super(ItemEntity, self).schema_validations() def create_schema_object(self, *args, **kwargs): From c28a2acfdf50b4b39662c6eeb806da0cba21cd21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:28:30 +0200 Subject: [PATCH 256/311] added settings for tvpaint LoadImage and ImportImage plugins --- .../schema_project_tvpaint.json | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index ab404f03ff..903c5de842 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -47,6 +47,84 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "load", + "label": "Loader plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "LoadImage", + "label": "Load Image", + "children": [ + { + "key": "defaults", + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "stretch", + "label": "Stretch" + }, + { + "type": "boolean", + "key": "timestretch", + "label": "TimeStretch" + }, + { + "type": "boolean", + "key": "preload", + "label": "Preload" + } + ] + }, + { + "type": "list", + "key": "families", + "label": "Families", + "object_type": "text" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ImportImage", + "label": "Import Image", + "children": [ + { + "key": "defaults", + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "stretch", + "label": "Stretch" + }, + { + "type": "boolean", + "key": "timestretch", + "label": "TimeStretch" + }, + { + "type": "boolean", + "key": "preload", + "label": "Preload" + } + ] + }, + { + "type": "list", + "key": "families", + "label": "Families", + "object_type": "text" + } + ] + } + ] + }, { "type": "schema", "name": "schema_publish_gui_filter" From 99c4f11fa6375e8df910bc0654060e15a2c15085 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:28:42 +0200 Subject: [PATCH 257/311] saved defaults for the settings --- .../defaults/project_settings/tvpaint.json | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 4a424b1c03..d7fc46763c 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -16,5 +16,33 @@ "active": true } }, + "load": { + "LoadImage": { + "defaults": { + "stretch": true, + "timestretch": true, + "preload": true + }, + "families": [ + "render", + "image", + "background", + "plate" + ] + }, + "ImportImage": { + "defaults": { + "stretch": true, + "timestretch": true, + "preload": true + }, + "families": [ + "render", + "image", + "background", + "plate" + ] + } + }, "filters": {} } \ No newline at end of file From 8fab6f8e7015a679c36bcaf2c5999f9b4d69d22a Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Wed, 19 May 2021 12:26:36 +0200 Subject: [PATCH 258/311] manual for users --- website/docs/artist_install.md | 80 +++++++++++++++++++++++++ website/docs/assets/artist_systray.png | Bin 0 -> 11397 bytes website/docs/assets/install_01.png | Bin 0 -> 12023 bytes website/docs/assets/install_02.png | Bin 0 -> 9290 bytes website/docs/assets/install_03.png | Bin 0 -> 26277 bytes website/docs/assets/install_04.png | Bin 0 -> 31451 bytes website/docs/assets/install_05.png | Bin 0 -> 8057 bytes website/sidebars.js | 1 + 8 files changed, 81 insertions(+) create mode 100644 website/docs/artist_install.md create mode 100644 website/docs/assets/artist_systray.png create mode 100644 website/docs/assets/install_01.png create mode 100644 website/docs/assets/install_02.png create mode 100644 website/docs/assets/install_03.png create mode 100644 website/docs/assets/install_04.png create mode 100644 website/docs/assets/install_05.png diff --git a/website/docs/artist_install.md b/website/docs/artist_install.md new file mode 100644 index 0000000000..94a8bcdfe1 --- /dev/null +++ b/website/docs/artist_install.md @@ -0,0 +1,80 @@ +--- +id: artist_install +title: Installation +sidebar_label: Installation +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## Installation + +OpenPype comes in packages for Windows (10 or Server), Mac OS X (Mojave or higher), and Linux distribution (Centos, Ubuntu), and you can install them on your machine the same way as you are used to. + +:::important +To install OpenPype you will need administrator permissions. +::: + + + + + +For installation on Windows, download and run the executable file `OpenPype-3.0.0.exe`. +During the installation process, you can change the destination location path of the application, + +![Windows installation](assets/install_01.png) + +and create an icon on the desktop. + +![Windows create icon](assets/install_02.png) + + + + + + +For installation on your Linux distribution, download and unzip `OpenPype-3.0.0.zip`. A new folder `OpenPype-3.0.0` will be created. +Inside this folder find and run `openpype_gui`, + +![Linux launch](assets/install_03.png) + + + + + + +For installation on Mac OS X, download and run dmg image file `OpenPype-3.0.0.dmg`. + +Drag the OpenPype icon into the Application folder. + +![Mac installation](assets/install_04.png) + +After the installation, you can find OpenPype among the other Applications. + + + + +## Run OpenPype + +To run OpenPype click on the icon or find executable file (e.g. `C:\Program Files (x86)\OpenPype\openpype_gui.exe`) in the application location. +On the very first run of OpenPype the user will be asked for OpenPype Mongo URL. +This piece of information will be provided by the administrator or project manager who set up the studio. + +![Mongo example](assets/install_05.png) + +Once the Mongo URL address is entered, press `Start`, and OpenPype will be initiated. +OpenPype will also remember the connection for the next launch, so it is a one-time process. + +:::note +If the launch was successful, the artist should see a turquoise OpenPype logo in their +tray menu. Keep in mind that on Windows this icon might be hidden by default, in which case, the artist can simply drag the icon down to the tray. + +![Systray](assets/artist_systray.png) +::: \ No newline at end of file diff --git a/website/docs/assets/artist_systray.png b/website/docs/assets/artist_systray.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0dd23375d0eb8223229a5547476e89d544ba75 GIT binary patch literal 11397 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_01.png b/website/docs/assets/install_01.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b55826a3c3f2b427f42a2b5f7a0338b073002d GIT binary patch literal 12023 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

nhD7*CDKvXRlD~BAWDpSh3 zMN!W%`;u?oFv`6mDE=dc(bZ5uOZs5BOHh~<#U{a6b7)OFQ}G;Y>!M&$d8@Mba9Pd6 zOTIS!9+?CazNlm4s1{-a$2lF}ADDMwj0Pq>c6$UX&w2Yb_K_8_gw?VX+irD6{RRle zj>LQ<`oe>QW09*c2u7H6zJA;)jJnI>X3(Y==G=I7Eq#Es=3d&@!_S3h%w3;vrnsp7 z&TFFtys{)d5n0+Vk0&$MLv5Cxb>9eWW?0sASTU`ydYc$%qB5nuvK=?x7Nz5~0(vxM z;e;B_$)4`lP086;zJ5{m72frFBx+4%LB)w5ied{nQXHfu9 z;)mYCjvZVlMU*zNdm5bKg=2HGGz;QuAOZZ2PpF4CHn}9*-Bhh!wAu0I@D)7We4Lkb zAPasMawI|qX4G3}wQnXZMk3=UWR_KH_9k2F7u&l=7Pr|YjccPa>X73VqUi?eshVx) zDp!uWhV_LV)dh`}M?^MIZ&EKtOyt7(LQ3z9f2`BtKT@N90ljLX*v_&}jj-M`Dnc>u z*;U9}3`;+Hxri5bRau&`t;M&4-Kggvxv5BHeZ6EQ1})VMu}a2p={6;3Rs9dB5 z8i+9OWpoRVXT`uUzthV?-T24Cet6FgMvjQtW01xXMh}I_w+K^%#CcGAN<)_{mhmY4 zFq!fM)O}v&5BI~Jb{11E^~Po(weis4=BCX$0q@-{ZY&^FiF3pi(ItgDnqhD?FxxU1}kHrneyzx4C3y8WF`Y~alAS9 zxg)<%upU#=kEcJO&Xy2;T4po&QO^cW_rJ$Gp03Db9NS(Nu@PLxX>iG_yBuLM{3(LE zIxexsz-do_0p?4Q-0p~5^0{RyD~65VOy%332;H;*e0*TTH?ySf+Ah@GSliE7JX*}P zeRuiBoxLHx7;%QI&I2Z!K59`*m)2zM)d01yF53d*Vz<@Qnhu6scNQ3e8aDKzf{+Q5 zj-XC2a?OD2<{3)uSe>2?!p8@r_9|ck++i}Tlg|jWQq&_A-X|7wYy0SA1?9tj*&ht} z7r&lZ)I0BpG_A>g73kX>!PzrUjg?o3JNLc(&0=$lRPEb|KddTtTc>)rwEUzwV+$|< zKo`_gigtJ{JXONIUXe0`_&+=>{de%)*A6Lda=WBFSGvYkS2G}}j=wjIpkF$xo~3;L z?-DNUYpn8fy!_xL^yg1r@DKl=2?G5*mp9O?t3*Dj*8lGO5#sY;1|0Wg_CP=4$Ff;> zav0k%CDqeE;hb@vnNp~QGw!?9i6xU6%JWsw7qLWTm(bqIHwt?Nee;qvJ#OD=_Jgmx1$A!C5+7=A# z5)vp&b)bd4lkfXZSDT5}BH~6NG@OBbZL^KX7d9iwG0OAMTi(*=oHe&l7}GgI5v}H4 zK;FAMVK*>lhqQ%&@BV|*9$G~`o?euHvg9{zWtT9h=g5w_3Jxzhp?KgCypvQ&K9)U3 zuplt&_KqZ^;KB#Jw2XsQy}L!a_~l$0KScB?h!xmEB8Z5LL#WIw-_FlwQE`_Xz)I&_ zWF@j_VHUqL)2;;i8Zou96G++*eU}1SZyfyA=`!P4V{Og8>pZa6z1@7CrEcek)7MkI z<29V)pQL^^uV&Sfg%fkse0{-kX1h*N6u=9_l>3T=D@a&u^`Ilm+XLx N=bV44JA327{{rn$3-$m2 literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_02.png b/website/docs/assets/install_02.png new file mode 100644 index 0000000000000000000000000000000000000000..1e97fd8139106d799fcfa4a8d6a6c30dcb467fd1 GIT binary patch literal 9290 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 Q{|9jVsQ(X-ea`0m2gB+U6951J literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_03.png b/website/docs/assets/install_03.png new file mode 100644 index 0000000000000000000000000000000000000000..1ddd0eea8b7c720100db84068d79b8b8ebd37df3 GIT binary patch literal 26277 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 DI@hsL literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_04.png b/website/docs/assets/install_04.png new file mode 100644 index 0000000000000000000000000000000000000000..6c03c5abd44b02e5bde73ff817d4cc069ac307f0 GIT binary patch literal 31451 zcmXteWl&u`*Y?5P4(@gi?(Xhx#VHhbDDLhK#c6S;xI2{M?ykk%T|VyTo$trqNoJCj z$+a{qD^f*C8U>L65dZ+7$jV5l0RRx;pZzy@$j|>$9RuHIgWxEm;|u`cfdBg-WYs{I z000?4Rzg(6G;0gqS)J<1rUiV8$c!zK?cAe6OBTP%(qn40MR8f zP^U`tN8O0)+>eZ)9bC!FrY=VLn|gwUZJE7k6NyH3;=rRx;EaVVFKr;XT=FA87TsaY zh-LQX^q}(M^p$5*tJ!`My~pKY>=yZ4M?~3uy}Zr!MDvW#BFA@Po7jCVGE&jj7;}}| zeDMW9#Z~(`8lx)Z_^UhXwYP;1*5WMkd^8Q!=3{QT_5)3GsF z=bV*0^ViK1go}e?ZedaV5U5~lN?VjXGd4E%Ij27w7i@4rxe}>J(Gq#~SB8m^I8$V% ztMQXwQIR<4bNcDu|F-+J^yiP*EOOO@s>12~Z!1_NB&5X`8aHE1jm#x%JxAHk0oDBq zPV(lRP!xR=0-fS%lpUljcfxvv;R19Dm0Q0XrI6odjT@z(&i|IINWsQ6&9K4eN~cgsjK2x1{cpQsb~dAk)J(lij+PTf^@k2$#~0$Jj(E&F!4slmEW-_4NqSFHQZ5(Dptna(O+1sQ{xrZoj*?7 zN$k(PDi|38E(NeaVtvT&gd7|8Ph~&)4zh`bUKCztqM!aE7;?tbkbul6hReJ1Sc z`npA>fVdV3_QlK0o#y->h+&*S0GvAeL$~*pFR9&mSGd%J<~fWqh!EMw#^i+1Q14@n z)L!Tb-0hKPi?{{LIOqYj^dH<%1PEk|MEV{V(g&n35sVC8B!AXn5)#G~t}9Ej^ViTLpnjX~g56hkSTA3h*~fwK=co9|=UaPx8IU>-FP0dKvO zOSC9*9bU?b#dqsUndq_SfexGoz`%m(Rja>yT~zkpH?;HnH(L60`c7yk@@(xt;%|1K zz~yG)j#ok9Y@9TGRD`@ZtWVPal&gLA4J-ZMssXn@EHT@^+XcyCu69e!;Ghz}KgLgX z{S&-ikney+tl6!w(DJ)GSm=5z>G{jsR%ZL(#55_$bl<@OY2hcsvJA}$BWzAYbAb<1 z|F<=Vk9>zJ<;%nU`cGN1{lK?UXCK{sE4V@Pli`gwW}6=4GW4?EOZzxl7(NJ$jCOHe z`9!&CGKPqOxP34DqkaFHn;kh_mkmA-#BiUl4br#SL^>yfs5l*2zBw6El&D|(VA8S318oEzk8tcF(N#C zQt^W=@&DNL(jx;AF8pqSw0UnpDFZ1ob^1#BHZtPyF2M3Faf9YRs0|z_W%;k)KJM1| zNlAl20lOpBuk}d&@2fxOX1Wsp1ERf3ij7m+@3K3?`=#$1Ic7hNwVC;2)W7EHREsL$ zR_Z?xPM>u=WOJU&KDqGwP+pIqDzH`X%KzhbKhlCqb(L_6Y8INx&6nZutZ(z9*<$l5 zViyt+1(2Ae_Ph7b7MUSjpz50WTb0G=|0;dZefK5=1V;i=lwVIj+B{#67Q+fxfDh$a zB4P&akP03T6*m7Fg*%p&9dF^T%frDncCRV1%RT=qv`^fR=N(>V=1sp@Ii47d2U4!h z+kST|P}!+A1}BFkH+E%mYc@7k{k(Zf@$rLEA;T1v{Smd*+4$cPG6{LTFM{PsUoak! zZ^?#dMylTxrdF%lT2bsKJf5RVKE{Q2D#FEf|FjEv>|zSjZXQ`S$rIBU*n8s6*1k_1bJsF~K%#LON4;n6 zW1Sb`^=N`&y=2`!L}!i+bR8Q_ZEeph^k>xnS*)rhw_rK>oX6UuW}$!}DQwgxE~WRi z&4^KVB>{_n!4-*f+tihc=~KdtQB$kBYqf+zscn%KKN>uvD91ftc-o*_+A zHhX*iY_B0iO`#eIh%}(2%bfB+(~2sicZSp~C&QsHGw8 z#e(p)gg@F(4BuXDW@I_CIxjF}_JsUhp(zs#Y)+PzRQU5lt$7w;%uLbsjPAk^fsuve zA_-D>SSNZsauVfAKOoB9la17hi!D%J(}Y>y9$%Q=$39XfbFx?zwM_q$%0?E*aG@)1 zR!%4cEbCPI9AW4E%@5&fi<<@yo^fJFUVgd=G5i+6uH*gGZYSLnLXGj_=@I$TF0`6; z?5L05`AHaLh$O1DYlRX#-vW4|IGU`Gb^#rBS?+7jhb12+Hy;PWBtmys1M;s__jFv5 zTU2g$u|GXug4xjosOCfgSOA+$_U`*hL*+ERK&x#sxZql|L&!2ER?sh=qsh+PJJ(w% zNnUjH;&P5-hxwb1WBrS^!fx|spXA+wXFUX(EO%#34u0RQ?hpRi3x8%?;7I4!|FOC% zv7vV^@8X5^$&tvr;c4xMjp5VUNsPVao-U)ius09J((ZDO_cXKqT9x%&(bX?N8XLqH4^jl0uGsq(ZRaQhRP=cE*G#s5Qr(Qo$! z-PQWbDQ4Hu)8<<e%{2wg6p?NQpwmgklP&3z*SM|YS%>bnSLGX*fFoMi8b7< z=HVJmROXAf|LFd{8 z2U05!y)Ub&JDv&Ou)IM@Td(H02JSMtX&8)S4Y2nY9Uui(yKaiX|(mf zJq;Jh5DUor7ai{QN9ml3uvWEgCK#usqN-{Fo3!-x$sq*T`^1Ra|%=XsQha zKE{6ZMO#$gg!}X@?MMqDP(q6`*aLrCO?+%HUs!khis0K@Ip1Wp2^QuaBE>1GY@8KB zyS&AISyWM#M{Lx*$O=g)M|&WdSNsNT+UZYL6feN>SRhO)bfwjf!FqL1DbQSAxvf|h zvSL{3a!4HC@dULwJH{YmRBn~V(8@s0FDpG~FD_*+{UaTmbPo9q9U9I^fu+TiY3grY z1*_)-weZI>rvL2;_ugV_e2l^kk~5lcl~)d~5k(lYVExmn*2DhmkG7UJ!D7OHWzok; zznp;o=nd5%wE}3V?&?;NyaHS!CDnhNC3`$58^!?|&*nj}#7X8%>}Dcx0t znaGsesa5y3H^1h$Ah~+!O_$T@_+fJQ4D^r4eP|jnrd>Clu~yf~j4{Wl-Ru;-mV?s~ ze5a%#jJ0VpkZ4kSvzaCjTRIS9HU= zqc~ry@kPr?v1!^p44rJ--Zq*J8=eT|A^u2iGCuLI6vcCy;T;66Ah($@0A{3VGML@e z3y2BtK1CEDk)Blqf?C`1TYVAYa^7u1U@-t^bzKCy1W=E^ z%uA){X>AJAv5MkG-RY**G{YHT4c-}KI&Is_)!R05-u4?p(R?f>`mW*2+-o_5=O;gUC)~8zbknf$ z^Da;jidrf!pyxHb?(*6cy@>83(&#F1bghv^y74y!o1&htHukO-o10*BJT~oLi9Vi@ z?kAcZzuoUUr&S5l$tF#W5m`EG+KWa}IFWdr*ySrIi3o9|(Y`pQubwjPF*w`QH|v~R zJKy8LIzwcKSFXXRE{{cOGPPVgmYxP+dVa=}gCuWDe+^blC}(?%%9_{VVmcy#OpRHD z%J2R5_mzmsCh+s|-9Zurp&qM-XWJSMt6sZT@dNfZcIO*rh9^Qt>DtF^(!xN*gp1ZR zcJ?*=Vi{}pZ2k6$2*Xe;_GRx_KT>pGyf6ct5ZY;f+CT%x) zI0i+=6pq|;;#yPfgg4EYc5}?`q>90uDucCI4X4y3-sf`4_aYlGOF|HM1VDPiEp~! zu9MP?Gf_3!V6r@94+=I5TIGw770ojX)kbGI3Zx$wMBYI)$X}J7a=o5Y-*QSCC(Xb(K2J*)9W+v%Z+;wY9pULfW~H{u=4nsKGBG z#xiFe4oU*?7zGtDs<$Q$*Ech<>(;2Mr6Q48MP8ov;RNrxi0-?%LyS4X5#A`4G?FEP zuNDToPCoH$z1&&!`Dg6;F12K-Sx9re#Y7vp?-=J5pE%qx&Z0e3N@hkDs9Gc&fGk^r z?2>#h#zE5u1WYbl!?C3_S3GxG0(Y#S3zQCl+p~}+r)?=zoh_W;?i8xJ*XotbKX{*_0xb$u@?kJ5jB>|IY8x)vi zfU)QJ#&QIx--adkQbBo5{c}5iu6@xU86?s&BqbuyCBW0MK}jHam+)k(5NJ4cr^qCE zDK=3yQ*yF-5lsE$NDwX0%2@Gt+@L@$DmAwf9kzW<(BGMF$V}6CexgLJDTJzIiX%#L zY}0C`8?tyzLND|Gx<4*f?xGh@D}J^;2ysE7tnPqaQ8t)<+9-F_tajFqI?s2v5`WiF zQke9`BUjCVgab6{RT%n19#YDdXzkqEIS4*(Tx9s&2%?hCa?T6tQ#@{*^LJ*(1oYH` zxnft`;8ZI?zwI7xsIvw)GW<^>vm!&5xJwIxoe)*0`3T~sX)=^Ftq^wK1)+C``ef-A z!6#6W=vHT`P%cTZn4vL73!UI?Pnkp(G&=$v zO{cdq93gUhWqFLQu4~4XK#-|3f7Ty1I&U$RKdyyGPZslw1OkuT1$WQTe;2Z1=jU!m zE04NYnKOA`_~GP~X?45t;+2n_Utu>LbqwCge#h^xi%rX3c?|vd*F#Avc=@iJugsKE zQNw4O#-o;I(#|YOgK;zK_EP(L8ojCTbbBmC=KrtgcF0!-VT4<;^-q^*5kpHI+BQ0o zW0T{L#BAC|8dYR0zgEa`+XqOo(jkVvguTMybenGei3N z;7tleQ^;M3iRJV{WHSPUFkz{ZS#SXngKz+i1{vXSM(21^XmK29I%yw3%+`~e$`lx- zJ=Y5i%;;OPCqpz>0MO9$mJ(qQDd;QZ+h^1ecF8fa7#b; ze3SZPdu;a*Bz6=`IGvoH8C!kM<epSp3HU}D=bo9>N$(kXT3E3oJB9=U0m z>X7l0Iw%EKGk!)ut|ZVW*RpwY^c+5UEQT1#BM@(4I9bwtdez%~HM3Q4QR{6#X}Em! z^*-keSHsD+3@p%Y5uUU}N9m0tv;(v)OC)+Ra_2yvQaIr% zaw8dPO2hj0$lbk2q}!;~{RJT*AUaQy@&Q<^08PaoMA`UXNv(9qKw=|R5~gx8DDXYu z8`>0MO6;thk`9t!MeM1(`vn@0FkFD+Vyc`kakt4~@a zW!q5bE$6~q7@I+fzM=*R{^7$)J4 zPwWYwDX}+hn~!^Ybq&uc^(_V|GU||pB1cXOI=*-uB`qVP4t*!0g^?I_sLA*mmYZ8N zUsmO$73N3gKrT*pvbjU2^Ct%L?<8667v{!`tr8JZ4!4cbeQDX71>Cff^#$IoHkU(k zqELb?e-3lLm(UKRcEoXOd~IhDcv{<9;Fo@({ivHuiP`DpX^FFmNEytUOfgAmZ{YBT zA$^+>=l7G#>ihIGVcoaO>!%im%YLHZM#iem8 zB2!VO?Qns79=a0786J`ekxO8jfaYDEEanbGseY*`#PtjVI85g+l?Dn01TQeiyQ+*8 zF#T21EKZt2A0$664hxVBnQu$#MNmhOu2eD3#iB3CRJdj$D*rH_=~Sq z9BRUj6~Y4_zoWnA+{hT6q@=+%udlCmJht4GT6ZjTE}FUd&^Ce;E{;j&Mm%jBk18FQ z6jALFSD8zb!}~Ihmfd|W`I}&onC+Xg=(9wxlld9t+JDvMD)lD>@YhS`f+N^^moZda z)$!6{(2HvaZ5g#FCKu*uJ)ZB&Z~EKle;O?+8j9|ekjv-aNVERhK*q>z{n^Pn_xw1c&Uqa@$a$qF zyrWB0?gEQ6{2rXDA*bnp z1%T?Zl^7Ka_tdBF^O; zzkITAN0ZIx`(5$yy*u2rB&NJ~X2|oHT;suF{qPPiu(#JN7T(o%u%*_(iYU*k+u28! zXP3XJC1TQGcUrvT4Bj3`V}&_rzrX!_)pnoB){NH@0-9q=orPVrDH#x^F_F)XzejT{ zDjwmFDNh|Z6TzlUk&)(Tze9OrhUE-_o>n<0S^B>}1z0C6SZW znGS1^l;=ZeU{JOO8`F!h%}`w6hz5iXY13{95jCSTA^`-#KSFU}yu`HdL$9O~^FyN` z_gV5FEZ35AzLGL^?EPBYYHuns8ynBnvQ*m{Uu&ZNx&p?*!B#s6a9s6gp!^J$F0Sul zvBzUgH;L@7y*7x``P`_F56Tnfj`{~jPug;bP2=*kb$}BMT-@%Viy=9k5vLtzL-ua< z)wbP_a_WQU+IE5@lA-cI3QYSD?2Dg>1c9~yS#mphb4l+rNQ@dPbs7Ye08z21xz>3j z6`+whNUV4lQkFNyk{D8~89w33Zp58GsHDxQjYF>D>!#yL)I_fv=Iff|X!dzWHo-_|&I$Sbps4LSf!r~jE|78d(Q6bPHm=?@g;En#4 zEk^f+WMHh9f%m7qR3y2v^7lc+APJ9w)WYTdiZ^ia9=H`3SHz%{M%Y_-=1_Az*45p8 z(v`p@u}Kzea^%rdL_*Tp#1;VUc-SYr6y2)AQmV}EiZ}m42u?OV^UzU}VW1|1n!G?% zfInQL3vn5&Y+s)2`cIR=t%S{Nc`^c`v@=$@g<%Pj-|^AOL{u5rAd-o6lAT$$$R+KC zzEUp^l;^T#OU63Uy@tu}2UCX`G07m~o zwDI*DBbO(F)uz=BuQ^JV#&w&rY7i1%}^Sw%fBl1l!`c&O8-*z=8e|dGhr6r!A=q^2);tXz|X|%T(2S8 zfwmye9UqNLxctsos8(?QqDddJ+D-e6;2Hg=XEIgMDiR6Cm22t30fV-0p(87e>_nz= zX{6Z!t);U;=dfh)`WX!NSX#StGA&S%iAeGb!^yjl&`61t7)iDO^d#+K0rIFrhLoR1 zCHG8!l%cGmGL_PXrnn(~Qv|CJrZ!EDh|mDxp)p7t7ZS1djqmo{dwlTdFQ_Lb5m&y{{ihx}w{*6hbvB;?&}=j&zqbl}n?ry_ghU+0rvx zZ{Eqe4S0%tA3{=iC!VngSM@Z`X!L zM^LJIoTwsN5$MhuStW%8AK?RW_8v)&jL3^hu?X)T0O;j83Luk2g0P>owfRV<9LX_+ zw#j`~1{i07GK%CLyE3&D^N3Vo#yzox$c4d4S{T41Tg$IOlF;e^&@@z5{v{L5tqQYB zYtOHt+`%oNpj5L2Mh#KT3IVvx*xFH&NoUOxl|Y9jf{L4KB8Kh~$}$8Gb$9+9c-9jL ziG76tOqXdHu(-<^&0H)G)d0@29`1GR^G8!~k*vYzE9%FQ<`c@(vLaC&G5 zWtZuLE)mN}<9B|sJG4Z>+wHxc((Wd$`&s$_UkiX_q1D*dwK^zy0+?PC=*)gVq4VTH zyqc=3vXh8%(Fj7K)ffw$0}MIUEPyL#nB-<9cVb;(%FBh?CQI_ufSP#u-C`AV^$V zlrBZuMrODmsH#EnyLJL4d<#Y7uEh2<;*q9SO;b<*@Mfyo%YM*c4mpgu#h8LcI1X^;K!<>}s1qK0UY>K5vRq^dW9N?qt571b>obxUy~H zu&)O;c$?JE>cMintalt*c1f5{PE;Wm+kChJnKvRmf$fLoZ-Fsm%}Bx^9O`5-5p-Gs zSCd_%0n7eWU{=TggUrp2j$Z3|{?3=6EcCB<^Y-?r&0MgGv&gW)v|Q>|=5G6^^cGH8 zvf09|Vziu*iq@H7!RFRp&!@xnjo1hbwU^kDkt}RFs!N3})kNE}8ImpsU#*HAqjnw< zv>^#~Y1hRY@FK$fK#Nf_WTOmXSYX{t9kQ4fSl*VykS`YlGmj!R72YZka1w1+pvZ={ zn%9YI`IZPNZRHW|uVS8MH(`9&80%KjzChT}J)8!@T!^ir*bDh7-c=*ad4$gS=0$#-|jPm(#SaYdSsdcZ}%p)z`2P ztcw*dO+#4MI#(Wl1HXpC(|huRLq_-$`eI_IX$aH{QRV&O$(f`>{N^vdsk-9E_rx@Y z`medl-HiwGjon#i9zUqj#gMD4^w~RO;MXP}V1*#w))LU%y%7v}=JAD;2VEqhca&Z- zODe{hD1LOG8+i@4*FIJ)!q@A0X0eTgcKa+S=d6RjVs8B*dL?N2vp8`t4{cE>TS=L+ zqoI4{ma8@&wbc@Qpjb@9dor`5c7>JxrA}LwK~E0}dfG06i_(!&gMRk>F!xXL8-s3PNoQAbPkSoT?3auoA>b^20Po-jz^}- z_Oma+1qlc>VciDkiwEu_lTpJ4?RRRv12gkHWznvqep13FP`CHX(MMA+MQOq6&56hN z|8oD9^)4{Kz1vU9w!Fy0o&39?s{IU0$f>MnXxK!jc@cv`xdSb+{^Z|^a)@H~WO`X} zLb?kFo|9)WPep zdEIJ7h%mikXOe4E7uodEi~cfg^i!Hy)OwIh&J|J*K8C@8E8jmr6p1a1I(|OwRqy|q z%YbxI;I!2E&$8KEGXI_VyNuxJ2|Sgng!zT#;6$L~wU6J+D z!W5Xp#OkQH9=4Nl%jNaNUW9q5D-gdP7iWw zgv1HU^hhhO%V{o%n}-d73tBbo7C9zh%T^|nlcgxMsGm%gfwhOjl36S6iJqg-BXic2 zj5UIcrOH%IBj&vpI{BR{c^D{ug(dj21M3|)JVbz?+e&FBLu;?4ad$ZHpV_8$APJ6R z-@R)TTonfYXrn>KM&etR_!dV-kE{0G`Zv<8*P8mAB6}U38t&g(wOJxb&wqJD1F1$t zF99Q-4-ns9?~0e7KPzpP(F5g`5LkrWGagzJ?LKc5iY5^h%|>)6sB}z?rmucSMx z=kPHe*>vJqP@U7`_9|+irHWJN_lP1JwcTqIHW9JNQ@K-G?0InL5O;Y9rP7HhPgSNF zwE8Y=p;UHz6@!8%9KJ+L#SZ-w=l7KB&}c=g>OrQM>qgFV&2kR=UrCe`iM1uzcrf-b zI49TFf3%aeLgdMn)XMoa4;*5(L2(2fvERlmJ+)hQ(s15fRjC28li9ZZ2a$q2v>;d% zG{)4HE%Vy99s07n7Jsq7IGsNj`s;@kf`adqU@0_JLF;(w#xhb#yp+#U^@SzyAr)WR z_;vMN@HJs80-bCurNpug0`Rf6(i>e)6Nn1QRdc~mu}ZAu_!QneHAl-C%dM?H1retx z))<;YBgAT=5QeTa(()z*S;U6oN-4j=%l^x#Oj0?{Uf-_5Wj$b_;#I{sN?}#alLj+p z7Lgfr87z}zpIFI+Q9t1(FvvPCRr7d^2wy-sviwhw7(^^f#j-qQWw*Utx(OCYQ|dTm$jXy+N} ze;E-1lRXB;JXuwdYOIamT7A4k3vON0zeq=r#Yw-}#M#PFoeQvZ%HuF)BCmYR5NQMO zzyx%jwaNK^!BK)T)?VgbetxMK0-Ufd_@V@CTO3eFc0xX<3GHfhDM(~&1>e=IwoDTV zTnZ&{cei_4fiw#F!!g4~2`O-%i6o*jn&-pC5P!&7Ygyrklaa~!9WjH;+3?MZsLpgH z9R1#Xu;Db4`n?nz-7ONOf6pZy`x7sTOVyqf&*dq@slhZsX3Zg;>{iXp8iin*(0;@j zi7{u=vu-`A4)B>-#h;zuuM}=i`cwJ;YaIo>xfww_=qLq;0W?1|f{RpX8NdR$0xX%D zrr%uTwG;ft65!mvDCaN7x{49+^$11h3SeBrk&}Whrrp#;=BAyJDYb+la)F4>RL!Jw zT|O@|i(wNZ%*dsmntZ+{S3P3ZuwZ7K_?Z?n5ELdk4L?yfvJ-NQxoO`uKY%50zdSGy zu9lRvqk$wLpJp%yL58eIlny_jMLg?}>+AU@4bI=jqmCcSRb)DhJMkLHUc)!*IRWBi7Jb8r8m$BDh_y*@>y(fQ1ra?kU7n)7u)q0P|PmS2f%wSR1q$sZrIK;L$_`$8d;N zj=jn@P|zINIVD6701ssBP_sQhrlrYK12mO9Ph251Ec2n~RMZU0Bfwtfi8Vm5(vh%R zolC(k+>uaoVn+K}4qcJQ9#HzkRMO~9BY0mP&zfSg^Joq6{mEvs`t3p($_VYoY`oJ! z)h*$v|A%+u62Hjd9#W59fE6WxO1_M-l6y4vp%+<>fh7nCh*l>v-fkd4g z`R@{JadMCv<&(hQ$naOXz?}pP*8p^gVY{gzv#=;Oqtb{NxHL2g8}8I6NEwOy(&k`@ z%dIduC^_s(C^a92DT8UZs_QaJ5DcB_aYH_Sgtch~JQ+j)y>$I|mtQ(EQ=zcDG`AHF ze7+0L(Q!U#ns)INJh>;PK_uuZ;OWTc78O#=od>_9t|(YKzYxsIgzix5vSNY&g5UFU zxpvy>s1*+-XvV@@1Sgi!DsQE7!F{t{Xg!q{el`^po z*!vWrJyvJ1Px+&zhViXZe$k(z0RxO)@eC4V)A;Rnt;N?AY+HLelpQH%N}YS#ogaMP z-1i(*-iohF%aV#P%46QkvXvC$+#bbRC^~_sYWk4|_o&eMbJQDEFU?&%QjGJ>6>44( zMsj-o&j%GBw>dV>Cd}lK4SV7ObPi=`#?R;UQYrx@2`m}{RAUg&-p~c zBjX7^pV2(7lh?8vxKv!RkEDq5!?=fHtj;fo}ukk zild(2#L<&^3;r?X59(P7E{-2y!x!4j?1}Z|c%F-L#c*cKxu$_UJa3bkhQ zy<9RFqL;^|h(`a)@@c3v(pd~cP`2DRE5@{+%iVy-eIrvMtXjf`432%@0=6Ehz5-Ws z@~6F1mS3tX^!Zu=&T71$UYA4A9iFsf%o+jjcU>d{v==2?BT2jh$ z+>!zYVd{kQKwjCZ~#!|pPL(yd>uW={C+x#lH-fB3y4Ef2VbhlPrGYk2|_aCGH z&YvTwC_8u-?aJ#z!i}sveYf#Z;#Y9jTSxy60a|?RI_H_W;*ZH+I&Wjuc0TtHTi&_y zZ83=)Sze28wGRGAX6q7kBcDni;)Dcd8I@wHPZ2VgEY)X9HcAXu3!+5G7<0nZB`dm& z2e9?0?6CIPCI3;~!i}O%5j?bj!NvUP%`vpTLQs`$_$!lm2)j&>A`HuNyGNj8=6IP? zHkJVN`=>Uq*B~PXNxCa%?e+QaNnEwoE(|!)6*S`j1*|x4zDH7ReZOO&A zdfNS`HW}Z$8;+q>9(Aw0>Gx5?R}HP7Zj?XBGY%9DJ6QzY*PhcpjBiH$6DQ|nj_HKq zV9~lOhIG`dyPt1AmF(ruJqjH*PlR0aOjA}J*Ki>fQ7EhC%D*hQ66rA>Gxg}@fh-ueFG81RHKUrITVYny?qgt z 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_05.png b/website/docs/assets/install_05.png new file mode 100644 index 0000000000000000000000000000000000000000..a7119a49fa586ed9ceed4ddc4915f0a5e779d400 GIT binary patch literal 8057 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 Date: Wed, 19 May 2021 12:42:02 +0200 Subject: [PATCH 259/311] rename_task skip removed tasks --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 6e20dd368f..90b4734a28 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1810,6 +1810,9 @@ class AssetItem(BaseItem): _item.setData(False, DUPLICATED_ROLE) def _rename_task(self, item): + if item.data(REMOVED_ROLE): + return + new_name = item.data(QtCore.Qt.EditRole, "name").lower() item_id = item.data(IDENTIFIER_ROLE) prev_name = self._task_name_by_item_id[item_id] From 6305a10656585d08cd972383c6448950ee2cb912 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 12:42:27 +0200 Subject: [PATCH 260/311] AssetItem has callback on task remove state change --- .../project_manager/project_manager/model.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 90b4734a28..5dae12901e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1843,6 +1843,32 @@ class AssetItem(BaseItem): def on_task_name_change(self, task_item): self._rename_task(task_item) + def on_task_remove_state_change(self, task_item): + is_removed = task_item.data(REMOVED_ROLE) + item_id = task_item.data(IDENTIFIER_ROLE) + if is_removed: + name = self._task_name_by_item_id.pop(item_id) + self._task_items_by_name[name].remove(task_item) + + else: + name = task_item.data(QtCore.Qt.EditRole, "name").lower() + self._task_name_by_item_id[item_id] = name + self._task_items_by_name[name].append(task_item) + + # Remove from previous name mapping + if not self._task_items_by_name[name]: + self._task_items_by_name.pop(name) + + elif len(self._task_items_by_name[name]) == 1: + if name in self._duplicated_task_names: + self._duplicated_task_names.remove(name) + task_item.setData(False, DUPLICATED_ROLE) + + else: + self._duplicated_task_names.add(name) + for _item in self._task_items_by_name[name]: + _item.setData(True, DUPLICATED_ROLE) + def add_child(self, item, row=None): if item in self._children: return From 56acc5026e7ab8e6b18291cc935f7168ee0ec42c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 12:42:55 +0200 Subject: [PATCH 261/311] task is calling `on_task_remove_state_change` on it's parent when REMOVED_ROLE state has changed --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5dae12901e..18dd5bc46d 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -2004,7 +2004,10 @@ class TaskItem(BaseItem): return True if role == REMOVED_ROLE: + if value == self._removed: + return False self._removed = value + self.parent().on_task_remove_state_change(self) return True if ( From 0fc5a542c88b44220d65d49e1004cf0a4f20b4e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 12:43:13 +0200 Subject: [PATCH 262/311] _remove task skip item ids that are not in _task_name_by_item_id --- openpype/tools/project_manager/project_manager/model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 18dd5bc46d..5000729adf 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1797,7 +1797,11 @@ class AssetItem(BaseItem): item.setData(False, DUPLICATED_ROLE) def _remove_task(self, item): + # This method is probably obsolete with changed logic and added + # `on_task_remove_state_change` method. item_id = item.data(IDENTIFIER_ROLE) + if item_id not in self._task_name_by_item_id: + return name = self._task_name_by_item_id.pop(item_id) self._task_items_by_name[name].remove(item) From 86d5e1c068fa739d58ad2e366fd27ce989e508ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 19 May 2021 14:37:15 +0200 Subject: [PATCH 263/311] make ascii art compatible, docstrings update --- tools/build.ps1 | 30 ++++++++++++++++++----------- tools/build_win_installer.ps1 | 29 +++++++++++++++++----------- tools/create_env.ps1 | 27 +++++++++++++++----------- tools/create_zip.ps1 | 31 +++++++++++++++++------------- tools/make_docs.ps1 | 22 ++++++++++----------- tools/run_documentation.ps1 | 36 ++++++++++++++++++++++++----------- tools/run_mongo.ps1 | 22 ++++++++++----------- tools/run_project_manager.ps1 | 4 ++-- tools/run_tests.ps1 | 22 ++++++++++----------- 9 files changed, 131 insertions(+), 92 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index 611d8af668..d9fef0f471 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -12,6 +12,14 @@ PS> .\build.ps1 +.EXAMPLE + +To build without automatical submodule update: +PS> .\build.ps1 --no-submodule-update + +.LINK +https://openpype.io/docs + #> $arguments=$ARGS @@ -82,17 +90,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index 4a4d011258..e46cd6a84d 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -1,16 +1,15 @@ <# .SYNOPSIS - Helper script to build OpenPype. + Helper script to build OpenPype Installer. .DESCRIPTION - This script will detect Python installation, and build OpenPype to `build` - directory using existing virtual environment created by Poetry (or - by running `/tools/create_venv.ps1`). It will then shuffle dependencies in - build folder to optimize for different Python versions (2/3) in Python host. + This script will use already built OpenPype (in `build` directory) and + create Windows installer from it using Inno Setup (https://jrsoftware.org/) + #> .EXAMPLE -PS> .\build.ps1 +PS> .\build_win_installer.ps1 #> @@ -76,11 +75,19 @@ function Install-Poetry() { $art = @" -▒█▀▀▀█ █▀▀█ █▀▀ █▀▀▄ ▒█▀▀█ █░░█ █▀▀█ █▀▀ ▀█▀ ▀█▀ ▀█▀ -▒█░░▒█ █░░█ █▀▀ █░░█ ▒█▄▄█ █▄▄█ █░░█ █▀▀ ▒█░ ▒█░ ▒█░ -▒█▄▄▄█ █▀▀▀ ▀▀▀ ▀░░▀ ▒█░░░ ▄▄▄█ █▀▀▀ ▀▀▀ ▄█▄ ▄█▄ ▄█▄ - .---= [ by Pype Club ] =---. - https://openpype.io + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index 5600ae71c7..7ada92c1e8 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -11,6 +11,11 @@ PS> .\create_env.ps1 +.EXAMPLE + +Print verbose information from Poetry: +PS> .\create_env.ps1 --verbose + #> $arguments=$ARGS @@ -98,17 +103,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index 2fef4d216b..a34af89159 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -4,14 +4,19 @@ .DESCRIPTION This script will detect Python installation and run OpenPype to create - zip. It needs mongodb running. I will create zip from current source code - version and copy it top `%LOCALAPPDATA%/pypeclub/pype` if `--path` or `-p` + zip. It will create zip from current source code + version and copy it top `%LOCALAPPDATA%/pypeclub/openpype` if `--path` or `-p` argument is not used. .EXAMPLE PS> .\create_zip.ps1 +.EXAMPLE + +To put generated zip to C:\OpenPype directory: +PS> .\create_zip.ps1 --path C:\OpenPype + #> function Exit-WithCode($exitcode) { @@ -52,17 +57,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/make_docs.ps1 b/tools/make_docs.ps1 index 01edaf9c58..2f9350eff0 100644 --- a/tools/make_docs.ps1 +++ b/tools/make_docs.ps1 @@ -32,17 +32,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/run_documentation.ps1 b/tools/run_documentation.ps1 index 1be3709642..a3e3a9b8dd 100644 --- a/tools/run_documentation.ps1 +++ b/tools/run_documentation.ps1 @@ -1,23 +1,38 @@ <# .SYNOPSIS - Helper script to run mongodb. + Helper script to run Docusaurus for easy editing of OpenPype documentation. .DESCRIPTION - This script will detect mongodb, add it to the PATH and launch it on specified port and db location. + This script is using `yarn` package manager to run Docusaurus. If you don't + have `yarn`, install Node.js (https://nodejs.org/) and then run: + + npm install -g yarn + + It take some time to run this script. If all is successful you should see + new browser window with OpenPype documentation. All changes is markdown files + under .\website should be immediately seen in browser. .EXAMPLE -PS> .\run_mongo.ps1 +PS> .\run_documentation.ps1 #> $art = @" -▒█▀▀▀█ █▀▀█ █▀▀ █▀▀▄ ▒█▀▀█ █░░█ █▀▀█ █▀▀ ▀█▀ ▀█▀ ▀█▀ -▒█░░▒█ █░░█ █▀▀ █░░█ ▒█▄▄█ █▄▄█ █░░█ █▀▀ ▒█░ ▒█░ ▒█░ -▒█▄▄▄█ █▀▀▀ ▀▀▀ ▀░░▀ ▒█░░░ ▄▄▄█ █▀▀▀ ▀▀▀ ▄█▄ ▄█▄ ▄█▄ - .---= [ by Pype Club ] =---. - https://openpype.io + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ @@ -26,7 +41,6 @@ Write-Host $art -ForegroundColor DarkGreen $script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent $openpype_root = (Get-Item $script_dir).parent.FullName -cd $openpype_root/website - -yarn run start +Set-Location $openpype_root/website +& yarn run start diff --git a/tools/run_mongo.ps1 b/tools/run_mongo.ps1 index 05fc497d34..6719e520fe 100644 --- a/tools/run_mongo.ps1 +++ b/tools/run_mongo.ps1 @@ -15,17 +15,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/run_project_manager.ps1 b/tools/run_project_manager.ps1 index 78dce19df1..67c2d2eb5e 100644 --- a/tools/run_project_manager.ps1 +++ b/tools/run_project_manager.ps1 @@ -1,13 +1,13 @@ <# .SYNOPSIS - Helper script OpenPype Tray. + Helper script to run Project Manager. .DESCRIPTION .EXAMPLE -PS> .\run_tray.ps1 +PS> .\run_project_manager.ps1 #> $current_dir = Get-Location diff --git a/tools/run_tests.ps1 b/tools/run_tests.ps1 index 7b9a5c841d..30e1f29e59 100644 --- a/tools/run_tests.ps1 +++ b/tools/run_tests.ps1 @@ -34,17 +34,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ From 0ef14e7ff25264c797e3716c9abfbd7d72f8d379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 19 May 2021 14:39:27 +0200 Subject: [PATCH 264/311] fix premature docstring end --- tools/build_win_installer.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index e46cd6a84d..05ec0f9823 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -5,7 +5,6 @@ .DESCRIPTION This script will use already built OpenPype (in `build` directory) and create Windows installer from it using Inno Setup (https://jrsoftware.org/) - #> .EXAMPLE From e13fb230f2d711ab494e56ef65e6871f1fe3a4a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 14:40:14 +0200 Subject: [PATCH 265/311] use AVALON_APP to get host name if is available --- openpype/plugins/publish/collect_anatomy_context_data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 5b955a0592..5c4bc6cb75 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -71,6 +71,10 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): app = app_manager.applications.get(app_name) if app: context_data["app"] = app.host_name + # Use AVALON_APP as first if available it is the same as host name + # - only if is not defined use AVALON_APP_NAME (e.g. on Farm) and + # set it back to AVALON_APP env variable + host_name = os.environ.get("AVALON_APP") datetime_data = context.data.get("datetimeData") or {} context_data.update(datetime_data) From 85abe800392979be139a36a7da75c59acbafe3f4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 14:40:33 +0200 Subject: [PATCH 266/311] use AVALON_APP_NAME if AVALON_APP is not available --- .../publish/collect_anatomy_context_data.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 5c4bc6cb75..ce23aa82bf 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -65,16 +65,18 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): "username": context.data["user"] } - app_manager = ApplicationManager() - app_name = os.environ.get("AVALON_APP_NAME") - if app_name: - app = app_manager.applications.get(app_name) - if app: - context_data["app"] = app.host_name # Use AVALON_APP as first if available it is the same as host name # - only if is not defined use AVALON_APP_NAME (e.g. on Farm) and # set it back to AVALON_APP env variable host_name = os.environ.get("AVALON_APP") + if not host_name: + app_manager = ApplicationManager() + app_name = os.environ.get("AVALON_APP_NAME") + if app_name: + app = app_manager.applications.get(app_name) + if app: + host_name = app.host_name + context_data["app"] = host_name datetime_data = context.data.get("datetimeData") or {} context_data.update(datetime_data) From cebcfb4a1517d6ae73783cacd892dc2047c4a185 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 14:40:54 +0200 Subject: [PATCH 267/311] set host name back to AVALON_APP as it's used across other plugins --- openpype/plugins/publish/collect_anatomy_context_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index ce23aa82bf..f121760e27 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -76,6 +76,7 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): app = app_manager.applications.get(app_name) if app: host_name = app.host_name + os.environ["AVALON_APP"] = host_name context_data["app"] = host_name datetime_data = context.data.get("datetimeData") or {} From e9a38a5f53ff48912b9747f113dde56be643f91b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:23:13 +0200 Subject: [PATCH 268/311] created base of InvalidValueType (BaseInvalidValueType) to be able pass reason directly --- openpype/settings/entities/__init__.py | 2 ++ openpype/settings/entities/base_entity.py | 3 ++- openpype/settings/entities/exceptions.py | 16 +++++++++------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 2c71b622ee..5d83a7cde4 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -57,6 +57,7 @@ from .exceptions import ( SchemaError, DefaultsNotDefined, StudioDefaultsNotDefined, + BaseInvalidValueType, InvalidValueType, InvalidKeySymbols, SchemaMissingFileInfo, @@ -115,6 +116,7 @@ from .anatomy_entities import AnatomyEntity __all__ = ( "DefaultsNotDefined", "StudioDefaultsNotDefined", + "BaseInvalidValueType", "InvalidValueType", "InvalidKeySymbols", "SchemaMissingFileInfo", diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 3e73fa8aa6..76150950b5 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -9,6 +9,7 @@ from .lib import ( ) from .exceptions import ( + BaseInvalidValueType, InvalidValueType, SchemeGroupHierarchyBug, EntitySchemaError @@ -377,7 +378,7 @@ class BaseItemEntity(BaseEntity): try: new_value = self.convert_to_valid_type(value) - except InvalidValueType: + except BaseInvalidValueType: new_value = NOT_SET if new_value is not NOT_SET: diff --git a/openpype/settings/entities/exceptions.py b/openpype/settings/entities/exceptions.py index 3649e63ab7..f352c94f20 100644 --- a/openpype/settings/entities/exceptions.py +++ b/openpype/settings/entities/exceptions.py @@ -15,20 +15,22 @@ class StudioDefaultsNotDefined(Exception): super(StudioDefaultsNotDefined, self).__init__(msg) -class InvalidValueType(Exception): - msg_template = "{}" +class BaseInvalidValueType(Exception): + def __init__(self, reason, path): + msg = "Path \"{}\". {}".format(path, reason) + self.msg = msg + super(BaseInvalidValueType, self).__init__(msg) + +class InvalidValueType(BaseInvalidValueType): def __init__(self, valid_types, invalid_type, path): - msg = "Path \"{}\". ".format(path) - joined_types = ", ".join( [str(valid_type) for valid_type in valid_types] ) - msg += "Got invalid type \"{}\". Expected: {}".format( + msg = "Got invalid type \"{}\". Expected: {}".format( invalid_type, joined_types ) - self.msg = msg - super(InvalidValueType, self).__init__(msg) + super(InvalidValueType, self).__init__(msg, path) class RequiredKeyModified(KeyError): From b420ea6971c01410ec49b81cde5356cec9f18532 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:24:53 +0200 Subject: [PATCH 269/311] base implementation of ColorEntity --- openpype/settings/entities/__init__.py | 4 +- openpype/settings/entities/color_entity.py | 53 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 openpype/settings/entities/color_entity.py diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 5d83a7cde4..33881a6097 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -97,7 +97,7 @@ from .input_entities import ( PathInput, RawJsonEntity ) - +from .color_entity import ColorEntity from .enum_entity import ( BaseEnumEntity, EnumEntity, @@ -148,6 +148,8 @@ __all__ = ( "PathInput", "RawJsonEntity", + "ColorEntity", + "BaseEnumEntity", "EnumEntity", "AppsEnumEntity", diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py new file mode 100644 index 0000000000..7d31ba42b9 --- /dev/null +++ b/openpype/settings/entities/color_entity.py @@ -0,0 +1,53 @@ +from .lib import STRING_TYPE +from .input_entities import InputEntity +from .exceptions import ( + BaseInvalidValueType, + InvalidValueType +) + + +class ColorEntity(InputEntity): + schema_types = ["color"] + def _item_initalization(self): + self.valid_value_types = (list, ) + self.value_on_not_set = [0, 0, 0, 255] + + def convert_to_valid_type(self, value): + """Conversion to valid type. + + Complexity of entity requires to override BaseEntity implementation. + """ + # Convertion to valid value type `list` + if isinstance(value, (set, tuple)): + value = list(value) + + # Skip other validations if is not `list` + if not isinstance(value, list): + raise InvalidValueType( + self.valid_value_types, type(value), self.path + ) + + # Allow list of len 3 (last aplha is set to max) + if len(value) == 3: + value.append(255) + + if len(value) != 4: + reason = "Color entity expect 4 items in list got {}".format( + len(value) + ) + raise BaseInvalidValueType(reason, self.path) + + new_value = [] + for item in value: + if not isinstance(item, int): + if isinstance(item, (STRING_TYPE, float)): + item = int(item) + + is_valid = isinstance(item, int) and -1 < item < 256 + if not is_valid: + reason = ( + "Color entity expect 4 integers in range 0-255 got {}" + ).format(value) + raise BaseInvalidValueType(reason, self.path) + new_value.append(item) + return new_value From d3673ae627b4e115cd6574183b33f921e2b84690 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:25:54 +0200 Subject: [PATCH 270/311] copy pasted PyQtColorTriangle project --- openpype/widgets/color_widgets/__init__.py | 6 + .../widgets/color_widgets/color_inputs.py | 514 ++++++ .../color_widgets/color_picker_widget.py | 115 ++ .../color_widgets/color_screen_pick.py | 248 +++ .../widgets/color_widgets/color_triangle.py | 1431 +++++++++++++++++ openpype/widgets/color_widgets/color_view.py | 78 + 6 files changed, 2392 insertions(+) create mode 100644 openpype/widgets/color_widgets/__init__.py create mode 100644 openpype/widgets/color_widgets/color_inputs.py create mode 100644 openpype/widgets/color_widgets/color_picker_widget.py create mode 100644 openpype/widgets/color_widgets/color_screen_pick.py create mode 100644 openpype/widgets/color_widgets/color_triangle.py create mode 100644 openpype/widgets/color_widgets/color_view.py diff --git a/openpype/widgets/color_widgets/__init__.py b/openpype/widgets/color_widgets/__init__.py new file mode 100644 index 0000000000..3423e26cf8 --- /dev/null +++ b/openpype/widgets/color_widgets/__init__.py @@ -0,0 +1,6 @@ +from .color_picker_widget import ColorPickerWidget + + +__all__ = ( + "ColorPickerWidget", +) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py new file mode 100644 index 0000000000..ddf8aebd4e --- /dev/null +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -0,0 +1,514 @@ +import re +from Qt import QtWidgets, QtCore, QtGui + + +slide_style = """ +QSlider::groove:horizontal { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff); + height: 8px; + border-radius: 4px; +} + +QSlider::handle:horizontal { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb); + border: 1px solid #777; + width: 8px; + margin-top: -1px; + margin-bottom: -1px; + border-radius: 4px; +} + +QSlider::handle:horizontal:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd); + border: 1px solid #444;ff + border-radius: 4px; +}""" + + +class AlphaInputs(QtWidgets.QGroupBox): + alpha_changed = QtCore.Signal(int) + + def __init__(self, parent=None): + super(AlphaInputs, self).__init__("Alpha", parent) + + self._block_changes = False + self.alpha_value = None + + # Opacity slider + alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self) + alpha_slider.setSingleStep(1) + alpha_slider.setMinimum(0) + alpha_slider.setMaximum(255) + alpha_slider.setStyleSheet(slide_style) + alpha_slider.setValue(255) + + inputs_widget = QtWidgets.QWidget(self) + inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + + percent_input = QtWidgets.QDoubleSpinBox(self) + percent_input.setMinimum(0) + percent_input.setMaximum(100) + percent_input.setDecimals(2) + + int_input = QtWidgets.QSpinBox(self) + int_input.setMinimum(0) + int_input.setMaximum(255) + + inputs_layout.addWidget(int_input) + inputs_layout.addWidget(QtWidgets.QLabel("0-255")) + inputs_layout.addWidget(percent_input) + inputs_layout.addWidget(QtWidgets.QLabel("%")) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(alpha_slider) + layout.addWidget(inputs_widget) + + alpha_slider.valueChanged.connect(self._on_slider_change) + percent_input.valueChanged.connect(self._on_percent_change) + int_input.valueChanged.connect(self._on_int_change) + + self.alpha_slider = alpha_slider + self.percent_input = percent_input + self.int_input = int_input + + self.set_alpha(255) + + def set_alpha(self, alpha): + if alpha == self.alpha_value: + return + self.alpha_value = alpha + + self.update_alpha() + + def _on_slider_change(self): + if self._block_changes: + return + self.alpha_value = self.alpha_slider.value() + self.alpha_changed.emit(self.alpha_value) + self.update_alpha() + + def _on_percent_change(self): + if self._block_changes: + return + self.alpha_value = int(self.percent_input.value() * 255 / 100) + self.alpha_changed.emit(self.alpha_value) + self.update_alpha() + + def _on_int_change(self): + if self._block_changes: + return + + self.alpha_value = self.int_input.value() + self.alpha_changed.emit(self.alpha_value) + self.update_alpha() + + def update_alpha(self): + self._block_changes = True + + if self.alpha_slider.value() != self.alpha_value: + self.alpha_slider.setValue(self.alpha_value) + + if self.int_input.value() != self.alpha_value: + self.int_input.setValue(self.alpha_value) + + percent = round(100 * self.alpha_value / 255, 2) + if self.percent_input.value() != percent: + self.percent_input.setValue(percent) + + self._block_changes = False + + +class RGBInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(RGBInputs, self).__init__("RGB", parent) + + self._block_changes = False + + self.color = color + + input_red = QtWidgets.QSpinBox(self) + input_green = QtWidgets.QSpinBox(self) + input_blue = QtWidgets.QSpinBox(self) + + input_red.setMinimum(0) + input_green.setMinimum(0) + input_blue.setMinimum(0) + + input_red.setMaximum(255) + input_green.setMaximum(255) + input_blue.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_red) + layout.addWidget(input_green) + layout.addWidget(input_blue) + + input_red.valueChanged.connect(self._on_red_change) + input_green.valueChanged.connect(self._on_green_change) + input_blue.valueChanged.connect(self._on_blue_change) + + self.input_red = input_red + self.input_green = input_green + self.input_blue = input_blue + + def _on_red_change(self, value): + if self._block_changes: + return + self.color.setRed(value) + self._on_change() + + def _on_green_change(self, value): + if self._block_changes: + return + self.color.setGreen(value) + self._on_change() + + def _on_blue_change(self, value): + if self._block_changes: + return + self.color.setBlue(value) + self._on_change() + + def _on_change(self): + self.value_changed.emit() + + def color_changed(self): + if ( + self.input_red.value() == self.color.red() + and self.input_green.value() == self.color.green() + and self.input_blue.value() == self.color.blue() + ): + return + + self._block_changes = True + + self.input_red.setValue(self.color.red()) + self.input_green.setValue(self.color.green()) + self.input_blue.setValue(self.color.blue()) + + self._block_changes = False + + +class CMYKInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(CMYKInputs, self).__init__("CMYK", parent) + + self.color = color + + self._block_changes = False + + input_cyan = QtWidgets.QSpinBox(self) + input_magenta = QtWidgets.QSpinBox(self) + input_yellow = QtWidgets.QSpinBox(self) + input_black = QtWidgets.QSpinBox(self) + + input_cyan.setMinimum(0) + input_magenta.setMinimum(0) + input_yellow.setMinimum(0) + input_black.setMinimum(0) + + input_cyan.setMaximum(255) + input_magenta.setMaximum(255) + input_yellow.setMaximum(255) + input_black.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_cyan) + layout.addWidget(input_magenta) + layout.addWidget(input_yellow) + layout.addWidget(input_black) + + input_cyan.valueChanged.connect(self._on_change) + input_magenta.valueChanged.connect(self._on_change) + input_yellow.valueChanged.connect(self._on_change) + input_black.valueChanged.connect(self._on_change) + + self.input_cyan = input_cyan + self.input_magenta = input_magenta + self.input_yellow = input_yellow + self.input_black = input_black + + def _on_change(self): + if self._block_changes: + return + self.color.setCmyk( + self.input_cyan.value(), + self.input_magenta.value(), + self.input_yellow.value(), + self.input_black.value() + ) + self.value_changed.emit() + + def color_changed(self): + if self._block_changes: + return + _cur_color = QtGui.QColor() + _cur_color.setCmyk( + self.input_cyan.value(), + self.input_magenta.value(), + self.input_yellow.value(), + self.input_black.value() + ) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + + c, m, y, k, _ = self.color.getCmyk() + self._block_changes = True + + self.input_cyan.setValue(c) + self.input_magenta.setValue(m) + self.input_yellow.setValue(y) + self.input_black.setValue(k) + + self._block_changes = False + + +class HEXInputs(QtWidgets.QGroupBox): + hex_regex = re.compile("^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$") + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(HEXInputs, self).__init__("HEX", parent) + self.color = color + + input_field = QtWidgets.QLineEdit() + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_field) + + input_field.textChanged.connect(self._on_change) + + self.input_field = input_field + + def _on_change(self): + if self._block_changes: + return + input_value = self.input_field.text() + # TODO what if does not match? + if self.hex_regex.match(input_value): + self.color.setNamedColor(input_value) + self.value_changed.emit() + + def color_changed(self): + input_value = self.input_field.text() + if self.hex_regex.match(input_value): + _cur_color = QtGui.QColor() + _cur_color.setNamedColor(input_value) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + self._block_changes = True + + self.input_field.setText(self.color.name()) + + self._block_changes = False + + +class HSVInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(HSVInputs, self).__init__("HSV", parent) + + self._block_changes = False + + self.color = color + + input_hue = QtWidgets.QSpinBox(self) + input_sat = QtWidgets.QSpinBox(self) + input_val = QtWidgets.QSpinBox(self) + + input_hue.setMinimum(0) + input_sat.setMinimum(0) + input_val.setMinimum(0) + + input_hue.setMaximum(359) + input_sat.setMaximum(255) + input_val.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_hue) + layout.addWidget(input_sat) + layout.addWidget(input_val) + + input_hue.valueChanged.connect(self._on_change) + input_sat.valueChanged.connect(self._on_change) + input_val.valueChanged.connect(self._on_change) + + self.input_hue = input_hue + self.input_sat = input_sat + self.input_val = input_val + + def _on_change(self): + if self._block_changes: + return + self.color.setHsv( + self.input_hue.value(), + self.input_sat.value(), + self.input_val.value() + ) + self.value_changed.emit() + + def color_changed(self): + _cur_color = QtGui.QColor() + _cur_color.setHsv( + self.input_hue.value(), + self.input_sat.value(), + self.input_val.value() + ) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + + self._block_changes = True + h, s, v, _ = self.color.getHsv() + + self.input_hue.setValue(h) + self.input_sat.setValue(s) + self.input_val.setValue(v) + + self._block_changes = False + + +class HSLInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(HSLInputs, self).__init__("HSL", parent) + + self._block_changes = False + + self.color = color + + input_hue = QtWidgets.QSpinBox(self) + input_sat = QtWidgets.QSpinBox(self) + input_light = QtWidgets.QSpinBox(self) + + input_hue.setMinimum(0) + input_sat.setMinimum(0) + input_light.setMinimum(0) + + input_hue.setMaximum(359) + input_sat.setMaximum(255) + input_light.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_hue) + layout.addWidget(input_sat) + layout.addWidget(input_light) + + input_hue.valueChanged.connect(self._on_change) + input_sat.valueChanged.connect(self._on_change) + input_light.valueChanged.connect(self._on_change) + + self.input_hue = input_hue + self.input_sat = input_sat + self.input_light = input_light + + def _on_change(self): + if self._block_changes: + return + self.color.setHsl( + self.input_hue.value(), + self.input_sat.value(), + self.input_light.value() + ) + self.value_changed.emit() + + def color_changed(self): + _cur_color = QtGui.QColor() + _cur_color.setHsl( + self.input_hue.value(), + self.input_sat.value(), + self.input_light.value() + ) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + + self._block_changes = True + h, s, l, _ = self.color.getHsl() + + self.input_hue.setValue(h) + self.input_sat.setValue(s) + self.input_light.setValue(l) + + self._block_changes = False + + +class ColorInputsWidget(QtWidgets.QWidget): + color_changed = QtCore.Signal(QtGui.QColor) + + def __init__(self, parent=None, **kwargs): + super(ColorInputsWidget, self).__init__(parent) + + color = QtGui.QColor() + + input_fields = [] + + if kwargs.get("use_hex", True): + input_fields.append(HEXInputs(color, self)) + + if kwargs.get("use_rgb", True): + input_fields.append(RGBInputs(color, self)) + + if kwargs.get("use_hsl", True): + input_fields.append(HSLInputs(color, self)) + + if kwargs.get("use_hsv", True): + input_fields.append(HSVInputs(color, self)) + + if kwargs.get("use_cmyk", True): + input_fields.append(CMYKInputs(color, self)) + + inputs_widget = QtWidgets.QWidget(self) + inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + + for input_field in input_fields: + inputs_layout.addWidget(input_field) + input_field.value_changed.connect(self._on_value_change) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(inputs_widget, 0) + spacer = QtWidgets.QWidget(self) + layout.addWidget(spacer, 1) + + self.input_fields = input_fields + + self.color = color + + def set_color(self, color): + if ( + color.red() == self.color.red() + and color.green() == self.color.green() + and color.blue() == self.color.blue() + ): + return + self.color.setRed(color.red()) + self.color.setGreen(color.green()) + self.color.setBlue(color.blue()) + self._on_value_change() + + def _on_value_change(self): + for input_field in self.input_fields: + input_field.color_changed() + self.color_changed.emit(self.color) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py new file mode 100644 index 0000000000..d06af73cbf --- /dev/null +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -0,0 +1,115 @@ +from Qt import QtWidgets, QtCore, QtGui + +from .color_triangle import QtColorTriangle +from .color_view import ColorViewer +from .color_screen_pick import PickScreenColorWidget +from .color_inputs import ( + ColorInputsWidget, + AlphaInputs +) + + +class ColorPickerWidget(QtWidgets.QWidget): + color_changed = QtCore.Signal(QtGui.QColor) + + def __init__(self, color=None, parent=None): + super(ColorPickerWidget, self).__init__(parent) + + # Eye picked widget + pick_widget = PickScreenColorWidget() + + # Color utils + utils_widget = QtWidgets.QWidget(self) + utils_layout = QtWidgets.QVBoxLayout(utils_widget) + + bottom_utils_widget = QtWidgets.QWidget(utils_widget) + + # Color triangle + color_triangle = QtColorTriangle(utils_widget) + + # Color preview + color_view = ColorViewer(bottom_utils_widget) + color_view.setMaximumHeight(50) + + # Color pick button + btn_pick_color = QtWidgets.QPushButton( + "Pick a color", bottom_utils_widget + ) + + # Color inputs widget + color_inputs = ColorInputsWidget(self) + + # Alpha inputs + alpha_input_wrapper_widget = QtWidgets.QWidget(self) + alpha_input_wrapper_layout = QtWidgets.QVBoxLayout( + alpha_input_wrapper_widget + ) + + alpha_inputs = AlphaInputs(alpha_input_wrapper_widget) + alpha_input_wrapper_layout.addWidget(alpha_inputs) + alpha_input_wrapper_layout.addWidget(QtWidgets.QWidget(), 1) + + bottom_utils_layout = QtWidgets.QHBoxLayout(bottom_utils_widget) + bottom_utils_layout.setContentsMargins(0, 0, 0, 0) + bottom_utils_layout.addWidget(color_view, 1) + bottom_utils_layout.addWidget(btn_pick_color, 0) + + utils_layout.addWidget(bottom_utils_widget, 0) + utils_layout.addWidget(color_triangle, 1) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(utils_widget, 1) + layout.addWidget(color_inputs, 0) + layout.addWidget(alpha_input_wrapper_widget, 0) + + color_view.set_color(color_triangle.cur_color) + color_inputs.set_color(color_triangle.cur_color) + + color_triangle.color_changed.connect(self.triangle_color_changed) + pick_widget.color_selected.connect(self.on_color_change) + color_inputs.color_changed.connect(self.on_color_change) + alpha_inputs.alpha_changed.connect(self.alpha_changed) + btn_pick_color.released.connect(self.pick_color) + + self.pick_widget = pick_widget + self.utils_widget = utils_widget + self.bottom_utils_widget = bottom_utils_widget + + self.color_triangle = color_triangle + self.color_view = color_view + self.btn_pick_color = btn_pick_color + self.color_inputs = color_inputs + self.alpha_inputs = alpha_inputs + + if color: + self.set_color(color) + self.alpha_changed(color.alpha()) + + def showEvent(self, event): + super(ColorPickerWidget, self).showEvent(event) + triangle_width = int(( + self.utils_widget.height() - self.bottom_utils_widget.height() + ) / 5 * 4) + self.color_triangle.setMinimumWidth(triangle_width) + + def color(self): + return self.color_view.color() + + def set_color(self, color): + self.alpha_inputs.set_alpha(color.alpha()) + self.on_color_change(color) + + def pick_color(self): + self.pick_widget.pick_color() + + def triangle_color_changed(self, color): + self.color_view.set_color(color) + self.color_inputs.set_color(color) + + def on_color_change(self, color): + self.color_view.set_color(color) + self.color_triangle.set_color(color) + self.color_inputs.set_color(color) + + def alpha_changed(self, alpha): + self.color_view.set_alpha(alpha) diff --git a/openpype/widgets/color_widgets/color_screen_pick.py b/openpype/widgets/color_widgets/color_screen_pick.py new file mode 100644 index 0000000000..87f50745eb --- /dev/null +++ b/openpype/widgets/color_widgets/color_screen_pick.py @@ -0,0 +1,248 @@ +import Qt +from Qt import QtWidgets, QtCore, QtGui + + +class PickScreenColorWidget(QtWidgets.QWidget): + color_selected = QtCore.Signal(QtGui.QColor) + + def __init__(self, parent=None): + super(PickScreenColorWidget, self).__init__(parent) + self.labels = [] + self.magnification = 2 + + self._min_magnification = 1 + self._max_magnification = 10 + + def add_magnification_delta(self, delta): + _delta = abs(delta / 1000) + if delta > 0: + self.magnification += _delta + else: + self.magnification -= _delta + + if self.magnification > self._max_magnification: + self.magnification = self._max_magnification + elif self.magnification < self._min_magnification: + self.magnification = self._min_magnification + + def pick_color(self): + if self.labels: + if self.labels[0].isVisible(): + return + self.labels = [] + + for screen in QtWidgets.QApplication.screens(): + label = PickLabel(self) + label.pick_color(screen) + label.color_selected.connect(self.on_color_select) + label.close_session.connect(self.end_pick_session) + self.labels.append(label) + + def end_pick_session(self): + for label in self.labels: + label.close() + self.labels = [] + + def on_color_select(self, color): + self.color_selected.emit(color) + self.end_pick_session() + + +class PickLabel(QtWidgets.QLabel): + color_selected = QtCore.Signal(QtGui.QColor) + close_session = QtCore.Signal() + + def __init__(self, pick_widget): + super(PickLabel, self).__init__() + self.setMouseTracking(True) + + self.pick_widget = pick_widget + + self.radius_pen = QtGui.QPen(QtGui.QColor(27, 27, 27), 2) + self.text_pen = QtGui.QPen(QtGui.QColor(127, 127, 127), 4) + self.text_bg = QtGui.QBrush(QtGui.QColor(27, 27, 27)) + self._mouse_over = False + + self.radius = 100 + self.radius_ratio = 11 + + @property + def magnification(self): + return self.pick_widget.magnification + + def pick_color(self, screen_obj): + self.show() + self.windowHandle().setScreen(screen_obj) + geo = screen_obj.geometry() + args = ( + QtWidgets.QApplication.desktop().winId(), + geo.x(), geo.y(), geo.width(), geo.height() + ) + if Qt.__binding__ in ("PyQt4", "PySide"): + pix = QtGui.QPixmap.grabWindow(*args) + else: + pix = screen_obj.grabWindow(*args) + + if pix.width() > pix.height(): + size = pix.height() + else: + size = pix.width() + + self.radius = int(size / self.radius_ratio) + + self.setPixmap(pix) + self.showFullScreen() + + def wheelEvent(self, event): + y_delta = event.angleDelta().y() + self.pick_widget.add_magnification_delta(y_delta) + self.update() + + def enterEvent(self, event): + self._mouse_over = True + super().enterEvent(event) + + def leaveEvent(self, event): + self._mouse_over = False + super().leaveEvent(event) + self.update() + + def mouseMoveEvent(self, event): + self.update() + + def paintEvent(self, event): + super().paintEvent(event) + if not self._mouse_over: + return + + mouse_pos_to_widet = self.mapFromGlobal(QtGui.QCursor.pos()) + + magnified_half_size = self.radius / self.magnification + magnified_size = magnified_half_size * 2 + + zoom_x_1 = mouse_pos_to_widet.x() - magnified_half_size + zoom_x_2 = mouse_pos_to_widet.x() + magnified_half_size + zoom_y_1 = mouse_pos_to_widet.y() - magnified_half_size + zoom_y_2 = mouse_pos_to_widet.y() + magnified_half_size + pix_width = magnified_size + pix_height = magnified_size + draw_pos_x = 0 + draw_pos_y = 0 + if zoom_x_1 < 0: + draw_pos_x = abs(zoom_x_1) + pix_width -= draw_pos_x + zoom_x_1 = 1 + elif zoom_x_2 > self.pixmap().width(): + pix_width -= zoom_x_2 - self.pixmap().width() + + if zoom_y_1 < 0: + draw_pos_y = abs(zoom_y_1) + pix_height -= draw_pos_y + zoom_y_1 = 1 + elif zoom_y_2 > self.pixmap().height(): + pix_height -= zoom_y_2 - self.pixmap().height() + + new_pix = QtGui.QPixmap(magnified_size, magnified_size) + new_pix.fill(QtCore.Qt.transparent) + new_pix_painter = QtGui.QPainter(new_pix) + new_pix_painter.drawPixmap( + QtCore.QRect(draw_pos_x, draw_pos_y, pix_width, pix_height), + self.pixmap().copy(zoom_x_1, zoom_y_1, pix_width, pix_height) + ) + new_pix_painter.end() + + painter = QtGui.QPainter(self) + + ellipse_rect = QtCore.QRect( + mouse_pos_to_widet.x() - self.radius, + mouse_pos_to_widet.y() - self.radius, + self.radius * 2, + self.radius * 2 + ) + ellipse_rect_f = QtCore.QRectF(ellipse_rect) + path = QtGui.QPainterPath() + path.addEllipse(ellipse_rect_f) + painter.setClipPath(path) + + new_pix_rect = QtCore.QRect( + mouse_pos_to_widet.x() - self.radius + 1, + mouse_pos_to_widet.y() - self.radius + 1, + new_pix.width() * self.magnification, + new_pix.height() * self.magnification + ) + + painter.drawPixmap(new_pix_rect, new_pix) + + painter.setClipping(False) + + painter.setRenderHint(QtGui.QPainter.Antialiasing) + + painter.setPen(self.radius_pen) + painter.drawEllipse(ellipse_rect_f) + + image = self.pixmap().toImage() + if image.valid(mouse_pos_to_widet): + color = QtGui.QColor(image.pixel(mouse_pos_to_widet)) + else: + color = QtGui.QColor() + + color_text = "Red: {} - Green: {} - Blue: {}".format( + color.red(), color.green(), color.blue() + ) + font = painter.font() + font.setPointSize(self.radius / 10) + painter.setFont(font) + + text_rect_height = int(painter.fontMetrics().height() + 10) + text_rect = QtCore.QRect( + ellipse_rect.x(), + ellipse_rect.bottom(), + ellipse_rect.width(), + text_rect_height + ) + if text_rect.bottom() > self.pixmap().height(): + text_rect.moveBottomLeft(ellipse_rect.topLeft()) + + rect_radius = text_rect_height / 2 + path = QtGui.QPainterPath() + path.addRoundedRect( + QtCore.QRectF(text_rect), + rect_radius, + rect_radius + ) + painter.fillPath(path, self.text_bg) + + painter.setPen(self.text_pen) + painter.drawText( + text_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignCenter, + color_text + ) + + color_rect_x = ellipse_rect.x() - text_rect_height + if color_rect_x < 0: + color_rect_x += (text_rect_height + ellipse_rect.width()) + + color_rect = QtCore.QRect( + color_rect_x, + ellipse_rect.y(), + text_rect_height, + ellipse_rect.height() + ) + path = QtGui.QPainterPath() + path.addRoundedRect( + QtCore.QRectF(color_rect), + rect_radius, + rect_radius + ) + painter.fillPath(path, color) + painter.drawRoundedRect(color_rect, rect_radius, rect_radius) + painter.end() + + def mouseReleaseEvent(self, event): + color = QtGui.QColor(self.pixmap().toImage().pixel(event.pos())) + self.color_selected.emit(color) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + self.close_session.emit() diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py new file mode 100644 index 0000000000..d4db175d84 --- /dev/null +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -0,0 +1,1431 @@ +from enum import Enum +from math import floor, sqrt, sin, cos, acos, pi as PI +from Qt import QtWidgets, QtCore, QtGui + +TWOPI = PI * 2 + + +class TriangleState(Enum): + IdleState = object() + SelectingHueState = object() + SelectingSatValueState = object() + + +class DoubleColor: + def __init__(self, r, g=None, b=None): + if g is None: + g = r.g + b = r.b + r = r.r + self.r = float(r) + self.g = float(g) + self.b = float(b) + + +class Vertex: + def __init__(self, color, point): + # Convert GlobalColor to QColor as globals don't have red, green, blue + if isinstance(color, QtCore.Qt.GlobalColor): + color = QtGui.QColor(color) + + # Convert QColor to DoubleColor + if isinstance(color, QtGui.QColor): + color = DoubleColor(color.red(), color.green(), color.blue()) + + self.color = color + self.point = point + + +class QtColorTriangle(QtWidgets.QWidget): + """The QtColorTriangle class provides a triangular color selection widget. + + This widget uses the HSV color model, and is therefore useful for + selecting colors by eye. + + The triangle in the center of the widget is used for selecting + saturation and value, and the surrounding circle is used for + selecting hue. + + Use set_color() and color() to set and get the current color. + """ + color_changed = QtCore.Signal(QtGui.QColor) + + # Thick of color wheel ratio where 1 is fully filled circle + inner_radius_ratio = 5.0 + # Ratio where hue selector on wheel is relative to `inner_radius_ratio` + # - middle of the wheel is twice `inner_radius_ratio` + selector_radius_ratio = inner_radius_ratio * 2 + # Size ratio of selectors on wheel and in triangle + ellipse_size_ratio = 10.0 + # Ration of selectors thickness + ellipse_thick_ratio = 50.0 + # Hue offset on color wheel (0 - 359) + # - red on top if set to "0" + hue_offset = 90 + + def __init__(self, parent=None): + super(QtColorTriangle, self).__init__(parent) + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Minimum + ) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + self.angle_a = float() + self.angle_b = float() + self.angle_c = float() + + self.bg_image = QtGui.QImage( + self.sizeHint(), QtGui.QImage.Format_RGB32 + ) + self.cur_color = QtGui.QColor() + self.point_a = QtCore.QPointF() + self.point_b = QtCore.QPointF() + self.point_c = QtCore.QPointF() + self.point_d = QtCore.QPointF() + + self.cur_hue = int() + + self.pen_width = int() + self.ellipse_size = int() + self.outer_radius = int() + self.selector_pos = QtCore.QPointF() + + self.sel_mode = TriangleState.IdleState + + self._triangle_outline_pen = QtGui.QPen( + QtGui.QColor(40, 40, 40, 128), + 2 + ) + # Prepare hue numbers for color circle + _hue_circle_range = [] + for idx in range(11): + # Some Qt versions may require: + # hue = int(idx * 360.0) + percent_idx = idx * 0.1 + hue = int(360.0 - (percent_idx * 360.0)) + _hue_circle_range.append((percent_idx, hue)) + self._hue_circle_range = tuple(_hue_circle_range) + + color = QtGui.QColor() + color.setHsv(0, 255, 255) + self.set_color(color) + + def set_color(self, col): + if ( + col.red() == self.cur_color.red() + and col.green() == self.cur_color.green() + and col.blue() == self.cur_color.blue() + ): + return + + self.cur_color = col + + hue, *_ = self.cur_color.getHsv() + + # Never use an invalid hue to display colors + if hue != -1: + self.cur_hue = hue + + angle_with_offset = (360 - self.cur_hue - self.hue_offset) % 360 + self.angle_a = (angle_with_offset * TWOPI) / 360.0 + self.angle_a += PI / 2.0 + if self.angle_a > TWOPI: + self.angle_a -= TWOPI + + self.angle_b = self.angle_a + TWOPI / 3 + self.angle_c = self.angle_b + TWOPI / 3 + + if self.angle_b > TWOPI: + self.angle_b -= TWOPI + if self.angle_c > TWOPI: + self.angle_c -= TWOPI + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + self.update() + + self.color_changed.emit(self.cur_color) + + def heightForWidth(self, width): + return width + + def polish(self): + size_w = self.contentsRect().width() + size_h = self.contentsRect().height() + if size_w < size_h: + size = size_w + else: + size = size_h + + self.outer_radius = (size - 1) / 2 + + self.pen_width = int( + floor(self.outer_radius / self.ellipse_thick_ratio) + ) + self.ellipse_size = int( + floor(self.outer_radius / self.ellipse_size_ratio) + ) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + + self.update() + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + if event.rect().intersects(self.contentsRect()): + event_region = event.region() + if hasattr(event_region, "intersect"): + clip_region = event_region.intersect(self.contentsRect()) + else: + clip_region = event_region.intersected( + self.contentsRect() + ) + painter.setClipRegion(clip_region) + + self.paint_bg() + + # Blit the static generated background with the hue gradient onto + # the double buffer. + buf = QtGui.QImage(self.bg_image.copy()) + + # Draw the trigon + # Find the color with only the hue, and max value and saturation + hue_color = QtGui.QColor() + hue_color.setHsv(self.cur_hue, 255, 255) + + # Draw the triangle + self.drawTrigon( + buf, self.point_a, self.point_b, self.point_c, hue_color + ) + + # Slow step: convert the image to a pixmap + pix = QtGui.QPixmap.fromImage(buf) + pix_painter = QtGui.QPainter(pix) + pix_painter.setRenderHint(QtGui.QPainter.Antialiasing) + + # Draw an outline of the triangle + pix_painter.setPen(self._triangle_outline_pen) + pix_painter.drawLine(self.point_a, self.point_b) + pix_painter.drawLine(self.point_b, self.point_c) + pix_painter.drawLine(self.point_c, self.point_a) + + # Draw the color wheel selector + pix_painter.setPen(QtGui.QPen(QtCore.Qt.white, self.pen_width)) + pix_painter.drawEllipse( + int(self.point_d.x() - self.ellipse_size / 2.0), + int(self.point_d.y() - self.ellipse_size / 2.0), + self.ellipse_size, self.ellipse_size + ) + + # Draw the triangle selector + pix_painter.setBrush(self.cur_color) + pix_painter.drawEllipse( + QtCore.QRectF( + self.selector_pos.x() - self.ellipse_size / 2.0, + self.selector_pos.y() - self.ellipse_size / 2.0, + self.ellipse_size + 0.5, + self.ellipse_size + 0.5 + ) + ) + + pix_painter.end() + # Blit + painter.drawPixmap(self.contentsRect().topLeft(), pix) + painter.end() + + def mouseMoveEvent(self, event): + if (event.buttons() & QtCore.Qt.LeftButton) == 0: + return + + depos = QtCore.QPointF( + event.pos().x(), + event.pos().y() + ) + new_color = False + + if self.sel_mode is TriangleState.SelectingHueState: + self.angle_a = self._angle_at(depos, self.contentsRect()) + self.angle_b = self.angle_a + (TWOPI / 3.0) + self.angle_c = self.angle_b + (TWOPI / 3.0) + if self.angle_b > TWOPI: + self.angle_b -= TWOPI + if self.angle_c > TWOPI: + self.angle_c -= TWOPI + + am = self.angle_a - (PI / 2) + if am < 0: + am += TWOPI + self.cur_hue = ( + 360 - int((am * 360.0) / TWOPI) - self.hue_offset + ) % 360 + hue, sat, val, _ = self.cur_color.getHsv() + + if self.cur_hue != hue: + new_color = True + self.cur_color.setHsv(self.cur_hue, sat, val) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + else: + aa = Vertex(QtCore.Qt.transparent, self.point_a) + bb = Vertex(QtCore.Qt.transparent, self.point_b) + cc = Vertex(QtCore.Qt.transparent, self.point_c) + + self.selector_pos = self._move_point_to_triangle( + depos.x(), depos.y(), aa, bb, cc + ) + col = self._color_from_point(self.selector_pos) + if col != self.cur_color: + # Ensure that hue does not change when selecting + # saturation and value. + _, sat, val, _ = col.getHsv() + self.cur_color.setHsv(self.cur_hue, sat, val) + new_color = True + + if new_color: + self.color_changed.emit(self.cur_color) + + self.update() + + def mousePressEvent(self, event): + # Only respond to the left mouse button. + if event.button() != QtCore.Qt.LeftButton: + return + + depos = QtCore.QPointF( + event.pos().x(), + event.pos().y() + ) + rad = self._radius_at(depos, self.contentsRect()) + new_color = False + + # As in mouseMoveEvent, either find the a, b, c angles or the + # radian position of the selector, then order an update. + inner_radius = ( + self.outer_radius - (self.outer_radius / self.inner_radius_ratio) + ) + if rad > inner_radius: + self.sel_mode = TriangleState.SelectingHueState + + self.angle_a = self._angle_at(depos, self.contentsRect()) + self.angle_b = self.angle_a + TWOPI / 3.0 + self.angle_c = self.angle_b + TWOPI / 3.0 + if self.angle_b > TWOPI: + self.angle_b -= TWOPI + if self.angle_c > TWOPI: + self.angle_c -= TWOPI + + am = self.angle_a - PI / 2 + if am < 0: + am += TWOPI + + self.cur_hue = ( + 360 - int((am * 360.0) / TWOPI) - self.hue_offset + ) % 360 + hue, sat, val, _ = self.cur_color.getHsv() + + if hue != self.cur_hue: + new_color = True + self.cur_color.setHsv(self.cur_hue, sat, val) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + self.color_changed.emit(self.cur_color) + else: + self.sel_mode = TriangleState.SelectingSatValueState + + aa = Vertex(QtCore.Qt.transparent, self.point_a) + bb = Vertex(QtCore.Qt.transparent, self.point_b) + cc = Vertex(QtCore.Qt.transparent, self.point_c) + + self.selector_pos = self._move_point_to_triangle( + depos.x(), depos.y(), aa, bb, cc + ) + col = self._color_from_point(self.selector_pos) + if col != self.cur_color: + self.cur_color = col + new_color = True + + if new_color: + self.color_changed.emit(self.cur_color) + + self.update() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.sel_mode = TriangleState.IdleState + + def keyPressEvent(self, event): + key = event.key() + if key == QtCore.Qt.Key_Left: + self.cur_hue -= 1 + if self.cur_hue < 0: + self.cur_hue += 360 + _, sat, val, _ = self.cur_color.getHsv() + + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + elif key == QtCore.Qt.Key_Right: + self.cur_hue += 1 + if (self.cur_hue > 359): + self.cur_hue -= 360 + _, sat, val, _ = self.cur_color.getHsv() + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + elif key == QtCore.Qt.Key_Up: + _, sat, val, _ = self.cur_color.getHsv() + if event.modifiers() & QtCore.Qt.ShiftModifier: + if sat > 5: + sat -= 5 + else: + sat = 0 + else: + if val > 5: + val -= 5 + else: + val = 0 + + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + elif key == QtCore.Qt.Key_Down: + _, sat, val, _ = self.cur_color.getHsv() + if event.modifiers() & QtCore.Qt.ShiftModifier: + if sat < 250: + sat += 5 + else: + sat = 255 + else: + if val < 250: + val += 5 + else: + val = 255 + + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + def resizeEvent(self, _event): + size_w = self.contentsRect().width() + size_h = self.contentsRect().height() + if size_w < size_h: + size = size_w + else: + size = size_h + + self.outer_radius = (size - 1) / 2 + + self.pen_width = int( + floor(self.outer_radius / self.ellipse_thick_ratio) + ) + self.ellipse_size = int( + floor(self.outer_radius / self.ellipse_size_ratio) + ) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + # Find the current position of the selector + self.selector_pos = self._point_from_color(self.cur_color) + + self.update() + + def drawTrigon(self, buf, pa, pb, pc, color): + # Create three Vertex objects. A Vertex contains a double-point + # coordinate and a color. + # pa is the tip of the arrow + # pb is the black corner + # pc is the white corner + p1 = Vertex(color, pa) + p2 = Vertex(QtCore.Qt.black, pb) + p3 = Vertex(QtCore.Qt.white, pc) + + # Sort. Make p1 above p2, which is above p3 (using y coordinate). + # Bubble sorting is fastest here. + if p1.point.y() > p2.point.y(): + p1, p2 = p2, p1 + if p1.point.y() > p3.point.y(): + p1, p3 = p3, p1 + if p2.point.y() > p3.point.y(): + p2, p3 = p3, p2 + + # All the three y deltas are >= 0 + p1p2ydist = float(p2.point.y() - p1.point.y()) + p1p3ydist = float(p3.point.y() - p1.point.y()) + p2p3ydist = float(p3.point.y() - p2.point.y()) + p1p2xdist = float(p2.point.x() - p1.point.x()) + p1p3xdist = float(p3.point.x() - p1.point.x()) + p2p3xdist = float(p3.point.x() - p2.point.x()) + + # The first x delta decides wether we have a lefty or a righty + # trigon. + lefty = p1p2xdist < 0 + + # Left and right colors and X values. The key in this map is the + # y values. Our goal is to fill these structures with all the + # information needed to do a single pass top-to-bottom, + # left-to-right drawing of the trigon. + leftColors = {} + rightColors = {} + leftX = {} + rightX = {} + + # Scan longy - find all left and right colors and X-values for + # the tallest edge (p1-p3). + # Initialize with known values + x = p1.point.x() + source = p1.color + dest = p3.color + r = source.r + g = source.g + b = source.b + y1 = int(floor(p1.point.y())) + y2 = int(floor(p3.point.y())) + + # Find slopes (notice that if the y dists are 0, we don't care + # about the slopes) + xdelta = 0.0 + rdelta = 0.0 + gdelta = 0.0 + bdelta = 0.0 + if p1p3ydist != 0.0: + xdelta = p1p3xdist / p1p3ydist + rdelta = (dest.r - r) / p1p3ydist + gdelta = (dest.g - g) / p1p3ydist + bdelta = (dest.b - b) / p1p3ydist + + # Calculate gradients using linear approximation + for y in range(y1, y2): + if lefty: + rightColors[y] = DoubleColor(r, g, b) + rightX[y] = x + else: + leftColors[y] = DoubleColor(r, g, b) + leftX[y] = x + + r += rdelta + g += gdelta + b += bdelta + x += xdelta + + # Scan top shorty - find all left and right colors and x-values + # for the topmost of the two not-tallest short edges. + x = p1.point.x() + source = p1.color + dest = p2.color + r = source.r + g = source.g + b = source.b + y1 = int(floor(p1.point.y())) + y2 = int(floor(p2.point.y())) + + # Find slopes (notice that if the y dists are 0, we don't care + # about the slopes) + xdelta = 0.0 + rdelta = 0.0 + gdelta = 0.0 + bdelta = 0.0 + if p1p2ydist != 0.0: + xdelta = p1p2xdist / p1p2ydist + rdelta = (dest.r - r) / p1p2ydist + gdelta = (dest.g - g) / p1p2ydist + bdelta = (dest.b - b) / p1p2ydist + + # Calculate gradients using linear approximation + for y in range(y1, y2): + if lefty: + leftColors[y] = DoubleColor(r, g, b) + leftX[y] = x + else: + rightColors[y] = DoubleColor(r, g, b) + rightX[y] = x + + r += rdelta + g += gdelta + b += bdelta + x += xdelta + + # Scan bottom shorty - find all left and right colors and + # x-values for the bottommost of the two not-tallest short edges. + x = p2.point.x() + source = p2.color + dest = p3.color + r = source.r + g = source.g + b = source.b + y1 = int(floor(p2.point.y())) + y2 = int(floor(p3.point.y())) + + # Find slopes (notice that if the y dists are 0, we don't care + # about the slopes) + xdelta = 0.0 + rdelta = 0.0 + gdelta = 0.0 + bdelta = 0.0 + if p2p3ydist != 0.0: + xdelta = p2p3xdist / p2p3ydist + rdelta = (dest.r - r) / p2p3ydist + gdelta = (dest.g - g) / p2p3ydist + bdelta = (dest.b - b) / p2p3ydist + + # Calculate gradients using linear approximation + for y in range(y1, y2): + if lefty: + leftColors[y] = DoubleColor(r, g, b) + leftX[y] = x + else: + rightColors[y] = DoubleColor(r, g, b) + rightX[y] = x + + r += rdelta + g += gdelta + b += bdelta + x += xdelta + + # Inner loop. For each y in the left map of x-values, draw one + # line from left to right. + p3yfloor = int(floor(p3.point.y())) + p1yfloor = int(floor(p1.point.y())) + for y in range(p1yfloor, p3yfloor): + lx = leftX[y] + rx = rightX[y] + + lxi = int(floor(lx)) + rxi = int(floor(rx)) + rc = rightColors[y] + lc = leftColors[y] + + # if the xdist is 0, don't draw anything. + xdist = rx - lx + if xdist != 0.0: + r = lc.r + g = lc.g + b = lc.b + rdelta = (rc.r - r) / xdist + gdelta = (rc.g - g) / xdist + bdelta = (rc.b - b) / xdist + + # Inner loop 2. Draws the line from left to right. + for x in range(lxi, rxi + 1): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + r += rdelta + g += gdelta + b += bdelta + + def _radius_at(self, pos, rect): + mousexdist = pos.x() - float(rect.center().x()) + mouseydist = pos.y() - float(rect.center().y()) + return sqrt(mousexdist ** 2 + mouseydist ** 2) + + def _angle_at(self, pos, rect): + mousexdist = pos.x() - float(rect.center().x()) + mouseydist = pos.y() - float(rect.center().y()) + mouserad = sqrt(mousexdist ** 2 + mouseydist ** 2) + if mouserad == 0.0: + return 0.0 + + angle = acos(mousexdist / mouserad) + if mouseydist >= 0: + angle = TWOPI - angle + + return angle + + def _point_from_color(self, col): + # Simplifications for the corner cases. + if col == QtCore.Qt.black: + return self.point_b + elif col == QtCore.Qt.white: + return self.point_c + + # Find the x and y slopes + ab_deltax = self.point_b.x() - self.point_a.x() + ab_deltay = self.point_b.y() - self.point_a.y() + bc_deltax = self.point_c.x() - self.point_b.x() + bc_deltay = self.point_c.y() - self.point_b.y() + ac_deltax = self.point_c.x() - self.point_a.x() + ac_deltay = self.point_c.y() - self.point_a.y() + + # Extract the h,s,v values of col. + _, sat, val, _ = col.getHsv() + + # Find the line that passes through the triangle where the value + # is equal to our color's value. + p1 = self.point_a.x() + (ab_deltax * float(255 - val)) / 255.0 + q1 = self.point_a.y() + (ab_deltay * float(255 - val)) / 255.0 + p2 = self.point_b.x() + (bc_deltax * float(val)) / 255.0 + q2 = self.point_b.y() + (bc_deltay * float(val)) / 255.0 + + # Find the line that passes through the triangle where the + # saturation is equal to our color's value. + p3 = self.point_a.x() + (ac_deltax * float(255 - sat)) / 255.0 + q3 = self.point_a.y() + (ac_deltay * float(255 - sat)) / 255.0 + p4 = self.point_b.x() + q4 = self.point_b.y() + + # Find the intersection between these lines. + if p1 != p2: + a = (q2 - q1) / (p2 - p1) + c = (q4 - q3) / (p4 - p3) + b = q1 - a * p1 + d = q3 - c * p3 + + x = (d - b) / (a - c) + y = a * x + b + else: + x = p1 + p4_p3 = p4 - p3 + if p4_p3 == 0: + y = 0 + else: + y = q3 + (x - p3) * (q4 - q3) / p4_p3 + + return QtCore.QPointF(x, y) + + def _color_from_point(self, p): + # Find the outer radius of the hue gradient. + size_w = self.contentsRect().width() + size_h = self.contentsRect().height() + if size_w < size_h: + size = size_w + else: + size = size_h + outer_radius = (size - 1) / 2 + + # Find the center coordinates + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + + # Find the a, b and c from their angles, the center of the rect + # and the radius of the hue gradient donut. + inner_radius = outer_radius - (outer_radius / self.inner_radius_ratio) + pa = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + pb = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + pc = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + + # Find the hue value from the angle of the 'a' point. + angle = self.angle_a - PI / 2.0 + if angle < 0: + angle += TWOPI + hue = ( + 360 + - int(floor((360.0 * angle) / TWOPI)) + - self.hue_offset + ) % 360 + + # Create the color of the 'a' corner point. We know that b is + # black and c is white. + color = QtGui.QColor() + color.setHsv(hue, 255, 255) + + # See also drawTrigon(), which basically does exactly the same to + # determine all colors in the trigon. + p1 = Vertex(color, pa) + p2 = Vertex(QtCore.Qt.black, pb) + p3 = Vertex(QtCore.Qt.white, pc) + + # Make sure p1 is above p2, which is above p3. + if p1.point.y() > p2.point.y(): + p1, p2 = p2, p1 + if p1.point.y() > p3.point.y(): + p1, p3 = p3, p1 + if p2.point.y() > p3.point.y(): + p2, p3 = p3, p2 + + # Find the slopes of all edges in the trigon. All the three y + # deltas here are positive because of the above sorting. + p1p2ydist = p2.point.y() - p1.point.y() + p1p3ydist = p3.point.y() - p1.point.y() + p2p3ydist = p3.point.y() - p2.point.y() + p1p2xdist = p2.point.x() - p1.point.x() + p1p3xdist = p3.point.x() - p1.point.x() + p2p3xdist = p3.point.x() - p2.point.x() + + # The first x delta decides wether we have a lefty or a righty + # trigon. A lefty trigon has its tallest edge on the right hand + # side of the trigon. The righty trigon has it on its left side. + # This property determines wether the left or the right set of x + # coordinates will be continuous. + lefty = p1p2xdist < 0 + + # Find whether the selector's y is in the first or second shorty, + # counting from the top and downwards. This is used to find the + # color at the selector point. + firstshorty = (p.y() >= p1.point.y() and p.y() < p2.point.y()) + + # From the y value of the selector's position, find the left and + # right x values. + if lefty: + if firstshorty: + if (floor(p1p2ydist) != 0.0): + leftx = ( + p1.point.x() + + ((p1p2xdist * (p.y() - p1.point.y())) / p1p2ydist) + ) + else: + leftx = min(p1.point.x(), p2.point.x()) + + else: + if (floor(p2p3ydist) != 0.0): + leftx = ( + p2.point.x() + + (p2p3xdist * (p.y() - p2.point.y())) / p2p3ydist + ) + else: + leftx = min(p2.point.x(), p3.point.x()) + + rightx = ( + p1.point.x() + + ((p1p3xdist * (p.y() - p1.point.y())) / p1p3ydist) + ) + else: + leftx = ( + p1.point.x() + + ((p1p3xdist * (p.y() - p1.point.y())) / p1p3ydist) + ) + if firstshorty: + if floor(p1p2ydist) != 0.0: + rightx = ( + p1.point.x() + + ((p1p2xdist * (p.y() - p1.point.y())) / p1p2ydist) + ) + else: + rightx = max(p1.point.x(), p2.point.x()) + + else: + if floor(p2p3ydist) != 0.0: + rightx = ( + p2.point.x() + + ((p2p3xdist * (p.y() - p2.point.y())) / p2p3ydist) + ) + else: + rightx = max(p2.point.x(), p3.point.x()) + + # Find the r,g,b values of the points on the trigon's edges that + # are to the left and right of the selector. + if firstshorty: + if floor(p1p2ydist) != 0.0: + p_p1_ratio = (p.y() - p1.point.y()) / p1p2ydist + p2_p_ratio = (p2.point.y() - p.y()) / p1p2ydist + rshort = (p2.color.r * p_p1_ratio) + (p1.color.r * p2_p_ratio) + gshort = (p2.color.g * p_p1_ratio) + (p1.color.g * p2_p_ratio) + bshort = (p2.color.b * p_p1_ratio) + (p1.color.b * p2_p_ratio) + elif lefty: + if p1.point.x() <= p2.point.x(): + rshort = p1.color.r + gshort = p1.color.g + bshort = p1.color.b + else: + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + + else: + if p1.point.x() > p2.point.x(): + rshort = p1.color.r + gshort = p1.color.g + bshort = p1.color.b + else: + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + + else: + if floor(p2p3ydist) != 0.0: + p_p2_ratio = (p.y() - p2.point.y()) / p2p3ydist + p3_p_ratio = (p3.point.y() - p.y()) / p2p3ydist + rshort = (p3.color.r * p_p2_ratio) + (p2.color.r * p3_p_ratio) + gshort = (p3.color.g * p_p2_ratio) + (p2.color.g * p3_p_ratio) + bshort = (p3.color.b * p_p2_ratio) + (p2.color.b * p3_p_ratio) + elif lefty: + if p2.point.x() <= p3.point.x(): + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + else: + rshort = p3.color.r + gshort = p3.color.g + bshort = p3.color.b + + else: + if p2.point.x() > p3.point.x(): + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + else: + rshort = p3.color.r + gshort = p3.color.g + bshort = p3.color.b + + # p1p3ydist is never 0 + p_p1_ratio = (p.y() - p1.point.y()) / p1p3ydist + p3_p_ratio = (p3.point.y() - p.y()) / p1p3ydist + rlong = (p3.color.r * p_p1_ratio) + (p1.color.r * p3_p_ratio) + glong = (p3.color.g * p_p1_ratio) + (p1.color.g * p3_p_ratio) + blong = (p3.color.b * p_p1_ratio) + (p1.color.b * p3_p_ratio) + + # rshort,gshort,bshort is the color on one of the shortys. + # rlong,glong,blong is the color on the longy. So depending on + # wether we have a lefty trigon or not, we can determine which + # colors are on the left and right edge. + if lefty: + rl = rshort + gl = gshort + bl = bshort + rr = rlong + gr = glong + br = blong + else: + rl = rlong + gl = glong + bl = blong + rr = rshort + gr = gshort + br = bshort + + # Find the distance from the left x to the right x (xdist). Then + # find the distances from the selector to each of these (saxdist + # and saxdist2). These distances are used to find the color at + # the selector. + xdist = rightx - leftx + saxdist = p.x() - leftx + saxdist2 = xdist - saxdist + + # Now determine the r,g,b values of the selector using a linear + # approximation. + if xdist != 0.0: + r = (saxdist2 * rl / xdist) + (saxdist * rr / xdist) + g = (saxdist2 * gl / xdist) + (saxdist * gr / xdist) + b = (saxdist2 * bl / xdist) + (saxdist * br / xdist) + else: + # In theory, the left and right color will be equal here. But + # because of the loss of precision, we get an error on both + # colors. The best approximation we can get is from adding + # the two errors, which in theory will eliminate the error + # but in practise will only minimize it. + r = (rl + rr) / 2 + g = (gl + gr) / 2 + b = (bl + br) / 2 + + # Now floor the color components and fit them into proper + # boundaries. This again is to compensate for the error caused by + # loss of precision. + ri = int(floor(r)) + gi = int(floor(g)) + bi = int(floor(b)) + if ri < 0: + ri = 0 + elif ri > 255: + ri = 255 + + if gi < 0: + gi = 0 + elif gi > 255: + gi = 255 + + if bi < 0: + bi = 0 + elif bi > 255: + bi = 255 + + # Voila, we have the color at the point of the selector. + return QtGui.QColor(ri, gi, bi) + + def paint_bg(self): + bg_image = QtGui.QPixmap(self.contentsRect().size()) + bg_image.fill(QtCore.Qt.transparent) + self.bg_image = bg_image + + painter = QtGui.QPainter(self.bg_image) + + painter.setRenderHint(QtGui.QPainter.Antialiasing) + + hue_gradient = QtGui.QConicalGradient( + bg_image.rect().center(), 90 - self.hue_offset + ) + sat_val_gradient = QtGui.QConicalGradient( + bg_image.rect().center(), 90 - self.hue_offset + ) + + hue_color = QtGui.QColor() + sat_val_color = QtGui.QColor() + _, sat, val, _ = self.cur_color.getHsv() + + for idx, hue in self._hue_circle_range: + hue_color.setHsv(hue, 255, 255) + sat_val_color.setHsv(hue, sat, val) + + hue_gradient.setColorAt(idx, hue_color) + sat_val_gradient.setColorAt(idx, sat_val_color) + + inner_radius = self.outer_radius - ( + self.outer_radius / self.inner_radius_ratio + ) + half_radius = self.outer_radius - ( + (self.outer_radius - inner_radius) / 2 + ) + + hue_inner_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - inner_radius, + bg_image.rect().center().y() - inner_radius, + inner_radius * 2 + 1, + inner_radius * 2 + 1 + ) + hue_outer_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - half_radius - 1, + bg_image.rect().center().y() - half_radius - 1, + half_radius * 2 + 3, + half_radius * 2 + 3 + ) + sat_val_inner_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - half_radius, + bg_image.rect().center().y() - half_radius, + half_radius * 2 + 1, + half_radius * 2 + 1 + ) + sat_val_outer_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - self.outer_radius, + bg_image.rect().center().y() - self.outer_radius, + self.outer_radius * 2 + 1, + self.outer_radius * 2 + 1 + ) + hue_path = QtGui.QPainterPath() + hue_path.addEllipse(hue_inner_radius_rect) + hue_path.addEllipse(hue_outer_radius_rect) + + sat_val_path = QtGui.QPainterPath() + sat_val_path.addEllipse(sat_val_inner_radius_rect) + sat_val_path.addEllipse(sat_val_outer_radius_rect) + + painter.save() + painter.setClipPath(hue_path) + painter.fillRect(self.bg_image.rect(), hue_gradient) + painter.restore() + + painter.save() + painter.setClipPath(sat_val_path) + painter.fillRect(self.bg_image.rect(), sat_val_gradient) + painter.restore() + + painter.end() + + @staticmethod + def vlen(x, y): + return sqrt((x ** 2) + (y ** 2)) + + @staticmethod + def vprod(x1, y1, x2, y2): + return x1 * x2 + y1 * y2 + + @staticmethod + def _angle_between_angles(p, a1, a2): + if a1 > a2: + a2 += TWOPI + if p < PI: + p += TWOPI + + return p >= a1 and p < a2 + + @staticmethod + def _point_above_point(x, y, px, py, ax, ay, bx, by): + floored_ax = floor(ax) + floored_bx = floor(bx) + floored_ay = floor(ay) + floored_by = floor(by) + + if floored_ax == floored_bx: + # line is vertical + if floored_ay < floored_by: + return x < ax + elif floored_ay > floored_by: + return x > ax + return not (x == ax and y == ay) + + if floored_ax > floored_bx: + if floored_ay < floored_by: + # line is draw upright-to-downleft + return (floor(x) < floor(px) or floor(y) < floor(py)) + elif floored_ay > floored_by: + # line is draw downright-to-upleft + return (floor(x) > floor(px) or floor(y) < floor(py)) + # line is flat horizontal + return y < ay + + if floored_ay < floored_by: + # line is draw upleft-to-downright + return (floor(x) < floor(px) or floor(y) > floor(py)) + elif floored_ay > floored_by: + # line is draw downleft-to-upright + return (floor(x) > floor(px) or floor(y) > floor(py)) + # line is flat horizontal + return y > ay + + @staticmethod + def _point_in_line(x, y, ax, ay, bx, by): + if ax > bx: + if ay < by: + # line is draw upright-to-downleft + + # if (x,y) is in on or above the upper right point, + # return -1. + if y <= ay and x >= ax: + return -1 + + # if (x,y) is in on or below the lower left point, + # return 1. + if y >= by and x <= bx: + return 1 + else: + # line is draw downright-to-upleft + + # If the line is flat, only use the x coordinate. + if floor(ay) == floor(by): + # if (x is to the right of the rightmost point, + # return -1. otherwise if x is to the left of the + # leftmost point, return 1. + if x >= ax: + return -1 + elif x <= bx: + return 1 + else: + # if (x,y) is on or below the lower right point, + # return -1. + if y >= ay and x >= ax: + return -1 + + # if (x,y) is on or above the upper left point, return 1. + if y <= by and x <= bx: + return 1 + else: + if ay < by: + # line is draw upleft-to-downright + + # If (x,y) is on or above the upper left point, return -1. + if y <= ay and x <= ax: + return -1 + + # If (x,y) is on or below the lower right point, return 1. + if y >= by and x >= bx: + return 1 + else: + # line is draw downleft-to-upright + + # If the line is flat, only use the x coordinate. + if floor(ay) == floor(by): + if x <= ax: + return -1 + elif x >= bx: + return 1 + else: + # If (x,y) is on or below the lower left point, return -1. + if y >= ay and x <= ax: + return -1 + + # If (x,y) is on or above the upper right point, return 1. + if y <= by and x >= bx: + return 1 + + # No tests proved that (x,y) was outside [(ax,ay),(bx,by)], so we + # assume it's inside the line's bounds. + return 0 + + def _move_point_to_triangle(self, x, y, a, b, c): + # Let v1A be the vector from (x,y) to a. + # Let v2A be the vector from a to b. + # Find the angle alphaA between v1A and v2A. + v1xA = x - a.point.x() + v1yA = y - a.point.y() + v2xA = b.point.x() - a.point.x() + v2yA = b.point.y() - a.point.y() + vpA = self.vprod(v1xA, v1yA, v2xA, v2yA) + cosA = vpA / (self.vlen(v1xA, v1yA) * self.vlen(v2xA, v2yA)) + alphaA = acos(cosA) + + # Let v1B be the vector from x to b. + # Let v2B be the vector from b to c. + v1xB = x - b.point.x() + v1yB = y - b.point.y() + v2xB = c.point.x() - b.point.x() + v2yB = c.point.y() - b.point.y() + vpB = self.vprod(v1xB, v1yB, v2xB, v2yB) + cosB = vpB / (self.vlen(v1xB, v1yB) * self.vlen(v2xB, v2yB)) + alphaB = acos(cosB) + + # Let v1C be the vector from x to c. + # Let v2C be the vector from c back to a. + v1xC = x - c.point.x() + v1yC = y - c.point.y() + v2xC = a.point.x() - c.point.x() + v2yC = a.point.y() - c.point.y() + vpC = self.vprod(v1xC, v1yC, v2xC, v2yC) + cosC = vpC / (self.vlen(v1xC, v1yC) * self.vlen(v2xC, v2yC)) + alphaC = acos(cosC) + + # Find the radian angles between the (1,0) vector and the points + # A, B, C and (x,y). Use this information to determine which of + # the edges we should project (x,y) onto. + angleA = self._angle_at(a.point, self.contentsRect()) + angleB = self._angle_at(b.point, self.contentsRect()) + angleC = self._angle_at(c.point, self.contentsRect()) + angleP = self._angle_at(QtCore.QPointF(x, y), self.contentsRect()) + + # If (x,y) is in the a-b area, project onto the a-b vector. + if self._angle_between_angles(angleP, angleA, angleB): + # Find the distance from (x,y) to a. Then use the slope of + # the a-b vector with this distance and the angle between a-b + # and a-(x,y) to determine the point of intersection of the + # perpendicular projection from (x,y) onto a-b. + pdist = sqrt( + ((x - a.point.x()) ** 2) + ((y - a.point.y()) ** 2) + ) + + # the length of all edges is always > 0 + p0x = ( + a.point.x() + + ((b.point.x() - a.point.x()) / self.vlen(v2xB, v2yB)) + * cos(alphaA) * pdist + ) + p0y = ( + a.point.y() + + ((b.point.y() - a.point.y()) / self.vlen(v2xB, v2yB)) + * cos(alphaA) * pdist + ) + + # If (x,y) is above the a-b line, which basically means it's + # outside the triangle, then return its projection onto a-b. + if self._point_above_point( + x, y, + p0x, p0y, + a.point.x(), a.point.y(), + b.point.x(), b.point.y() + ): + # If the projection is "outside" a, return a. If it is + # outside b, return b. Otherwise return the projection. + n = self._point_in_line( + p0x, p0y, + a.point.x(), a.point.y(), + b.point.x(), b.point.y() + ) + if n < 0: + return a.point + elif n > 0: + return b.point + + return QtCore.QPointF(p0x, p0y) + + elif self._angle_between_angles(angleP, angleB, angleC): + # If (x,y) is in the b-c area, project onto the b-c vector. + pdist = sqrt( + ((x - b.point.x()) ** 2) + ((y - b.point.y()) ** 2) + ) + + # the length of all edges is always > 0 + p0x = ( + b.point.x() + + ((c.point.x() - b.point.x()) / self.vlen(v2xC, v2yC)) + * cos(alphaB) * pdist + ) + p0y = ( + b.point.y() + + ((c.point.y() - b.point.y()) / self.vlen(v2xC, v2yC)) + * cos(alphaB) + * pdist + ) + + if self._point_above_point( + x, y, + p0x, p0y, + b.point.x(), b.point.y(), + c.point.x(), c.point.y() + ): + n = self._point_in_line( + p0x, p0y, + b.point.x(), b.point.y(), + c.point.x(), c.point.y() + ) + if n < 0: + return b.point + elif n > 0: + return c.point + return QtCore.QPointF(p0x, p0y) + + elif self._angle_between_angles(angleP, angleC, angleA): + # If (x,y) is in the c-a area, project onto the c-a vector. + pdist = sqrt( + ((x - c.point.x()) ** 2) + ((y - c.point.y()) ** 2) + ) + + # the length of all edges is always > 0 + p0x = ( + c.point.x() + + ((a.point.x() - c.point.x()) / self.vlen(v2xA, v2yA)) + * cos(alphaC) + * pdist + ) + p0y = ( + c.point.y() + + ((a.point.y() - c.point.y()) / self.vlen(v2xA, v2yA)) + * cos(alphaC) * pdist + ) + + if self._point_above_point( + x, y, + p0x, p0y, + c.point.x(), c.point.y(), + a.point.x(), a.point.y() + ): + n = self._point_in_line( + p0x, p0y, + c.point.x(), c.point.y(), + a.point.x(), a.point.y() + ) + if n < 0: + return c.point + elif n > 0: + return a.point + return QtCore.QPointF(p0x, p0y) + + # (x,y) is inside the triangle (inside a-b, b-c and a-c). + return QtCore.QPointF(x, y) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py new file mode 100644 index 0000000000..a4393a6625 --- /dev/null +++ b/openpype/widgets/color_widgets/color_view.py @@ -0,0 +1,78 @@ +from Qt import QtWidgets, QtCore, QtGui + + +class ColorViewer(QtWidgets.QWidget): + def __init__(self, parent=None): + super(ColorViewer, self).__init__(parent) + + self.setMinimumSize(10, 10) + + self.alpha = 255 + self.actual_pen = QtGui.QPen() + self.actual_color = QtGui.QColor() + self._checkerboard = None + + def checkerboard(self): + if not self._checkerboard: + checkboard_piece_size = 10 + color_1 = QtGui.QColor(188, 188, 188) + color_2 = QtGui.QColor(90, 90, 90) + + pix = QtGui.QPixmap( + checkboard_piece_size * 2, + checkboard_piece_size * 2 + ) + pix_painter = QtGui.QPainter(pix) + + rect = QtCore.QRect( + 0, 0, checkboard_piece_size, checkboard_piece_size + ) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, checkboard_piece_size) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, 0) + pix_painter.fillRect(rect, color_2) + rect.moveTo(0, checkboard_piece_size) + pix_painter.fillRect(rect, color_2) + pix_painter.end() + self._checkerboard = pix + + return self._checkerboard + + def color(self): + return self.actual_color + + def set_color(self, color): + if color == self.actual_color: + return + + # Create copy of entered color + self.actual_color = QtGui.QColor(color) + # Set alpha by current alpha value + self.actual_color.setAlpha(self.alpha) + # Repaint + self.update() + + def set_alpha(self, alpha): + if alpha == self.alpha: + return + # Change alpha of current color + self.actual_color.setAlpha(alpha) + # Store the value + self.alpha = alpha + # Repaint + self.update() + + def paintEvent(self, event): + rect = event.rect() + + # Paint everything to pixmap as it has transparency + pix = QtGui.QPixmap(rect.width(), rect.height()) + pix_painter = QtGui.QPainter(pix) + pix_painter.drawTiledPixmap(rect, self.checkerboard()) + pix_painter.fillRect(rect, self.actual_color) + pix_painter.end() + + painter = QtGui.QPainter(self) + painter.drawPixmap(rect, pix) + painter.end() From ac3fbcb7ce1c2ab34ef30f80f44a61acfb922b2f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:26:19 +0200 Subject: [PATCH 271/311] base implementation of ColorEntity view --- .../tools/settings/settings/categories.py | 6 +- .../tools/settings/settings/color_widget.py | 174 ++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 openpype/tools/settings/settings/color_widget.py diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 4762aa4b6b..01d4babd0f 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -21,6 +21,7 @@ from openpype.settings.entities import ( TextEntity, PathInput, RawJsonEntity, + ColorEntity, DefaultsNotDefined, StudioDefaultsNotDefined, @@ -44,7 +45,7 @@ from .item_widgets import ( PathWidget, PathInputWidget ) - +from .color_widget import ColorWidget from avalon.vendor import qtawesome @@ -113,6 +114,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): elif isinstance(entity, RawJsonEntity): return RawJsonWidget(*args) + elif isinstance(entity, ColorEntity): + return ColorWidget(*args) + elif isinstance(entity, BaseEnumEntity): return EnumeratorWidget(*args) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py new file mode 100644 index 0000000000..54545d7450 --- /dev/null +++ b/openpype/tools/settings/settings/color_widget.py @@ -0,0 +1,174 @@ +from Qt import QtWidgets, QtCore, QtGui + +from .item_widgets import InputWidget + +from openpype.widgets.color_widgets import ColorPickerWidget + + +class ColorWidget(InputWidget): + def _add_inputs_to_layout(self): + self.input_field = ColorViewer(self.content_widget) + + self.setFocusProxy(self.input_field) + + self.content_layout.addWidget(self.input_field, 1) + + self.input_field.clicked.connect(self._on_click) + + self._dialog = None + + def _on_click(self): + if self._dialog: + self._dialog.open() + return + + dialog = ColorDialog(self.input_field.color(), self) + self._dialog = dialog + + dialog.open() + dialog.finished.connect(self._on_dialog_finish) + + def _on_dialog_finish(self, *_args): + if not self._dialog: + return + + color = self._dialog.result() + if color is not None: + self.input_field.set_color(color) + self._on_value_change() + + self._dialog.deleteLater() + self._dialog = None + + def _on_entity_change(self): + if self.entity.value != self.input_value(): + self.set_entity_value() + + def set_entity_value(self): + self.input_field.set_color(*self.entity.value) + + def input_value(self): + color = self.input_field.color() + return [color.red(), color.green(), color.blue(), color.alpha()] + + def _on_value_change(self): + if self.ignore_input_changes: + return + + self.entity.set(self.input_value()) + + +class ColorViewer(QtWidgets.QWidget): + clicked = QtCore.Signal() + + def __init__(self, parent=None): + super(ColorViewer, self).__init__(parent) + + self.setMinimumSize(10, 10) + + self.actual_pen = QtGui.QPen() + self.actual_color = QtGui.QColor() + self._checkerboard = None + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + super(ColorViewer, self).mouseReleaseEvent(event) + + def checkerboard(self): + if not self._checkerboard: + checkboard_piece_size = 10 + color_1 = QtGui.QColor(188, 188, 188) + color_2 = QtGui.QColor(90, 90, 90) + + pix = QtGui.QPixmap( + checkboard_piece_size * 2, + checkboard_piece_size * 2 + ) + pix_painter = QtGui.QPainter(pix) + + rect = QtCore.QRect( + 0, 0, checkboard_piece_size, checkboard_piece_size + ) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, checkboard_piece_size) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, 0) + pix_painter.fillRect(rect, color_2) + rect.moveTo(0, checkboard_piece_size) + pix_painter.fillRect(rect, color_2) + pix_painter.end() + self._checkerboard = pix + + return self._checkerboard + + def color(self): + return self.actual_color + + def set_color(self, *args): + # Create copy of entered color + self.actual_color = QtGui.QColor(*args) + # Repaint + self.update() + + def set_alpha(self, alpha): + # Change alpha of current color + self.actual_color.setAlpha(alpha) + # Repaint + self.update() + + def paintEvent(self, event): + rect = event.rect() + + # Paint everything to pixmap as it has transparency + pix = QtGui.QPixmap(rect.width(), rect.height()) + pix_painter = QtGui.QPainter(pix) + pix_painter.drawTiledPixmap(rect, self.checkerboard()) + pix_painter.fillRect(rect, self.actual_color) + pix_painter.end() + + painter = QtGui.QPainter(self) + painter.drawPixmap(rect, pix) + painter.end() + + +class ColorDialog(QtWidgets.QDialog): + def __init__(self, color=None, parent=None): + super(ColorDialog, self).__init__(parent) + + self.setWindowTitle("Color picker dialog") + + picker_widget = ColorPickerWidget(color, self) + + footer_widget = QtWidgets.QWidget(self) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + + ok_btn = QtWidgets.QPushButton("Ok", footer_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget) + + footer_layout.addWidget(ok_btn) + footer_layout.addWidget(cancel_btn) + footer_layout.addWidget(QtWidgets.QWidget(self), 1) + + layout = QtWidgets.QVBoxLayout(self) + + layout.addWidget(picker_widget, 1) + layout.addWidget(footer_widget, 0) + + ok_btn.clicked.connect(self.on_ok_clicked) + cancel_btn.clicked.connect(self.on_cancel_clicked) + + self.picker_widget = picker_widget + + self._result = None + + def on_ok_clicked(self): + self._result = self.picker_widget.color() + self.close() + + def on_cancel_clicked(self): + self._result = None + self.close() + + def result(self): + return self._result From 7c1e8f47b46091d2b1e617652cf5b92a593f7fae Mon Sep 17 00:00:00 2001 From: jezscha Date: Wed, 19 May 2021 15:37:47 +0000 Subject: [PATCH 272/311] Create draft PR for #1538 From 655063e9f60dabdfd619875131f3af2c91f16a44 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 19 May 2021 17:42:22 +0200 Subject: [PATCH 273/311] PS - fix hardcoding 'image' family into subset name --- .../photoshop/plugins/create/create_image.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 1df8502959..c0549106c0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -16,7 +16,9 @@ class CreateImage(openpype.api.Creator): create_group = False stub = photoshop.stub() + useSelection = False if (self.options or {}).get("useSelection"): + useSelection = True multiple_instances = False selection = stub.get_selected_layers() self.log.info("selection {}".format(selection)) @@ -61,7 +63,9 @@ class CreateImage(openpype.api.Creator): # No selection creates an empty group. create_group = True else: - create_group = True + stub.select_layers(stub.get_layers()) + group = stub.group_selected_layers(self.name) + groups.append(group) if create_group: group = stub.create_group(self.name) @@ -77,13 +81,20 @@ class CreateImage(openpype.api.Creator): group.name = group.name.replace(stub.PUBLISH_ICON, ''). \ replace(stub.LOADED_ICON, '') + if useSelection: + clean_subset_name = self.data["subset"].replace("Default", '') + subset_name = clean_subset_name + group.name + else: + # use value provided by user from Creator + subset_name = self.data["subset"] + if group.long_name: for directory in group.long_name[::-1]: name = directory.replace(stub.PUBLISH_ICON, '').\ replace(stub.LOADED_ICON, '') long_names.append(name) - self.data.update({"subset": "image" + group.name}) + self.data.update({"subset": subset_name}) self.data.update({"uuid": str(group.id)}) self.data.update({"long_name": "_".join(long_names)}) stub.imprint(group, self.data) From 77295b0d7e014c3ee8cc115db753e58cb5af076a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 19 May 2021 17:44:23 +0200 Subject: [PATCH 274/311] PS - ExtractReview fill produce flattened image if no instances created --- .../plugins/publish/extract_review.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 3b6d8ef951..b52078fd5f 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -6,7 +6,12 @@ from avalon import photoshop class ExtractReview(openpype.api.Extractor): - """Produce a flattened image file from all instances.""" + """ + Produce a flattened image file from all 'image' instances. + + If no 'image' instance is created, it produces flattened image from + all visible layers. + """ label = "Extract Review" hosts = ["photoshop"] @@ -30,14 +35,15 @@ class ExtractReview(openpype.api.Extractor): ) output_image_path = os.path.join(staging_dir, output_image) with photoshop.maintained_visibility(): - # Hide all other layers. - extract_ids = set([ll.id for ll in stub. - get_layers_in_layers(layers)]) - self.log.info("extract_ids {}".format(extract_ids)) - for layer in stub.get_layers(): - # limit unnecessary calls to client - if layer.visible and layer.id not in extract_ids: - stub.set_visible(layer.id, False) + if layers: + # Hide all other layers. + extract_ids = set([ll.id for ll in stub. + get_layers_in_layers(layers)]) + self.log.debug("extract_ids {}".format(extract_ids)) + for layer in stub.get_layers(): + # limit unnecessary calls to client + if layer.visible and layer.id not in extract_ids: + stub.set_visible(layer.id, False) stub.saveAs(output_image_path, 'jpg', True) From 589dcfd6d0e1e311e240d631e2753532ce2ef10a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:56:00 +0200 Subject: [PATCH 275/311] Converted inputs to widgets --- .../widgets/color_widgets/color_inputs.py | 79 ++++++++++++------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index ddf8aebd4e..a4409988b2 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -25,11 +25,11 @@ QSlider::handle:horizontal:hover { }""" -class AlphaInputs(QtWidgets.QGroupBox): +class AlphaInputs(QtWidgets.QWidget): alpha_changed = QtCore.Signal(int) def __init__(self, parent=None): - super(AlphaInputs, self).__init__("Alpha", parent) + super(AlphaInputs, self).__init__(parent) self._block_changes = False self.alpha_value = None @@ -47,11 +47,13 @@ class AlphaInputs(QtWidgets.QGroupBox): inputs_layout.setContentsMargins(0, 0, 0, 0) percent_input = QtWidgets.QDoubleSpinBox(self) + percent_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) percent_input.setMinimum(0) percent_input.setMaximum(100) percent_input.setDecimals(2) int_input = QtWidgets.QSpinBox(self) + int_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) int_input.setMinimum(0) int_input.setMaximum(255) @@ -61,6 +63,7 @@ class AlphaInputs(QtWidgets.QGroupBox): inputs_layout.addWidget(QtWidgets.QLabel("%")) layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(QtWidgets.QLabel("Alpha", self)) layout.addWidget(alpha_slider) layout.addWidget(inputs_widget) @@ -119,11 +122,11 @@ class AlphaInputs(QtWidgets.QGroupBox): self._block_changes = False -class RGBInputs(QtWidgets.QGroupBox): +class RGBInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(RGBInputs, self).__init__("RGB", parent) + super(RGBInputs, self).__init__(parent) self._block_changes = False @@ -133,6 +136,10 @@ class RGBInputs(QtWidgets.QGroupBox): input_green = QtWidgets.QSpinBox(self) input_blue = QtWidgets.QSpinBox(self) + input_red.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_green.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_blue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_red.setMinimum(0) input_green.setMinimum(0) input_blue.setMinimum(0) @@ -142,9 +149,10 @@ class RGBInputs(QtWidgets.QGroupBox): input_blue.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_red) - layout.addWidget(input_green) - layout.addWidget(input_blue) + layout.addWidget(QtWidgets.QLabel("RGB", self), 0) + layout.addWidget(input_red, 1) + layout.addWidget(input_green, 1) + layout.addWidget(input_blue, 1) input_red.valueChanged.connect(self._on_red_change) input_green.valueChanged.connect(self._on_green_change) @@ -192,11 +200,11 @@ class RGBInputs(QtWidgets.QGroupBox): self._block_changes = False -class CMYKInputs(QtWidgets.QGroupBox): +class CMYKInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(CMYKInputs, self).__init__("CMYK", parent) + super(CMYKInputs, self).__init__(parent) self.color = color @@ -207,6 +215,11 @@ class CMYKInputs(QtWidgets.QGroupBox): input_yellow = QtWidgets.QSpinBox(self) input_black = QtWidgets.QSpinBox(self) + input_cyan.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_magenta.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_yellow.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_black.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_cyan.setMinimum(0) input_magenta.setMinimum(0) input_yellow.setMinimum(0) @@ -218,10 +231,11 @@ class CMYKInputs(QtWidgets.QGroupBox): input_black.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_cyan) - layout.addWidget(input_magenta) - layout.addWidget(input_yellow) - layout.addWidget(input_black) + layout.addWidget(QtWidgets.QLabel("CMYK", self)) + layout.addWidget(input_cyan, 1) + layout.addWidget(input_magenta, 1) + layout.addWidget(input_yellow, 1) + layout.addWidget(input_black, 1) input_cyan.valueChanged.connect(self._on_change) input_magenta.valueChanged.connect(self._on_change) @@ -272,18 +286,19 @@ class CMYKInputs(QtWidgets.QGroupBox): self._block_changes = False -class HEXInputs(QtWidgets.QGroupBox): +class HEXInputs(QtWidgets.QWidget): hex_regex = re.compile("^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$") value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(HEXInputs, self).__init__("HEX", parent) + super(HEXInputs, self).__init__(parent) self.color = color - input_field = QtWidgets.QLineEdit() + input_field = QtWidgets.QLineEdit(self) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_field) + layout.addWidget(QtWidgets.QLabel("HEX", self), 0) + layout.addWidget(input_field, 1) input_field.textChanged.connect(self._on_change) @@ -316,11 +331,11 @@ class HEXInputs(QtWidgets.QGroupBox): self._block_changes = False -class HSVInputs(QtWidgets.QGroupBox): +class HSVInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(HSVInputs, self).__init__("HSV", parent) + super(HSVInputs, self).__init__(parent) self._block_changes = False @@ -330,6 +345,10 @@ class HSVInputs(QtWidgets.QGroupBox): input_sat = QtWidgets.QSpinBox(self) input_val = QtWidgets.QSpinBox(self) + input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_val.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_hue.setMinimum(0) input_sat.setMinimum(0) input_val.setMinimum(0) @@ -339,9 +358,10 @@ class HSVInputs(QtWidgets.QGroupBox): input_val.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_hue) - layout.addWidget(input_sat) - layout.addWidget(input_val) + layout.addWidget(QtWidgets.QLabel("HSV", self), 0) + layout.addWidget(input_hue, 1) + layout.addWidget(input_sat, 1) + layout.addWidget(input_val, 1) input_hue.valueChanged.connect(self._on_change) input_sat.valueChanged.connect(self._on_change) @@ -385,11 +405,11 @@ class HSVInputs(QtWidgets.QGroupBox): self._block_changes = False -class HSLInputs(QtWidgets.QGroupBox): +class HSLInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(HSLInputs, self).__init__("HSL", parent) + super(HSLInputs, self).__init__(parent) self._block_changes = False @@ -399,6 +419,10 @@ class HSLInputs(QtWidgets.QGroupBox): input_sat = QtWidgets.QSpinBox(self) input_light = QtWidgets.QSpinBox(self) + input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_light.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_hue.setMinimum(0) input_sat.setMinimum(0) input_light.setMinimum(0) @@ -408,9 +432,10 @@ class HSLInputs(QtWidgets.QGroupBox): input_light.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_hue) - layout.addWidget(input_sat) - layout.addWidget(input_light) + layout.addWidget(QtWidgets.QLabel("HSL", self), 0) + layout.addWidget(input_hue, 1) + layout.addWidget(input_sat, 1) + layout.addWidget(input_light, 1) input_hue.valueChanged.connect(self._on_change) input_sat.valueChanged.connect(self._on_change) From ed6848d9d02e76d3bd05d6f5817eb6c8bce5939a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:02:12 +0200 Subject: [PATCH 276/311] used color type at 2 places --- .../schemas/schema_global_publish.json | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 0efe3b8fea..426da4b71e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -228,14 +228,9 @@ ] }, { - "type": "schema_template", - "name": "template_rgba_color", - "template_data": [ - { - "label": "Fill Color", - "name": "fill_color" - } - ] + "type": "color", + "label": "Fill Color", + "key": "fill_color" }, { "key": "line_thickness", @@ -245,14 +240,9 @@ "maximum": 1000 }, { - "type": "schema_template", - "name": "template_rgba_color", - "template_data": [ - { - "label": "Line Color", - "name": "line_color" - } - ] + "type": "color", + "label": "Line Color", + "key": "line_color" } ] } From 3e1286531f27ab78441ba2f517e5f50da865bcea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:11:31 +0200 Subject: [PATCH 277/311] add empty line --- openpype/settings/entities/color_entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index 7d31ba42b9..7a1b1d9848 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -8,6 +8,7 @@ from .exceptions import ( class ColorEntity(InputEntity): schema_types = ["color"] + def _item_initalization(self): self.valid_value_types = (list, ) self.value_on_not_set = [0, 0, 0, 255] From 496e550d658c001409d5d977510c408d3702f390 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 May 2021 18:24:00 +0200 Subject: [PATCH 278/311] Nuke: creator was to setting correct settings --- openpype/hosts/nuke/api/lib.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ea6476485b..bafffe36cb 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -55,9 +55,10 @@ def get_created_node_imageio_setting(**kwarg): log.info(node) if (node["nukeNodeClass"] != nodeclass) and ( creator not in node["plugins"]): - continue - - imageio_node = node + if (nodeclass in node["nukeNodeClass"]) and ( + creator in node["plugins"]): + imageio_node = node + break log.info("ImageIO node: {}".format(imageio_node)) return imageio_node @@ -340,9 +341,9 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): nuke.message(msg) # build file path to workfiles - fpath = str(anatomy_filled["work"]["folder"]).replace("\\", "/") + fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") fpath = data["fpath_template"].format( - work=fpath, version=data["version"], subset=data["subset"], + work=fdir, version=data["version"], subset=data["subset"], frame=data["frame"], ext=representation ) From 2861078620d62cca8fc6d24d72a3c60a05e66ec5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:24:13 +0200 Subject: [PATCH 279/311] added small info about color entity --- openpype/settings/entities/schemas/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 18312a8364..6c31b61f59 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -420,6 +420,18 @@ } ``` +### color +- preimplemented entity to store and load color values +- entity store and expect list of 4 integers in range 0-255 + - integers represents rgba [Red, Green, Blue, Alpha] + +``` +{ + "type": "color", + "key": "bg_color", + "label": "Background Color" +} +``` ## Noninteractive widgets - have nothing to do with data From ac2e759afc45e1347bc85994a10f2e33670447e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:24:34 +0200 Subject: [PATCH 280/311] added color entity to examples --- .../entities/schemas/system_schema/example_schema.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index 48a21cc0c6..a4ed56df32 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -4,6 +4,11 @@ "type": "dict", "is_file": true, "children": [ + { + "key": "color", + "label": "Color input", + "type": "color" + }, { "type": "dict", "key": "schema_template_exaples", From a4b1a90e3118be0621f86edf7ded43425749047b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 May 2021 18:25:16 +0200 Subject: [PATCH 281/311] hound: suggestion --- openpype/hosts/nuke/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index bafffe36cb..e6dde813a7 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -53,8 +53,6 @@ def get_created_node_imageio_setting(**kwarg): imageio_node = None for node in imageio_nodes: log.info(node) - if (node["nukeNodeClass"] != nodeclass) and ( - creator not in node["plugins"]): if (nodeclass in node["nukeNodeClass"]) and ( creator in node["plugins"]): imageio_node = node From f8936f5e4038f2516363c645fd04182ec309c9d8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:34:29 +0200 Subject: [PATCH 282/311] replaced pick color text with an icon --- .../widgets/color_widgets/color_picker_widget.py | 9 +++++++-- openpype/widgets/color_widgets/eyedropper.png | Bin 0 -> 3178 bytes 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 openpype/widgets/color_widgets/eyedropper.png diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index d06af73cbf..5786c529ed 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -1,3 +1,4 @@ +import os from Qt import QtWidgets, QtCore, QtGui from .color_triangle import QtColorTriangle @@ -32,9 +33,13 @@ class ColorPickerWidget(QtWidgets.QWidget): color_view.setMaximumHeight(50) # Color pick button - btn_pick_color = QtWidgets.QPushButton( - "Pick a color", bottom_utils_widget + btn_pick_color = QtWidgets.QPushButton(bottom_utils_widget) + icon_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "eyedropper.png" ) + btn_pick_color.setIcon(QtGui.QIcon(icon_path)) + btn_pick_color.setToolTip("Pick a color") # Color inputs widget color_inputs = ColorInputsWidget(self) diff --git a/openpype/widgets/color_widgets/eyedropper.png b/openpype/widgets/color_widgets/eyedropper.png new file mode 100644 index 0000000000000000000000000000000000000000..baf6209e0be3d0f32cf76aebbfc0ba5edef7dc3a GIT binary patch literal 3178 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 M`viEOSsR=CU*aw9$^ZZW literal 0 HcmV?d00001 From 101134bbdfdfe847e647f515296089d43c2e0b09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:37:20 +0200 Subject: [PATCH 283/311] moved with buttons --- openpype/tools/settings/settings/color_widget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index 54545d7450..7b534b749c 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -141,14 +141,15 @@ class ColorDialog(QtWidgets.QDialog): picker_widget = ColorPickerWidget(color, self) footer_widget = QtWidgets.QWidget(self) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) ok_btn = QtWidgets.QPushButton("Ok", footer_widget) cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) footer_layout.addWidget(ok_btn) footer_layout.addWidget(cancel_btn) - footer_layout.addWidget(QtWidgets.QWidget(self), 1) layout = QtWidgets.QVBoxLayout(self) From 60e3b1ee4745e853bb020ea92a3d1bf2ae83e7b9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 19:17:17 +0200 Subject: [PATCH 284/311] remove tvpaint families from settings --- .../defaults/project_settings/tvpaint.json | 16 ++-------------- .../projects_schema/schema_project_tvpaint.json | 12 ------------ 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index d7fc46763c..9d5b922b8e 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -22,26 +22,14 @@ "stretch": true, "timestretch": true, "preload": true - }, - "families": [ - "render", - "image", - "background", - "plate" - ] + } }, "ImportImage": { "defaults": { "stretch": true, "timestretch": true, "preload": true - }, - "families": [ - "render", - "image", - "background", - "plate" - ] + } } }, "filters": {} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 903c5de842..3844d12e1b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -79,12 +79,6 @@ "label": "Preload" } ] - }, - { - "type": "list", - "key": "families", - "label": "Families", - "object_type": "text" } ] }, @@ -114,12 +108,6 @@ "label": "Preload" } ] - }, - { - "type": "list", - "key": "families", - "label": "Families", - "object_type": "text" } ] } From 6827f3283cce7a6979e7eb4d279450b683dd40a1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 21:02:24 +0200 Subject: [PATCH 285/311] don't remove Default from subset name --- openpype/hosts/photoshop/plugins/create/create_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index c0549106c0..967a704ccf 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -9,6 +9,7 @@ class CreateImage(openpype.api.Creator): name = "imageDefault" label = "Image" family = "image" + defaults = ["Main"] def process(self): groups = [] @@ -82,8 +83,7 @@ class CreateImage(openpype.api.Creator): replace(stub.LOADED_ICON, '') if useSelection: - clean_subset_name = self.data["subset"].replace("Default", '') - subset_name = clean_subset_name + group.name + subset_name = self.data["subset"] + group.name else: # use value provided by user from Creator subset_name = self.data["subset"] From 0f6794e2baba21351da78786c22be5fc478adf3d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 21:03:14 +0200 Subject: [PATCH 286/311] make extract formats configurable (within current possibilities) --- .../hosts/photoshop/plugins/publish/extract_image.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index b56f128831..87574d1269 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -35,21 +35,16 @@ class ExtractImage(openpype.api.Extractor): if layer.visible and layer.id not in extract_ids: stub.set_visible(layer.id, False) - save_options = [] - if "png" in self.formats: - save_options.append('png') - if "jpg" in self.formats: - save_options.append('jpg') - file_basename = os.path.splitext( stub.get_active_document_name() )[0] - for extension in save_options: + for extension in self.formats: _filename = "{}.{}".format(file_basename, extension) files[extension] = _filename full_filename = os.path.join(staging_dir, _filename) stub.saveAs(full_filename, extension, True) + self.log.info(f"Extracted: {extension}") representations = [] for extension, filename in files.items(): From a2deaca8005d2523f9ab4e77db8b0f2af3be3cf0 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 21:03:36 +0200 Subject: [PATCH 287/311] add photoshop create and publish options --- .../defaults/project_settings/photoshop.json | 17 ++++++ .../schemas/projects_schema/schema_main.json | 4 ++ .../schema_project_photoshop.json | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 openpype/settings/defaults/project_settings/photoshop.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json new file mode 100644 index 0000000000..0db6e8248d --- /dev/null +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -0,0 +1,17 @@ +{ + "create": { + "CreateImage": { + "defaults": [ + "Main" + ] + } + }, + "publish": { + "ExtractImage": { + "formats": [ + "png", + "jpg" + ] + } + } +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index b4666b302a..e77f13d351 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -82,6 +82,10 @@ "type": "schema", "name": "schema_project_aftereffects" }, + { + "type": "schema", + "name": "schema_project_photoshop" + }, { "type": "schema", "name": "schema_project_harmony" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json new file mode 100644 index 0000000000..3a20b4e79c --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -0,0 +1,57 @@ +{ + "type": "dict", + "collapsible": true, + "key": "photoshop", + "label": "Photoshop", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CreateImage", + "label": "Create Image", + "children": [ + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + } + ] + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "ExtractImage", + "label": "Extract Image", + "children": [ + { + "type": "label", + "label": "Currently only jpg and png are supported" + }, + { + "type": "list", + "key": "formats", + "label": "Extract Formats", + "object_type": "text" + } + ] + } + ] + } + ] +} From 8384f39656a08d2108eaa53226fc151128f36896 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 22:20:44 +0200 Subject: [PATCH 288/311] wrap platform tabs in a coloured box --- website/docs/artist_install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/artist_install.md b/website/docs/artist_install.md index 94a8bcdfe1..6fb5abe9e2 100644 --- a/website/docs/artist_install.md +++ b/website/docs/artist_install.md @@ -16,6 +16,7 @@ OpenPype comes in packages for Windows (10 or Server), Mac OS X (Mojave or highe To install OpenPype you will need administrator permissions. ::: +:::note pick your platform +::: ## Run OpenPype From 9ff9aab0292fd5defb6d49134a730ea5f6bc80ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:09:15 +0200 Subject: [PATCH 289/311] implemented alpha slider that can move pointer on click --- .../widgets/color_widgets/color_inputs.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index a4409988b2..b8737afd99 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -25,6 +25,40 @@ QSlider::handle:horizontal:hover { }""" +class AlphaSlider(QtWidgets.QSlider): + def __init__(self, *args, **kwargs): + super(AlphaSlider, self).__init__(*args, **kwargs) + self._mouse_clicked = False + self.setSingleStep(1) + self.setMinimum(0) + self.setMaximum(255) + self.setValue(255) + + self.setStyleSheet(slide_style) + + def mousePressEvent(self, event): + self._mouse_clicked = True + if event.button() == QtCore.Qt.LeftButton: + self._set_value_to_pos(event.pos().x()) + return event.accept() + return super(AlphaSlider, self).mousePressEvent(event) + + def _set_value_to_pos(self, pos_x): + value = ( + self.maximum() - self.minimum() + ) * pos_x / self.width() + self.minimum() + self.setValue(value) + + def mouseMoveEvent(self, event): + if self._mouse_clicked: + self._set_value_to_pos(event.pos().x()) + super(AlphaSlider, self).mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self._mouse_clicked = True + super(AlphaSlider, self).mouseReleaseEvent(event) + + class AlphaInputs(QtWidgets.QWidget): alpha_changed = QtCore.Signal(int) From ce6c129f9ea27002d9d7b4d4a1a120776b61120a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:18:30 +0200 Subject: [PATCH 290/311] removed labels from input widgets and set content's margines --- openpype/widgets/color_widgets/color_inputs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index b8737afd99..f19e28ba1d 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -183,7 +183,7 @@ class RGBInputs(QtWidgets.QWidget): input_blue.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("RGB", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_red, 1) layout.addWidget(input_green, 1) layout.addWidget(input_blue, 1) @@ -265,7 +265,7 @@ class CMYKInputs(QtWidgets.QWidget): input_black.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("CMYK", self)) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_cyan, 1) layout.addWidget(input_magenta, 1) layout.addWidget(input_yellow, 1) @@ -331,7 +331,7 @@ class HEXInputs(QtWidgets.QWidget): input_field = QtWidgets.QLineEdit(self) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("HEX", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_field, 1) input_field.textChanged.connect(self._on_change) @@ -392,7 +392,7 @@ class HSVInputs(QtWidgets.QWidget): input_val.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("HSV", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_hue, 1) layout.addWidget(input_sat, 1) layout.addWidget(input_val, 1) @@ -466,7 +466,7 @@ class HSLInputs(QtWidgets.QWidget): input_light.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("HSL", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_hue, 1) layout.addWidget(input_sat, 1) layout.addWidget(input_light, 1) From b9b18f34af3eef3d93dfb3f7e75b85385af8797b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:19:18 +0200 Subject: [PATCH 291/311] removed alpha slider from alpha inputs --- .../widgets/color_widgets/color_inputs.py | 40 +++---------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index f19e28ba1d..9aa35021c2 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -68,18 +68,6 @@ class AlphaInputs(QtWidgets.QWidget): self._block_changes = False self.alpha_value = None - # Opacity slider - alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self) - alpha_slider.setSingleStep(1) - alpha_slider.setMinimum(0) - alpha_slider.setMaximum(255) - alpha_slider.setStyleSheet(slide_style) - alpha_slider.setValue(255) - - inputs_widget = QtWidgets.QWidget(self) - inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) - inputs_layout.setContentsMargins(0, 0, 0, 0) - percent_input = QtWidgets.QDoubleSpinBox(self) percent_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) percent_input.setMinimum(0) @@ -91,21 +79,16 @@ class AlphaInputs(QtWidgets.QWidget): int_input.setMinimum(0) int_input.setMaximum(255) - inputs_layout.addWidget(int_input) - inputs_layout.addWidget(QtWidgets.QLabel("0-255")) - inputs_layout.addWidget(percent_input) - inputs_layout.addWidget(QtWidgets.QLabel("%")) + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(int_input) + layout.addWidget(QtWidgets.QLabel("0-255")) + layout.addWidget(percent_input) + layout.addWidget(QtWidgets.QLabel("%")) - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("Alpha", self)) - layout.addWidget(alpha_slider) - layout.addWidget(inputs_widget) - - alpha_slider.valueChanged.connect(self._on_slider_change) percent_input.valueChanged.connect(self._on_percent_change) int_input.valueChanged.connect(self._on_int_change) - self.alpha_slider = alpha_slider self.percent_input = percent_input self.int_input = int_input @@ -118,13 +101,6 @@ class AlphaInputs(QtWidgets.QWidget): self.update_alpha() - def _on_slider_change(self): - if self._block_changes: - return - self.alpha_value = self.alpha_slider.value() - self.alpha_changed.emit(self.alpha_value) - self.update_alpha() - def _on_percent_change(self): if self._block_changes: return @@ -142,10 +118,6 @@ class AlphaInputs(QtWidgets.QWidget): def update_alpha(self): self._block_changes = True - - if self.alpha_slider.value() != self.alpha_value: - self.alpha_slider.setValue(self.alpha_value) - if self.int_input.value() != self.alpha_value: self.int_input.setValue(self.alpha_value) From cbed588a75b8682f0ab3cbc9e4956df6dc29ec12 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:28:28 +0200 Subject: [PATCH 292/311] formatting changes --- openpype/widgets/color_widgets/color_inputs.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index 9aa35021c2..ddb832c655 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -4,13 +4,17 @@ from Qt import QtWidgets, QtCore, QtGui slide_style = """ QSlider::groove:horizontal { - background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff); + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff + ); height: 8px; border-radius: 4px; } QSlider::handle:horizontal { - background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb); + background: qlineargradient( + x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb + ); border: 1px solid #777; width: 8px; margin-top: -1px; @@ -19,7 +23,9 @@ QSlider::handle:horizontal { } QSlider::handle:horizontal:hover { - background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd); + background: qlineargradient( + x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd + ); border: 1px solid #444;ff border-radius: 4px; }""" From 9f86bcff621298f82bdb11eda7d904ad3ada53ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:29:13 +0200 Subject: [PATCH 293/311] inputs are handled inside ColorPickerWidget --- .../color_widgets/color_picker_widget.py | 141 ++++++++++++------ 1 file changed, 93 insertions(+), 48 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 5786c529ed..7158a426a4 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -6,7 +6,13 @@ from .color_view import ColorViewer from .color_screen_pick import PickScreenColorWidget from .color_inputs import ( ColorInputsWidget, - AlphaInputs + AlphaSlider, + AlphaInputs, + HEXInputs, + RGBInputs, + HSLInputs, + HSVInputs, + CMYKInputs ) @@ -16,24 +22,27 @@ class ColorPickerWidget(QtWidgets.QWidget): def __init__(self, color=None, parent=None): super(ColorPickerWidget, self).__init__(parent) - # Eye picked widget - pick_widget = PickScreenColorWidget() - - # Color utils - utils_widget = QtWidgets.QWidget(self) - utils_layout = QtWidgets.QVBoxLayout(utils_widget) - - bottom_utils_widget = QtWidgets.QWidget(utils_widget) + top_part = QtWidgets.QWidget(self) + left_side = QtWidgets.QWidget(top_part) # Color triangle - color_triangle = QtColorTriangle(utils_widget) + color_triangle = QtColorTriangle(left_side) - # Color preview - color_view = ColorViewer(bottom_utils_widget) - color_view.setMaximumHeight(50) + alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, left_side) + + left_layout = QtWidgets.QVBoxLayout(left_side) + left_layout.setContentsMargins(0, 0, 0, 0) + left_layout.addWidget(color_triangle) + left_layout.addWidget(alpha_slider) + + right_side = QtWidgets.QWidget(top_part) + + # Eye picked widget + pick_widget = PickScreenColorWidget() + pick_widget.setMaximumHeight(50) # Color pick button - btn_pick_color = QtWidgets.QPushButton(bottom_utils_widget) + btn_pick_color = QtWidgets.QPushButton(right_side) icon_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "eyedropper.png" @@ -41,50 +50,62 @@ class ColorPickerWidget(QtWidgets.QWidget): btn_pick_color.setIcon(QtGui.QIcon(icon_path)) btn_pick_color.setToolTip("Pick a color") - # Color inputs widget - color_inputs = ColorInputsWidget(self) + # Color preview + color_view = ColorViewer(right_side) + color_view.setMaximumHeight(50) - # Alpha inputs - alpha_input_wrapper_widget = QtWidgets.QWidget(self) - alpha_input_wrapper_layout = QtWidgets.QVBoxLayout( - alpha_input_wrapper_widget - ) + row = 0 + right_layout = QtWidgets.QGridLayout(right_side) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.addWidget(btn_pick_color, row, 0) + right_layout.addWidget(color_view, row, 1) - alpha_inputs = AlphaInputs(alpha_input_wrapper_widget) - alpha_input_wrapper_layout.addWidget(alpha_inputs) - alpha_input_wrapper_layout.addWidget(QtWidgets.QWidget(), 1) + color_inputs_color = QtGui.QColor() + col_inputs_by_label = [ + ("HEX", HEXInputs(color_inputs_color, right_side)), + ("RGB", RGBInputs(color_inputs_color, right_side)), + ("HSL", HSLInputs(color_inputs_color, right_side)), + ("HSV", HSVInputs(color_inputs_color, right_side)) + ] + color_input_fields = [] + for label, input_field in col_inputs_by_label: + row += 1 + right_layout.addWidget(QtWidgets.QLabel(label, right_side), row, 0) + right_layout.addWidget(input_field, row, 1) + input_field.value_changed.connect( + self._on_color_input_value_change + ) + color_input_fields.append(input_field) - bottom_utils_layout = QtWidgets.QHBoxLayout(bottom_utils_widget) - bottom_utils_layout.setContentsMargins(0, 0, 0, 0) - bottom_utils_layout.addWidget(color_view, 1) - bottom_utils_layout.addWidget(btn_pick_color, 0) - - utils_layout.addWidget(bottom_utils_widget, 0) - utils_layout.addWidget(color_triangle, 1) + row += 1 + alpha_inputs = AlphaInputs(right_side) + right_layout.addWidget(QtWidgets.QLabel("Alpha", right_side), row, 0) + right_layout.addWidget(alpha_inputs, row, 1) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(utils_widget, 1) - layout.addWidget(color_inputs, 0) - layout.addWidget(alpha_input_wrapper_widget, 0) + layout.setSpacing(5) + layout.addWidget(left_side, 1) + layout.addWidget(right_side, 0) color_view.set_color(color_triangle.cur_color) - color_inputs.set_color(color_triangle.cur_color) color_triangle.color_changed.connect(self.triangle_color_changed) + alpha_slider.valueChanged.connect(self._on_alpha_slider_change) pick_widget.color_selected.connect(self.on_color_change) - color_inputs.color_changed.connect(self.on_color_change) - alpha_inputs.alpha_changed.connect(self.alpha_changed) + alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed) btn_pick_color.released.connect(self.pick_color) + self.color_input_fields = color_input_fields + self.color_inputs_color = color_inputs_color + self.pick_widget = pick_widget - self.utils_widget = utils_widget - self.bottom_utils_widget = bottom_utils_widget self.color_triangle = color_triangle + self.alpha_slider = alpha_slider + self.color_view = color_view - self.btn_pick_color = btn_pick_color - self.color_inputs = color_inputs self.alpha_inputs = alpha_inputs + self.btn_pick_color = btn_pick_color if color: self.set_color(color) @@ -92,9 +113,7 @@ class ColorPickerWidget(QtWidgets.QWidget): def showEvent(self, event): super(ColorPickerWidget, self).showEvent(event) - triangle_width = int(( - self.utils_widget.height() - self.bottom_utils_widget.height() - ) / 5 * 4) + triangle_width = int(self.height() / 5 * 4) self.color_triangle.setMinimumWidth(triangle_width) def color(self): @@ -109,12 +128,38 @@ class ColorPickerWidget(QtWidgets.QWidget): def triangle_color_changed(self, color): self.color_view.set_color(color) - self.color_inputs.set_color(color) + if self.color_inputs_color != color: + self.color_inputs_color.setRgb( + color.red(), color.green(), color.blue() + ) + for color_input in self.color_input_fields: + color_input.color_changed() def on_color_change(self, color): self.color_view.set_color(color) self.color_triangle.set_color(color) - self.color_inputs.set_color(color) + if self.color_inputs_color != color: + self.color_inputs_color.setRgb( + color.red(), color.green(), color.blue() + ) + for color_input in self.color_input_fields: + color_input.color_changed() - def alpha_changed(self, alpha): - self.color_view.set_alpha(alpha) + def _on_color_input_value_change(self): + for input_field in self.color_input_fields: + input_field.color_changed() + self.on_color_change(QtGui.QColor(self.color_inputs_color)) + + def alpha_changed(self, value): + self.color_view.set_alpha(value) + if self.alpha_slider.value() != value: + self.alpha_slider.setValue(value) + + if self.alpha_inputs.alpha_value != value: + self.alpha_inputs.set_alpha(value) + + def _on_alpha_inputs_changed(self, value): + self.alpha_changed(value) + + def _on_alpha_slider_change(self, value): + self.alpha_changed(value) From 3245e97884c724037de5a14c1ba130f5d818857e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:29:37 +0200 Subject: [PATCH 294/311] checkerboard can be drawn out of ColorViewer --- openpype/widgets/color_widgets/color_view.py | 52 +++++++++++--------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py index a4393a6625..d6d7f0a666 100644 --- a/openpype/widgets/color_widgets/color_view.py +++ b/openpype/widgets/color_widgets/color_view.py @@ -1,6 +1,34 @@ from Qt import QtWidgets, QtCore, QtGui +def draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None): + if piece_size is None: + piece_size = 7 + + if color_1 is None: + color_1 = QtGui.QColor(188, 188, 188) + + if color_2 is None: + color_2 = QtGui.QColor(90, 90, 90) + + pix = QtGui.QPixmap(piece_size * 2, piece_size * 2) + pix_painter = QtGui.QPainter(pix) + + rect = QtCore.QRect( + 0, 0, piece_size, piece_size + ) + pix_painter.fillRect(rect, color_1) + rect.moveTo(piece_size, piece_size) + pix_painter.fillRect(rect, color_1) + rect.moveTo(piece_size, 0) + pix_painter.fillRect(rect, color_2) + rect.moveTo(0, piece_size) + pix_painter.fillRect(rect, color_2) + pix_painter.end() + + return pix + + class ColorViewer(QtWidgets.QWidget): def __init__(self, parent=None): super(ColorViewer, self).__init__(parent) @@ -14,29 +42,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - checkboard_piece_size = 10 - color_1 = QtGui.QColor(188, 188, 188) - color_2 = QtGui.QColor(90, 90, 90) - - pix = QtGui.QPixmap( - checkboard_piece_size * 2, - checkboard_piece_size * 2 - ) - pix_painter = QtGui.QPainter(pix) - - rect = QtCore.QRect( - 0, 0, checkboard_piece_size, checkboard_piece_size - ) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, checkboard_piece_size) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, 0) - pix_painter.fillRect(rect, color_2) - rect.moveTo(0, checkboard_piece_size) - pix_painter.fillRect(rect, color_2) - pix_painter.end() - self._checkerboard = pix - + self._checkerboard = draw_checkerboard_tile() return self._checkerboard def color(self): From c4ec9cb7f1f0d5a2798fe1dc9acd902e2ca84095 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:10:49 +0200 Subject: [PATCH 295/311] use draw_checkerboard_tile to draw checkerboard --- .../tools/settings/settings/color_widget.py | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index 7b534b749c..b84c8dd9cc 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -2,7 +2,10 @@ from Qt import QtWidgets, QtCore, QtGui from .item_widgets import InputWidget -from openpype.widgets.color_widgets import ColorPickerWidget +from openpype.widgets.color_widgets import ( + ColorPickerWidget, + draw_checkerboard_tile +) class ColorWidget(InputWidget): @@ -77,29 +80,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - checkboard_piece_size = 10 - color_1 = QtGui.QColor(188, 188, 188) - color_2 = QtGui.QColor(90, 90, 90) - - pix = QtGui.QPixmap( - checkboard_piece_size * 2, - checkboard_piece_size * 2 - ) - pix_painter = QtGui.QPainter(pix) - - rect = QtCore.QRect( - 0, 0, checkboard_piece_size, checkboard_piece_size - ) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, checkboard_piece_size) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, 0) - pix_painter.fillRect(rect, color_2) - rect.moveTo(0, checkboard_piece_size) - pix_painter.fillRect(rect, color_2) - pix_painter.end() - self._checkerboard = pix - + self._checkerboard = draw_checkerboard_tile() return self._checkerboard def color(self): From 5680f96a499a59844d84b591d39c3b52cb431242 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:10 +0200 Subject: [PATCH 296/311] changed how triangle size is set --- openpype/widgets/color_widgets/color_picker_widget.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 7158a426a4..725d0b374e 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -107,14 +107,21 @@ class ColorPickerWidget(QtWidgets.QWidget): self.alpha_inputs = alpha_inputs self.btn_pick_color = btn_pick_color + self._minimum_size_set = False + if color: self.set_color(color) self.alpha_changed(color.alpha()) def showEvent(self, event): super(ColorPickerWidget, self).showEvent(event) - triangle_width = int(self.height() / 5 * 4) - self.color_triangle.setMinimumWidth(triangle_width) + if self._minimum_size_set: + return + + triangle_size = max(int(self.width() / 5 * 3), 180) + self.color_triangle.setMinimumWidth(triangle_size) + self.color_triangle.setMinimumHeight(triangle_size) + self._minimum_size_set = True def color(self): return self.color_view.color() From 20c119f3555ee267c5784ce1e8d4e7db8d65ae9a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:20 +0200 Subject: [PATCH 297/311] added draw_checkerboard_tile to init file --- openpype/widgets/color_widgets/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/__init__.py b/openpype/widgets/color_widgets/__init__.py index 3423e26cf8..324b23543d 100644 --- a/openpype/widgets/color_widgets/__init__.py +++ b/openpype/widgets/color_widgets/__init__.py @@ -1,6 +1,14 @@ -from .color_picker_widget import ColorPickerWidget +from .color_picker_widget import ( + ColorPickerWidget +) + +from .color_view import ( + draw_checkerboard_tile +) __all__ = ( "ColorPickerWidget", + + "draw_checkerboard_tile" ) From 4bb432284fe4dea23bb61e2272d84d952e8ff4a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:39 +0200 Subject: [PATCH 298/311] added stretch at the end of gridlayout --- openpype/widgets/color_widgets/color_picker_widget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 725d0b374e..3b1eb1aca0 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -82,6 +82,9 @@ class ColorPickerWidget(QtWidgets.QWidget): right_layout.addWidget(QtWidgets.QLabel("Alpha", right_side), row, 0) right_layout.addWidget(alpha_inputs, row, 1) + row += 1 + right_layout.setRowStretch(row, 1) + layout = QtWidgets.QHBoxLayout(self) layout.setSpacing(5) layout.addWidget(left_side, 1) From a688a1510e8dcb439267308112a72e81e61c546c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:50 +0200 Subject: [PATCH 299/311] alpha slider has proxy --- openpype/widgets/color_widgets/color_picker_widget.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 3b1eb1aca0..117e1c9aef 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -28,7 +28,12 @@ class ColorPickerWidget(QtWidgets.QWidget): # Color triangle color_triangle = QtColorTriangle(left_side) - alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, left_side) + alpha_slider_proxy = QtWidgets.QWidget(left_side) + alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy) + + alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) + alpha_slider_layout.setContentsMargins(5, 5, 5, 5) + alpha_slider_layout.addWidget(alpha_slider, 1) left_layout = QtWidgets.QVBoxLayout(left_side) left_layout.setContentsMargins(0, 0, 0, 0) From e40847f61ff49b1ae47bce8b471f87ec98352852 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:14:05 +0200 Subject: [PATCH 300/311] added stretched of triangle and alpha slider --- openpype/widgets/color_widgets/color_picker_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 117e1c9aef..23c06929c0 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -37,8 +37,8 @@ class ColorPickerWidget(QtWidgets.QWidget): left_layout = QtWidgets.QVBoxLayout(left_side) left_layout.setContentsMargins(0, 0, 0, 0) - left_layout.addWidget(color_triangle) - left_layout.addWidget(alpha_slider) + left_layout.addWidget(color_triangle, 1) + left_layout.addWidget(alpha_slider_proxy, 0) right_side = QtWidgets.QWidget(top_part) From 97c1a14de48b48713bee78c1248573bf0f969e7e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:14:17 +0200 Subject: [PATCH 301/311] added spacing between left and right widget --- openpype/widgets/color_widgets/color_picker_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 23c06929c0..40e20496f6 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -91,7 +91,7 @@ class ColorPickerWidget(QtWidgets.QWidget): right_layout.setRowStretch(row, 1) layout = QtWidgets.QHBoxLayout(self) - layout.setSpacing(5) + layout.setSpacing(20) layout.addWidget(left_side, 1) layout.addWidget(right_side, 0) From 0579cb2983d32205deed83d778bd4fbbac3c44e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 15:33:43 +0200 Subject: [PATCH 302/311] nicer alpha slider --- .../widgets/color_widgets/color_inputs.py | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index ddb832c655..d3c801bc0f 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -1,6 +1,8 @@ import re from Qt import QtWidgets, QtCore, QtGui +from .color_view import draw_checkerboard_tile + slide_style = """ QSlider::groove:horizontal { @@ -40,7 +42,14 @@ class AlphaSlider(QtWidgets.QSlider): self.setMaximum(255) self.setValue(255) - self.setStyleSheet(slide_style) + self._checkerboard = None + + def checkerboard(self): + if self._checkerboard is None: + self._checkerboard = draw_checkerboard_tile( + 3, QtGui.QColor(255, 255, 255), QtGui.QColor(27, 27, 27) + ) + return self._checkerboard def mousePressEvent(self, event): self._mouse_clicked = True @@ -64,6 +73,85 @@ class AlphaSlider(QtWidgets.QSlider): self._mouse_clicked = True super(AlphaSlider, self).mouseReleaseEvent(event) + def paintEvent(self, event): + painter = QtGui.QPainter(self) + opt = QtWidgets.QStyleOptionSlider() + self.initStyleOption(opt) + + painter.fillRect(event.rect(), QtCore.Qt.transparent) + + painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) + rect = self.style().subControlRect( + QtWidgets.QStyle.CC_Slider, + opt, + QtWidgets.QStyle.SC_SliderGroove, + self + ) + final_height = 9 + offset_top = 0 + if rect.height() > final_height: + offset_top = int((rect.height() - final_height) / 2) + rect = QtCore.QRect( + rect.x(), + offset_top, + rect.width(), + final_height + ) + + pix_rect = QtCore.QRect(event.rect()) + pix_rect.setX(rect.x()) + pix_rect.setWidth(rect.width() - (2 * rect.x())) + pix = QtGui.QPixmap(pix_rect.width(), pix_rect.height()) + pix_painter = QtGui.QPainter(pix) + pix_painter.drawTiledPixmap(pix_rect, self.checkerboard()) + gradient = QtGui.QLinearGradient(rect.topLeft(), rect.bottomRight()) + gradient.setColorAt(0, QtCore.Qt.transparent) + gradient.setColorAt(1, QtCore.Qt.white) + pix_painter.fillRect(pix_rect, gradient) + pix_painter.end() + + brush = QtGui.QBrush(pix) + painter.save() + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(brush) + ratio = rect.height() / 2 + painter.drawRoundedRect(rect, ratio, ratio) + painter.restore() + + _handle_rect = self.style().subControlRect( + QtWidgets.QStyle.CC_Slider, + opt, + QtWidgets.QStyle.SC_SliderHandle, + self + ) + + handle_rect = QtCore.QRect(rect) + if offset_top > 1: + height = handle_rect.height() + handle_rect.setY(handle_rect.y() - 1) + handle_rect.setHeight(height + 2) + handle_rect.setX(_handle_rect.x()) + handle_rect.setWidth(handle_rect.height()) + + painter.save() + + gradient = QtGui.QRadialGradient() + radius = handle_rect.height() / 2 + center_x = handle_rect.width() / 2 + handle_rect.x() + center_y = handle_rect.height() + gradient.setCenter(center_x, center_y) + gradient.setCenterRadius(radius) + gradient.setFocalPoint(center_x, center_y) + + gradient.setColorAt(0.9, QtGui.QColor(127, 127, 127)) + gradient.setColorAt(1, QtCore.Qt.transparent) + + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(gradient) + painter.drawEllipse(handle_rect) + + painter.restore() + class AlphaInputs(QtWidgets.QWidget): alpha_changed = QtCore.Signal(int) From dbf70d34ea039a31ebd3b22dfbdc90d48a8ed61c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 15:38:03 +0200 Subject: [PATCH 303/311] darker white --- openpype/widgets/color_widgets/color_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index d3c801bc0f..eda8c618f1 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -47,7 +47,7 @@ class AlphaSlider(QtWidgets.QSlider): def checkerboard(self): if self._checkerboard is None: self._checkerboard = draw_checkerboard_tile( - 3, QtGui.QColor(255, 255, 255), QtGui.QColor(27, 27, 27) + 3, QtGui.QColor(173, 173, 173), QtGui.QColor(27, 27, 27) ) return self._checkerboard From faa6fdcd08065e9de928a68f3a82b093f3318795 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 15:39:19 +0200 Subject: [PATCH 304/311] removed unused imports --- openpype/widgets/color_widgets/color_picker_widget.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 40e20496f6..fd0f31e342 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -5,14 +5,12 @@ from .color_triangle import QtColorTriangle from .color_view import ColorViewer from .color_screen_pick import PickScreenColorWidget from .color_inputs import ( - ColorInputsWidget, AlphaSlider, AlphaInputs, HEXInputs, RGBInputs, HSLInputs, - HSVInputs, - CMYKInputs + HSVInputs ) From a32624f22bbf75d964d9b235a05a903ea5e336f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 17:10:46 +0200 Subject: [PATCH 305/311] fake blender addons from blender user scripts --- openpype/hosts/blender/api/lib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 6aa1cb46ac..e4d063a583 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -3,6 +3,7 @@ import traceback import importlib import bpy +import addon_utils def load_scripts(paths): @@ -74,6 +75,23 @@ def load_scripts(paths): for mod in bpy.utils.modules_from_path(path, loaded_modules): test_register(mod) + addons_paths = [] + for base_path in paths: + addons_path = os.path.join(base_path, "addons") + if os.path.exists(addons_path): + addons_paths.append(addons_path) + + if addons_paths: + # Fake addons + origin_paths = addon_utils.paths + + def new_paths(): + paths = origin_paths() + addons_paths + return paths + + addon_utils.paths = new_paths + addon_utils.modules_refresh() + # load template (if set) if any(bpy.utils.app_template_paths()): import bl_app_template_utils From df5c4aebd8cd2adc31144eaecb3819c9561421ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 17:13:23 +0200 Subject: [PATCH 306/311] also prepend modules to sys path --- openpype/hosts/blender/api/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index e4d063a583..fe5d3f93e9 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -78,8 +78,12 @@ def load_scripts(paths): addons_paths = [] for base_path in paths: addons_path = os.path.join(base_path, "addons") - if os.path.exists(addons_path): - addons_paths.append(addons_path) + if not os.path.exists(addons_path): + continue + addons_paths.append(addons_path) + addons_module_path = os.path.join(addons_path, "modules") + if os.path.exists(addons_module_path): + bpy.utils._sys_path_ensure_prepend(addons_module_path) if addons_paths: # Fake addons From c496386b47ef126bda0a6623f0411dc4920ac79c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 19:59:54 +0200 Subject: [PATCH 307/311] ok btn has same width as cancel btn --- openpype/tools/settings/settings/color_widget.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index b84c8dd9cc..7aa3a4bba3 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -141,9 +141,18 @@ class ColorDialog(QtWidgets.QDialog): cancel_btn.clicked.connect(self.on_cancel_clicked) self.picker_widget = picker_widget + self.ok_btn = ok_btn + self.cancel_btn = cancel_btn self._result = None + def showEvent(self, event): + super(ColorDialog, self).showEvent(event) + + btns_width = max(self.ok_btn.width(), self.cancel_btn.width()) + self.ok_btn.setFixedWidth(btns_width) + self.cancel_btn.setFixedWidth(btns_width) + def on_ok_clicked(self): self._result = self.picker_widget.color() self.close() From 69ed30f15c5fd8c5f7c19c2a8e34541a56a390e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 20:23:26 +0200 Subject: [PATCH 308/311] colorpicker widget is one big grid --- .../color_widgets/color_picker_widget.py | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index fd0f31e342..27b9f8fc82 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -21,31 +21,23 @@ class ColorPickerWidget(QtWidgets.QWidget): super(ColorPickerWidget, self).__init__(parent) top_part = QtWidgets.QWidget(self) - left_side = QtWidgets.QWidget(top_part) # Color triangle - color_triangle = QtColorTriangle(left_side) + color_triangle = QtColorTriangle(self) - alpha_slider_proxy = QtWidgets.QWidget(left_side) + alpha_slider_proxy = QtWidgets.QWidget(self) alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy) alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) alpha_slider_layout.setContentsMargins(5, 5, 5, 5) alpha_slider_layout.addWidget(alpha_slider, 1) - left_layout = QtWidgets.QVBoxLayout(left_side) - left_layout.setContentsMargins(0, 0, 0, 0) - left_layout.addWidget(color_triangle, 1) - left_layout.addWidget(alpha_slider_proxy, 0) - - right_side = QtWidgets.QWidget(top_part) - # Eye picked widget pick_widget = PickScreenColorWidget() pick_widget.setMaximumHeight(50) # Color pick button - btn_pick_color = QtWidgets.QPushButton(right_side) + btn_pick_color = QtWidgets.QPushButton(self) icon_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "eyedropper.png" @@ -54,44 +46,52 @@ class ColorPickerWidget(QtWidgets.QWidget): btn_pick_color.setToolTip("Pick a color") # Color preview - color_view = ColorViewer(right_side) + color_view = ColorViewer(self) color_view.setMaximumHeight(50) - row = 0 - right_layout = QtWidgets.QGridLayout(right_side) - right_layout.setContentsMargins(0, 0, 0, 0) - right_layout.addWidget(btn_pick_color, row, 0) - right_layout.addWidget(color_view, row, 1) + alpha_inputs = AlphaInputs(self) color_inputs_color = QtGui.QColor() col_inputs_by_label = [ - ("HEX", HEXInputs(color_inputs_color, right_side)), - ("RGB", RGBInputs(color_inputs_color, right_side)), - ("HSL", HSLInputs(color_inputs_color, right_side)), - ("HSV", HSVInputs(color_inputs_color, right_side)) + ("HEX", HEXInputs(color_inputs_color, self)), + ("RGB", RGBInputs(color_inputs_color, self)), + ("HSL", HSLInputs(color_inputs_color, self)), + ("HSV", HSVInputs(color_inputs_color, self)) ] + + layout = QtWidgets.QGridLayout(self) + empty_col = 1 + label_col = empty_col + 1 + input_col = label_col + 1 + empty_widget = QtWidgets.QWidget(self) + empty_widget.setFixedWidth(10) + layout.addWidget(empty_widget, 0, empty_col) + + row = 0 + layout.addWidget(btn_pick_color, row, label_col) + layout.addWidget(color_view, row, input_col) + row += 1 + color_input_fields = [] for label, input_field in col_inputs_by_label: - row += 1 - right_layout.addWidget(QtWidgets.QLabel(label, right_side), row, 0) - right_layout.addWidget(input_field, row, 1) + layout.addWidget(QtWidgets.QLabel(label, self), row, label_col) + layout.addWidget(input_field, row, input_col) input_field.value_changed.connect( self._on_color_input_value_change ) color_input_fields.append(input_field) + row += 1 + layout.addWidget(color_triangle, 0, 0, row + 1, 1) + layout.setRowStretch(row, 1) row += 1 - alpha_inputs = AlphaInputs(right_side) - right_layout.addWidget(QtWidgets.QLabel("Alpha", right_side), row, 0) - right_layout.addWidget(alpha_inputs, row, 1) + layout.addWidget(alpha_slider_proxy, row, 0) + + layout.addWidget(QtWidgets.QLabel("Alpha", self), row, label_col) + layout.addWidget(alpha_inputs, row, input_col) row += 1 - right_layout.setRowStretch(row, 1) - - layout = QtWidgets.QHBoxLayout(self) - layout.setSpacing(20) - layout.addWidget(left_side, 1) - layout.addWidget(right_side, 0) + layout.setRowStretch(row, 1) color_view.set_color(color_triangle.cur_color) From c77094aeb6bd5666d791c4486834123f06780362 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 20:52:44 +0200 Subject: [PATCH 309/311] nicer view of color in setting ui --- .../tools/settings/settings/color_widget.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index 7aa3a4bba3..fa0cd2c989 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -80,7 +80,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - self._checkerboard = draw_checkerboard_tile() + self._checkerboard = draw_checkerboard_tile(self.height() / 4) return self._checkerboard def color(self): @@ -101,15 +101,21 @@ class ColorViewer(QtWidgets.QWidget): def paintEvent(self, event): rect = event.rect() - # Paint everything to pixmap as it has transparency - pix = QtGui.QPixmap(rect.width(), rect.height()) - pix_painter = QtGui.QPainter(pix) - pix_painter.drawTiledPixmap(rect, self.checkerboard()) - pix_painter.fillRect(rect, self.actual_color) - pix_painter.end() - painter = QtGui.QPainter(self) - painter.drawPixmap(rect, pix) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + + radius = rect.height() / 2 + rounded_rect = QtGui.QPainterPath() + rounded_rect.addRoundedRect(QtCore.QRectF(rect), radius, radius) + painter.setClipPath(rounded_rect) + + pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67)) + pen.setWidth(1) + painter.setPen(pen) + painter.drawTiledPixmap(rect, self.checkerboard()) + painter.fillRect(rect, self.actual_color) + painter.drawPath(rounded_rect) + painter.end() From c4a7a3ac5013476cab7079c31b6d614422551bea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 21:16:46 +0200 Subject: [PATCH 310/311] added border to color viewer in the dialog --- openpype/widgets/color_widgets/color_view.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py index d6d7f0a666..8644281a1d 100644 --- a/openpype/widgets/color_widgets/color_view.py +++ b/openpype/widgets/color_widgets/color_view.py @@ -42,7 +42,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - self._checkerboard = draw_checkerboard_tile() + self._checkerboard = draw_checkerboard_tile(4) return self._checkerboard def color(self): @@ -70,15 +70,14 @@ class ColorViewer(QtWidgets.QWidget): self.update() def paintEvent(self, event): - rect = event.rect() - - # Paint everything to pixmap as it has transparency - pix = QtGui.QPixmap(rect.width(), rect.height()) - pix_painter = QtGui.QPainter(pix) - pix_painter.drawTiledPixmap(rect, self.checkerboard()) - pix_painter.fillRect(rect, self.actual_color) - pix_painter.end() + clip_rect = event.rect() + rect = clip_rect.adjusted(0, 0, -1, -1) painter = QtGui.QPainter(self) - painter.drawPixmap(rect, pix) + painter.setClipRect(clip_rect) + painter.drawTiledPixmap(rect, self.checkerboard()) + painter.setBrush(self.actual_color) + pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67)) + painter.setPen(pen) + painter.drawRect(rect) painter.end() From 0d40c89f471f6abccfbe6ae58667bb5304086422 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 21:18:16 +0200 Subject: [PATCH 311/311] removed unused variable --- openpype/widgets/color_widgets/color_picker_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 27b9f8fc82..81ec1f87aa 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -20,8 +20,6 @@ class ColorPickerWidget(QtWidgets.QWidget): def __init__(self, color=None, parent=None): super(ColorPickerWidget, self).__init__(parent) - top_part = QtWidgets.QWidget(self) - # Color triangle color_triangle = QtColorTriangle(self)