-
Notifications
You must be signed in to change notification settings - Fork 42
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
Implement sections for instances and plug-ins #21
Comments
This is currently the only feature that's stopping us from trying this in production. Without the categories we can't tell what we are publishing at all, so I'd humbly say that this is the key for making pyblish-lite usable. Most of other issues are icing, this is core functionality. |
I'm having trouble figuring this one out. I'd rather not add "virtual" items to the model as it breaks the model-view-controller nature of the program - where view-related data overlap into the model. The view itself is what is supposed to handle this functionality, in the same way it adds scrollbars. Scrollbars depend on the model, but are only in the view where they belong. I'd expect sections to be there as well. But, I haven't yet figured out how to do it.. This is the closest I found on the subject. Help! |
@konstep could you look at this when you find a minute? You have more experience with Qt, maybe you'll have some elegant solution ;) |
Adding the sections as in a |
It's not necessarily complicated, it's just different. I'd rather not use a treeview, as it would mean locking the data in to just being usable in a tree view, yet the data is flat; not hierarchical. I.e. it would be a hack and harm the longevity and extensibility of the project. |
I guess you mean the results data, and not the instances? |
I mean the nature of plug-ins and instances not having parents and children. Especially not in terms of which family they belong to. Families are properties of each object, not parents. It sounds like what you are suggesting is that we make families parents of plug-ins and instances, to "cheat" the look we've established in pyblish-qml by mutilating the data in order to use it with a view that doesn't represent our actual data. I'd rather do it right for as long as possible, and save the hacks till last. |
Maybe I'm wrong, but isn't it splitting hairs a little bit? Seems to be like a choice between different flavors of hacks. |
Wouldn't a proxy model that represents the data in a different way (grouped by) suffice for this use case? The original data remains the structure as used internally by pyblish, there's just a proxy model layer on top of that reorganizing it by category. # pseudo
model = InstanceModel()
proxy = GroupByProxyModel()
proxy.setSourceModel(model)
proxy.setGroupBy("order")
view = QtGui.QTreeView()
view.setModel(proxy) The original model remains unaltered. Theoretically the proxy could even allow the artist to group by different things (e.g. family, failed/success, order, etc.) |
Yes, a proxy model can achieve this, I'm just not sure how.
It's a bit pedantic, yes. I just don't hink its worth working around and I'd rather learn something new and do it right this time. I've hacked around similar things in similar ways in the past and it always ends up a big hairy mess. Today, pyblish-lite is hack-free and as ideal as I can think to make it. I'd like to keep the bugs out for longer and make better software. |
Googling reveals there are some implementations out there. Probably worth investigating those for a bit. http://qadvanceditemviews.sourceforge.net/class_q_grouping_proxy_model.html So it seems there is a way to do it like that :) |
So just to be clear. You aren't disputing the QTreeView, just that we don't mutilate the data? |
Yes, that is fine. I think either are equally challenging to get right, it's adding these "virtual" items via the proxy that provide the challenge. Then whether you draw them in a list- or tree-view is pure cosmetics. |
I was looking at this today, but quite frankly it's way above my skillset :). What @BigRoy posted with the QAbstractProxyModel, looks very promising as far as I can tell. |
@BigRoy QGroupingProxyModel doesn't seem to be available for PySide |
What I don't get about |
That's probably correct. I pointed to these to showcase that it is actually possible to implement, wasn't sure whether it was actually implemented in PySide thus far.
When are you expecting it to be called? |
I don't know :) Bu the documentation says that you have to reimplement the function, so I was confused about when it gets called. |
Here is a wip that at least has the right order, just needs to fetch the objects rather than just text data. Also I'm iterating over the objects continously to get the right order, which might be slow with lots of objects; import sys
from PySide import QtCore, QtGui
Label = QtCore.Qt.DisplayRole
Section = QtCore.Qt.UserRole + 1
IsSection = QtCore.Qt.UserRole + 2
class Item(object):
@classmethod
def paint(cls, painter, option, index):
rect = QtCore.QRectF(option.rect)
painter.save()
if option.state & QtGui.QStyle.State_MouseOver:
painter.fillRect(rect, QtGui.QColor("#DEE"))
if option.state & QtGui.QStyle.State_Selected:
painter.fillRect(rect, QtGui.QColor("#CDD"))
painter.drawText(rect.adjusted(20, 0, 0, 0),
index.data(Label))
painter.restore()
@classmethod
def sizeHint(cls, option, index):
return QtCore.QSize(option.rect.width(), 20)
class Section(object):
@classmethod
def paint(self, painter, option, index):
painter.save()
painter.setPen(QtGui.QPen(QtGui.QColor("#666")))
painter.drawText(QtCore.QRectF(option.rect), index.data(Label))
painter.restore()
@classmethod
def sizeHint(self, option, index):
return QtCore.QSize(option.rect.width(), 20)
class Delegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
if index.data(IsSection):
return Section.paint(painter, option, index)
else:
return Item.paint(painter, option, index)
def sizeHint(self, option, index):
if index.data(IsSection):
return Section.sizeHint(option, index)
else:
return Item.sizeHint(option, index)
class Model(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super(Model, self).__init__(parent)
self.items = list()
def data(self, index, role=QtCore.Qt.DisplayRole):
return self.items[index.row()]
def append(self, item):
self.beginInsertRows(QtCore.QModelIndex(),
self.rowCount(),
self.rowCount())
self.items.append(item)
self.endInsertRows()
def rowCount(self, parent=None):
return len(self.items)
class Proxy(QtGui.QSortFilterProxyModel):
def __init__(self):
super(Proxy, self).__init__()
self.items = [[]]
def data(self, index, role=QtCore.Qt.DisplayRole):
return self.mapFromSource(index)
def rowCount(self, parent):
sections = 0
prev = None
for item in self.sourceModel().items:
cur = item["section"]
if cur != prev:
sections += 1
prev = cur
# Note: This includes 1 additional, duplicate, section
# for the bottom item. Ordering of items in model is important.
return self.sourceModel().rowCount() + sections
def index(self, row, column, parent):
return self.createIndex(row, column, parent)
def mapFromSource(self, index):
prev = None
items = []
for item in self.sourceModel().items:
cur = item["section"]
if cur == prev:
items.append(item["label"])
else:
items.append(item["section"])
items.append(item["label"])
prev = cur
return items[index.row()]
def mapToSource(self, index):
return self.createIndex(index.row(),
index.column(),
QtCore.QModelIndex())
def parent(self, index):
return QtCore.QModelIndex()
app = QtGui.QApplication(sys.argv)
model = Model()
data = [{"label": "Ben", "section": "Human"},
{"label": "Steve", "section": "Human"},
{"label": "Alpha12", "section": "Robot"},
{"label": "Mike", "section": "Toaster"},
{"label": "Steve", "section": "Human"}]
for item in data:
model.append(item)
proxy = Proxy()
proxy.setSourceModel(model)
delegate = Delegate()
view = QtGui.QTreeView()
view.setWindowTitle("My View")
view.setModel(proxy)
view.setItemDelegate(delegate)
view.show()
app.exec_() |
Hmm, this doesn't look right to me. For example: def mapToSource(self, index):
return self.createIndex(index.row(),
index.column(),
QtCore.QModelIndex()) This basically says that the index (row, column) in this proxy corresponds one-to-one with that on the source model. Which sounds odd to me, because what you're basically doing in your proxy model is trying to shift/bump over indices down a row (or more) to make room for the headers. As such this seems to map incorrectly. Shouldn't this return an invalid index if it's a header, because a header is not present in the source and subtract row numbers based on what N-th grouping its under to correct it back to the source model. Basically reversing that shift of rows. Similarly it feels odd that Or that data is now returning an Item, instead of the actual data based on a role sounds like it won't work. Even though I like the short amount of code you have there, it looks a lot like an oversimplification that is just wrong. I'll have to note that this is hardly my expertise so I could be looking at this in the wrong way myself (hopefully). Because to me it felt more complicated than what is shown here. Am I misunderstanding how you're going about this? |
Okay, I think it's a good step forward. A little over the top there, @BigRoy, I don't think we're completely off the beaten path. Just so we're seeing the same thing, here's what I'm looking at with the above code. Which, when swapping the TreeView with a ListView, looks like this. Which is probably more close to our end result, and clearly a step forward from the original. So I would say, keep experimenting! |
As I was typing I felt it came across over as "harsh" and this puts my comment in a similar perspective. So let me just quickly chip in and say that was far from my intent. The whole comment was intended to be an explanation of how it felt off to me, not that it was off given as fact. Maybe even pointing out that I was myself looking at this completely the wrong way. As such I was probably looking for some more guidance or explanations in a way for me, instead of the other way around. Not sure if that makes sense, but the whole goal was to open it up into the discussion.
+1. Based on Stackoverflow question:
Sorting them into place would mean setting their "row" index as you create the indices from source in-between the row indices of the headers. Right? |
I agree completely :) In fact as I said earlier, I don't know when
This was my next step. I couldn't visualize the code without simplifying it first. I actually started by just trying to get the indices for the data; data = [{"label": "Ben", "section": "Human"},
{"label": "Steve", "section": "Human"},
{"label": "Alpha12", "section": "Robot"},
{"label": "Mike", "section": "Toaster"},
{"label": "Steve", "section": "Human"}]
prev = None
row = 0
origRow = 0
for item in data:
cur = item["section"]
if cur == prev:
print "\t" + item["label"] + "(%s, %s)" % (row, origRow)
row += 1
origRow += 1
else:
print item["section"] + "(%s)" % row
row += 1
print "\t" + item["label"] + "(%s, %s)" % (row, origRow)
row += 1
origRow += 1
prev = cur |
Forgot I'd changed that :) What I was thinking was, that we could use a TreeView for collapsing unwanted sections. For example we could have the collectors section collapsed by default, which would unclutter the view for end users. |
THIS. We'd love that |
Alright, I think I have a working version of the example. Again I don't know how this will scale up with more data, since we are rebuilding the data structure when ever the item data is called. import sys
from PySide import QtCore, QtGui
Label = QtCore.Qt.DisplayRole
Section = QtCore.Qt.UserRole + 1
IsSection = QtCore.Qt.UserRole + 2
class Item(object):
@classmethod
def paint(cls, painter, option, index):
rect = QtCore.QRectF(option.rect)
painter.save()
if option.state & QtGui.QStyle.State_MouseOver:
painter.fillRect(rect, QtGui.QColor("#DEE"))
if option.state & QtGui.QStyle.State_Selected:
painter.fillRect(rect, QtGui.QColor("#CDD"))
painter.drawText(rect.adjusted(20, 0, 0, 0),
index.data(Label))
painter.restore()
@classmethod
def sizeHint(cls, option, index):
return QtCore.QSize(option.rect.width(), 20)
class Section(object):
@classmethod
def paint(self, painter, option, index):
painter.save()
painter.setPen(QtGui.QPen(QtGui.QColor("#666")))
painter.drawText(QtCore.QRectF(option.rect), index.data(Label))
painter.restore()
@classmethod
def sizeHint(self, option, index):
return QtCore.QSize(option.rect.width(), 20)
class Delegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
if index.data(IsSection):
return Section.paint(painter, option, index)
else:
return Item.paint(painter, option, index)
def sizeHint(self, option, index):
if index.data(IsSection):
return Section.sizeHint(option, index)
else:
return Item.sizeHint(option, index)
class Model(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super(Model, self).__init__(parent)
self.items = list()
def data(self, index, role):
item = self.items[index.row()]
return {
Label: item["label"],
Section: item["section"],
IsSection: False
}.get(role)
def append(self, item):
self.beginInsertRows(QtCore.QModelIndex(),
self.rowCount(),
self.rowCount())
self.items.append(item)
self.endInsertRows()
def rowCount(self, parent=None):
return len(self.items)
class Proxy(QtGui.QSortFilterProxyModel):
def __init__(self):
super(Proxy, self).__init__()
self.items = [[]]
def data(self, index, role):
prev_section = None
items = []
for count in range(0, self.sourceModel().rowCount()):
item_index = self.sourceModel().createIndex(count, 0)
cur_section = self.sourceModel().data(item_index, Section)
if cur_section == prev_section:
items.append(self.sourceModel().data(item_index, role))
else:
items.append({Label: cur_section,
Section: "Virtual Section",
IsSection: True}.get(role))
items.append(self.sourceModel().data(item_index, role))
prev_section = cur_section
return items[index.row()]
def rowCount(self, parent):
sections = 0
prev = None
for item in self.sourceModel().items:
cur = item["section"]
if cur != prev:
sections += 1
prev = cur
# Note: This includes 1 additional, duplicate, section
# for the bottom item. Ordering of items in model is important.
return self.sourceModel().rowCount() + sections
def index(self, row, column, parent):
return self.createIndex(row, column, parent)
def mapToSource(self, index):
return self.createIndex(index.row(),
index.column(),
QtCore.QModelIndex())
def parent(self, index):
return QtCore.QModelIndex()
app = QtGui.QApplication(sys.argv)
model = Model()
data = [{"label": "Ben", "section": "Human"},
{"label": "Steve", "section": "Human"},
{"label": "Alpha12", "section": "Robot"},
{"label": "Mike", "section": "Toaster"},
{"label": "Steve", "section": "Human"}]
for item in data:
model.append(item)
proxy = Proxy()
proxy.setSourceModel(model)
delegate = Delegate()
view = QtGui.QListView()
view.setWindowTitle("My View")
view.setModel(proxy)
view.setItemDelegate(delegate)
view.show()
app.exec_() |
Ah, nice to see that work. :) Here's a version of how I thought the proxy model would've had to be implemented. Basically the Proxy has its own indices, so This doesn't have special painter delegate, so it s not as pretty. But it's functional (it should be) with any flat list model that Qt offers, e.g. here it is used with QStringListModel. And you can group it by a specific "role" by setting the group role. import sys
from PySide import QtCore, QtGui
from itertools import groupby
import logging
log = logging.getLogger(__name__)
class Proxy(QtGui.QAbstractProxyModel):
"""Proxy that groups by based on a specific role
This assumes the source data is a flat list and not a tree.
"""
def __init__(self):
super(Proxy, self).__init__()
self.indices = list()
self.mapping = dict() # source row to proxy row
self.headers = dict() # header proxy indices to group role data
self.index_mapping = dict() # proxy index to source index
self.group_role = QtCore.Qt.DisplayRole
def set_group_role(self, role):
self.group_role = role
def rebuild(self):
"""Update mappings and sections upon source model changes"""
# Clear previous information
self.mapping.clear()
self.headers.clear()
self.index_mapping.clear()
# Get indices from source model
source = self.sourceModel()
source_rows = source.rowCount()
source_indices = [source.index(i, 0) for i in range(source_rows)]
# Group by sort role
key_getter = lambda source_index: source.data(source_index,
self.group_role)
# Collect rows
row = 0
for section, group in groupby(source_indices, key=key_getter):
self.headers[row] = section # header label
header_index = self.createIndex(row, 0, None)
self.indices.append(header_index)
row += 1
for index in group:
proxy_index = self.createIndex(row, 0, index)
self.mapping[index] = row
self.index_mapping[proxy_index.row()] = index
self.indices.append(proxy_index)
row += 1
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
# Override data methods for section headers because they can't
# return data from the source model.
if self.is_header(index):
row = index.row()
section = self.headers.get(row, "fallback")
if role == QtCore.Qt.DisplayRole:
return "Section: {0}".format(section)
if role == QtCore.Qt.FontRole:
font = QtGui.QFont()
font.setBold(True)
font.setPointSize(11)
return font
return None
else:
source_index = self.index_mapping.get(index.row(), None)
if source_index is None:
log.warning("No index mapping for non-header. "
"This would be a bug for index: {0}".format(index))
return QtCore.QModelIndex()
if not source_index.isValid():
return
if source_index is index:
return
source = self.sourceModel()
data = source.data(source_index, role)
return data
def is_header(self, index):
"""Return whether index is a header"""
if index.row() in self.index_mapping:
return False
else:
return True
def mapFromSource(self, index):
if index.row() not in self.mapping:
return QtCore.QModelIndex()
return self.createIndex(self.mapping[index], index.column())
def mapToSource(self, index):
if index.row() not in self.index_mapping:
return QtCore.QModelIndex()
return self.index_mapping[index.row()]
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def rowCount(self, parent):
count = len(self.indices) if not parent.isValid() else 0
return count
def index(self, row, column, parent):
if parent.isValid():
return QtCore.QModelIndex()
return self.createIndex(row, column)
def parent(self, index):
return QtCore.QModelIndex() # no parent ever
app = QtGui.QApplication(sys.argv)
model = QtGui.QStringListModel()
model.setStringList(["a", "b", "c", "c", "d", "e", "e"])
proxy = Proxy()
proxy.set_group_role(QtCore.Qt.DisplayRole)
proxy.setSourceModel(model)
proxy.rebuild()
view = QtGui.QListView()
view.setWindowTitle("My View")
view.setModel(proxy)
view.setAlternatingRowColors(True)
view.show()
app.exec_() The code groups by the display role, so by the actual string in the QStringListModel. But other roles should work when present. Theoretically you could group a flat file system structure by a "file size" or "file extension" role. This grouping by display role results in: Again, this is just a quick experiment. Hope it helps. :) I think it'll actually be easier to have the proxy remap it to an actual "Tree" so things like collapsing and functions (like toggling) on all children become easier and more sensical. |
I agree. Since you can flatten a tree to look like a view (http://stackoverflow.com/questions/21564976/how-to-create-a-proxy-model-that-would-flatten-nodes-of-a-qabstractitemmodel-int) |
@BigRoy wouldn't this method need to be called every time the source model is updated? In which case you would need to call it from the source model, or connect a slot if one exists for when the source model changes. |
Exactly. I noticed the Even better would be if it could be implemented in a way where only actually changed indices are updated instead of the whole proxy, that way it could be somewhat optimized. With the downside of the code being a bit more complex. |
Also, some more experimenting. Here's a similar implementation that remaps the flat list to a treeview grouping: import sys
from PySide import QtCore, QtGui
from itertools import groupby
import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
class Node(object):
def __init__(self):
self._parent = None
self._children = list()
def parent(self):
return self._parent
def addChild(self, node):
node._parent = self
self._children.append(node)
def rowCount(self):
return len(self._children)
def row(self):
parent = self.parent()
if not parent:
return 0
else:
return self.parent().children().index(self)
def columnCount(self):
return 1
def child(self, row):
return self._children[row]
def children(self):
return self._children
def data(self, role=QtCore.Qt.DisplayRole):
return None
class ProxyItem(Node):
def __init__(self, source_index):
super(ProxyItem, self).__init__()
self.source_index = source_index
def data(self, role=QtCore.Qt.DisplayRole):
#print self.source_index.data(role)
return self.source_index.data(role)
class ProxySectionItem(Node):
def __init__(self, label):
super(ProxySectionItem, self).__init__()
self.label = "{0}".format(label)
def data(self, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
return self.label
elif role == QtCore.Qt.FontRole:
font = QtGui.QFont()
font.setBold(True)
font.setPointSize(15)
return font
class Proxy(QtGui.QAbstractProxyModel):
"""Proxy that groups by based on a specific role
This assumes the source data is a flat list and not a tree.
"""
def __init__(self):
super(Proxy, self).__init__()
self.root = Node()
# TODO: I think this could even do without storing such mapping?
self.to_source = dict() # proxy index to source index
self.from_source = dict() # from source row to proxy index
self.group_role = QtCore.Qt.DisplayRole
def set_group_role(self, role):
self.group_role = role
def rebuild(self):
"""Update mappings and sections upon source model changes"""
# Clear previous information
self.to_source.clear()
self.from_source.clear()
self.root = Node()
# Get indices from source model
source = self.sourceModel()
source_rows = source.rowCount()
source_indices = [source.index(i, 0) for i in range(source_rows)]
# Group by sort role
key_getter = lambda source_index: source.data(source_index,
self.group_role)
# Collect rows
section_num = 0
for section, group in groupby(source_indices, key=key_getter):
# section
section_item = ProxySectionItem(section)
section_num += 1
self.root.addChild(section_item)
# items in section
for i, index in enumerate(group):
proxy_item = ProxyItem(index)
section_item.addChild(proxy_item)
# TODO: Check if we can do without this code
proxy_index = self.createIndex(i, 0, proxy_item)
self.to_source[proxy_index] = index
self.from_source[index] = proxy_index
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
node = index.internalPointer()
if not node:
return
return node.data(role)
def is_header(self, index):
"""Return whether index is a header"""
if index in self.to_source:
return False
else:
return True
def mapFromSource(self, index):
if index not in self.from_source:
return QtCore.QModelIndex()
return self.createIndex(self.from_source[index].row(), index.column())
def mapToSource(self, index):
if index not in self.to_source:
return QtCore.QModelIndex()
return self.to_source[index]
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def rowCount(self, parent):
if not parent.isValid():
node = self.root
else:
node = parent.internalPointer()
if not node:
return 0
return node.rowCount()
def index(self, row, column, parent):
if parent.isValid():
parent_node = parent.internalPointer()
else:
parent_node = self.root
item = parent_node.child(row)
if item:
return self.createIndex(row, column, item)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
node = index.internalPointer()
if not node:
return QtCore.QModelIndex()
else:
parent = node.parent()
if not parent:
return QtCore.QModelIndex()
row = parent.row()
return self.createIndex(row, 0, parent)
app = QtGui.QApplication(sys.argv)
model = QtGui.QStringListModel()
model.setStringList(["a", "b", "c", "c", "d", "e", "e", "ff", "ff", "ff",
"g", "a"])
proxy = Proxy()
proxy.set_group_role(QtCore.Qt.DisplayRole)
proxy.setSourceModel(model)
proxy.rebuild()
view = QtGui.QTreeView()
view.setWindowTitle("My View")
view.setModel(proxy)
view.setHeaderHidden(True)
view.setAlternatingRowColors(True)
view.show()
app.exec_() I'm assuming this can be simplified some more, and especially cleaned up a lot. But at least it is working! :) |
Hey Roy, how come you moved away from the data we've modeled? I'm having trouble seeing how this can be applied to the actual data, since they are all single values (strings) and sections seem based on that rather than some property of an actual object. If you can, it'd be great to apply your idea to this data, just so we're sure it will actually work with the real problem. data = [{"label": "Ben", "section": "Human"},
{"label": "Steve", "section": "Human"},
{"label": "Alpha12", "section": "Robot"},
{"label": "Mike", "section": "Toaster"},
{"label": "Steve", "section": "Human"}] |
Actually, the idea is that as long as the source model holds true to Qt's Model/View implementations this should work. It doesn't filter just on "random" values but retrieves the data by a data role from the source model using its real original index (QModelIndex). As such if our custom model works like it should this should just work one to one. The tricky bit was that the 'test model' that was there to begin with didn't seem to be actually a fully valid model in Qt. I noticed that was mostly just me messing up in my proxy, but I found it easier to think more abstract with the simpler data plus wanted to make sure it wasn't messing up just because we had custom data. Anyway, here's a quick working version with the model that was used before. Note that this is the exact same Proxy as shown before, but now grouping by a different role; import sys
from PySide import QtCore, QtGui
import sys
from PySide import QtCore, QtGui
from itertools import groupby
import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
log = logging.getLogger(__name__)
LabelRole = QtCore.Qt.DisplayRole
SectionRole = QtCore.Qt.UserRole + 1
IsSectionRole = QtCore.Qt.UserRole + 2
class Item(object):
@classmethod
def paint(cls, painter, option, index):
rect = QtCore.QRectF(option.rect)
painter.save()
if option.state & QtGui.QStyle.State_MouseOver:
painter.fillRect(rect, QtGui.QColor("#DEE"))
if option.state & QtGui.QStyle.State_Selected:
painter.fillRect(rect, QtGui.QColor("#CDD"))
painter.drawText(rect.adjusted(20, 0, 0, 0),
index.data(LabelRole))
painter.restore()
@classmethod
def sizeHint(cls, option, index):
return QtCore.QSize(option.rect.width(), 20)
class Section(object):
@classmethod
def paint(self, painter, option, index):
painter.save()
painter.setPen(QtGui.QPen(QtGui.QColor("#666")))
painter.drawText(QtCore.QRectF(option.rect), index.data(LabelRole))
painter.restore()
@classmethod
def sizeHint(self, option, index):
return QtCore.QSize(option.rect.width(), 20)
class Delegate(QtGui.QStyledItemDelegate):
def paint(self, painter, option, index):
if index.data(IsSectionRole):
return SectionRole.paint(painter, option, index)
else:
return Item.paint(painter, option, index)
def sizeHint(self, option, index):
if index.data(IsSectionRole):
return SectionRole.sizeHint(option, index)
else:
return Item.sizeHint(option, index)
class Model(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super(Model, self).__init__(parent)
self.items = list()
def data(self, index, role):
item = self.items[index.row()]
return {
LabelRole: item["label"],
SectionRole: item["section"],
IsSectionRole: False
}.get(role)
def append(self, item):
self.beginInsertRows(QtCore.QModelIndex(),
self.rowCount(),
self.rowCount())
self.items.append(item)
self.endInsertRows()
def rowCount(self, parent=None):
return len(self.items)
class Node(object):
def __init__(self):
self._parent = None
self._children = list()
def parent(self):
return self._parent
def addChild(self, node):
node._parent = self
self._children.append(node)
def rowCount(self):
return len(self._children)
def row(self):
parent = self.parent()
if not parent:
return 0
else:
return self.parent().children().index(self)
def columnCount(self):
return 1
def child(self, row):
return self._children[row]
def children(self):
return self._children
def data(self, role=QtCore.Qt.DisplayRole):
return None
class ProxyItem(Node):
def __init__(self, source_index):
super(ProxyItem, self).__init__()
self.source_index = source_index
def data(self, role=QtCore.Qt.DisplayRole):
#print self.source_index.data(role)
return self.source_index.data(role)
class ProxySectionItem(Node):
def __init__(self, label):
super(ProxySectionItem, self).__init__()
self.label = "{0}".format(label)
def data(self, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
return self.label
elif role == QtCore.Qt.FontRole:
font = QtGui.QFont()
font.setBold(True)
font.setPointSize(15)
return font
class Proxy(QtGui.QAbstractProxyModel):
"""Proxy that groups by based on a specific role
This assumes the source data is a flat list and not a tree.
"""
def __init__(self):
super(Proxy, self).__init__()
self.root = Node()
# TODO: I think this could even do without storing such mapping?
self.to_source = dict() # proxy index to source index
self.from_source = dict() # from source row to proxy index
self.group_role = QtCore.Qt.DisplayRole
def set_group_role(self, role):
self.group_role = role
def rebuild(self):
"""Update mappings and sections upon source model changes"""
# Clear previous information
self.to_source.clear()
self.from_source.clear()
self.root = Node()
# Get indices from source model
source = self.sourceModel()
source_rows = source.rowCount()
source_indices = [source.index(i, 0) for i in range(source_rows)]
# Group by sort role
key_getter = lambda source_index: source.data(source_index,
self.group_role)
# Collect rows
section_num = 0
for section, group in groupby(source_indices, key=key_getter):
# section
section_item = ProxySectionItem(section)
section_num += 1
self.root.addChild(section_item)
# items in section
for i, index in enumerate(group):
proxy_item = ProxyItem(index)
section_item.addChild(proxy_item)
# TODO: Check if we can do without this code
proxy_index = self.createIndex(i, 0, proxy_item)
self.to_source[proxy_index] = index
self.from_source[index] = proxy_index
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
node = index.internalPointer()
if not node:
return
return node.data(role)
def is_header(self, index):
"""Return whether index is a header"""
if index in self.to_source:
return False
else:
return True
def mapFromSource(self, index):
if index not in self.from_source:
return QtCore.QModelIndex()
return self.createIndex(self.from_source[index].row(), index.column())
def mapToSource(self, index):
if index not in self.to_source:
return QtCore.QModelIndex()
return self.to_source[index]
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def rowCount(self, parent):
if not parent.isValid():
node = self.root
else:
node = parent.internalPointer()
if not node:
return 0
return node.rowCount()
def index(self, row, column, parent):
if parent.isValid():
parent_node = parent.internalPointer()
else:
parent_node = self.root
item = parent_node.child(row)
if item:
return self.createIndex(row, column, item)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
node = index.internalPointer()
if not node:
return QtCore.QModelIndex()
else:
parent = node.parent()
if not parent:
return QtCore.QModelIndex()
row = parent.row()
return self.createIndex(row, 0, parent)
app = QtGui.QApplication(sys.argv)
model = Model()
data = [{"label": "Ben", "section": "Human"},
{"label": "Steve", "section": "Human"},
{"label": "Alpha12", "section": "Robot"},
{"label": "Beta06", "section": "Robot"},
{"label": "Mike", "section": "Toaster"},
{"label": "Steve", "section": "Human"},
{"label": "Jack", "section": "Human"},
{"label": "Stella", "section": "Human"}]
for item in data:
model.append(item)
proxy = Proxy()
proxy.set_group_role(SectionRole)
proxy.setSourceModel(model)
proxy.rebuild()
view = QtGui.QTreeView()
view.setWindowTitle("My View")
view.setModel(proxy)
view.setHeaderHidden(True)
view.setAlternatingRowColors(True)
view.show()
app.exec_() The group by implemention doesn't sort the data upfront and works like python's Does that help? Note that the delegate doesn't work with this proxy currently, but that could be fixed of course. Consider the Proxy some 'pseudocode' as I didn't really clean it up yet. 😄 |
I think that's great!
Which is the right thing to do! Remember that we're using this proxy with instances and plug-ins, and their original order is important.
That's what I'd expect. What I wasn't expecting is So again, I think this looks great! I say let's plug it in to see what the costs are (performance, accuracy, maintainability etc). |
Yeah, it definitely isn't in there, although the qt version PySide uses should support it. Maybe they haven't exposed it? |
Before that I'd say it's important to look at:
As described in point 1 here it should work just as well if we know when its optimal to update for our model. Then at least we can connect the callback. Nevertheless if you want a generic Qt GroupProxy solution it could be interesting to investigate more into that. |
Here's a cleaned up version of the proxy (this should mapFromSource and mapToSource correctly!) import sys
from PySide import QtCore, QtGui
from itertools import groupby
LabelRole = QtCore.Qt.DisplayRole
SectionRole = QtCore.Qt.UserRole + 1
IsSectionRole = QtCore.Qt.UserRole + 2
class Model(QtCore.QAbstractListModel):
"""Simple Sample Model"""
def __init__(self, parent=None):
super(Model, self).__init__(parent)
self.items = list()
def data(self, index, role):
item = self.items[index.row()]
return {
LabelRole: item["label"],
SectionRole: item["section"],
IsSectionRole: False
}.get(role)
def append(self, item):
self.beginInsertRows(QtCore.QModelIndex(),
self.rowCount(),
self.rowCount())
self.items.append(item)
self.endInsertRows()
def rowCount(self, parent=None):
return len(self.items)
class Item(object):
"""Base class for an Item in the Group By Proxy"""
def __init__(self):
self._parent = None
self._children = list()
def parent(self):
return self._parent
def addChild(self, node):
node._parent = self
self._children.append(node)
def rowCount(self):
return len(self._children)
def row(self):
parent = self.parent()
if not parent:
return 0
else:
return self.parent().children().index(self)
def columnCount(self):
return 1
def child(self, row):
return self._children[row]
def children(self):
return self._children
def data(self, role=QtCore.Qt.DisplayRole):
return None
class ProxyItem(Item):
def __init__(self, source_index):
super(ProxyItem, self).__init__()
self.source_index = source_index
def data(self, role=QtCore.Qt.DisplayRole):
return self.source_index.data(role)
class ProxySectionItem(Item):
def __init__(self, label):
super(ProxySectionItem, self).__init__()
self.label = "{0}".format(label)
def data(self, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
return self.label
elif role == QtCore.Qt.FontRole:
font = QtGui.QFont()
font.setPointSize(10)
font.setWeight(900)
return font
elif role == QtCore.Qt.TextColorRole:
return QtGui.QColor(50, 20, 20)
elif role == QtCore.Qt.BackgroundColorRole:
return QtGui.QColor(220, 220, 220)
class Proxy(QtGui.QAbstractProxyModel):
"""Proxy that groups by based on a specific role
This assumes the source data is a flat list and not a tree.
"""
def __init__(self):
super(Proxy, self).__init__()
self.root = Item()
self.group_role = QtCore.Qt.DisplayRole
def set_group_role(self, role):
self.group_role = role
def rebuild(self):
"""Update proxy sections and items
This should be called after changes in the source model that require
changes in this list (for example new indices, less indices or update
sections)
"""
# Start with new root node
self.root = Item()
# Get indices from source model
source = self.sourceModel()
source_rows = source.rowCount()
source_indices = [source.index(i, 0) for i in range(source_rows)]
def key_getter(source_index):
"""Return group role data for source index"""
return source.data(source_index, self.group_role)
for section, group in groupby(source_indices, key=key_getter):
# section
section_item = ProxySectionItem(section)
self.root.addChild(section_item)
# items in section
for i, index in enumerate(group):
proxy_item = ProxyItem(index)
section_item.addChild(proxy_item)
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
node = index.internalPointer()
if not node:
return
return node.data(role)
def is_header(self, index):
"""Return whether index is a header"""
if index in self.to_source:
return False
else:
return True
def mapFromSource(self, index):
for section_item in self.root.children():
for item in section_item.children():
if item.source_index == index:
return self.createIndex(item.row(),
index.column(),
item)
return QtCore.QModelIndex()
def mapToSource(self, index):
if not index.isValid():
return QtCore.QModelIndex()
node = index.internalPointer()
if not node:
return QtCore.QModelIndex()
if not hasattr(node, "source_index"):
return QtCore.QModelIndex()
return node.source_index
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def rowCount(self, parent):
if not parent.isValid():
node = self.root
else:
node = parent.internalPointer()
if not node:
return 0
return node.rowCount()
def index(self, row, column, parent):
if parent and parent.isValid():
parent_node = parent.internalPointer()
else:
parent_node = self.root
item = parent_node.child(row)
if item:
return self.createIndex(row, column, item)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
node = index.internalPointer()
if not node:
return QtCore.QModelIndex()
else:
parent = node.parent()
if not parent:
return QtCore.QModelIndex()
row = parent.row()
return self.createIndex(row, 0, parent)
app = QtGui.QApplication(sys.argv)
model = Model()
data = [{"label": "Ben", "section": "Human"},
{"label": "Steve", "section": "Human"},
{"label": "Alpha12", "section": "Robot"},
{"label": "Beta06", "section": "Robot"},
{"label": "Mike", "section": "Toaster"},
{"label": "Steve", "section": "Human"},
{"label": "Jack", "section": "Human"},
{"label": "Stella", "section": "Human"}]
for item in data:
model.append(item)
proxy = Proxy()
proxy.set_group_role(SectionRole)
proxy.setSourceModel(model)
proxy.rebuild()
view = QtGui.QTreeView()
view.setWindowTitle("My View")
view.setModel(proxy)
view.setHeaderHidden(True)
view.setRootIsDecorated(False)
view.setIndentation(10)
view.setItemsExpandable(False)
view.expandAll()
view.show()
app.exec_() This result: At this point I think the bigger step is to know when to update the proxy model and hook it up to a callback. And basically "plugging it in" |
Plug it in and try. Doo iiit! :D |
Now I just need to remap it to Qt.py so it seems! The horror! Haha. Working on it! |
Go Go @BigRoy :) |
Not exactly what I was expecting as first result. :) I couldn't find the exact callbacks to connect to so I hooked it up to all the controller's signal for now, just to ensure it updates whenever. Only left side has a proxy there. Just for testing purposes. Also because I couldn't find an "OrderRule" that I could retrieve using the data method for the right list. |
For sake of reference (and little time to experiment, clean up and improve on this) I've pushed the state above into a branch on my fork so you can play around with it: https://github.com/BigRoy/pyblish-lite/tree/sections Whenever I'll have some time on my hands I'll try to experiment some more of course. :) Also you'll see some of the signals on the TreeView (currently in tree.py, even though I'm assuming it shouldn't end up there) that is as much as possible a copy of the list view will need some reimplementing to work on the actual tree view. E.g. you won't be able to toggle instances yet. |
Here's a preview of the current state: I bumped up the indentation (bit extreme) for debugging purposes to have a clear view of what is going on. This has some of the sections collapsed. Pretty experimental and not usable other than visually getting closer to what we are looking for. Playing around with it a bit it's good to see the coloring (success/failure) seems to be working without changes to the proxy, also the middle-mouse click on items still show their info. |
This looks great. For the family, have a look at how qml does it. Other than that, I think it's a good time to make a it a PR and talk code and design. |
Basically I would like the source model to give me the necessary information through a data role. There doesn't seem to be a way to retrieve the
Sure. Would be great if this could "be played with" even though it's hardly functioning like the list view version, because toggling/actions don't work yet. Also would love to see how much this still works with @tokejepsen 's work on Action icons, etc. The idea of course is to have these kind of things work with at least re-work done when swapping to a TreeView. |
Pull-request please. |
Done. |
…after_processing Feature/PYPE-413 buttons after processing
|
Goal
In Pyblish QML, groups of items have a handy section label. Implement similar functionality for Lite.
Implementation
In QML, the built-in ListView has a
section
attribute that does exactly this. In the case of Widgets, we'll need to implement this ourselves.One way would be to add null items to the actual models, make them inactive and they would appear in the proper positions and faded.
At this point, this should be fine. But a more proper method is to use a proxy model and add these "virtual" items there. In that way, the model contains real data and proxies provide necessary auxiliary data, such as sections.
We'll need proxy models regardless, for sorting and filtering, so it could be good idea to keep things uniform and hack less.
The text was updated successfully, but these errors were encountered: