Skip to content

Commit

Permalink
Merge pull request #391 from davidlatwe/loader-subsets
Browse files Browse the repository at this point in the history
Loader GUI subset grouping
  • Loading branch information
davidlatwe authored Jul 10, 2019
2 parents 447abf1 + 061f240 commit 7842b09
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 29 deletions.
1 change: 1 addition & 0 deletions avalon/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions avalon/schema/config-1.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
8 changes: 8 additions & 0 deletions avalon/tests/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}

Expand Down
131 changes: 123 additions & 8 deletions avalon/tools/cbloader/app.py
Original file line number Diff line number Diff line change
@@ -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
from .lib import (
refresh_family_config,
refresh_group_config,
get_active_group_config,
)
from .widgets import SubsetWidget, VersionWidget, FamilyListWidget

module = sys.modules[__name__]
Expand Down Expand Up @@ -95,6 +103,7 @@ def __init__(self, parent=None):
"root": None,
"project": None,
"asset": None,
"assetId": None,
"silo": None,
"subset": None,
"version": None,
Expand All @@ -109,6 +118,7 @@ def __init__(self, parent=None):
subsets.version_changed.connect(self.on_versionschanged)

refresh_family_config()
refresh_group_config()

# Defaults
self.resize(1330, 700)
Expand Down Expand Up @@ -177,15 +187,11 @@ def _assetschanged(self):
document = asset_item.data(DocumentRole)
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):
subsets.view.resizeColumnToContents(i)

# Clear the version information on asset change
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))

Expand All @@ -202,7 +208,8 @@ def _versionschanged(self):
rows = selection.selectedRows(column=active.column())
if active in rows:
node = active.data(subsets.model.NodeRole)
version = node['version_document']['_id']
if node is not None and not node.get("isGroup"):
version = node['version_document']['_id']

self.data['model']['version'].set_version(version)

Expand Down Expand Up @@ -273,6 +280,114 @@ def closeEvent(self, event):
print("Good bye")
return super(Window, self).closeEvent(event)

def keyPressEvent(self, event):
modifiers = event.modifiers()
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers

# Grouping subsets on pressing Ctrl + G
if (ctrl_pressed and event.key() == QtCore.Qt.Key_G and
not event.isAutoRepeat()):
self.show_grouping_dialog()
return

return super(Window, self).keyPressEvent(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.")
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.setMinimumWidth(250)
self.setModal(True)

self.items = items
self.subsets = parent.data["model"]["subsets"]
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()
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)
group_btn.setAutoDefault(True)
group_btn.setDefault(True)

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
Expand Down
4 changes: 4 additions & 0 deletions avalon/tools/cbloader/delegates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
113 changes: 111 additions & 2 deletions avalon/tools/cbloader/lib.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from ...vendor import qtawesome
from ... import io, api
from ...vendor import qtawesome, Qt
from ... import io, api, style

FAMILY_ICON_COLOR = "#0091B2"
FAMILY_CONFIG = {}
GROUP_CONFIG = {}


def get(config, name):
Expand All @@ -11,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
Expand Down Expand Up @@ -71,3 +82,101 @@ 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, include_predefined=False):
"""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"])

# Remap order to list index
orders = sorted(_orders)

# Collect groups from subsets
group_names = set(io.distinct("data.subsetGroup",
{"type": "subset", "parent": asset_id}))
if include_predefined:
# Ensure all predefined group configs will be included
group_names.update(predefineds.keys())

groups = list()

for name in group_names:
# Get group config
config = predefineds.get(name, default_group_config)
# Base order
remapped_order = orders.index(config["order"])

data = {
"name": name,
"icon": config["icon"],
"_order": remapped_order,
}

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 ordered
Loading

0 comments on commit 7842b09

Please sign in to comment.