How to add extra data to qfilesystemmodel items? - qt

I'm trying to make an pyqt app, which examines directory structure and the files in it.
What I want to do is to add some extra info to the items in the qfilesystemmodel, for example, tagging files as 'checked' or 'unchecked'.
I have found that each item (file or folder) in the model has four columns, name, size, timestamp and type.
Is it possible to make the item have fifth column, which will contain custom information (tags) ?
Or, Is there any way to annotate extra info on the items in the model ?
If not, I think I have to have another model, such as qstanarditemmodel, to keep the tags. But I don't want it to be in a saperated model.
Thanks!

The QFileSystemModel class delegates to a live file-system. So it doesn't really contain any items that you can add information to. The items are actually the files and directories inside the file-system.
Given this, it follows that a second data structure will be required to hold the additional information. Otherwise, you'd need to somehow store the information in the files and directories themselves. This might be possible for certain file-types (e.g. images). But it is obviously unfeasible to do this for arbitrary file-types.
It should be possible to sub-class QFileSystemModel and reimplement the usual methods so that extra columns can be added. As a bare minimum, you would need to reimplement columnCount() and data() - but obviously the exact details of how you go about this will depend on the data structure you choose to hold the additional information. Very roughly, it might look something like this:
class FileSystemModel(QFileSystemModel):
def __init__(self, parent=None):
super(FileSystemModel, self).__init__(parent)
self._data = DataStructure()
def columnCount(self, parent):
return super(FileSystemModel, self).columnCount(parent) + 1
def data(self, index, role):
if index.isValid() and index.column() == self.columnCount() - 1:
if role == QtCore.Qt.DisplayRole:
# return the relevant additional data
elif role == QtCore.Qt.CheckStateRole:
# etc, etc
else:
return super(FileSystemModel, self).data(index,role)

In short, no, not easily. The Qt developers stopped working on QFileSystemModel for complexity reasons. Basically, the backend file system engine is way too complex and fragile, and QFileSystemModel relies entirely upon it.
You could reimplement QFileSystemModel, but it would require a lot of painful work and accessing the private headers.

Related

QMimeData: setting and getting the right MIME types for arbitrary widgets

Using PySide, I construct a draggable label that works exactly how I want:
class DraggableLabel(QtGui.QLabel):
def __init__(self, txt, parent):
QtGui.QLabel.__init__(self, txt, parent)
self.setStyleSheet("QLabel { background-color: rgb(255, 255, 0)}")
def mouseMoveEvent(self, event):
drag=QtGui.QDrag(self)
dragMimeData=QtCore.QMimeData()
drag.setMimeData(dragMimeData)
drag.exec_(QtCore.Qt.MoveAction)
(Note a full example that uses DraggableLabel is pasted below). Unfortunately, I do not understand what is going on with QMimeData, and fear I am going to run into big problems when I use similar code in real-world examples.
In particular, I am worried that my reimplementation of mouseMoveEvent creates an instance of QMimeData without any argument passed: QtCore.QMimeData(). Is this normal? Within more complex widgets will I be OK if I keep doing that within the relevant event handler: will the program automatically create the right type of MIME data for dragging and dropping?
The reason I fear I am missing something is because at the Qt Drag and Drop documentation, it has lines of code like:
mimeData -> setText(commentEdit->toPlainText());
which seems decidedly not like just letting the program take care of things within a reimplementation of an event handler.
Also, the QMimeData Documentation discusses convenience functions to test, get, and set data, but those are for standard data types (e.g., text, urls). I have found no clear way to define such convenience functions for widgets like my draggable QLabel. Am I missing it? Is there a simple way to find out if I am dragging around a widget of type X?
Edit: I've tried the same code above with much more complicated widgets than QLabels, and it does not work.
Potentially relevant posts:
Dragging a QWidget in QT 5
How to Drag and Drop Custom Widgets?
https://stackoverflow.com/questions/18272650/fill-the-system-clipboard-with-data-of-custom-mime-type
Python object in QMimeData
Important Caveat: if you just want to move a widget in a window, you do not need to invoke esoteric drag-drop mechanisms, but more vanilla event handling. See this: Dragging/Moving a QPushButton in PyQt.
Full working self-contained code example that incorporates the above:
# -*- coding: utf-8 -*-
from PySide import QtGui, QtCore
class LabelDrag(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.initUI()
def initUI(self):
self.lbl=DraggableLabel("Drag me", self)
self.setAcceptDrops(True)
self.setGeometry(40,50,200,200)
self.show()
def dragEnterEvent(self,event):
event.accept()
def dropEvent(self, event):
self.lbl.move(event.pos()) #moves label to position once the movement finishes (dropped)
event.accept()
class DraggableLabel(QtGui.QLabel):
def __init__(self, txt, parent):
QtGui.QLabel.__init__(self, txt, parent)
self.setStyleSheet("QLabel { background-color: rgb(255, 255, 0)}")
def mouseMoveEvent(self, event):
drag=QtGui.QDrag(self)
dragMimeData=QtCore.QMimeData()
drag.setMimeData(dragMimeData)
drag.exec_(QtCore.Qt.MoveAction)
def main():
import sys
qt_app=QtGui.QApplication(sys.argv)
myMover=LabelDrag()
sys.exit(qt_app.exec_())
if __name__=="__main__":
main()
Note I'm posting this at QtCentre as well, will post anything useful from there.
In my experience, MimeData is used to filter drag/drop operations so that the action actually makes sense. For instance, you shouldn't be able to drag your QLabel into the middle of a QTextEdit or your browsers address bar, or the desktop of your computer.
From the docs:
QMimeData is used to describe information that can be stored in the clipboard, and transferred via the drag and drop mechanism. QMimeData objects associate the data that they hold with the corresponding MIME types to ensure that information can be safely transferred between applications, and copied around within the same application.
If you were doing something standard, like dragging/dropping text from one place to another, you would use one of the standard MIME types (like setting the MIME data of your drag using dragMimeData.setText('your text') or equivalently dragMimeData.setData('text/plain', 'your text')). However, since you are doing something completely custom, you should probably specify a custom MIME type so that you can't accidentally do things that don't make sense.
So I would set the MIME data to something like dragMimeData.setData('MoveQLabel', QByteArray('any string you want')) which stores an arbitrary string for the MIME type MoveQLabel. This arbitrary string could be used to look up which widget you want to move at the end of the drag (maybe by storing it's position?).
You should modify your LabelDrag class above to check the MIME type of the event (use event.mimeData() to get the QMimeData object you set when you started the drag), and accept or reject the event depending on whether the MIME type matches MoveQLabel (or whatever you call your custom MIME type). This should be done in both the dragEnterEvent and dropEvent methods.
You code would look something like:
def dragEnterEvent(self, event):
# you may also need to check that event.mimeData() returns a valid QMimeData object
if event.mimeData().hasFormat('MoveQLabel'):
event.accept()
else:
event.reject()
If you also store some useful MIME data with the MIME type (aka something else instead of 'any string you want' above), you could use it to dynamically select, within LabelDrag.dropEvent, which widget it is that you want to move. You can access it through event.mimeData().data('MoveQLabel'). This means that your QWidget can handle moving multiple labels as it will always move the one that is being dropped.

Deleting a row which has children in QTreeView

I want to delete a row which has children from a QTreeView. I use QAbstractItemModel's removeRow ( int row, const QModelIndex & parent = QModelIndex() ) method,pass the row and parent index of the deleted row. But this method returns false.
How can I delete a row which has children? Do I need write a method to recursively delete rows?
You should look at this: removeRow
This is a convenience function that calls removeRows(). The QAbstractItemModel implementation of removeRows() does nothing.
And here in QAbstractItemModel::removeRows():
The base class implementation does nothing and returns false.
If you implement your own model, you can reimplement this function if you want to support removing. Alternatively, you can provide your own API for altering the data.
I know I'm a bit late to the party, but I wanted to document this because I can't seem to find any good answer to the original question.
You need to implement QAbstractItemModel::removeRows in your model. There is a Qt example, Editable Tree Model, that shows how this should be done. The basic procedure is to call beginRemoveRows, delete the item(s), then call endRemoveRows.
Your tree item class should have a method for deleting a range of its child items. You call this method on the parent item from within your reimplementation of removeRows. The tree item class should be set up to delete its children when it is destroyed. The Qt example does this through the destructor, though with C++11 and later this is best done by storing children in a container of smart pointers so they get automatically deleted when the container goes out of scope.
You do not need to account for child items being collapsed or expanded in the tree view - QTreeView knows whether or not child items are visible or not and will update the view accordingly. In other words, if you delete a tree item with a few child items that were visible in the tree view, they will be automatically removed from the view. This is something that I can't find documented anywhere, but from personal experience I can say that it works (as long as you call beginRemoveRows and endRemoveRows correctly).

A clean and intuitive way to implement a list of clickable strings to invoke functions?

For the Qt App I'm writing, I'd like to have a list of clickable functions, which, when clicked will allow the user to supply required input arguments.
What I'm specifically looking for is a selection of widgets which provide a clean and intuitive interface for the following tasks:
User scrolls through a list of functions for performing computations (in my case, from glm).
Once a function is found, the user clicks on the item; a popup window opens, which specifies the required input arguments (e.g., vec3, vec4, etc.).
The idea here is that the functions themselves already exist: they just need an interface, which in a nutshell, provides a pseudo interpreter to process and output their results to a GLWidget, which will update the data passed accordingly by sending it to its corresponding shader.
I've looked at QListView, and its Widget variant, but it appears to be more suited towards filesystem data, such as images or text files, though I'm not quite sure. So far, it seems to be the only thing which could be considered as realistically usable in this scenario.
Is there a recommended way to do this? I'm fairly new to Qt in general, thus my knowledge is pretty limited.
The view isn't really important in your case. You need to create/reuse a adapted model.
This model have to contain the relation between what your view displays and the action that you want to launch.
For example, if your commands are text like bash commands, you can create a view that displays "list files", "Copy files" and a model that contains the data ("list files" = 'ls -l'), ("copy files" = 'ls -l'), etc.
You can store different data (using QVariant) in a same item with different roles: Qt::DisplayRole corresponds to the data that the view displays and Qt::UserRole what you want.
So, if you only have to store a command line associated to a name, you can store the name in the item with the Qt::DisplayRole and the command line as a QString (or other) using Qt::UserRole.
See QAbstractItemModel::data() and QAbstractItemModel::setData(), for more information.

QDataWidgetMapper and multiple delegates

mapper = QtGui.QDataWidgetMapper()
mapper.setModel(my_table_model)
mapper.addMapping(widgetA, 0) #mapping widget to a column
mapper.addMapping(widgetB, 1) #mapping widget to a column
mapper.setItemDelegate(MyDelegateA(widgetA)) #Hmm. Where is the 'column' parameter?
mapper.setItemDelegate(MyDelegateB(widgetB)) #now itemDelegate is rewritten, MyDelegateB will be used
So... How do I set up mutiple delegates for a single QDataWidgetMapper? As far as I understand there is no QDataWidgetMapper.setItemDelegateForColumn() Or do I have to create some delegate factory, which will use appropriate delegates?
Thanks!
You have to use one single delegate and handle the way behavior of the different widgets in the setEditorData and setModelData functions of the delegate. For an example (C++ but straight forward) check this article from Qt Quarterly.
Ok, I got it. (At least, it works for me). So, the main idea is this class (a simplified version), which keeps a list of real delegate instances and routes data to\from them:
class DelegateProxy(QtGui.QStyledItemDelegate):
def __init__(self, delegates, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.delegates = delegates
def setEditorData(self, editor, index):
delegate = self.delegates[index.column()]
delegate.setEditorData(editor, index)
def setModelData(self, editor, model, index):
delegate = self.delegates[index.column()]
delegate.setModelData(editor, model, index)
Fully working example is in the pastebin
I found this problem too, and it really sucks. I'm right now trying to subclass QtGui.QDataWidgetMapper in order to workaround this, the subclass having its own addMapping() with a delegate argument, a dict to store the delegate for each widget, and a matching meta-delegate that calls the appropiate delegate for each case.
The weirdest thing about this is the problem also existed in earlier versions of Qt 4 in QAbstractItemView (i.e tables and trees) and later was fixed adding the setItemDelegateForColumn() method, but QDataWidgetMapper didn't get the fix.
An alternative could be using more than a mapper, and connect them to keep them in sync if necessary, but it is a bit messy, specially if you need lots of different special delegates:
mainMapper = QtGui.QDataWidgetMapper()
mainMapper.setModel(my_table_model)
auxMapper1 = QtGui.QDataWidgetMapper()
auxMapper1.setModel(my_table_model)
# If you move the index in the main mapper, the auxiliary will follow
mainMapper.currentIndexChanged.connect(auxMapper1.setCurrentIndex)
mainMapper.addMapping(widgetA, 0) #mapping widget to a column
auxMapper1.addMapping(widgetB, 1) #mapping widget to a column
mainMapper.setItemDelegate(MyDelegateA(widgetA))
auxMapper1.setItemDelegate(MyDelegateB(widgetB))

Change text of QStandardItem when editing starts

Using Qt4's model/view framework, I have a list of items displayed as "Foo (38 bars)". I want to make the text editable, but when the user starts editing, I want the control to just show "Foo" (the name), and then to reinstate the extra info "(38 bars)" when editing is complete.
I think I can work out how to add the info back, but is there a way to change the text of the item when editing starts?
The model is a QStandardItemModel, and the items are fairly trivial subclasses of QStandardItem. They are displayed primarily in a QListView.
The UI is written in PyQt, and I'd prefer not to dive into C++ just for this, but I'll do my best to translate C++ solutions if they appear.
After #Chris put me on the right track, I found this note in the docs for QStandardItem:
The default implementation treats Qt::EditRole and Qt::DisplayRole as
referring to the same data.
So I needed to override the method QStandardItem.data(). My Python code looked like this:
def data(self, role=QtCore.Qt.UserRole+1):
if role == QtCore.Qt.DisplayRole:
return "{} ({} bars)".format(self.name, len(self.ds))
return super().data(role) # Fall back to the default method
On the model's itemChanged signal, I updated the .name attribute I'm using:
def update_name(self):
self.name = self.data(QtCore.Qt.EditRole)
Finally, the number of "bars" can be changed elsewhere, and this should trigger any views to update the item. So after changing that, I called the item.emitDataChanged() method (docs).
With this done, it seems to be working as intended.
You should be able to set the Qt::EditRole data on your items to achieve this. In C++ it would be:
myStandareItem->setText( "Foo (38 bars)" );
myStandardItem->setData( Qt::EditRole, "Foo" );
When Qt sees something in the EditRole, it will use that for editing purposes instead of text (which is stored under the Qt::DisplayRole).
You can then use things like the QStandardItemModel::itemChanged() signal to update the display text of the item when it gets edited.

Resources