Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loader GUI subset grouping #391

Merged
merged 28 commits into from
Jul 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0f1e7fc
Cosmetic
davidlatwe Jun 12, 2019
771875a
Implement 'subset grouping' in Loader GUI
davidlatwe Jun 12, 2019
4e438fa
Make group items always on top of the view
davidlatwe Jun 12, 2019
2a3876b
Disable context menu on group
davidlatwe Jun 12, 2019
b7a731f
Disable version delegate on group
davidlatwe Jun 12, 2019
e641d4d
Fix to reset version info on group
davidlatwe Jun 12, 2019
42a874a
Keep group in view when filtering subsets
davidlatwe Jun 12, 2019
1f7b122
Improved group item ordering and customizable icon
davidlatwe Jun 13, 2019
293ae9d
Re-implement groups ordering
davidlatwe Jun 14, 2019
04193ec
Add property `groups` into project config schema
davidlatwe Jun 14, 2019
148749f
Include `groups` when saving config by `avalon --save`
davidlatwe Jun 17, 2019
ced6ebe
Fix test
davidlatwe Jun 19, 2019
e67d269
Add Ctrl+G key release event handler for manual subset grouping
davidlatwe Jul 3, 2019
ab96a6d
Re-implement group config collecting mechanism
davidlatwe Jul 3, 2019
096dd01
Implement manual subset grouping dialog
davidlatwe Jul 3, 2019
3df0240
Add placeholder text and set width to dialog
davidlatwe Jul 3, 2019
98d2f85
Hide group when all members have been filtered
davidlatwe Jul 7, 2019
1af5e8d
Fix disorder when filtering multiple groups that having same order
davidlatwe Jul 8, 2019
b9914db
Filtering groups by the acceptance of it's members
davidlatwe Jul 8, 2019
8b868ef
Fixed `Ctrl+G` key event keep passing to parent App
davidlatwe Jul 8, 2019
63ac36a
Make 'Apply' button in `SubsetGroupingDialog` as default button
davidlatwe Jul 8, 2019
093a0d7
Improved commit "Filtering groups by the acceptance of it's members"
davidlatwe Jul 8, 2019
faeb6de
Fix auto columns resizing feature
davidlatwe Jul 8, 2019
65940c3
Enforce all columns resize to content and fixed
davidlatwe Jul 8, 2019
66f678c
Auto expand groups on text filtering
davidlatwe Jul 8, 2019
c8a0468
Add "Enable Grouping" checkbox
davidlatwe Jul 8, 2019
3d23f6e
Ensure `node` is not `None`
davidlatwe Jul 8, 2019
061f240
Cosmetic change
davidlatwe Jul 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -91,6 +99,7 @@ def __init__(self, parent=None):
"root": None,
"project": None,
"asset": None,
"assetId": None,
"silo": None,
"subset": None,
"version": None,
Expand All @@ -105,6 +114,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 @@ -173,15 +183,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 @@ -198,7 +204,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 @@ -269,6 +276,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