From 55ad901ffaca00418e0ca823e6f725c1a5a38076 Mon Sep 17 00:00:00 2001 From: rebeccalin209 Date: Tue, 7 Jul 2020 14:24:59 +0800 Subject: [PATCH 01/12] Auto update model --- avalon/pipeline.py | 19 +++- avalon/tools/projectmanager/app.py | 172 ++++++++++++++++++++++++++++- 2 files changed, 179 insertions(+), 12 deletions(-) diff --git a/avalon/pipeline.py b/avalon/pipeline.py index c1d9f1f18..3a42df403 100644 --- a/avalon/pipeline.py +++ b/avalon/pipeline.py @@ -1153,12 +1153,19 @@ def load(Loader, representation, namespace=None, name=None, options=None, def _get_container_loader(container): """Return the Loader corresponding to the container""" - loader = container["loader"] - for Plugin in discover(Loader): - - # TODO: Ensure the loader is valid - if Plugin.__name__ == loader: - return Plugin + # loader = container["loader"] + # for Plugin in discover(Loader): + # + # # TODO: Ensure the loader is valid + # if Plugin.__name__ == loader: + # return Plugin + + from avalon import api + loaders = api.loaders_from_representation(api.discover(api.Loader), + container['representation']) + Loader = next((i for i in loaders if i.__name__ == container["loader"]), None) + print('Get loader from container: {}\n'.format(Loader)) + return Loader def remove(container): diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 03069f5fc..84a6e6418 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -1,6 +1,9 @@ import sys +import copy -from ...vendor.Qt import QtWidgets, QtCore +from functools import partial + +from ...vendor.Qt import QtWidgets, QtCore, QtGui from ... import io, schema, api, style from .. import lib as tools_lib @@ -12,6 +15,10 @@ module = sys.modules[__name__] module.window = None +import avalon.api +import avalon.io +from avalon.vendor import qargparse, qtawesome + class Window(QtWidgets.QDialog): """Project manager interface @@ -44,7 +51,8 @@ def __init__(self, is_silo_project=None, parent=None): # tasks tasks_widgets = QtWidgets.QWidget() tasks_widgets.setContentsMargins(0, 0, 0, 0) - tasks_layout = QtWidgets.QVBoxLayout(tasks_widgets) + tasks_layout = QtWidgets.QGridLayout(tasks_widgets) + label = QtWidgets.QLabel("Tasks") label.setFixedHeight(28) task_view = QtWidgets.QTreeView() @@ -52,9 +60,82 @@ def __init__(self, is_silo_project=None, parent=None): task_model = TasksModel() task_view.setModel(task_model) add_task = QtWidgets.QPushButton("Add task") - tasks_layout.addWidget(label) - tasks_layout.addWidget(task_view) - tasks_layout.addWidget(add_task) + + # task option widget + self.tasks_option_view = QtWidgets.QWidget() + self.tasks_option_view.setContentsMargins(0, 0, 0, 0) + self.tasks_option_layout = QtWidgets.QVBoxLayout(self.tasks_option_view) + self.tasks_option_layout.setAlignment(QtCore.Qt.AlignTop) + + option_top_label = QtWidgets.QLabel("Tasks Options") + option_top_label.setFixedHeight(28) + + # "No option found" widget + self.top_msg_group = QtWidgets.QGroupBox("", self) + top_msg_group_layout = QtWidgets.QHBoxLayout(self.top_msg_group) + self.option_msg_label = QtWidgets.QLabel('Make a task selection to view options') + _option_icon_label = QtWidgets.QLabel() + _icon = qtawesome.icon("fa.exclamation-circle", color="#c6c6c6") + _pixmap = _icon.pixmap(18, 18) + _option_icon_label.setPixmap(_pixmap) + top_msg_group_layout.addWidget(_option_icon_label, 0) + top_msg_group_layout.addWidget(self.option_msg_label, 0) + self.tasks_option_layout.addWidget(self.top_msg_group, 1) + + # Generate option widget + self.project = avalon.io.find_one({"name": avalon.api.Session["AVALON_PROJECT"], "type": "project"}) + self.assets = avalon.io.find({"type": "asset"}) + + self.options_tasks_data = {} + for _asset in self.assets: + asset_name = _asset['name'] + asset_tasks = _asset['data'].get('tasks', '') + if asset_tasks: + self.options_tasks_data[asset_name] = {} + for _task_data in self.project['config']['tasks']: + if 'options' in _task_data.keys() and _task_data['name'] in asset_tasks: + self.options_tasks_data[asset_name][_task_data['name']] = copy.deepcopy(_task_data['options']) # _task_data['options'] + + for asset_name, task_data in self.options_tasks_data.items(): + for _task_name, _options in task_data.items(): + _option_group = QtWidgets.QGroupBox(asset_name) + _option_group.hide() + _option_layout = QtWidgets.QVBoxLayout(_option_group) + for _option_name, _data in _options.items(): + if _option_name == 'widget': + continue + _default_value = _data['default_value'] + if type(_default_value) == bool: + options = [ + qargparse.Boolean(_option_name, + label=_data['label'], default=_default_value, help=_data.get('help', '')) + ] + self.__set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) + + if type(_default_value) == int: + options = [ + qargparse.Integer(_option_name, label=_data['label'], + default=_default_value, + max=_data.get('max', 99), + min=_data.get('min', 0), + write=8, + help=_data.get('help', '')) + ] + + self.__set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) + + _options['widget'] = _option_group + self.tasks_option_layout.addWidget(_option_group, 1) + + spacerItem = QtWidgets.QSpacerItem(20, 900, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.tasks_option_layout.addItem(spacerItem) + + # set layout + tasks_layout.addWidget(label, 0, 0) + tasks_layout.addWidget(option_top_label, 0, 1) + tasks_layout.addWidget(task_view, 1, 0) + tasks_layout.addWidget(self.tasks_option_view, 1, 1) + tasks_layout.addWidget(add_task, 2, 0, 1, 2) body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) @@ -63,7 +144,7 @@ def __init__(self, is_silo_project=None, parent=None): body.setOrientation(QtCore.Qt.Horizontal) body.addWidget(assets_widgets) body.addWidget(tasks_widgets) - body.setStretchFactor(0, 100) + body.setStretchFactor(0, 50) body.setStretchFactor(1, 65) # statusbar @@ -97,11 +178,49 @@ def __init__(self, is_silo_project=None, parent=None): add_asset.clicked.connect(self.on_add_asset) add_task.clicked.connect(self.on_add_task) assets.selection_changed.connect(self.on_asset_changed) + task_view.selectionModel().selectionChanged.connect(self.on_task_changed) self.resize(800, 500) self.echo("Connected to project: {0}".format(project_name)) + def __set_option_widget(self, options, asset_name, option_name, task_name, option_layout): + parser = qargparse.QArgumentParser(arguments=options) + + # Set value + _filter = {"type": "asset", "name": asset_name} + option_data = io.find(_filter, projection={"data.task_options": True}) + for _datas in option_data: + value = _datas['data'].get('task_options', {}).get(task_name, {}).get(option_name, {}).get('value', '') + if value: + parser._arguments[option_name].write(value) + + # Set connection + parser.changed.connect(partial(self.on_changed, task_name=task_name)) + option_layout.addWidget(parser) + + def on_changed(self, argument, task_name=''): + # self._options[argument["name"]] = argument.read() + option_name = argument["name"] + option_value = argument.read() + + # Get selection asset name + model = self.data["model"]["assets"] + self.asset_selected = model.get_selected_assets() + sel_asset_name = self.asset_selected[0]['data']['label'] + + asset = io.find_one({"type": "asset", "name": sel_asset_name}) + asset_id = asset["_id"] + + filter_ = {"_id": asset_id} + value_dict = { + 'value': option_value + } + update = {"$set": { + "data.task_options.{}.{}".format(task_name, option_name): value_dict} + } + io.update_many(filter_, update) + def keyPressEvent(self, event): """Custom keyPressEvent. @@ -238,6 +357,47 @@ def on_asset_changed(self): selected = model.get_selected_assets() self.data["model"]["tasks"].set_assets(selected) + self._hide_task_options() + self.top_msg_group.show() + self.option_msg_label.setText('Make a task selection to view options') + + def _hide_task_options(self): + for _asset_name, _data in self.options_tasks_data.items(): + for _, _op_data in _data.items(): + if 'widget' in _op_data.keys(): + _op_data['widget'].hide() + + def on_task_changed(self, selected, deselected): + """ + Callback on task selection changed. + + This updates the task extra option view. + """ + tasks_model = self.data["model"]["tasks"] + + model = self.data["model"]["assets"] + self.asset_selected = model.get_selected_assets() + sel_asset_name = self.asset_selected[0]['data']['label'] + + indexes = [] + for index in selected.indexes(): + if index.column() == 0: + indexes.append(index) + + task_name = tasks_model.data(indexes[0], 0) + + self._hide_task_options() + + _option_data = self.options_tasks_data[sel_asset_name] + if task_name in _option_data.keys(): + widget = _option_data[task_name].get('widget', '') + if widget: + widget.show() + self.top_msg_group.hide() + else: + self.top_msg_group.show() + self.option_msg_label.setText('No task options found') + def show(root=None, debug=False, parent=None): """Display Loader GUI From 1ccc9c3a97c17a8125632306dda47783f6c699ff Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 21 Jul 2020 23:52:45 +0800 Subject: [PATCH 02/12] Revert --- avalon/pipeline.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/avalon/pipeline.py b/avalon/pipeline.py index 3a42df403..c1d9f1f18 100644 --- a/avalon/pipeline.py +++ b/avalon/pipeline.py @@ -1153,19 +1153,12 @@ def load(Loader, representation, namespace=None, name=None, options=None, def _get_container_loader(container): """Return the Loader corresponding to the container""" - # loader = container["loader"] - # for Plugin in discover(Loader): - # - # # TODO: Ensure the loader is valid - # if Plugin.__name__ == loader: - # return Plugin - - from avalon import api - loaders = api.loaders_from_representation(api.discover(api.Loader), - container['representation']) - Loader = next((i for i in loaders if i.__name__ == container["loader"]), None) - print('Get loader from container: {}\n'.format(Loader)) - return Loader + loader = container["loader"] + for Plugin in discover(Loader): + + # TODO: Ensure the loader is valid + if Plugin.__name__ == loader: + return Plugin def remove(container): From 765e8b58c9e4250ccc7daf474dfb0781914bad62 Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 21 Jul 2020 23:54:56 +0800 Subject: [PATCH 03/12] Extend `io.find` to allow passing other query options --- avalon/io.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/avalon/io.py b/avalon/io.py index 5243660c9..a9f0b5898 100644 --- a/avalon/io.py +++ b/avalon/io.py @@ -353,22 +353,26 @@ def insert_many(items, ordered=True): @auto_reconnect -def find(filter, projection=None, sort=None): +def find(filter, projection=None, sort=None, *args, **kwargs): return self._database[Session["AVALON_PROJECT"]].find( filter=filter, projection=projection, - sort=sort + sort=sort, + *args, + **kwargs ) @auto_reconnect -def find_one(filter, projection=None, sort=None): +def find_one(filter, projection=None, sort=None, *args, **kwargs): assert isinstance(filter, dict), "filter must be " return self._database[Session["AVALON_PROJECT"]].find_one( filter=filter, projection=projection, - sort=sort + sort=sort, + *args, + **kwargs ) From a77ebc0d505a8b73940b0d7da2d0067d21f71851 Mon Sep 17 00:00:00 2001 From: David Lai Date: Tue, 21 Jul 2020 23:58:05 +0800 Subject: [PATCH 04/12] Avoid KeyError when asset has no task options --- avalon/tools/projectmanager/app.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 84a6e6418..2ee746b20 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -388,15 +388,16 @@ def on_task_changed(self, selected, deselected): self._hide_task_options() - _option_data = self.options_tasks_data[sel_asset_name] - if task_name in _option_data.keys(): - widget = _option_data[task_name].get('widget', '') + _option_data = self.options_tasks_data.get(sel_asset_name) + if _option_data and task_name in _option_data.keys(): + widget = _option_data[task_name].get('widget') if widget: widget.show() self.top_msg_group.hide() - else: - self.top_msg_group.show() - self.option_msg_label.setText('No task options found') + return + + self.top_msg_group.show() + self.option_msg_label.setText('No task options found') def show(root=None, debug=False, parent=None): From 06b601472b70ef3c8ecb0edba7a8dd65dc7fc35c Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 22 Jul 2020 00:00:44 +0800 Subject: [PATCH 05/12] Change to use relative import --- avalon/tools/projectmanager/app.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 2ee746b20..8d7b1cd10 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -4,6 +4,7 @@ from functools import partial from ...vendor.Qt import QtWidgets, QtCore, QtGui +from ...vendor import qargparse, qtawesome from ... import io, schema, api, style from .. import lib as tools_lib @@ -15,10 +16,6 @@ module = sys.modules[__name__] module.window = None -import avalon.api -import avalon.io -from avalon.vendor import qargparse, qtawesome - class Window(QtWidgets.QDialog): """Project manager interface @@ -83,8 +80,8 @@ def __init__(self, is_silo_project=None, parent=None): self.tasks_option_layout.addWidget(self.top_msg_group, 1) # Generate option widget - self.project = avalon.io.find_one({"name": avalon.api.Session["AVALON_PROJECT"], "type": "project"}) - self.assets = avalon.io.find({"type": "asset"}) + self.project = io.find_one({"name": api.Session["AVALON_PROJECT"], "type": "project"}) + self.assets = io.find({"type": "asset"}) self.options_tasks_data = {} for _asset in self.assets: From 202e257f080f60f1937278804e054688ddf40fa1 Mon Sep 17 00:00:00 2001 From: David Lai Date: Wed, 22 Jul 2020 17:07:09 +0800 Subject: [PATCH 06/12] Unhidden method --- avalon/tools/projectmanager/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 8d7b1cd10..d90b3c72b 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -107,7 +107,7 @@ def __init__(self, is_silo_project=None, parent=None): qargparse.Boolean(_option_name, label=_data['label'], default=_default_value, help=_data.get('help', '')) ] - self.__set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) + self._set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) if type(_default_value) == int: options = [ @@ -119,7 +119,7 @@ def __init__(self, is_silo_project=None, parent=None): help=_data.get('help', '')) ] - self.__set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) + self._set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) _options['widget'] = _option_group self.tasks_option_layout.addWidget(_option_group, 1) @@ -181,7 +181,7 @@ def __init__(self, is_silo_project=None, parent=None): self.echo("Connected to project: {0}".format(project_name)) - def __set_option_widget(self, options, asset_name, option_name, task_name, option_layout): + def _set_option_widget(self, options, asset_name, option_name, task_name, option_layout): parser = qargparse.QArgumentParser(arguments=options) # Set value From 98f73e79db39afce9289f9eb0709495dab8e775f Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 23 Jul 2020 01:51:29 +0800 Subject: [PATCH 07/12] WIP: Refactoring --- avalon/tools/projectmanager/app.py | 329 +++++++++++++++++------------ 1 file changed, 189 insertions(+), 140 deletions(-) diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index d90b3c72b..14651cda9 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -1,7 +1,7 @@ import sys -import copy from functools import partial +from copy import deepcopy from ...vendor.Qt import QtWidgets, QtCore, QtGui from ...vendor import qargparse, qtawesome @@ -17,6 +17,84 @@ module.window = None +class GroupBoxMessage(QtWidgets.QGroupBox): + + def __init__(self, title, parent=None): + super(GroupBoxMessage, self).__init__(title, parent) + + status_label = QtWidgets.QLabel("") + status_icon = QtWidgets.QLabel() + status_icon.setPixmap( + qtawesome.icon("fa.exclamation-circle", + color="#c6c6c6").pixmap(18, 18) + ) + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(status_icon) + layout.addWidget(status_label) + + self.message = status_label + + def set_message(self, message): + self.message.setText(message) + + +class TaskOptionContainer(QtWidgets.QWidget): + + def __init__(self, parent=None): + super(TaskOptionContainer, self).__init__(parent) + + container = QtWidgets.QVBoxLayout(self) + status = GroupBoxMessage("") + + self.container = container + self.status = status + self.task = None + self.is_batch = False + self.option_names = list() + + self.clear() + + def clear(self): + for child in self.container.children(): + self.container.removeWidget(child) + self.status.hide() + + def empty(self, message): + self.container.addWidget(self.status) + self.status.set_message(message) + self.status.show() + # Reset + self.task = None + self.is_batch = False + self.option_names = list() + + def add_options(self, parsers, task, is_batch): + self.task = task + self.is_batch = is_batch + for parser in parsers: + self.container.addWidget(parser) + + def save_options(self, assets): + # (TODO) WIP + changes = dict() + task_name = self.task["name"] + option_names = self.task["options"].values() + + for parser in self.container: + # changes[] + option_name = parser["name"] + option_value = parser.read() + + filter_ = {"_id": assets} + value_dict = { + 'value': option_value + } + update = {"$set": { + "data.task_options.{}.{}".format(task_name, option_name): value_dict} + } + io.update_many(filter_, update) + + class Window(QtWidgets.QDialog): """Project manager interface @@ -48,7 +126,7 @@ def __init__(self, is_silo_project=None, parent=None): # tasks tasks_widgets = QtWidgets.QWidget() tasks_widgets.setContentsMargins(0, 0, 0, 0) - tasks_layout = QtWidgets.QGridLayout(tasks_widgets) + tasks_layout = QtWidgets.QVBoxLayout(tasks_widgets) label = QtWidgets.QLabel("Tasks") label.setFixedHeight(28) @@ -57,83 +135,29 @@ def __init__(self, is_silo_project=None, parent=None): task_model = TasksModel() task_view.setModel(task_model) add_task = QtWidgets.QPushButton("Add task") + tasks_layout.addWidget(label) + tasks_layout.addWidget(task_view) + tasks_layout.addWidget(add_task) # task option widget - self.tasks_option_view = QtWidgets.QWidget() - self.tasks_option_view.setContentsMargins(0, 0, 0, 0) - self.tasks_option_layout = QtWidgets.QVBoxLayout(self.tasks_option_view) - self.tasks_option_layout.setAlignment(QtCore.Qt.AlignTop) - - option_top_label = QtWidgets.QLabel("Tasks Options") - option_top_label.setFixedHeight(28) - - # "No option found" widget - self.top_msg_group = QtWidgets.QGroupBox("", self) - top_msg_group_layout = QtWidgets.QHBoxLayout(self.top_msg_group) - self.option_msg_label = QtWidgets.QLabel('Make a task selection to view options') - _option_icon_label = QtWidgets.QLabel() - _icon = qtawesome.icon("fa.exclamation-circle", color="#c6c6c6") - _pixmap = _icon.pixmap(18, 18) - _option_icon_label.setPixmap(_pixmap) - top_msg_group_layout.addWidget(_option_icon_label, 0) - top_msg_group_layout.addWidget(self.option_msg_label, 0) - self.tasks_option_layout.addWidget(self.top_msg_group, 1) - - # Generate option widget - self.project = io.find_one({"name": api.Session["AVALON_PROJECT"], "type": "project"}) - self.assets = io.find({"type": "asset"}) - - self.options_tasks_data = {} - for _asset in self.assets: - asset_name = _asset['name'] - asset_tasks = _asset['data'].get('tasks', '') - if asset_tasks: - self.options_tasks_data[asset_name] = {} - for _task_data in self.project['config']['tasks']: - if 'options' in _task_data.keys() and _task_data['name'] in asset_tasks: - self.options_tasks_data[asset_name][_task_data['name']] = copy.deepcopy(_task_data['options']) # _task_data['options'] - - for asset_name, task_data in self.options_tasks_data.items(): - for _task_name, _options in task_data.items(): - _option_group = QtWidgets.QGroupBox(asset_name) - _option_group.hide() - _option_layout = QtWidgets.QVBoxLayout(_option_group) - for _option_name, _data in _options.items(): - if _option_name == 'widget': - continue - _default_value = _data['default_value'] - if type(_default_value) == bool: - options = [ - qargparse.Boolean(_option_name, - label=_data['label'], default=_default_value, help=_data.get('help', '')) - ] - self._set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) - - if type(_default_value) == int: - options = [ - qargparse.Integer(_option_name, label=_data['label'], - default=_default_value, - max=_data.get('max', 99), - min=_data.get('min', 0), - write=8, - help=_data.get('help', '')) - ] - - self._set_option_widget(options, asset_name, _option_name, _task_name, _option_layout) - - _options['widget'] = _option_group - self.tasks_option_layout.addWidget(_option_group, 1) - - spacerItem = QtWidgets.QSpacerItem(20, 900, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.tasks_option_layout.addItem(spacerItem) - - # set layout - tasks_layout.addWidget(label, 0, 0) - tasks_layout.addWidget(option_top_label, 0, 1) - tasks_layout.addWidget(task_view, 1, 0) - tasks_layout.addWidget(self.tasks_option_view, 1, 1) - tasks_layout.addWidget(add_task, 2, 0, 1, 2) + options_widgets = QtWidgets.QWidget() + options_widgets.setContentsMargins(0, 0, 0, 0) + + options_label = QtWidgets.QLabel("Tasks Options") + options_batch = QtWidgets.QCheckBox("Batch Edit") + options_body = QtWidgets.QScrollArea() + options_accept = QtWidgets.QPushButton("Save") + options_accept.setEnabled(False) + + options_container = TaskOptionContainer() + options_body.setWidget(options_container) + + options_layout = QtWidgets.QVBoxLayout(options_widgets) + options_layout.addWidget(options_label) + options_layout.addWidget(options_body, stretch=True) + options_layout.addWidget(options_accept) + # set body layout body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, @@ -141,8 +165,10 @@ def __init__(self, is_silo_project=None, parent=None): body.setOrientation(QtCore.Qt.Horizontal) body.addWidget(assets_widgets) body.addWidget(tasks_widgets) + body.addWidget(options_widgets) body.setStretchFactor(0, 50) body.setStretchFactor(1, 65) + body.setStretchFactor(2, 65) # statusbar message = QtWidgets.QLabel() @@ -168,12 +194,19 @@ def __init__(self, is_silo_project=None, parent=None): "buttons": { "add_asset": add_asset, "add_task": add_task - } + }, + "options": { + "accept": options_accept, + "batch": options_batch, + "container": options_container, + }, + "project": project_doc, } # signals add_asset.clicked.connect(self.on_add_asset) add_task.clicked.connect(self.on_add_task) + options_accept.clicked.connect(self.on_task_options_accepted) assets.selection_changed.connect(self.on_asset_changed) task_view.selectionModel().selectionChanged.connect(self.on_task_changed) @@ -181,43 +214,6 @@ def __init__(self, is_silo_project=None, parent=None): self.echo("Connected to project: {0}".format(project_name)) - def _set_option_widget(self, options, asset_name, option_name, task_name, option_layout): - parser = qargparse.QArgumentParser(arguments=options) - - # Set value - _filter = {"type": "asset", "name": asset_name} - option_data = io.find(_filter, projection={"data.task_options": True}) - for _datas in option_data: - value = _datas['data'].get('task_options', {}).get(task_name, {}).get(option_name, {}).get('value', '') - if value: - parser._arguments[option_name].write(value) - - # Set connection - parser.changed.connect(partial(self.on_changed, task_name=task_name)) - option_layout.addWidget(parser) - - def on_changed(self, argument, task_name=''): - # self._options[argument["name"]] = argument.read() - option_name = argument["name"] - option_value = argument.read() - - # Get selection asset name - model = self.data["model"]["assets"] - self.asset_selected = model.get_selected_assets() - sel_asset_name = self.asset_selected[0]['data']['label'] - - asset = io.find_one({"type": "asset", "name": sel_asset_name}) - asset_id = asset["_id"] - - filter_ = {"_id": asset_id} - value_dict = { - 'value': option_value - } - update = {"$set": { - "data.task_options.{}.{}".format(task_name, option_name): value_dict} - } - io.update_many(filter_, update) - def keyPressEvent(self, event): """Custom keyPressEvent. @@ -229,6 +225,7 @@ def keyPressEvent(self, event): """ def refresh(self): + self.data["project"] = io.find_one({"type": "project"}) self.data["model"]["assets"].refresh() def echo(self, message): @@ -353,16 +350,7 @@ def on_asset_changed(self): model = self.data["model"]["assets"] selected = model.get_selected_assets() self.data["model"]["tasks"].set_assets(selected) - - self._hide_task_options() - self.top_msg_group.show() - self.option_msg_label.setText('Make a task selection to view options') - - def _hide_task_options(self): - for _asset_name, _data in self.options_tasks_data.items(): - for _, _op_data in _data.items(): - if 'widget' in _op_data.keys(): - _op_data['widget'].hide() + self.task_options_refresh() def on_task_changed(self, selected, deselected): """ @@ -370,31 +358,92 @@ def on_task_changed(self, selected, deselected): This updates the task extra option view. """ - tasks_model = self.data["model"]["tasks"] - - model = self.data["model"]["assets"] - self.asset_selected = model.get_selected_assets() - sel_asset_name = self.asset_selected[0]['data']['label'] - indexes = [] for index in selected.indexes(): if index.column() == 0: indexes.append(index) + if not indexes: + self.task_options_refresh() + return + + # Only one task will be selected + tasks_model = self.data["model"]["tasks"] task_name = tasks_model.data(indexes[0], 0) + self.task_options_refresh(task_name) + + def on_task_options_accepted(self): + container = self.data["options"]["container"] + model = self.data["model"]["assets"] + + assets = [] + for asset in model.get_selected_assets(): + assets.append(asset["_id"]) + container.save_options(assets) - self._hide_task_options() + def task_options_refresh(self, selected_task=None): + accept = self.data["options"]["accept"] + container = self.data["options"]["container"] + container.clear() - _option_data = self.options_tasks_data.get(sel_asset_name) - if _option_data and task_name in _option_data.keys(): - widget = _option_data[task_name].get('widget') - if widget: - widget.show() - self.top_msg_group.hide() - return + if selected_task is None: + message = "Make a task selection to view options" + container.empty(message) + accept.setEnabled(False) + return + + # Create options + project = self.data["project"] + model = self.data["model"]["assets"] + batch_edit = self.data["options"]["batch"] + task_options = None + + for task in project["config"]["tasks"]: + if task["name"] == selected_task and "options" in task: + task_options = deepcopy(task) + break + else: + message = "No task options found" + container.empty(message) + accept.setEnabled(False) + return - self.top_msg_group.show() - self.option_msg_label.setText('No task options found') + parsers = list() + is_batch = batch_edit.checkState() + + def add_options(_parser, data=None): + for name, opt in task_options["options"].items(): + kwargs = {k: opt[k] for k in ["label", "help"] + if k in opt} + arg = _parser.add_argument(name, + default=opt.get("default_value"), + **kwargs) + if data: + nested_keys = ["task_options", selected_task, name] + for key in nested_keys: + data = data.get(key, {}) + value = data.get("value") + if value is not None: + # Asset has task option setup + arg.write(value) + + parsers.append(_parser) + + if is_batch: + # Batch edit mode will not present asset's task setup + _label = "Batch set selected assets" + parser = qargparse.QArgumentParser(description=_label) + add_options(parser) + + else: + for asset in model.get_selected_assets(): + asset_name = asset["name"] + asset_data = asset["_document"]["data"] + parser = qargparse.QArgumentParser(description=asset_name) + add_options(parser, asset_data) + + container.add_options(parsers, task=task_options, is_batch=is_batch) + accept.setEnabled(True) def show(root=None, debug=False, parent=None): From 574fca85b8f4dcad9b93094b5d8143ccd50c84a3 Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 23 Jul 2020 04:45:07 +0800 Subject: [PATCH 08/12] WIP: Finishing up --- avalon/tools/projectmanager/app.py | 121 ++++++++++++++++++----------- 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 14651cda9..accf27518 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -23,6 +23,7 @@ def __init__(self, title, parent=None): super(GroupBoxMessage, self).__init__(title, parent) status_label = QtWidgets.QLabel("") + status_label.setWordWrap(True) status_icon = QtWidgets.QLabel() status_icon.setPixmap( qtawesome.icon("fa.exclamation-circle", @@ -30,7 +31,7 @@ def __init__(self, title, parent=None): ) layout = QtWidgets.QHBoxLayout(self) layout.addWidget(status_icon) - layout.addWidget(status_label) + layout.addWidget(status_label, stretch=True) self.message = status_label @@ -38,61 +39,83 @@ def set_message(self, message): self.message.setText(message) -class TaskOptionContainer(QtWidgets.QWidget): +class TaskOptionContainer(QtWidgets.QScrollArea): def __init__(self, parent=None): super(TaskOptionContainer, self).__init__(parent) - container = QtWidgets.QVBoxLayout(self) - status = GroupBoxMessage("") - - self.container = container - self.status = status self.task = None + self.parsers = None self.is_batch = False - self.option_names = list() - self.clear() + def empty(self, message): + status = GroupBoxMessage("") + status.set_message(message) - def clear(self): - for child in self.container.children(): - self.container.removeWidget(child) - self.status.hide() + widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(widget) + layout.addWidget(status) - def empty(self, message): - self.container.addWidget(self.status) - self.status.set_message(message) - self.status.show() # Reset self.task = None + self.parsers = None self.is_batch = False - self.option_names = list() + + self.setWidget(widget) + self.setWidgetResizable(True) def add_options(self, parsers, task, is_batch): + widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(widget) + for parser in parsers: + layout.addWidget(parser) + self.task = task + self.parsers = parsers self.is_batch = is_batch - for parser in parsers: - self.container.addWidget(parser) + + self.setWidget(widget) + self.setWidgetResizable(False) def save_options(self, assets): - # (TODO) WIP + if self.task is None or self.parsers is None: + return # Possibly a bug. + changes = dict() task_name = self.task["name"] - option_names = self.task["options"].values() - - for parser in self.container: - # changes[] - option_name = parser["name"] - option_value = parser.read() + option_names = self.task["options"].keys() + filed_template = "data.task_options.{task}.{option}" + + for parser in self.parsers: + asset_name = parser._desciption + if asset_name not in changes: + changes[asset_name] = dict() + for option_name in option_names: + arg = parser.find(option_name) + value = arg.read() + field = filed_template.format(task=task_name, + option=option_name) + changes[asset_name].update({field: {"value": value}}) + # Update cache + # (TODO) This doesn't work + print(assets[asset_name]["data"]) + doc = assets[asset_name]["data"] + nested_keys = ["task_options", task_name, option_name] + for key in nested_keys: + if key not in doc: + doc[key] = dict() + doc = doc[key] + doc["value"] = value + print(assets[asset_name]["data"]) + + if self.is_batch: + filter_ = {"_id": {"$in": [_["_id"] for _ in assets.values()]}} + io.update_many(filter_, {"$set": changes.popitem()[1]}) - filter_ = {"_id": assets} - value_dict = { - 'value': option_value - } - update = {"$set": { - "data.task_options.{}.{}".format(task_name, option_name): value_dict} - } - io.update_many(filter_, update) + else: + for asset_name, options in changes.items(): + filter_ = {"_id": assets[asset_name]["_id"]} + io.update_many(filter_, {"$set": options}) class Window(QtWidgets.QDialog): @@ -145,16 +168,14 @@ def __init__(self, is_silo_project=None, parent=None): options_label = QtWidgets.QLabel("Tasks Options") options_batch = QtWidgets.QCheckBox("Batch Edit") - options_body = QtWidgets.QScrollArea() + options_container = TaskOptionContainer() options_accept = QtWidgets.QPushButton("Save") options_accept.setEnabled(False) - options_container = TaskOptionContainer() - options_body.setWidget(options_container) - options_layout = QtWidgets.QVBoxLayout(options_widgets) options_layout.addWidget(options_label) - options_layout.addWidget(options_body, stretch=True) + options_layout.addWidget(options_batch) + options_layout.addWidget(options_container, stretch=True) options_layout.addWidget(options_accept) # set body layout @@ -207,9 +228,11 @@ def __init__(self, is_silo_project=None, parent=None): add_asset.clicked.connect(self.on_add_asset) add_task.clicked.connect(self.on_add_task) options_accept.clicked.connect(self.on_task_options_accepted) + options_batch.stateChanged.connect(self.on_batch_changed) assets.selection_changed.connect(self.on_asset_changed) task_view.selectionModel().selectionChanged.connect(self.on_task_changed) + self.task_options_refresh() # Init self.resize(800, 500) self.echo("Connected to project: {0}".format(project_name)) @@ -372,22 +395,28 @@ def on_task_changed(self, selected, deselected): task_name = tasks_model.data(indexes[0], 0) self.task_options_refresh(task_name) + def on_batch_changed(self, state): + task = None + container = self.data["options"]["container"] + if container.task is not None: + task = container.task["name"] + self.task_options_refresh(task) + def on_task_options_accepted(self): container = self.data["options"]["container"] model = self.data["model"]["assets"] - assets = [] + assets = dict() for asset in model.get_selected_assets(): - assets.append(asset["_id"]) + assets[asset["name"]] = asset container.save_options(assets) def task_options_refresh(self, selected_task=None): accept = self.data["options"]["accept"] container = self.data["options"]["container"] - container.clear() if selected_task is None: - message = "Make a task selection to view options" + message = "Select task to view options." container.empty(message) accept.setEnabled(False) return @@ -403,7 +432,7 @@ def task_options_refresh(self, selected_task=None): task_options = deepcopy(task) break else: - message = "No task options found" + message = "No task options." container.empty(message) accept.setEnabled(False) return @@ -438,7 +467,7 @@ def add_options(_parser, data=None): else: for asset in model.get_selected_assets(): asset_name = asset["name"] - asset_data = asset["_document"]["data"] + asset_data = asset["data"] parser = qargparse.QArgumentParser(description=asset_name) add_options(parser, asset_data) From 0fdcd7e9ca258ecf7ab269c120f6e60599683f97 Mon Sep 17 00:00:00 2001 From: David Lai Date: Thu, 23 Jul 2020 23:53:19 +0800 Subject: [PATCH 09/12] Implemented asset task option read/write interface --- avalon/tools/lib.py | 7 +- avalon/tools/models.py | 45 +++++ avalon/tools/projectmanager/app.py | 286 ++++++++++++++++------------- avalon/tools/widgets.py | 53 +++++- 4 files changed, 258 insertions(+), 133 deletions(-) diff --git a/avalon/tools/lib.py b/avalon/tools/lib.py index 40ac659c2..3b42a0ab0 100644 --- a/avalon/tools/lib.py +++ b/avalon/tools/lib.py @@ -110,7 +110,7 @@ def iter_model_rows(model, @contextlib.contextmanager def preserve_states(tree_view, column=0, - role=None, + role=QtCore.Qt.DisplayRole, preserve_expanded=True, preserve_selection=True, current_index=True, @@ -126,11 +126,6 @@ def preserve_states(tree_view, Returns: None """ - # When `role` is set then override both expanded and selection roles - if role: - expanded_role = role - selection_role = role - model = tree_view.model() selection_model = tree_view.selectionModel() flags = selection_model.Select | selection_model.Rows diff --git a/avalon/tools/models.py b/avalon/tools/models.py index 057d3becb..9c142cce4 100644 --- a/avalon/tools/models.py +++ b/avalon/tools/models.py @@ -329,6 +329,9 @@ class AssetModel(TreeModel): def __init__(self, parent=None): super(AssetModel, self).__init__(parent=parent) self.refresh() + # (TODO) A good model should NOT self refresh on init, should let + # the main app make this call, or some where that all signals been + # connected. def _add_hierarchy(self, assets, parent=None, silos=None): """Add the assets that are related to the parent as children items. @@ -417,6 +420,23 @@ def refresh(self): self.endResetModel() + def update_documents(self, indexes): + """Update items documents by indexes + + Collect items' document id from indexes, and query database + for updating item data with `DocumentRole` + + """ + doc_ids = dict() + for index in indexes: + doc_id = self.data(index, self.ObjectIdRole) + doc_ids[doc_id] = index + + documents = io.find({"_id": {"$in": list(doc_ids.keys())}}) + for doc in documents: + index = doc_ids[doc["_id"]] + self.setData(index, doc, role=self.DocumentRole) + def flags(self, index): return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable @@ -472,6 +492,31 @@ def data(self, index, role): return super(AssetModel, self).data(index, role) + def setData(self, index, value, role=QtCore.Qt.EditRole): + """Change the data on the items. + + Returns: + bool: Whether the edit was successful + """ + if not index.isValid(): + return False + + changed = False + + if role == self.DocumentRole: + item = index.internalPointer() + item["_document"] = value + changed = True + + if changed: + # passing `list()` for PyQt5 (see PYSIDE-462) + args = () if Qt.IsPySide or Qt.IsPyQt4 else ([role],) + self.dataChanged.emit(index, index, *args) + # must return true if successful + return True + else: + return super(AssetModel, self).setData(index, value, role) + class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): """Filters to the regex if any of the children matches allow parent""" diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index accf27518..7b669ac27 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -1,15 +1,11 @@ import sys -from functools import partial -from copy import deepcopy - from ...vendor.Qt import QtWidgets, QtCore, QtGui from ...vendor import qargparse, qtawesome from ... import io, schema, api, style from .. import lib as tools_lib -from ..widgets import AssetWidget -from ..models import TasksModel +from ..widgets import AssetWidget, TaskWidget from .dialogs import TasksCreateDialog, AssetCreateDialog @@ -17,10 +13,16 @@ module.window = None -class GroupBoxMessage(QtWidgets.QGroupBox): +class MessageBox(QtWidgets.QWidget): + """A widget that shows word wrapped message with exclamation icon + + Methods: + set_message(str): Set a text message to display + + """ - def __init__(self, title, parent=None): - super(GroupBoxMessage, self).__init__(title, parent) + def __init__(self, parent=None): + super(MessageBox, self).__init__(parent) status_label = QtWidgets.QLabel("") status_label.setWordWrap(True) @@ -33,89 +35,108 @@ def __init__(self, title, parent=None): layout.addWidget(status_icon) layout.addWidget(status_label, stretch=True) - self.message = status_label + self._message = status_label def set_message(self, message): - self.message.setText(message) + self._message.setText(message) class TaskOptionContainer(QtWidgets.QScrollArea): + """Scrollable task option widgets' container + + This widget hold a set of `qargparser.QArgumentParser` widget that + aim to read/write task options' config per asset from/to database. + + """ + fetch_all = QtCore.Signal() + BATCH = ":.batch.:" def __init__(self, parent=None): super(TaskOptionContainer, self).__init__(parent) - - self.task = None - self.parsers = None + self._has_active_read = False + self.changes = None self.is_batch = False + self.setAlignment(QtCore.Qt.AlignTop) + + def add_active_read(self, arg): + """Read QArgument object's value without waiting it's signal + """ + self.fetch_all.connect(arg.changed.emit) + self._has_active_read = True + + def clear_active_read(self): + # Avoid calling deleted objects + if self._has_active_read: + self.fetch_all.disconnect() + self._has_active_read = False def empty(self, message): - status = GroupBoxMessage("") + status = MessageBox() status.set_message(message) widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(widget) layout.addWidget(status) - # Reset - self.task = None - self.parsers = None + self.changes = None self.is_batch = False self.setWidget(widget) self.setWidgetResizable(True) - def add_options(self, parsers, task, is_batch): + def add_options(self, parsers, is_batch): + """Docking QArgumentParser widgets""" widget = QtWidgets.QWidget() layout = QtWidgets.QVBoxLayout(widget) for parser in parsers: layout.addWidget(parser) - self.task = task - self.parsers = parsers + self.changes = None self.is_batch = is_batch self.setWidget(widget) self.setWidgetResizable(False) - def save_options(self, assets): - if self.task is None or self.parsers is None: - return # Possibly a bug. - - changes = dict() - task_name = self.task["name"] - option_names = self.task["options"].keys() - filed_template = "data.task_options.{task}.{option}" - - for parser in self.parsers: - asset_name = parser._desciption - if asset_name not in changes: - changes[asset_name] = dict() - for option_name in option_names: - arg = parser.find(option_name) - value = arg.read() - field = filed_template.format(task=task_name, - option=option_name) - changes[asset_name].update({field: {"value": value}}) - # Update cache - # (TODO) This doesn't work - print(assets[asset_name]["data"]) - doc = assets[asset_name]["data"] - nested_keys = ["task_options", task_name, option_name] - for key in nested_keys: - if key not in doc: - doc[key] = dict() - doc = doc[key] - doc["value"] = value - print(assets[asset_name]["data"]) + def update_change(self, value, option, asset, task): + """Update option changes from QArgument object""" + if self.changes is None: + self.changes = dict() + if asset not in self.changes: + self.changes[asset] = dict() + if task not in self.changes[asset]: + self.changes[asset][task] = dict() + + self.changes[asset][task].update({option: value}) + def save_options(self, asset_ids): + """Write per asset's task option configurations into database""" if self.is_batch: - filter_ = {"_id": {"$in": [_["_id"] for _ in assets.values()]}} - io.update_many(filter_, {"$set": changes.popitem()[1]}) + self.fetch_all.emit() + + if self.changes is None: + return False + field_template = "data.taskOptions.{task}.{option}.value" + + def compose(changes): + operation = dict() + for task, options in changes.items(): + for option, value in options.items(): + field = field_template.format(task=task, option=option) + operation[field] = value + return operation + + if self.is_batch: + batch = compose(self.changes[self.BATCH]) + filter_ = {"_id": {"$in": list(asset_ids.values())}} + io.update_many(filter_, {"$set": batch}) else: - for asset_name, options in changes.items(): - filter_ = {"_id": assets[asset_name]["_id"]} - io.update_many(filter_, {"$set": options}) + for asset_name, tasks_options in self.changes.items(): + edits = compose(tasks_options) + filter_ = {"_id": asset_ids[asset_name]} + io.update_many(filter_, {"$set": edits}) + + return True class Window(QtWidgets.QDialog): @@ -153,13 +174,11 @@ def __init__(self, is_silo_project=None, parent=None): label = QtWidgets.QLabel("Tasks") label.setFixedHeight(28) - task_view = QtWidgets.QTreeView() - task_view.setIndentation(0) - task_model = TasksModel() - task_view.setModel(task_model) + tasks = TaskWidget() + tasks.view.setSelectionMode(tasks.view.ExtendedSelection) add_task = QtWidgets.QPushButton("Add task") tasks_layout.addWidget(label) - tasks_layout.addWidget(task_view) + tasks_layout.addWidget(tasks) tasks_layout.addWidget(add_task) # task option widget @@ -189,7 +208,7 @@ def __init__(self, is_silo_project=None, parent=None): body.addWidget(options_widgets) body.setStretchFactor(0, 50) body.setStretchFactor(1, 65) - body.setStretchFactor(2, 65) + body.setSizes([50, 65, 0]) # Hide task options by default # statusbar message = QtWidgets.QLabel() @@ -210,7 +229,7 @@ def __init__(self, is_silo_project=None, parent=None): }, "model": { "assets": assets, - "tasks": task_model, + "tasks": tasks, }, "buttons": { "add_asset": add_asset, @@ -228,15 +247,18 @@ def __init__(self, is_silo_project=None, parent=None): add_asset.clicked.connect(self.on_add_asset) add_task.clicked.connect(self.on_add_task) options_accept.clicked.connect(self.on_task_options_accepted) - options_batch.stateChanged.connect(self.on_batch_changed) + options_batch.stateChanged.connect(self.on_task_changed) + tasks.selection_changed.connect(self.on_task_changed) assets.selection_changed.connect(self.on_asset_changed) - task_view.selectionModel().selectionChanged.connect(self.on_task_changed) - self.task_options_refresh() # Init - self.resize(800, 500) + self.resize(900, 500) self.echo("Connected to project: {0}".format(project_name)) + # (TODO) Shouldn't need to call this, but since `AssetModel` + # already made changes on it's init.. + self.on_task_changed() + def keyPressEvent(self, event): """Custom keyPressEvent. @@ -373,50 +395,22 @@ def on_asset_changed(self): model = self.data["model"]["assets"] selected = model.get_selected_assets() self.data["model"]["tasks"].set_assets(selected) - self.task_options_refresh() + self.on_task_changed() - def on_task_changed(self, selected, deselected): - """ - Callback on task selection changed. + def on_task_changed(self): + """Callback on task selection changed This updates the task extra option view. - """ - indexes = [] - for index in selected.indexes(): - if index.column() == 0: - indexes.append(index) - if not indexes: - self.task_options_refresh() - return - - # Only one task will be selected - tasks_model = self.data["model"]["tasks"] - task_name = tasks_model.data(indexes[0], 0) - self.task_options_refresh(task_name) - - def on_batch_changed(self, state): - task = None - container = self.data["options"]["container"] - if container.task is not None: - task = container.task["name"] - self.task_options_refresh(task) - - def on_task_options_accepted(self): - container = self.data["options"]["container"] - model = self.data["model"]["assets"] - - assets = dict() - for asset in model.get_selected_assets(): - assets[asset["name"]] = asset - container.save_options(assets) - - def task_options_refresh(self, selected_task=None): + """ accept = self.data["options"]["accept"] container = self.data["options"]["container"] + tasks = self.data["model"]["tasks"] - if selected_task is None: - message = "Select task to view options." + task_names = tasks.get_selected_tasks() + + if not task_names: + message = "Select tasks to view options." container.empty(message) accept.setEnabled(False) return @@ -424,56 +418,96 @@ def task_options_refresh(self, selected_task=None): # Create options project = self.data["project"] model = self.data["model"]["assets"] - batch_edit = self.data["options"]["batch"] - task_options = None + options_batch = self.data["options"]["batch"] + task_options = dict() for task in project["config"]["tasks"]: - if task["name"] == selected_task and "options" in task: - task_options = deepcopy(task) - break - else: + if task["name"] in task_names and "options" in task: + task_options[task["name"]] = task + + if not task_options: message = "No task options." container.empty(message) accept.setEnabled(False) return parsers = list() - is_batch = batch_edit.checkState() - - def add_options(_parser, data=None): - for name, opt in task_options["options"].items(): - kwargs = {k: opt[k] for k in ["label", "help"] - if k in opt} - arg = _parser.add_argument(name, - default=opt.get("default_value"), - **kwargs) - if data: - nested_keys = ["task_options", selected_task, name] - for key in nested_keys: - data = data.get(key, {}) - value = data.get("value") + is_batch = options_batch.checkState() + container.clear_active_read() + + def walk(data, fields): + for key in fields: + data = data.get(key, {}) + return data + + def setup(_parser, data=None, _asset=None): + for _task in task_names: + if _task not in task_options: + continue # `_task` has no option registered in project + + for name, opt in task_options[_task]["options"].items(): + d = walk(data or {}, fields=["taskOptions", _task, name]) + value = d.get("value") + arg = _parser.add_argument( + name, + _asset=_asset, # additional info + _task=_task, # additional info + **opt + ) if value is not None: # Asset has task option setup arg.write(value) + if is_batch: + # When batch mode enabled, no matter user has changed + # the value or not, all setup should be written into + # database. + container.add_active_read(arg) + + _parser.changed.connect(self.on_task_options_changed) parsers.append(_parser) if is_batch: # Batch edit mode will not present asset's task setup _label = "Batch set selected assets" parser = qargparse.QArgumentParser(description=_label) - add_options(parser) + setup(parser, _asset=container.BATCH) else: for asset in model.get_selected_assets(): asset_name = asset["name"] asset_data = asset["data"] parser = qargparse.QArgumentParser(description=asset_name) - add_options(parser, asset_data) + setup(parser, asset_data, _asset=asset_name) - container.add_options(parsers, task=task_options, is_batch=is_batch) + container.add_options(parsers, is_batch=is_batch) accept.setEnabled(True) + def on_task_options_accepted(self): + container = self.data["options"]["container"] + options_batch = self.data["options"]["batch"] + model = self.data["model"]["assets"] + + selected_asset_ids = dict() + for asset in model.get_selected_assets(): + selected_asset_ids[asset["name"]] = asset["_id"] + + # Write database if any changed + changed = container.save_options(selected_asset_ids) + # Update asset model data if changed + if changed: + model.update_selected_assets() + # Disable batch edit to view update + if container.is_batch: + options_batch.setCheckState(QtCore.Qt.Unchecked) + + def on_task_options_changed(self, arg): + container = self.data["options"]["container"] + container.update_change(value=arg.read(), + option=arg["name"], + asset=arg["_asset"], + task=arg["_task"]) + def show(root=None, debug=False, parent=None): """Display Loader GUI diff --git a/avalon/tools/widgets.py b/avalon/tools/widgets.py index a5acd1e9d..ce78383f4 100644 --- a/avalon/tools/widgets.py +++ b/avalon/tools/widgets.py @@ -2,7 +2,7 @@ from . import lib -from .models import AssetModel, RecursiveSortFilterProxyModel +from .models import AssetModel, TasksModel, RecursiveSortFilterProxyModel from .views import DeselectableTreeView from ..vendor import qtawesome, qargparse from ..vendor.Qt import QtWidgets, QtCore, QtGui @@ -109,6 +109,19 @@ def get_selected_assets(self): # NOTE: skip None object assumed they are silo (backwards comp.) return [asset for asset in assets if asset] + def update_selected_assets(self): + """Update selected assets' document from database + + Fetch documents that have been written into database by user, and + update model. + Documents that have changed should be those being selected. + + """ + selection = self.view.selectionModel() + rows = selection.selectedRows() + indexes = [row.model().mapToSource(row) for row in rows] + self.model.update_documents(indexes) + def select_assets(self, assets, expand=True, key="name"): """Select assets by item key. @@ -163,6 +176,44 @@ def select_assets(self, assets, expand=True, key="name"): self.view.setCurrentIndex(index) +class TaskWidget(QtWidgets.QWidget): + # (TODO) Merge `workfiles.app.TasksWidget` + + selection_changed = QtCore.Signal() # on view selection change + + def __init__(self, parent=None): + super(TaskWidget, self).__init__(parent=parent) + + model = TasksModel() + + view = QtWidgets.QTreeView() + view.setIndentation(0) + view.setModel(model) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(view) + + # Signals/Slots + selection = view.selectionModel() + selection.selectionChanged.connect(self.selection_changed) + + self.setContentsMargins(0, 0, 0, 0) + + self.model = model + self.view = view + + def set_assets(self, asset_docs): + """Update task model with view state preserved""" + with lib.preserve_states(self.view, column=0): + self.model.set_assets(asset_docs) + + def get_selected_tasks(self): + """Returns a list of selected tasks' names""" + selection = self.view.selectionModel() + tasks = [row.data() for row in selection.selectedRows()] + return tasks + + class OptionalMenu(QtWidgets.QMenu): """A subclass of `QtWidgets.QMenu` to work with `OptionalAction` From 3d7438d33035516684bb31f4598ba2512ad479a8 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 25 Jul 2020 22:47:27 +0800 Subject: [PATCH 10/12] Fix missing import --- avalon/tools/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avalon/tools/models.py b/avalon/tools/models.py index 9c142cce4..d509ad2c5 100644 --- a/avalon/tools/models.py +++ b/avalon/tools/models.py @@ -2,7 +2,7 @@ import logging import collections -from ..vendor.Qt import QtCore, QtGui +from ..vendor.Qt import Qt, QtCore, QtGui from ..vendor import qtawesome from .. import io from .. import style From 7ca03e99e4461002ea8c2c50acda660ec13b12ff Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 25 Jul 2020 22:48:56 +0800 Subject: [PATCH 11/12] Refactor to asset options See discussion on getavalon/core#556 --- avalon/tools/projectmanager/app.py | 97 +++++++++++++----------------- 1 file changed, 43 insertions(+), 54 deletions(-) diff --git a/avalon/tools/projectmanager/app.py b/avalon/tools/projectmanager/app.py index 7b669ac27..f938e0c6f 100644 --- a/avalon/tools/projectmanager/app.py +++ b/avalon/tools/projectmanager/app.py @@ -41,8 +41,8 @@ def set_message(self, message): self._message.setText(message) -class TaskOptionContainer(QtWidgets.QScrollArea): - """Scrollable task option widgets' container +class AssetOptionContainer(QtWidgets.QScrollArea): + """Scrollable asset option widgets' container This widget hold a set of `qargparser.QArgumentParser` widget that aim to read/write task options' config per asset from/to database. @@ -52,7 +52,7 @@ class TaskOptionContainer(QtWidgets.QScrollArea): BATCH = ":.batch.:" def __init__(self, parent=None): - super(TaskOptionContainer, self).__init__(parent) + super(AssetOptionContainer, self).__init__(parent) self._has_active_read = False self.changes = None self.is_batch = False @@ -97,16 +97,14 @@ def add_options(self, parsers, is_batch): self.setWidget(widget) self.setWidgetResizable(False) - def update_change(self, value, option, asset, task): + def update_change(self, value, option, asset): """Update option changes from QArgument object""" if self.changes is None: self.changes = dict() if asset not in self.changes: self.changes[asset] = dict() - if task not in self.changes[asset]: - self.changes[asset][task] = dict() - self.changes[asset][task].update({option: value}) + self.changes[asset][option] = value def save_options(self, asset_ids): """Write per asset's task option configurations into database""" @@ -116,14 +114,13 @@ def save_options(self, asset_ids): if self.changes is None: return False - field_template = "data.taskOptions.{task}.{option}.value" + field_template = "data.{option}" def compose(changes): operation = dict() - for task, options in changes.items(): - for option, value in options.items(): - field = field_template.format(task=task, option=option) - operation[field] = value + for option, value in changes.items(): + field = field_template.format(option=option) + operation[field] = value return operation if self.is_batch: @@ -131,8 +128,8 @@ def compose(changes): filter_ = {"_id": {"$in": list(asset_ids.values())}} io.update_many(filter_, {"$set": batch}) else: - for asset_name, tasks_options in self.changes.items(): - edits = compose(tasks_options) + for asset_name, options in self.changes.items(): + edits = compose(options) filter_ = {"_id": asset_ids[asset_name]} io.update_many(filter_, {"$set": edits}) @@ -181,13 +178,13 @@ def __init__(self, is_silo_project=None, parent=None): tasks_layout.addWidget(tasks) tasks_layout.addWidget(add_task) - # task option widget + # asset option widget options_widgets = QtWidgets.QWidget() options_widgets.setContentsMargins(0, 0, 0, 0) - options_label = QtWidgets.QLabel("Tasks Options") + options_label = QtWidgets.QLabel("Asset Options") options_batch = QtWidgets.QCheckBox("Batch Edit") - options_container = TaskOptionContainer() + options_container = AssetOptionContainer() options_accept = QtWidgets.QPushButton("Save") options_accept.setEnabled(False) @@ -400,7 +397,7 @@ def on_asset_changed(self): def on_task_changed(self): """Callback on task selection changed - This updates the task extra option view. + This updates the asset option view. """ accept = self.data["options"]["accept"] @@ -410,7 +407,7 @@ def on_task_changed(self): task_names = tasks.get_selected_tasks() if not task_names: - message = "Select tasks to view options." + message = "Select tasks to view asset options." container.empty(message) accept.setEnabled(False) return @@ -419,14 +416,18 @@ def on_task_changed(self): project = self.data["project"] model = self.data["model"]["assets"] options_batch = self.data["options"]["batch"] - task_options = dict() + asset_options = dict() - for task in project["config"]["tasks"]: - if task["name"] in task_names and "options" in task: - task_options[task["name"]] = task + for asset_opt in project["config"].get("assetOptions", []): + specified_tasks = asset_opt.get("onTasks", []) + available_in_all_tasks = not specified_tasks - if not task_options: - message = "No task options." + if (available_in_all_tasks + or any(task in specified_tasks for task in task_names)): + asset_options[asset_opt["name"]] = asset_opt + + if not asset_options: + message = "No asset options in selected tasks." container.empty(message) accept.setEnabled(False) return @@ -435,34 +436,23 @@ def on_task_changed(self): is_batch = options_batch.checkState() container.clear_active_read() - def walk(data, fields): - for key in fields: - data = data.get(key, {}) - return data - def setup(_parser, data=None, _asset=None): - for _task in task_names: - if _task not in task_options: - continue # `_task` has no option registered in project - - for name, opt in task_options[_task]["options"].items(): - d = walk(data or {}, fields=["taskOptions", _task, name]) - value = d.get("value") - arg = _parser.add_argument( - name, - _asset=_asset, # additional info - _task=_task, # additional info - **opt - ) - if value is not None: - # Asset has task option setup - arg.write(value) - - if is_batch: - # When batch mode enabled, no matter user has changed - # the value or not, all setup should be written into - # database. - container.add_active_read(arg) + data = data or {} + for opt_name, opt_data in asset_options.items(): + arg = _parser.add_argument( + _asset=_asset, # additional info + **opt_data + ) + value = data.get(opt_name) + if value is not None: + # Asset has task option setup + arg.write(value) + + if is_batch: + # When batch mode enabled, no matter user has changed + # the value or not, all setup should be written into + # database. + container.add_active_read(arg) _parser.changed.connect(self.on_task_options_changed) parsers.append(_parser) @@ -505,8 +495,7 @@ def on_task_options_changed(self, arg): container = self.data["options"]["container"] container.update_change(value=arg.read(), option=arg["name"], - asset=arg["_asset"], - task=arg["_task"]) + asset=arg["_asset"]) def show(root=None, debug=False, parent=None): From b649b4ccf623d7a31663497dbb0044b10cca4e33 Mon Sep 17 00:00:00 2001 From: David Lai Date: Sat, 25 Jul 2020 22:53:39 +0800 Subject: [PATCH 12/12] Update config-1.0.json and inventory.py So the new property "assetOptions" can be saved to database. --- avalon/inventory.py | 1 + avalon/schema/config-1.0.json | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/avalon/inventory.py b/avalon/inventory.py index a33dc126e..e71eb0518 100644 --- a/avalon/inventory.py +++ b/avalon/inventory.py @@ -332,6 +332,7 @@ def _save_config_1_0(project_name, data): config["apps"] = data.get("apps", []) config["tasks"] = data.get("tasks", []) config["template"].update(data.get("template", {})) + config["assetOptions"] = data.get("assetOptions", []) config["families"] = data.get("families", []) config["groups"] = data.get("groups", []) diff --git a/avalon/schema/config-1.0.json b/avalon/schema/config-1.0.json index 4d4eb57e7..7f2362f28 100644 --- a/avalon/schema/config-1.0.json +++ b/avalon/schema/config-1.0.json @@ -53,6 +53,20 @@ "required": ["name"] } }, + "assetOptions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": {"type": "string"}, + "label": {"type": "string"}, + "help": {"type": "string"}, + "onTasks": {"type": "array"} + }, + "required": ["name"] + } + }, "families": { "type": "array", "items": {