Hey Marcus,
Thanks for the reply!
That is what I was initially looking to do, and soon realised that it isn’t likely possible due to the following:
( forgive me if I have completely misunderstood, this is my first proper deep dive into the guts of Pyblish )
- All valid plugins are gathered during discovery. These are stored in model.Plugin.items attribute and a generator item in control.Controller.pair_generator.
- Pyblish then filters model.Plugin.items through model.ProxyModel.filterAcceptsRow to display the currently valid plugins in the gui.
This means that the filtering happens after the collection and order sorting occurs, which means the two bits of data that I am looking for are currently at the two ends of the plugin gathering pipe:
gather valid plugins -> sort plugins by order attribute -> filter plugins by family.
I assume this is happening because the collection plugins have to run to gather whichever family of validation, extraction, integration plugins need to run next.
So it is not possible to know during discovery which family based order needs to be applied to the plugins. It has to happen at the end of the pipe. I just can’t figure out where the gui is getting the family to filter by.
The first bit of functionality to implement was to re-order the plugins every time the gui sends a signal that the user has changed the order of the plugins by dragging and dropping in the list, which as I stated above, I have been able to do.
Next was functionality to save and load new orders arbitrarily, which was relatively easy to do using the first bit of functionality.
To better explain my approach, I will paste the code I have written so far.
Please excuse the volume of potentially non-relevant code, it is hard to know what might be relevant or not, so better to include it all.
Additions to model.Plugin class:
Allows the drag and drop operations to occur, re-ordering model.Plugin.items to respect the drag and drop operation of view.Item. All plugins then have their order attribute modified to match their current position in model.Plugin.items via model.Plugin._reorder_plugins_by_list_order
plugin_order_reset = QtCore.Signal(list)
def _encode_plugin(self, plugin):
return (plugin.__module__, plugin.__name__)
def _decode_plugin(self, data):
module_path = data[0]
module_name_split = os.path.basename(module_path).split(".")
module_name = ".".join(module_name_split[:-1])
module = imp.load_source(module_name, module_path)
plugin = getattr(module, data[1])
# not quite sure the reason for this,
# but repeating the step from plugin.discover:
plugin.__module__ = module.__file__
return plugin
def _get_plugins_by_order_type(self, type_index):
out_list = []
next_type_index = type_index + 1
for plugin in self.items:
if plugin.order >= type_index and plugin.order < next_type_index:
out_list.append(plugin)
return out_list
def _reorder_plugins_by_list_order(self):
reset_order_plugins = []
for order_type in xrange(1, 4):
plugins = self._get_plugins_by_order_type(order_type)
plugins_count = len(plugins)
for index, plugin in enumerate(plugins):
new_order = order_type + (float(index) / float(plugins_count))
plugin.order = new_order
reset_order_plugins.append(plugin)
self.plugin_order_reset.emit(reset_order_plugins)
return True
def _reorder_plugins_by_order_id(self):
reset_order_plugins = []
for order_type in xrange(1, 4):
reset_order_plugins.extend(self._get_plugins_by_order_type(order_type))
pyblish.api.sort_plugins(reset_order_plugins)
all_plugins = self._get_plugins_by_order_type(0)
all_plugins.extend(reset_order_plugins)
self.items = all_plugins
self.plugin_order_reset.emit(reset_order_plugins)
return True
def save_plugin_order(self, path):
plugin_order_data = [(plugin.label, plugin.order) for plugin in self.items]
encoded_plugin_order = json.dumps(plugin_order_data)
with open(path, "w+") as file:
file.write(encoded_plugin_order)
return encoded_plugin_order
def load_plugin_order(self, path):
with open(path, "r") as file:
decoded_plugin_order = json.load(file)
for data in decoded_plugin_order:
for plugin in self.items:
if plugin.label == data[0]:
plugin.order = data[1]
return self._reorder_plugins_by_order_id()
def supportedDropActions(self):
return QtCore.Qt.MoveAction
def flags(self, index):
if index.isValid():
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsEnabled
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsEnabled
def insertRows(self, row, count, items, parent=QtCore.QModelIndex()):
if parent.isValid() or count <= 0:
return False
beginRow = max(0, row)
endRow = min(row + count - 1, len(self.items))
self.beginInsertRows(parent, beginRow, endRow)
for index in xrange(beginRow, endRow + 1):
self.items.insert(index, None)
self.endInsertRows()
return True
def removeRows(self, row, count, parent=QtCore.QModelIndex()):
if parent.isValid() or row >= len(self.items) or row + count <= 0:
return False
beginRow = max(0, row)
endRow = min(row + count - 1, len(self.items) - 1)
self.beginRemoveRows(parent, beginRow, endRow)
for index in xrange(beginRow, endRow + 1):
self.items.pop(index)
self.endRemoveRows()
if self.pendingRemoveRowsAfterDrop:
'''
If we got here, it means this call to removeRows is the automatic
cleanup' action after drag-n-drop performed by Qt
'''
self.pendingRemoveRowsAfterDrop = False
self.drag_and_drop_finished.emit()
self._reorder_plugins_by_list_order()
return True
def mimeTypes(self):
return ["pulginData"]
def mimeData(self, indices):
def get_mime_data(index):
return (
self._encode_plugin(self.items[index.row()]),
index.row()
)
mime_data = QtCore.QMimeData()
item_data = [get_mime_data(index) for index in indices if index.isValid()]
item_data = jsonpickle.encode(item_data)
mime_data.setData("pulginData", QtCore.QByteArray(item_data))
return mime_data
def canDropMimeData(self, data, action, row, column, parent):
if not data.hasFormat("pulginData"):
return False
if action == QtCore.Qt.IgnoreAction:
return True
if (not data.hasFormat("pulginData")) or column > 0 or self.rowCount() <= 0:
return False
return True
def dropMimeData(self, data, action, row, column, parent):
if not self.canDropMimeData(data, action, row, column, parent):
return False
encoded_data = data.data("pulginData")
new_items = jsonpickle.loads(encoded_data.data())
target_row = row
self.lastDroppedItems = []
for index, (item, row_index) in enumerate(new_items):
if target_row < 0:
target_row = row_index
self.beginInsertRows(QtCore.QModelIndex(), target_row, target_row)
item = self.items[row_index]
self.items.insert(target_row, item)
self.endInsertRows()
self.lastDroppedItems.append(item)
target_row += 1
self.pendingRemoveRowsAfterDrop = True
return True
Additions to control.Controller:
Allows control.Controller._load method on the controller to regenerate the pair_generator attribute to match the order of the plugins in model.Plugin.items.
def _load(self, plugins=None):
""" Initiate new generator and load first pair
The optional plugins arguments allows for the generator
to be updated arbitrarily.
Args:
plugins (list, optional): The list of plugins to load.
If None, will default to self.plugins.
Defaults to None
Returns:
bool: True on success.
"""
if plugins is None:
plugins = self.plugins
self.is_running = True
self.pair_generator = self._iterator(plugins, self.context)
self.current_pair = next(self.pair_generator, (None, None))
self.current_error = None
self.is_running = False
return True
Additions to window.Window:
This is where the two bits of functionality are hooked together; model.Plugin.plugin_order_reset -> window.Window.on_plugin_order_reset which calls control.Controller._load on the newly sorted plugins.
def __inti__(self, controller, parent=None):
plugin_model.plugin_order_reset.connect(self.on_plugin_order_reset)
@QtCore.Slot(list)
def on_plugin_order_reset(self, plugins):
self.controller._load(plugins)
So what I would like to do is effectively run model.Plugin.load_plugin_order for the specific family once it has reached the end of the plugin collection -> sort -> filter pipeline.
The main issue is my naivety with regards to both the model - view design pattern and pyblish’s architecture, so I am jumping around just trying to figure out what everything does.
Not sure if this explains it well enough, but it is the best I can do.
I will continue to try and figure something out in the mean time, but any suggestions would be great.
cheers,
Shea.