From 56153c1d6c4e0b85389f7c5ce8f8d2dbf6ab9459 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 19:56:32 +0200 Subject: [PATCH] first commit of added docstrings and comments --- .../project_manager/constants.py | 9 ++ .../project_manager/delegates.py | 46 ++++++++ .../project_manager/project_manager/model.py | 110 +++++++++++++++++- 3 files changed, 161 insertions(+), 4 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/constants.py b/openpype/tools/project_manager/project_manager/constants.py index 6fb4b991ed..67dea79e59 100644 --- a/openpype/tools/project_manager/project_manager/constants.py +++ b/openpype/tools/project_manager/project_manager/constants.py @@ -2,12 +2,21 @@ import re from Qt import QtCore +# Item identifier (unique ID - uuid4 is used) IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1 +# Item has duplicated name (Asset and Task items) DUPLICATED_ROLE = QtCore.Qt.UserRole + 2 +# It is possible to move and rename items +# - that is disabled if e.g. Asset has published content HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3 +# Item is marked for deletion +# - item will be deleted after hitting save REMOVED_ROLE = QtCore.Qt.UserRole + 4 +# Item type in string ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5 +# Item has opened editor (per column) EDITOR_OPENED_ROLE = QtCore.Qt.UserRole + 6 +# Allowed symbols for any name 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 51edff028f..0e8dd38e68 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -8,6 +8,10 @@ from .multiselection_combobox import MultiSelectionComboBox class ResizeEditorDelegate(QtWidgets.QStyledItemDelegate): + """Implementation of private method from QStyledItemDelegate. + + Force editor to resize into item size. + """ @staticmethod def _q_smart_min_size(editor): min_size_hint = editor.minimumSizeHint() @@ -67,6 +71,16 @@ class ResizeEditorDelegate(QtWidgets.QStyledItemDelegate): class NumberDelegate(QtWidgets.QStyledItemDelegate): + """Delegate for number attributes. + + Editor correspond passed arguments. + + Args: + minimum(int, float): Minimum possible value. + maximum(int, float): Maximum possible value. + decimals(int): How many decimal points can be used. Float will be used + as value if is higher than 0. + """ def __init__(self, minimum, maximum, decimals, *args, **kwargs): super(NumberDelegate, self).__init__(*args, **kwargs) self.minimum = minimum @@ -80,10 +94,13 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): editor = QtWidgets.QSpinBox(parent) editor.setObjectName("NumberEditor") + # Set min/max editor.setMinimum(self.minimum) editor.setMaximum(self.maximum) + # Hide spinbox buttons editor.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + # Try to set value from item value = index.data(QtCore.Qt.EditRole) if value is not None: try: @@ -98,6 +115,8 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): class NameDelegate(QtWidgets.QStyledItemDelegate): + """Specific delegate for "name" key.""" + def createEditor(self, parent, option, index): editor = NameTextEdit(parent) editor.setObjectName("NameEditor") @@ -108,11 +127,26 @@ class NameDelegate(QtWidgets.QStyledItemDelegate): class TypeDelegate(QtWidgets.QStyledItemDelegate): + """Specific delegate for "type" key. + + It is expected that will be used only for TaskItem which has modifiable + type. Type values are defined with cached project document. + + Args: + project_doc_cache(ProjectDocCache): Project cache shared across all + delegates (kind of a struct pointer). + """ + 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 is using filtrable combobox. + + Editor should not be possible to create new items or set values that + are not in this method. + """ editor = FilterComboBox(parent) editor.setObjectName("TypeEditor") editor.style().polish(editor) @@ -136,6 +170,18 @@ class TypeDelegate(QtWidgets.QStyledItemDelegate): class ToolsDelegate(QtWidgets.QStyledItemDelegate): + """Specific delegate for "tools_env" key. + + Exected that editor will be used only on AssetItem which is only item that + can have `tools_env` (except project). + + Delegate requires tools cache which is shared across all ToolsDelegate + objects. + + Args: + tools_cache (ToolsCache): Possible values of tools. + """ + def __init__(self, tools_cache, *args, **kwargs): self._tools_cache = tools_cache super(ToolsDelegate, self).__init__(*args, **kwargs) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5000729adf..026f6e0228 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -21,7 +21,11 @@ from Qt import QtCore, QtGui class ProjectModel(QtGui.QStandardItemModel): - project_changed = QtCore.Signal() + """Load possible projects to modify from MongoDB. + + Mongo collection must contain project document with "type" "project" and + matching "name" value with name of collection. + """ def __init__(self, dbcon, *args, **kwargs): self.dbcon = dbcon @@ -31,6 +35,7 @@ class ProjectModel(QtGui.QStandardItemModel): super(ProjectModel, self).__init__(*args, **kwargs) def refresh(self): + """Reload projects.""" self.dbcon.Session["AVALON_PROJECT"] = None project_items = [] @@ -63,6 +68,12 @@ class ProjectModel(QtGui.QStandardItemModel): class HierarchySelectionModel(QtCore.QItemSelectionModel): + """Selection model with defined allowed multiselection columns. + + This model allows to select multiple rows and enter one of their + editors to edit value of all selected rows. + """ + def __init__(self, multiselection_columns, *args, **kwargs): super(HierarchySelectionModel, self).__init__(*args, **kwargs) self.multiselection_columns = multiselection_columns @@ -78,6 +89,21 @@ class HierarchySelectionModel(QtCore.QItemSelectionModel): class HierarchyModel(QtCore.QAbstractItemModel): + """Main model for hierarchy modification and value changes. + + Main part of ProjectManager. + + Model should be able to load existing entities, create new, handle their + validations like name duplication and validate if is possible to save it's + data. + + Args: + dbcon (AvalonMongoDB): Connection to MongoDB with set AVALON_PROJECT in + it's Session to current project. + """ + + # Definition of all possible columns with their labels in default order + # - order is important as column names are used as keys for column indexes _columns_def = [ ("name", "Name"), ("type", "Type"), @@ -93,6 +119,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): ("pixelAspect", "Pixel aspect"), ("tools_env", "Tools") ] + # Columns allowing multiselection in edit mode + # - gives ability to set all of keys below on multiple items at once multiselection_columns = { "frameStart", "frameEnd", @@ -141,13 +169,19 @@ class HierarchyModel(QtCore.QAbstractItemModel): return self._items_by_id def _reset_root_item(self): + """Removes all previous content related to model.""" self._root_item = RootItem(self) def refresh_project(self): + """Reload project data and discard unsaved changes.""" self.set_project(self._current_project, True) @property def project_item(self): + """Access to current project item. + + Model can have 0-1 ProjectItems at once. + """ output = None for row in range(self._root_item.rowCount()): item = self._root_item.child(row) @@ -157,25 +191,41 @@ class HierarchyModel(QtCore.QAbstractItemModel): return output def set_project(self, project_name, force=False): + """Change project and discard unsaved changes. + + Args: + project_name(str): New project name. Or None if just clearing + content. + force(bool): Force to change project even if project name is same + as current project. + """ if self._current_project == project_name and not force: return + # Clear all current content self.clear() self._current_project = project_name + + # Skip if project is None if not project_name: return + # Find project'd document project_doc = self.dbcon.database[project_name].find_one( {"type": "project"}, ProjectItem.query_projection ) + # Skip if project document does not exist + # - this shouldn't happen using only UI elements if not project_doc: return + # Create project item project_item = ProjectItem(project_doc) self.add_item(project_item) + # Query all assets of the project asset_docs = self.dbcon.database[project_name].find( {"type": "asset"}, AssetItem.query_projection @@ -185,7 +235,8 @@ class HierarchyModel(QtCore.QAbstractItemModel): for asset_doc in asset_docs } - # Prepare booleans if asset item can be modified (name or hierarchy) + # Check if asset have published content and prepare booleans + # if asset item can be modified (name and hierarchy change) # - the same must be applied to all it's parents asset_ids = list(asset_docs_by_id.keys()) result = [] @@ -214,6 +265,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): count = item["count"] asset_modifiable[asset_id] = count < 1 + # Store assets by their visual parent to be able create their hierarchy asset_docs_by_parent_id = collections.defaultdict(list) for asset_doc in asset_docs_by_id.values(): parent_id = asset_doc["data"].get("visualParent") @@ -282,9 +334,11 @@ class HierarchyModel(QtCore.QAbstractItemModel): self.add_items(task_items, asset_item) + # Emit that project was successfully changed self.project_changed.emit() def rowCount(self, parent=None): + """Number of rows for passed parent.""" if parent is None or not parent.isValid(): parent_item = self._root_item else: @@ -292,9 +346,15 @@ class HierarchyModel(QtCore.QAbstractItemModel): return parent_item.rowCount() def columnCount(self, *args, **kwargs): + """Number of columns is static for this model.""" return self.columns_len def data(self, index, role): + """Access data for passed index and it's role. + + Model is using principles implemented in BaseItem so converts passed + index column into key and ask item to return value for passed role. + """ if not index.isValid(): return None @@ -305,18 +365,24 @@ class HierarchyModel(QtCore.QAbstractItemModel): return item.data(role, key) def setData(self, index, value, role=QtCore.Qt.EditRole): + """Store data to passed index under role. + + Pass values to corresponding item and behave by it's result. + """ if not index.isValid(): return False item = index.internalPointer() column = index.column() key = self.columns[column] + # Capture asset name changes for duplcated asset names validation. if ( key == "name" and role in (QtCore.Qt.EditRole, QtCore.Qt.DisplayRole) ): self._rename_asset(item, value) + # Pass values to item and by result emi dataChanged signal or not result = item.setData(value, role, key) if result: self.dataChanged.emit(index, index, [role]) @@ -324,6 +390,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): return result def headerData(self, section, orientation, role): + """Header labels.""" if role == QtCore.Qt.DisplayRole: if section < self.columnCount(): return self.column_labels[section] @@ -333,6 +400,7 @@ class HierarchyModel(QtCore.QAbstractItemModel): ) def flags(self, index): + """Index flags are defined by corresponding item.""" item = index.internalPointer() if item is None: return QtCore.Qt.NoItemFlags @@ -341,6 +409,11 @@ class HierarchyModel(QtCore.QAbstractItemModel): return item.flags(key) def parent(self, index=None): + """Parent for passed index as QModelIndex. + + Args: + index(QModelIndex): Parent index. Root item is used if not passed. + """ if not index.isValid(): return QtCore.QModelIndex() @@ -354,7 +427,13 @@ class HierarchyModel(QtCore.QAbstractItemModel): return self.createIndex(parent_item.row(), 0, parent_item) def index(self, row, column, parent=None): - """Return index for row/column under parent""" + """Return index for row/column under parent. + + Args: + row(int): Row number. + column(int): Column number. + parent(QModelIndex): Parent index. Root item is used if not passed. + """ parent_item = None if parent is not None and parent.isValid(): parent_item = parent.internalPointer() @@ -362,11 +441,31 @@ class HierarchyModel(QtCore.QAbstractItemModel): return self.index_from_item(row, column, parent_item) def index_for_item(self, item, column=0): + """Index for passet item. + + This is for cases that index operations are required on specific item. + + Args: + item(BaseItem): Item from model that will be converted to + corresponding QModelIndex. + column(int): Which column will be part of returned index. By + default is used column 0. + """ return self.index_from_item( item.row(), column, item.parent() ) def index_from_item(self, row, column, parent=None): + """Index for passed row, column and parent item. + + Same implementation as `index` method but "parent" is one of + BaseItem objects instead of QModelIndex. + + Args: + row(int): Row number. + column(int): Column number. + parent(BaseItem): Parent item. Root item is used if not passed. + """ if parent is None: parent = self._root_item @@ -377,15 +476,18 @@ class HierarchyModel(QtCore.QAbstractItemModel): return QtCore.QModelIndex() def add_new_asset(self, source_index): + """Method to create new asset item in hierarchy.""" item_id = source_index.data(IDENTIFIER_ROLE) item = self.items_by_id[item_id] if isinstance(item, (RootItem, ProjectItem)): name = "ep" new_row = None - else: + elif isinstance(item, AssetItem): name = None new_row = item.rowCount() + else: + return asset_data = {} if name: