From 0f1e7fcee9b3d5111a9e374697f6d55fa14bdda4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 12 Jun 2019 22:18:56 +0800 Subject: [PATCH 01/28] Cosmetic --- avalon/tools/cbloader/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index d559365ec..dca2a538b 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -109,14 +109,14 @@ def refresh(self): "parent": self._asset_id}): last_version = io.find_one({"type": "version", - "parent": subset['_id']}, + "parent": subset["_id"]}, sort=[("name", -1)]) if not last_version: # No published version for the subset continue data = subset.copy() - data['subset'] = data['name'] + data["subset"] = data["name"] node = Node() node.update(data) @@ -146,7 +146,7 @@ def data(self, index, role): # Add icon to subset column if index.column() == 0: - return self._icons['subset'] + return self._icons["subset"] # Add icon to family column if index.column() == 1: From 771875a972155f290f0948ca818503bf40b75de0 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 12 Jun 2019 22:33:28 +0800 Subject: [PATCH 02/28] Implement 'subset grouping' in Loader GUI --- avalon/tools/cbloader/app.py | 3 ++ avalon/tools/cbloader/model.py | 55 ++++++++++++++++++++++++++------ avalon/tools/cbloader/widgets.py | 2 +- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index e46456236..27d7faaeb 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -198,6 +198,9 @@ def _versionschanged(self): rows = selection.selectedRows(column=active.column()) if active in rows: node = active.data(subsets.model.NodeRole) + if node.get("isGroup"): + return + version = node['version_document']['_id'] self.data['model']['version'].set_version(version) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index dca2a538b..134cbc0dd 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -25,7 +25,11 @@ def __init__(self, parent=None): super(SubsetsModel, self).__init__(parent=parent) self._asset_id = None self._icons = {"subset": qta.icon("fa.file-o", - color=style.colors.default)} + color=style.colors.default), + "group": qta.icon("fa.object-group", + color=style.colors.default), + "grouped": qta.icon("fa.file", + color=style.colors.default)} def set_asset(self, asset_id): self._asset_id = asset_id @@ -104,9 +108,24 @@ def refresh(self): self.endResetModel() return - row = 0 - for subset in io.find({"type": "subset", - "parent": self._asset_id}): + filter = {"type": "subset", "parent": self._asset_id} + + # Collect subset groups + group_nodes = dict() + for group_name in io.distinct("data.subsetGroup", filter): + group = Node() + data = { + "subset": group_name, + "isGroup": True, + } + group.update(data) + group_nodes[group_name] = {"node": group, + "childRow": 0} + self.add_child(group) + + # Process subsets + row = len(group_nodes) + for subset in io.find(filter): last_version = io.find_one({"type": "version", "parent": subset["_id"]}, @@ -118,17 +137,29 @@ def refresh(self): data = subset.copy() data["subset"] = data["name"] + group_name = subset["data"].get("subsetGroup") + if group_name: + group = group_nodes[group_name] + parent = group["node"] + parent_index = self.createIndex(0, 0, parent) + row_ = group["childRow"] + group["childRow"] += 1 + data["isGrouped"] = True + else: + parent = None + parent_index = QtCore.QModelIndex() + row_ = row + row += 1 + node = Node() node.update(data) - self.add_child(node) + self.add_child(node, parent=parent) # Set the version information - index = self.index(row, 0, parent=QtCore.QModelIndex()) + index = self.index(row_, 0, parent=parent_index) self.set_version(index, last_version) - row += 1 - self.endResetModel() def data(self, index, role): @@ -146,7 +177,13 @@ def data(self, index, role): # Add icon to subset column if index.column() == 0: - return self._icons["subset"] + node = index.internalPointer() + if node.get("isGroup"): + return self._icons["group"] + elif node.get("isGrouped"): + return self._icons["grouped"] + else: + return self._icons["subset"] # Add icon to family column if index.column() == 1: diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 35f0233e0..30739f0ab 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -31,7 +31,7 @@ def __init__(self, parent=None): filter.setPlaceholderText("Filter subsets..") view = QtWidgets.QTreeView() - view.setIndentation(5) + view.setIndentation(20) view.setStyleSheet(""" QTreeView::item{ padding: 5px 1px; From 4e438faf70c452a68f36f39b62a33c9a0d1fc517 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 12 Jun 2019 22:34:32 +0800 Subject: [PATCH 03/28] Make group items always on top of the view --- avalon/tools/cbloader/model.py | 18 ++++++++++++++++++ avalon/tools/cbloader/widgets.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index 134cbc0dd..4e9c9e11e 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -21,9 +21,12 @@ class SubsetsModel(TreeModel): "handles", "step"] + SortRole = QtCore.Qt.UserRole + 2 + def __init__(self, parent=None): super(SubsetsModel, self).__init__(parent=parent) self._asset_id = None + self._sorter = None self._icons = {"subset": qta.icon("fa.file-o", color=style.colors.default), "group": qta.icon("fa.object-group", @@ -31,6 +34,9 @@ def __init__(self, parent=None): "grouped": qta.icon("fa.file", color=style.colors.default)} + def set_sorter(self, sorter): + self._sorter = sorter + def set_asset(self, asset_id): self._asset_id = asset_id self.refresh() @@ -190,6 +196,18 @@ def data(self, index, role): node = index.internalPointer() return node.get("familyIcon", None) + if role == self.SortRole: + node = index.internalPointer() + order = self._sorter.sortOrder() + column = self.COLUMNS[self._sorter.sortColumn()] + # This would make group items always be on top + if node.get("isGroup"): + prefix = "1" if order else "0" + else: + prefix = "0" if order else "1" + + return prefix + str(node.get(column)) + return super(SubsetsModel, self).data(index, role) def flags(self, index): diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 30739f0ab..0d808f39f 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -88,6 +88,8 @@ def __init__(self, parent=None): self.filter.textChanged.connect(self.proxy.setFilterRegExp) + self.family_proxy.setSortRole(self.model.SortRole) + self.model.set_sorter(self.family_proxy) self.model.refresh() # Expose this from the widget as a method From 2a3876b872aa0439f7f3076d52cb42bab9ed8843 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 12 Jun 2019 22:42:18 +0800 Subject: [PATCH 04/28] Disable context menu on group --- avalon/tools/cbloader/widgets.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 0d808f39f..bcbdf2365 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -101,11 +101,15 @@ def on_context_menu(self, point): if not point_index.isValid(): return + node = point_index.data(self.model.NodeRole) + if node.get("isGroup"): + return + # Get all representation->loader combinations available for the # index under the cursor, so we can list the user the options. available_loaders = api.discover(api.Loader) loaders = list() - node = point_index.data(self.model.NodeRole) + version_id = node['version_document']['_id'] representations = io.find({"type": "representation", "parent": version_id}) @@ -190,6 +194,9 @@ def sorter(value): # Trigger for row in rows: node = row.data(self.model.NodeRole) + if node.get("isGroup"): + continue + version_id = node["version_document"]["_id"] representation = io.find_one({"type": "representation", "name": representation_name, From b7a731fe9ab45350544d6bdfd12042be62e06f83 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 12 Jun 2019 23:10:40 +0800 Subject: [PATCH 05/28] Disable version delegate on group --- avalon/tools/cbloader/delegates.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/avalon/tools/cbloader/delegates.py b/avalon/tools/cbloader/delegates.py index 59d7fbbba..7b9772e45 100644 --- a/avalon/tools/cbloader/delegates.py +++ b/avalon/tools/cbloader/delegates.py @@ -120,6 +120,10 @@ def displayText(self, value, locale): return self._format_version(value) def createEditor(self, parent, option, index): + node = index.data(SubsetsModel.NodeRole) + if node.get("isGroup"): + return + editor = QtWidgets.QComboBox(parent) def commit_data(): From e641d4d865983608e6482799a9544a4d4f830e78 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 12 Jun 2019 23:16:09 +0800 Subject: [PATCH 06/28] Fix to reset version info on group --- avalon/tools/cbloader/app.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index 27d7faaeb..e96de0fc8 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -198,10 +198,8 @@ def _versionschanged(self): rows = selection.selectedRows(column=active.column()) if active in rows: node = active.data(subsets.model.NodeRole) - if node.get("isGroup"): - return - - version = node['version_document']['_id'] + if not node.get("isGroup"): + version = node['version_document']['_id'] self.data['model']['version'].set_version(version) From 42a874ab0a473ff640387da8e3c37479973134eb Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 13 Jun 2019 00:00:14 +0800 Subject: [PATCH 07/28] Keep group in view when filtering subsets --- avalon/tools/cbloader/model.py | 17 +++++++++++++++++ avalon/tools/cbloader/widgets.py | 8 ++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index 4e9c9e11e..f890ed464 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -220,6 +220,23 @@ def flags(self, index): return flags +class SubsetFilterProxyModel(QtCore.QSortFilterProxyModel): + + def filterAcceptsRow(self, row, parent): + + model = self.sourceModel() + index = model.index(row, + self.filterKeyColumn(), + parent) + node = index.internalPointer() + if node.get("isGroup"): + # Keep group in view + return True + else: + return super(SubsetFilterProxyModel, + self).filterAcceptsRow(row, parent) + + class FamiliesFilterProxyModel(QtCore.QSortFilterProxyModel): """Filters to specified families""" diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index bcbdf2365..09bbf0277 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -8,7 +8,11 @@ from ... import api from ... import pipeline -from .model import SubsetsModel, FamiliesFilterProxyModel +from .model import ( + SubsetsModel, + SubsetFilterProxyModel, + FamiliesFilterProxyModel, +) from .delegates import PrettyTimeDelegate, VersionDelegate from . import lib @@ -23,7 +27,7 @@ def __init__(self, parent=None): super(SubsetWidget, self).__init__(parent=parent) model = SubsetsModel() - proxy = QtCore.QSortFilterProxyModel() + proxy = SubsetFilterProxyModel() family_proxy = FamiliesFilterProxyModel() family_proxy.setSourceModel(proxy) From 1f7b122925077fdefc39b93e0668800b34e31ace Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 13 Jun 2019 20:26:18 +0800 Subject: [PATCH 08/28] Improved group item ordering and customizable icon --- avalon/tools/cbloader/model.py | 58 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index f890ed464..08d5b0802 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -28,11 +28,11 @@ def __init__(self, parent=None): self._asset_id = None self._sorter = None self._icons = {"subset": qta.icon("fa.file-o", - color=style.colors.default), - "group": qta.icon("fa.object-group", - color=style.colors.default), - "grouped": qta.icon("fa.file", - color=style.colors.default)} + color=style.colors.default)} + # Subset group item's default icon and order + self._default_group = {"icon": qta.icon("fa.object-group", + color=style.colors.default), + "order": 0} def set_sorter(self, sorter): self._sorter = sorter @@ -116,12 +116,39 @@ def refresh(self): filter = {"type": "subset", "parent": self._asset_id} + # Get pre-defined group name and apperance from project config + group_configs = io.find_one( + {"type": "project"}, {"config.groups": 1} + )["config"].get("groups") or [] + + # Build pre-defined group configs + predefineds = dict() + sort_orders = set([0]) # default order zero included + for config in group_configs: + name = config["name"] + icon = "fa." + config.get("icon", "object-group") + color = config.get("color", style.colors.default) + order = float(config.get("order")) + + predefineds[name] = {"icon": qta.icon(icon, color=color), + "order": order} + sort_orders.add(order) + + # For ensuring positive order + order_offset = abs(min(sort_orders)) + 1 + # For computing complement number for inverse order + order_cap = max(sort_orders) + order_offset + # Collect subset groups group_nodes = dict() for group_name in io.distinct("data.subsetGroup", filter): group = Node() + config = predefineds.get(group_name, self._default_group) data = { "subset": group_name, + "icon": config["icon"], + "fixAscending": config["order"] + order_offset, + "fixDescending": order_cap - config["order"] + order_offset, "isGroup": True, } group.update(data) @@ -150,7 +177,6 @@ def refresh(self): parent_index = self.createIndex(0, 0, parent) row_ = group["childRow"] group["childRow"] += 1 - data["isGrouped"] = True else: parent = None parent_index = QtCore.QModelIndex() @@ -185,9 +211,7 @@ def data(self, index, role): if index.column() == 0: node = index.internalPointer() if node.get("isGroup"): - return self._icons["group"] - elif node.get("isGrouped"): - return self._icons["grouped"] + return node["icon"] else: return self._icons["subset"] @@ -198,15 +222,21 @@ def data(self, index, role): if role == self.SortRole: node = index.internalPointer() - order = self._sorter.sortOrder() + descending = self._sorter.sortOrder() column = self.COLUMNS[self._sorter.sortColumn()] - # This would make group items always be on top + # This would make group items always be on top, and always in + # descending order. if node.get("isGroup"): - prefix = "1" if order else "0" + if descending: + order = ".%010.4f" % node["fixDescending"] + else: + order = ".+%010.4f" % node["fixAscending"] else: - prefix = "0" if order else "1" + # The `v` is for prefixing version number, so they won't + # ordering with group items which were using digit to sort. + order = ".+v" + str(node[column]) - return prefix + str(node.get(column)) + return order return super(SubsetsModel, self).data(index, role) From 293ae9df42a7dd9160b4dfd4cd4bed5a3648abb6 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 14 Jun 2019 18:03:56 +0800 Subject: [PATCH 09/28] Re-implement groups ordering --- avalon/tools/cbloader/model.py | 66 ++++++++++++++++++++------------ avalon/tools/cbloader/widgets.py | 2 - 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index 08d5b0802..ea5a41f2d 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -21,7 +21,8 @@ class SubsetsModel(TreeModel): "handles", "step"] - SortRole = QtCore.Qt.UserRole + 2 + SortAscendingRole = QtCore.Qt.UserRole + 2 + sortDescendingRole = QtCore.Qt.UserRole + 3 def __init__(self, parent=None): super(SubsetsModel, self).__init__(parent=parent) @@ -34,9 +35,6 @@ def __init__(self, parent=None): color=style.colors.default), "order": 0} - def set_sorter(self, sorter): - self._sorter = sorter - def set_asset(self, asset_id): self._asset_id = asset_id self.refresh() @@ -123,7 +121,7 @@ def refresh(self): # Build pre-defined group configs predefineds = dict() - sort_orders = set([0]) # default order zero included + _orders = set([0]) # default order zero included for config in group_configs: name = config["name"] icon = "fa." + config.get("icon", "object-group") @@ -132,24 +130,25 @@ def refresh(self): predefineds[name] = {"icon": qta.icon(icon, color=color), "order": order} - sort_orders.add(order) + _orders.add(order) - # For ensuring positive order - order_offset = abs(min(sort_orders)) + 1 - # For computing complement number for inverse order - order_cap = max(sort_orders) + order_offset + orders = sorted(_orders) + order_temp = "%0{}d".format(len(str(len(orders)))) # Collect subset groups group_nodes = dict() for group_name in io.distinct("data.subsetGroup", filter): group = Node() config = predefineds.get(group_name, self._default_group) + remapped_order = orders.index(config["order"]) + inverse_order = len(orders) - remapped_order data = { "subset": group_name, "icon": config["icon"], - "fixAscending": config["order"] + order_offset, - "fixDescending": order_cap - config["order"] + order_offset, "isGroup": True, + # Format orders into fixed length string for groups sorting + "order": order_temp % remapped_order, + "inverseOrder": order_temp % inverse_order, } group.update(data) group_nodes[group_name] = {"node": group, @@ -220,23 +219,29 @@ def data(self, index, role): node = index.internalPointer() return node.get("familyIcon", None) - if role == self.SortRole: + if role == self.sortDescendingRole: node = index.internalPointer() - descending = self._sorter.sortOrder() - column = self.COLUMNS[self._sorter.sortColumn()] - # This would make group items always be on top, and always in - # descending order. if node.get("isGroup"): - if descending: - order = ".%010.4f" % node["fixDescending"] - else: - order = ".+%010.4f" % node["fixAscending"] + # Ensure groups be on top when sorting by descending order + prefix = "1" + order = node["inverseOrder"] else: - # The `v` is for prefixing version number, so they won't - # ordering with group items which were using digit to sort. - order = ".+v" + str(node[column]) + prefix = "0" + order = str(super(SubsetsModel, + self).data(index, QtCore.Qt.DisplayRole)) + return prefix + order - return order + if role == self.SortAscendingRole: + node = index.internalPointer() + if node.get("isGroup"): + # Ensure groups be on top when sorting by ascending order + prefix = "0" + order = node["order"] + else: + prefix = "1" + order = str(super(SubsetsModel, + self).data(index, QtCore.Qt.DisplayRole)) + return prefix + order return super(SubsetsModel, self).data(index, role) @@ -304,3 +309,14 @@ def filterAcceptsRow(self, row=0, parent=QtCore.QModelIndex()): # We want to keep the families which are not in the list return family in self._families + + def sort(self, column, order): + proxy = self.sourceModel() + model = proxy.sourceModel() + # We need to know the sorting direction for pinning groups on top + if order == QtCore.Qt.AscendingOrder: + self.setSortRole(model.SortAscendingRole) + else: + self.setSortRole(model.sortDescendingRole) + + super(FamiliesFilterProxyModel, self).sort(column, order) diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 09bbf0277..9956765b1 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -92,8 +92,6 @@ def __init__(self, parent=None): self.filter.textChanged.connect(self.proxy.setFilterRegExp) - self.family_proxy.setSortRole(self.model.SortRole) - self.model.set_sorter(self.family_proxy) self.model.refresh() # Expose this from the widget as a method From 04193ece55236f67b90f75478c25535aa9627d63 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 14 Jun 2019 20:19:43 +0800 Subject: [PATCH 10/28] Add property `groups` into project config schema --- avalon/schema/config-1.0.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/avalon/schema/config-1.0.json b/avalon/schema/config-1.0.json index 97392e9dd..5645e5f91 100644 --- a/avalon/schema/config-1.0.json +++ b/avalon/schema/config-1.0.json @@ -65,6 +65,19 @@ "required": ["name"] } }, + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "icon": {"type": "string"}, + "color": {"type": "string"}, + "order": {"type": ["integer", "number"]} + }, + "required": ["name"] + } + }, "copy": { "type": "object" } From 148749fa64da6ffe0848bec4d0f8803b27544c9f Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 17 Jun 2019 12:39:10 +0800 Subject: [PATCH 11/28] Include `groups` when saving config by `avalon --save` --- avalon/inventory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/avalon/inventory.py b/avalon/inventory.py index a735cc0a6..54bc07572 100644 --- a/avalon/inventory.py +++ b/avalon/inventory.py @@ -326,6 +326,7 @@ def _save_config_1_0(project_name, data): config["tasks"] = data.get("tasks", []) config["template"].update(data.get("template", {})) config["families"] = data.get("families", []) + config["groups"] = data.get("groups", []) schema.validate(document) From ced6ebe9b45178fe7d74392e3f10b632345dbc86 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 19 Jun 2019 16:56:24 +0800 Subject: [PATCH 12/28] Fix test --- avalon/tests/test_inventory.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/avalon/tests/test_inventory.py b/avalon/tests/test_inventory.py index 8c653d14e..bc9471d4d 100644 --- a/avalon/tests/test_inventory.py +++ b/avalon/tests/test_inventory.py @@ -137,6 +137,14 @@ def test_save(): "families": [ {"name": "avalon.model", "label": "Model", "icon": "cube"} ], + "groups": [ + { + "name": "charCaches", + "icon": "diamond", + "color": "#C4CEDC", + "order": -99 + }, + ], "copy": {} } From e67d269ee416299dae7f625cf45ba0b65b57ea39 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 3 Jul 2019 15:06:21 +0800 Subject: [PATCH 13/28] Add Ctrl+G key release event handler for manual subset grouping --- avalon/tools/cbloader/app.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index e96de0fc8..eef1b030f 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -270,6 +270,18 @@ def closeEvent(self, event): print("Good bye") return super(Window, self).closeEvent(event) + def keyReleaseEvent(self, event): + modifiers = event.modifiers() + ctrl_pressed = QtCore.Qt.ControlModifier & modifiers + + # Grouping subsets on releasing Ctrl + G + if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and + not event.isAutoRepeat()): + print("GROUP...") + event.accept() + + return super(Window, self).keyReleaseEvent(event) + def show(debug=False, parent=None, use_context=False): """Display Loader GUI From ab96a6d8c0fc94645dc45668855aa7525dc3aae9 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 3 Jul 2019 19:16:01 +0800 Subject: [PATCH 14/28] Re-implement group config collecting mechanism --- avalon/tools/cbloader/app.py | 3 +- avalon/tools/cbloader/lib.py | 81 +++++++++++++++++++++++++++++++++- avalon/tools/cbloader/model.py | 50 +++++---------------- 3 files changed, 92 insertions(+), 42 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index eef1b030f..1c35d3f57 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -7,7 +7,7 @@ from ... import api, io, style from .. import lib -from .lib import refresh_family_config +from .lib import refresh_family_config, refresh_group_config from .widgets import SubsetWidget, VersionWidget, FamilyListWidget module = sys.modules[__name__] @@ -105,6 +105,7 @@ def __init__(self, parent=None): subsets.version_changed.connect(self.on_versionschanged) refresh_family_config() + refresh_group_config() # Defaults self.resize(1330, 700) diff --git a/avalon/tools/cbloader/lib.py b/avalon/tools/cbloader/lib.py index 897ac8931..9b3c04dbd 100644 --- a/avalon/tools/cbloader/lib.py +++ b/avalon/tools/cbloader/lib.py @@ -1,8 +1,9 @@ from ...vendor import qtawesome -from ... import io, api +from ... import io, api, style FAMILY_ICON_COLOR = "#0091B2" FAMILY_CONFIG = {} +GROUP_CONFIG = {} def get(config, name): @@ -71,3 +72,81 @@ def refresh_family_config(): FAMILY_CONFIG.update(families) return families + + +def refresh_group_config(): + """Get subset group configurations from the database + + The 'group' configuration must be stored in the project `config` field. + See schema `config-1.0.json` + + """ + + # Subset group item's default icon and order + default_group_icon = qtawesome.icon("fa.object-group", + color=style.colors.default) + default_group_config = {"icon": default_group_icon, + "order": 0} + + # Get pre-defined group name and apperance from project config + project = io.find_one({"type": "project"}, + projection={"config.groups": True}) + + assert project, "Project not found!" + group_configs = project["config"].get("groups", []) + + # Build pre-defined group configs + groups = dict() + for config in group_configs: + name = config["name"] + icon = "fa." + config.get("icon", "object-group") + color = config.get("color", style.colors.default) + order = float(config.get("order", 0)) + + groups[name] = {"icon": qtawesome.icon(icon, color=color), + "order": order} + + # Default configuration + groups["__default__"] = default_group_config + + GROUP_CONFIG.clear() + GROUP_CONFIG.update(groups) + + return groups + + +def get_active_group_config(asset_id): + """Collect all active groups from each subset + """ + predefineds = GROUP_CONFIG.copy() + default_group_config = predefineds.pop("__default__") + + _orders = set([0]) # default order zero included + for config in predefineds.values(): + _orders.add(config["order"]) + + orders = sorted(_orders) + order_temp = "%0{}d".format(len(str(len(orders)))) + + # Collect groups from subsets + active_groups = list() + + for group_name in set(io.distinct("data.subsetGroup", + {"type": "subset", "parent": asset_id})): + # Get group config + config = predefineds.get(group_name, default_group_config) + # Calculate order + remapped_order = orders.index(config["order"]) + inverse_order = len(orders) - remapped_order + + data = { + "name": group_name, + "icon": config["icon"], + # Format orders into fixed length string for groups sorting + "order": order_temp % remapped_order, + "inverseOrder": order_temp % inverse_order, + } + + active_groups.append(data) + + return active_groups diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index ea5a41f2d..4dd78b9c2 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -30,10 +30,6 @@ def __init__(self, parent=None): self._sorter = None self._icons = {"subset": qta.icon("fa.file-o", color=style.colors.default)} - # Subset group item's default icon and order - self._default_group = {"icon": qta.icon("fa.object-group", - color=style.colors.default), - "order": 0} def set_asset(self, asset_id): self._asset_id = asset_id @@ -112,49 +108,23 @@ def refresh(self): self.endResetModel() return - filter = {"type": "subset", "parent": self._asset_id} + asset_id = self._asset_id - # Get pre-defined group name and apperance from project config - group_configs = io.find_one( - {"type": "project"}, {"config.groups": 1} - )["config"].get("groups") or [] + active_groups = lib.get_active_group_config(asset_id) - # Build pre-defined group configs - predefineds = dict() - _orders = set([0]) # default order zero included - for config in group_configs: - name = config["name"] - icon = "fa." + config.get("icon", "object-group") - color = config.get("color", style.colors.default) - order = float(config.get("order")) - - predefineds[name] = {"icon": qta.icon(icon, color=color), - "order": order} - _orders.add(order) - - orders = sorted(_orders) - order_temp = "%0{}d".format(len(str(len(orders)))) - - # Collect subset groups + # Generate subset group nodes group_nodes = dict() - for group_name in io.distinct("data.subsetGroup", filter): + for data in active_groups: + name = data.pop("name") group = Node() - config = predefineds.get(group_name, self._default_group) - remapped_order = orders.index(config["order"]) - inverse_order = len(orders) - remapped_order - data = { - "subset": group_name, - "icon": config["icon"], - "isGroup": True, - # Format orders into fixed length string for groups sorting - "order": order_temp % remapped_order, - "inverseOrder": order_temp % inverse_order, - } + group.update({"subset": name, "isGroup": True}) group.update(data) - group_nodes[group_name] = {"node": group, - "childRow": 0} + + group_nodes[name] = {"node": group, "childRow": 0} self.add_child(group) + filter = {"type": "subset", "parent": asset_id} + # Process subsets row = len(group_nodes) for subset in io.find(filter): From 096dd01233a7a7365780aa3a88493007725f83ca Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 3 Jul 2019 21:11:37 +0800 Subject: [PATCH 15/28] Implement manual subset grouping dialog --- avalon/tools/cbloader/app.py | 104 ++++++++++++++++++++++++++++++- avalon/tools/cbloader/lib.py | 11 +++- avalon/tools/cbloader/widgets.py | 33 ++++++++++ 3 files changed, 142 insertions(+), 6 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index 1c35d3f57..df5d92ed5 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -1,13 +1,21 @@ import sys import time -from ..projectmanager.widget import AssetWidget, AssetModel +from ..projectmanager.widget import ( + AssetWidget, + AssetModel, + preserve_selection, +) from ...vendor.Qt import QtWidgets, QtCore from ... import api, io, style from .. import lib -from .lib import refresh_family_config, refresh_group_config +from .lib import ( + refresh_family_config, + refresh_group_config, + get_active_group_config, +) from .widgets import SubsetWidget, VersionWidget, FamilyListWidget module = sys.modules[__name__] @@ -91,6 +99,7 @@ def __init__(self, parent=None): "root": None, "project": None, "asset": None, + "assetId": None, "silo": None, "subset": None, "version": None, @@ -183,6 +192,7 @@ def _assetschanged(self): self.data['model']['version'].set_version(None) self.data["state"]["context"]["asset"] = document["name"] + self.data["state"]["context"]["assetId"] = document["_id"] self.data["state"]["context"]["silo"] = document["silo"] self.echo("Duration: %.3fs" % (time.time() - t1)) @@ -278,11 +288,99 @@ def keyReleaseEvent(self, event): # Grouping subsets on releasing Ctrl + G if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and not event.isAutoRepeat()): - print("GROUP...") + self.show_grouping_dialog() event.accept() return super(Window, self).keyReleaseEvent(event) + def show_grouping_dialog(self): + subsets = self.data["model"]["subsets"] + selected = subsets.selected_subsets() + if not selected: + self.echo("No selected subset.") + return + + dialog = SubsetGroupingDialog(items=selected, parent=self) + dialog.grouped.connect(self._assetschanged) + dialog.show() + + +class SubsetGroupingDialog(QtWidgets.QDialog): + + grouped = QtCore.Signal() + + def __init__(self, items, parent=None): + super(SubsetGroupingDialog, self).__init__(parent=parent) + self.setWindowTitle("Grouping Subsets") + self.setModal(True) + + self.items = items + self.subsets = parent.data["model"]["subsets"] + self.asset_id = parent.data["state"]["context"]["assetId"] + + name = QtWidgets.QLineEdit() + + # Menu for pre-defined subset groups + name_button = QtWidgets.QPushButton() + name_button.setFixedWidth(18) + name_button.setFixedHeight(20) + name_menu = QtWidgets.QMenu(name_button) + name_button.setMenu(name_menu) + + name_layout = QtWidgets.QHBoxLayout() + name_layout.addWidget(name) + name_layout.addWidget(name_button) + name_layout.setContentsMargins(0, 0, 0, 0) + + group_btn = QtWidgets.QPushButton("Apply") + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(QtWidgets.QLabel("Group Name")) + layout.addLayout(name_layout) + layout.addWidget(group_btn) + + group_btn.clicked.connect(self.on_group) + + self.name = name + self.name_menu = name_menu + + self._build_menu() + + def _build_menu(self): + menu = self.name_menu + button = menu.parent() + # Get and destroy the action group + group = button.findChild(QtWidgets.QActionGroup) + if group: + group.deleteLater() + + active_groups = get_active_group_config(self.asset_id, + include_predefined=True) + # Build new action group + group = QtWidgets.QActionGroup(button) + for data in sorted(active_groups, key=lambda x: x["order"]): + name = data["name"] + icon = data["icon"] + + action = group.addAction(name) + action.setIcon(icon) + menu.addAction(action) + + group.triggered.connect(self._on_action_clicked) + button.setEnabled(not menu.isEmpty()) + + def _on_action_clicked(self, action): + self.name.setText(action.text()) + + def on_group(self): + name = self.name.text().strip() + self.subsets.group_subsets(name, self.asset_id, self.items) + + with preserve_selection(tree_view=self.subsets.view, + current_index=False): + self.grouped.emit() + self.close() + def show(debug=False, parent=None, use_context=False): """Display Loader GUI diff --git a/avalon/tools/cbloader/lib.py b/avalon/tools/cbloader/lib.py index 9b3c04dbd..9ef99f23e 100644 --- a/avalon/tools/cbloader/lib.py +++ b/avalon/tools/cbloader/lib.py @@ -115,7 +115,7 @@ def refresh_group_config(): return groups -def get_active_group_config(asset_id): +def get_active_group_config(asset_id, include_predefined=False): """Collect all active groups from each subset """ predefineds = GROUP_CONFIG.copy() @@ -131,8 +131,13 @@ def get_active_group_config(asset_id): # Collect groups from subsets active_groups = list() - for group_name in set(io.distinct("data.subsetGroup", - {"type": "subset", "parent": asset_id})): + existed = set(io.distinct("data.subsetGroup", + {"type": "subset", "parent": asset_id})) + if include_predefined: + # Ensure all predefined group configs will be included + existed.update(predefineds.keys()) + + for group_name in existed: # Get group config config = predefineds.get(group_name, default_group_config) # Calculate order diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 9956765b1..2d59d7c04 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -216,6 +216,39 @@ def sorter(value): self.echo(exc) continue + def selected_subsets(self): + selection = self.view.selectionModel() + rows = selection.selectedRows(column=0) + + subsets = list() + for row in rows: + node = row.data(self.model.NodeRole) + if not node.get("isGroup"): + subsets.append(node) + + return subsets + + def group_subsets(self, name, asset_id, nodes): + field = "data.subsetGroup" + + if name: + update = {"$set": {field: name}} + self.echo("Group subsets to '%s'.." % name) + else: + update = {"$unset": {field: ""}} + self.echo("Ungroup subsets..") + + subsets = list() + for node in nodes: + subsets.append(node["subset"]) + + filter = { + "type": "subset", + "parent": asset_id, + "name": {"$in": subsets}, + } + io.update_many(filter, update) + def echo(self, message): print(message) From 3df024039551901cb4afdb73401e5dd218d40e20 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 3 Jul 2019 22:20:42 +0800 Subject: [PATCH 16/28] Add placeholder text and set width to dialog --- avalon/tools/cbloader/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index df5d92ed5..d06b1bb39 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -312,6 +312,7 @@ class SubsetGroupingDialog(QtWidgets.QDialog): def __init__(self, items, parent=None): super(SubsetGroupingDialog, self).__init__(parent=parent) self.setWindowTitle("Grouping Subsets") + self.setMinimumWidth(250) self.setModal(True) self.items = items @@ -319,6 +320,7 @@ def __init__(self, items, parent=None): self.asset_id = parent.data["state"]["context"]["assetId"] name = QtWidgets.QLineEdit() + name.setPlaceholderText("Remain blank to ungroup..") # Menu for pre-defined subset groups name_button = QtWidgets.QPushButton() From 98d2f85931a0212e8d64d7d591ada3871dfaa56e Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 01:47:10 +0800 Subject: [PATCH 17/28] Hide group when all members have been filtered --- avalon/tools/cbloader/model.py | 4 ++++ avalon/tools/cbloader/widgets.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index 4dd78b9c2..19e281ba5 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -272,6 +272,10 @@ def filterAcceptsRow(self, row=0, parent=QtCore.QModelIndex()): # Get the node data and validate node = model.data(index, TreeModel.NodeRole) + + if node.get("isGroup"): + return False + family = node.get("family", None) if not family: diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 2d59d7c04..3d108e70f 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -82,6 +82,9 @@ def __init__(self, parent=None): self.proxy.setDynamicSortFilter(True) self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.proxy.setRecursiveFilteringEnabled(True) + self.family_proxy.setRecursiveFilteringEnabled(True) + self.view.setModel(self.family_proxy) self.view.customContextMenuRequested.connect(self.on_context_menu) From 1af5e8d66cd2ca511c20a1ea4add29692e5ac574 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 12:31:53 +0800 Subject: [PATCH 18/28] Fix disorder when filtering multiple groups that having same order --- avalon/tools/cbloader/lib.py | 47 ++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/avalon/tools/cbloader/lib.py b/avalon/tools/cbloader/lib.py index 9ef99f23e..84483ab50 100644 --- a/avalon/tools/cbloader/lib.py +++ b/avalon/tools/cbloader/lib.py @@ -125,33 +125,48 @@ def get_active_group_config(asset_id, include_predefined=False): for config in predefineds.values(): _orders.add(config["order"]) + # Remap order to list index orders = sorted(_orders) - order_temp = "%0{}d".format(len(str(len(orders)))) # Collect groups from subsets - active_groups = list() - - existed = set(io.distinct("data.subsetGroup", - {"type": "subset", "parent": asset_id})) + group_names = set(io.distinct("data.subsetGroup", + {"type": "subset", "parent": asset_id})) if include_predefined: # Ensure all predefined group configs will be included - existed.update(predefineds.keys()) + group_names.update(predefineds.keys()) + + groups = list() - for group_name in existed: + for name in group_names: # Get group config - config = predefineds.get(group_name, default_group_config) - # Calculate order + config = predefineds.get(name, default_group_config) + # Base order remapped_order = orders.index(config["order"]) - inverse_order = len(orders) - remapped_order data = { - "name": group_name, + "name": name, "icon": config["icon"], - # Format orders into fixed length string for groups sorting - "order": order_temp % remapped_order, - "inverseOrder": order_temp % inverse_order, + "_order": remapped_order, } - active_groups.append(data) + groups.append(data) + + # Sort by tuple (base_order, name) + # If there are multiple groups in same order, will sorted by name. + ordered = sorted(groups, key=lambda dat: (dat.pop("_order"), dat["name"])) + + total = len(ordered) + order_temp = "%0{}d".format(len(str(total))) + + # Update sorted order to config + for index, data in enumerate(ordered): + order = index + inverse_order = total - order + + data.update({ + # Format orders into fixed length string for groups sorting + "order": order_temp % order, + "inverseOrder": order_temp % inverse_order, + }) - return active_groups + return ordered From b9914db74cfc95cc9be73ce5c8d4a32bdae675a8 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 17:12:20 +0800 Subject: [PATCH 19/28] Filtering groups by the acceptance of it's members --- avalon/tools/cbloader/lib.py | 12 +++++++- avalon/tools/cbloader/model.py | 49 ++++++++++++++++++++++++++------ avalon/tools/cbloader/widgets.py | 3 -- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/avalon/tools/cbloader/lib.py b/avalon/tools/cbloader/lib.py index 84483ab50..caa5edf5a 100644 --- a/avalon/tools/cbloader/lib.py +++ b/avalon/tools/cbloader/lib.py @@ -1,4 +1,4 @@ -from ...vendor import qtawesome +from ...vendor import qtawesome, Qt from ... import io, api, style FAMILY_ICON_COLOR = "#0091B2" @@ -12,6 +12,16 @@ def get(config, name): return config.get(name, config.get("__default__", None)) +def is_filtering_recursible(): + """Does Qt binding support recursive filtering for QSortFilterProxyModel ? + + (NOTE) Recursive filtering was introduced in Qt 5.10. + + """ + return hasattr(Qt.QtCore.QSortFilterProxyModel, + "setRecursiveFilteringEnabled") + + def refresh_family_config(): """Get the family configurations from the database diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index 19e281ba5..2d3f509b2 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -117,10 +117,10 @@ def refresh(self): for data in active_groups: name = data.pop("name") group = Node() - group.update({"subset": name, "isGroup": True}) + group.update({"subset": name, "isGroup": True, "childRow": 0}) group.update(data) - group_nodes[name] = {"node": group, "childRow": 0} + group_nodes[name] = group self.add_child(group) filter = {"type": "subset", "parent": asset_id} @@ -142,8 +142,8 @@ def refresh(self): group_name = subset["data"].get("subsetGroup") if group_name: group = group_nodes[group_name] - parent = group["node"] - parent_index = self.createIndex(0, 0, parent) + parent = group + parent_index = self.createIndex(0, 0, group) row_ = group["childRow"] group["childRow"] += 1 else: @@ -225,7 +225,39 @@ def flags(self, index): return flags -class SubsetFilterProxyModel(QtCore.QSortFilterProxyModel): +class GroupMemberFilterProxyModel(QtCore.QSortFilterProxyModel): + """Provide the feature of filtering group by the acceptance of members + + The subset group nodes will not be filtered directly, the group node's + acceptance depends on it's child subsets' acceptance. + + """ + + if lib.is_filtering_recursible(): + def _is_group_acceptable(self, index, node): + # (NOTE) With the help of `RecursiveFiltering` feature from + # Qt 5.10, group always not be accepted by default. + return False + filter_accepts_group = _is_group_acceptable + + else: + # Patch future function + setRecursiveFilteringEnabled = (lambda *args: None) + + def _is_group_acceptable(self, index, node): + # (NOTE) This is not recursive. + for child_row in range(node["childRow"]): + if self.filterAcceptsRow(child_row, index): + return True + return False + filter_accepts_group = _is_group_acceptable + + def __init__(self, *args, **kwargs): + super(GroupMemberFilterProxyModel, self).__init__(*args, **kwargs) + self.setRecursiveFilteringEnabled(True) + + +class SubsetFilterProxyModel(GroupMemberFilterProxyModel): def filterAcceptsRow(self, row, parent): @@ -235,14 +267,13 @@ def filterAcceptsRow(self, row, parent): parent) node = index.internalPointer() if node.get("isGroup"): - # Keep group in view - return True + return self.filter_accepts_group(index, node) else: return super(SubsetFilterProxyModel, self).filterAcceptsRow(row, parent) -class FamiliesFilterProxyModel(QtCore.QSortFilterProxyModel): +class FamiliesFilterProxyModel(GroupMemberFilterProxyModel): """Filters to specified families""" def __init__(self, *args, **kwargs): @@ -274,7 +305,7 @@ def filterAcceptsRow(self, row=0, parent=QtCore.QModelIndex()): node = model.data(index, TreeModel.NodeRole) if node.get("isGroup"): - return False + return self.filter_accepts_group(index, node) family = node.get("family", None) diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 3d108e70f..2d59d7c04 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -82,9 +82,6 @@ def __init__(self, parent=None): self.proxy.setDynamicSortFilter(True) self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - self.proxy.setRecursiveFilteringEnabled(True) - self.family_proxy.setRecursiveFilteringEnabled(True) - self.view.setModel(self.family_proxy) self.view.customContextMenuRequested.connect(self.on_context_menu) From 8b868ef324ab7620310415d5ecd41a8768dc1d62 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 17:15:35 +0800 Subject: [PATCH 20/28] Fixed `Ctrl+G` key event keep passing to parent App This also changed the key event type to `keyPressEvent`. --- avalon/tools/cbloader/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index d06b1bb39..2a4de6f56 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -281,17 +281,17 @@ def closeEvent(self, event): print("Good bye") return super(Window, self).closeEvent(event) - def keyReleaseEvent(self, event): + def keyPressEvent(self, event): modifiers = event.modifiers() ctrl_pressed = QtCore.Qt.ControlModifier & modifiers - # Grouping subsets on releasing Ctrl + G + # Grouping subsets on pressing Ctrl + G if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and not event.isAutoRepeat()): self.show_grouping_dialog() - event.accept() + return - return super(Window, self).keyReleaseEvent(event) + return super(Window, self).keyPressEvent(event) def show_grouping_dialog(self): subsets = self.data["model"]["subsets"] From 63ac36a9d2dcf16e5abe6d85a65d5bd36bbb47e0 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 17:17:04 +0800 Subject: [PATCH 21/28] Make 'Apply' button in `SubsetGroupingDialog` as default button --- avalon/tools/cbloader/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index 2a4de6f56..27b2da8d3 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -342,6 +342,8 @@ def __init__(self, items, parent=None): layout.addWidget(group_btn) group_btn.clicked.connect(self.on_group) + group_btn.setAutoDefault(True) + group_btn.setDefault(True) self.name = name self.name_menu = name_menu From 093a0d7775c3c266f7a999964892577c9c3955bf Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 17:27:16 +0800 Subject: [PATCH 22/28] Improved commit "Filtering groups by the acceptance of it's members" Improved commit b9914db74cfc95cc9be73ce5c8d4a32bdae675a8 --- avalon/tools/cbloader/model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index 2d3f509b2..0d9ab8baa 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -244,9 +244,9 @@ def _is_group_acceptable(self, index, node): # Patch future function setRecursiveFilteringEnabled = (lambda *args: None) - def _is_group_acceptable(self, index, node): + def _is_group_acceptable(self, index, model): # (NOTE) This is not recursive. - for child_row in range(node["childRow"]): + for child_row in range(model.rowCount(index)): if self.filterAcceptsRow(child_row, index): return True return False @@ -267,7 +267,7 @@ def filterAcceptsRow(self, row, parent): parent) node = index.internalPointer() if node.get("isGroup"): - return self.filter_accepts_group(index, node) + return self.filter_accepts_group(index, model) else: return super(SubsetFilterProxyModel, self).filterAcceptsRow(row, parent) @@ -305,7 +305,7 @@ def filterAcceptsRow(self, row=0, parent=QtCore.QModelIndex()): node = model.data(index, TreeModel.NodeRole) if node.get("isGroup"): - return self.filter_accepts_group(index, node) + return self.filter_accepts_group(index, model) family = node.get("family", None) From faeb6de0e51296b74c470af3db50d1c81e647d61 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 17:50:16 +0800 Subject: [PATCH 23/28] Fix auto columns resizing feature --- avalon/tools/cbloader/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index 27b2da8d3..3a236567f 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -184,8 +184,8 @@ def _assetschanged(self): subsets_model.set_asset(document['_id']) # Enforce the columns to fit the data (purely cosmetic) - rows = subsets_model.rowCount(QtCore.QModelIndex()) - for i in range(rows): + columns = subsets_model.columnCount(QtCore.QModelIndex()) + for i in range(columns): subsets.view.resizeColumnToContents(i) # Clear the version information on asset change From 65940c3042364e715e2076a24559d8e8d641a868 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 19:00:43 +0800 Subject: [PATCH 24/28] Enforce all columns resize to content and fixed This will also resize child row's columns on group item's expanding automatically, but the size will be fixed and user can not resize them. --- avalon/tools/cbloader/app.py | 5 ----- avalon/tools/cbloader/widgets.py | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index 3a236567f..533f55b70 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -183,11 +183,6 @@ def _assetschanged(self): document = asset_item.data(DocumentRole) subsets_model.set_asset(document['_id']) - # Enforce the columns to fit the data (purely cosmetic) - columns = subsets_model.columnCount(QtCore.QModelIndex()) - for i in range(columns): - subsets.view.resizeColumnToContents(i) - # Clear the version information on asset change self.data['model']['version'].set_version(None) diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 2d59d7c04..8dbeffd06 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -85,6 +85,10 @@ def __init__(self, parent=None): self.view.setModel(self.family_proxy) self.view.customContextMenuRequested.connect(self.on_context_menu) + header = self.view.header() + # Enforce the columns to fit the data (purely cosmetic) + header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) + selection = view.selectionModel() selection.selectionChanged.connect(self.active_changed) From 66f678c54a771e65db86ef0dc98d11c710b9d8cd Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 19:02:22 +0800 Subject: [PATCH 25/28] Auto expand groups on text filtering --- avalon/tools/cbloader/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 8dbeffd06..6e4573c05 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -95,6 +95,7 @@ def __init__(self, parent=None): version_delegate.version_changed.connect(self.version_changed) self.filter.textChanged.connect(self.proxy.setFilterRegExp) + self.filter.textChanged.connect(self.view.expandAll) self.model.refresh() From c8a04685ab7bd1c0b2a7400871ce63734c90b46f Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 20:49:11 +0800 Subject: [PATCH 26/28] Add "Enable Grouping" checkbox --- avalon/tools/cbloader/app.py | 4 ++++ avalon/tools/cbloader/model.py | 25 ++++++++++++++++--------- avalon/tools/cbloader/widgets.py | 28 +++++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index 533f55b70..e490a6a75 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -290,6 +290,10 @@ def keyPressEvent(self, event): def show_grouping_dialog(self): subsets = self.data["model"]["subsets"] + if not subsets.is_groupable(): + self.echo("Grouping not enabled.") + return + selected = subsets.selected_subsets() if not selected: self.echo("No selected subset.") diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index 0d9ab8baa..fa0afd880 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -24,10 +24,11 @@ class SubsetsModel(TreeModel): SortAscendingRole = QtCore.Qt.UserRole + 2 sortDescendingRole = QtCore.Qt.UserRole + 3 - def __init__(self, parent=None): + def __init__(self, grouping=True, parent=None): super(SubsetsModel, self).__init__(parent=parent) self._asset_id = None self._sorter = None + self._grouping = grouping self._icons = {"subset": qta.icon("fa.file-o", color=style.colors.default)} @@ -35,6 +36,10 @@ def set_asset(self, asset_id): self._asset_id = asset_id self.refresh() + def set_grouping(self, state): + self._grouping = state + self.refresh() + def setData(self, index, value, role=QtCore.Qt.EditRole): # Trigger additional edit when `version` column changed @@ -114,14 +119,16 @@ def refresh(self): # Generate subset group nodes group_nodes = dict() - for data in active_groups: - name = data.pop("name") - group = Node() - group.update({"subset": name, "isGroup": True, "childRow": 0}) - group.update(data) - group_nodes[name] = group - self.add_child(group) + if self._grouping: + for data in active_groups: + name = data.pop("name") + group = Node() + group.update({"subset": name, "isGroup": True, "childRow": 0}) + group.update(data) + + group_nodes[name] = group + self.add_child(group) filter = {"type": "subset", "parent": asset_id} @@ -140,7 +147,7 @@ def refresh(self): data["subset"] = data["name"] group_name = subset["data"].get("subsetGroup") - if group_name: + if self._grouping and group_name: group = group_nodes[group_name] parent = group parent_index = self.createIndex(0, 0, group) diff --git a/avalon/tools/cbloader/widgets.py b/avalon/tools/cbloader/widgets.py index 6e4573c05..bfcc609ee 100644 --- a/avalon/tools/cbloader/widgets.py +++ b/avalon/tools/cbloader/widgets.py @@ -8,6 +8,8 @@ from ... import api from ... import pipeline +from ..projectmanager.widget import preserve_selection + from .model import ( SubsetsModel, SubsetFilterProxyModel, @@ -23,10 +25,10 @@ class SubsetWidget(QtWidgets.QWidget): active_changed = QtCore.Signal() # active index changed version_changed = QtCore.Signal() # version state changed for a subset - def __init__(self, parent=None): + def __init__(self, enable_grouping=True, parent=None): super(SubsetWidget, self).__init__(parent=parent) - model = SubsetsModel() + model = SubsetsModel(grouping=enable_grouping) proxy = SubsetFilterProxyModel() family_proxy = FamiliesFilterProxyModel() family_proxy.setSourceModel(proxy) @@ -34,6 +36,13 @@ def __init__(self, parent=None): filter = QtWidgets.QLineEdit() filter.setPlaceholderText("Filter subsets..") + groupable = QtWidgets.QCheckBox("Enable Grouping") + groupable.setChecked(enable_grouping) + + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addWidget(filter) + top_bar_layout.addWidget(groupable) + view = QtWidgets.QTreeView() view.setIndentation(20) view.setStyleSheet(""" @@ -55,7 +64,7 @@ def __init__(self, parent=None): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(filter) + layout.addLayout(top_bar_layout) layout.addWidget(view) view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -68,6 +77,9 @@ def __init__(self, parent=None): "delegates": { "version": version_delegate, "time": time_delegate + }, + "state": { + "groupable": groupable } } @@ -94,6 +106,8 @@ def __init__(self, parent=None): version_delegate.version_changed.connect(self.version_changed) + groupable.stateChanged.connect(self.set_grouping) + self.filter.textChanged.connect(self.proxy.setFilterRegExp) self.filter.textChanged.connect(self.view.expandAll) @@ -102,6 +116,14 @@ def __init__(self, parent=None): # Expose this from the widget as a method self.set_family_filters = self.family_proxy.setFamiliesFilter + def is_groupable(self): + return self.data["state"]["groupable"].checkState() + + def set_grouping(self, state): + with preserve_selection(tree_view=self.view, + current_index=False): + self.model.set_grouping(state) + def on_context_menu(self, point): point_index = self.view.indexAt(point) From 3d23f6e3e2af67fe87c62d788918b13b59b5c477 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 20:52:56 +0800 Subject: [PATCH 27/28] Ensure `node` is not `None` --- avalon/tools/cbloader/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avalon/tools/cbloader/app.py b/avalon/tools/cbloader/app.py index e490a6a75..a8e0a79e4 100644 --- a/avalon/tools/cbloader/app.py +++ b/avalon/tools/cbloader/app.py @@ -204,7 +204,7 @@ def _versionschanged(self): rows = selection.selectedRows(column=active.column()) if active in rows: node = active.data(subsets.model.NodeRole) - if not node.get("isGroup"): + if node is not None and not node.get("isGroup"): version = node['version_document']['_id'] self.data['model']['version'].set_version(version) From 061f2402e6e0e0060d0abedd6f555ac33a8fa384 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 8 Jul 2019 22:17:35 +0800 Subject: [PATCH 28/28] Cosmetic change --- avalon/tools/cbloader/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/avalon/tools/cbloader/model.py b/avalon/tools/cbloader/model.py index fa0afd880..1757d1ba7 100644 --- a/avalon/tools/cbloader/model.py +++ b/avalon/tools/cbloader/model.py @@ -22,7 +22,7 @@ class SubsetsModel(TreeModel): "step"] SortAscendingRole = QtCore.Qt.UserRole + 2 - sortDescendingRole = QtCore.Qt.UserRole + 3 + SortDescendingRole = QtCore.Qt.UserRole + 3 def __init__(self, grouping=True, parent=None): super(SubsetsModel, self).__init__(parent=parent) @@ -196,7 +196,7 @@ def data(self, index, role): node = index.internalPointer() return node.get("familyIcon", None) - if role == self.sortDescendingRole: + if role == self.SortDescendingRole: node = index.internalPointer() if node.get("isGroup"): # Ensure groups be on top when sorting by descending order @@ -329,6 +329,6 @@ def sort(self, column, order): if order == QtCore.Qt.AscendingOrder: self.setSortRole(model.SortAscendingRole) else: - self.setSortRole(model.sortDescendingRole) + self.setSortRole(model.SortDescendingRole) super(FamiliesFilterProxyModel, self).sort(column, order)