QFileSystemModel sorting DirsFirst - qt

How do you do to sort a QFileSystemModel with QDir::DirsFirst like in QDirModel?
The QFileSystemModel does not have a setSorting method.

Maybe somebody will need this. I have implemented directories first sorting using QSortFilterProxyModel for QFileSystemModel as Kuba Ober mention in comment.
Might be not perfect yet, but still right direction.
bool MySortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
// If sorting by file names column
if (sortColumn() == 0) {
QFileSystemModel *fsm = qobject_cast<QFileSystemModel*>(sourceModel());
bool asc = sortOrder() == Qt::AscendingOrder ? true : false;
QFileInfo leftFileInfo = fsm->fileInfo(left);
QFileInfo rightFileInfo = fsm->fileInfo(right);
// If DotAndDot move in the beginning
if (sourceModel()->data(left).toString() == "..")
return asc;
if (sourceModel()->data(right).toString() == "..")
return !asc;
// Move dirs upper
if (!leftFileInfo.isDir() && rightFileInfo.isDir()) {
return !asc;
}
if (leftFileInfo.isDir() && !rightFileInfo.isDir()) {
return asc;
}
}
return QSortFilterProxyModel::lessThan(left, right);
}

As far as I can tell, you can't (in Qt4).
The default sort order (by the "name" column), or sorting by size behaves like QDir::DirsFirst (or DirsLast if in reverse order for ), but sorting by time or type doesn't treat directories differently from ordinary files.
The QFileSystemModel doesn't expose an API for changing the sort order, and I don't see any opportunity for influencing it in the QFileSystemModel code.
(I don't see anything in the current Qt5 docs to indicate that this has changed, but those aren't final and I haven't looked very closely.)

Related

Qt Sort using lessThan is extremely slow

I have an QTableView with QSortFilterProxyModel (on QFileSystemModel). I sort it using lessThen comparator like this:
bool SortProxy::lessThan ( const QModelIndex & left, const QModelIndex & right ) const
{
...
return (collator.compare(left.data().toString(),right.data().toString())) > 0;
...
}
And it works fine for sorting files by name, or filetype. But it works extremely slow on sorting files by name. What to do?

Should QAbstractItemModel::index(row, column, parent) check for invalid inputs?

Subclassing QAbstractItemModel, I have implemented my own index() method as required. I currently check for valid inputs, but I'm wondering if that's correct. I'm wondering if it's ever valid to create an index for a non-existent piece of data? Perhaps while inserting a row or column?
Code:
QModelIndex LicenseDataModel::index(int row, int column, const QModelIndex & /*parent*/) const
{
/// TODO: Is this necessary? Should we avoid creating invalid indexes? Or could this
/// be a bug?
if (validRowColumn(row, column))
return createIndex(row, column);
return QModelIndex();
}
[ I have a better answer. :-) ]
Although I merely repeat what Giuseppe D'Angelo from KDAB has to say about this...
You must distinguish between an invalid and an ill-formed QModelIndex:
About invalid indices (from Navigation and model index creation in Qt's Model/View Programming guide):
QAbstractItemModel::parent(): Provides a model index corresponding to the parent of any given child item. If the model index specified corresponds to a top-level item in the model, or if there is no valid parent item in the model, the function must return an invalid model index, created with the empty QModelIndex() constructor.
This explains the meaning of index.isValid():
A valid index refers to an existing item, the invalid index refers to the root of all items.
Giuseppe D'Angelo first notes that an invalid index (with .isValid() returning false) is still a valid input to functions like rowCount(), hasChildren() etc.
But a QModelIndex can also be ill-formed. It can have a non-existing row or column index, and it can even be from a different model. And QModelIndex::isValid() does not check that.
Giuseppe D'Angelo says:
I personally maintain quite a strong point of view about this issue: passing such indices is a violation of the API contract. A model should never be assumed to be able to handle illegal indices. In other words, in my (not so humble) opinion, the QAbstractItemModel API has a narrow contract.
But since All Programmers Make Mistakes (TM), it facilitates the debugging process to check indices for being well-formed. For this purpose, Giuseppe D'Angelo introduced QAbstractItemModel::checkIndex() into Qt 5.11.
If you're still using a lower Qt version, you can simply write that function yourself.
[ If anyone has a better answer, I'll gladly accept it. ]
Looking at the source for QListWidget, it seems that checking the inputs is what Qt does itself:
QModelIndex QListModel::index(int row, int column, const QModelIndex &parent) const
{
if (hasIndex(row, column, parent))
return createIndex(row, column, items.at(row));
return QModelIndex();
}
It also appears that I didn't know about hasIndex() which will do what my validRowColumn() method does.
bool QAbstractItemModel::hasIndex(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column < 0)
return false;
return row < rowCount(parent) && column < columnCount(parent);
}
For that matter, I'm not sure why the documentation uses index.isValid() everywhere when hasIndex(index.row(), index.column(), index.parent()) would be more appropriate. Then, I'm sure a hasIndex(QModelIndex &) method would be added. hasIndex() does the same check as QModelIndex::isValid() and more:
inline bool isValid() const { return (r >= 0) && (c >= 0) && (m != 0); }

QTableView: how to edit non-editable cells in the program?

How should this be done by using the model->setData() method call?
I have derived a class called "MyStandardItemModel" from QStandardItemModel. I have made my third and fourth columns non-editable by overriding the protected virtual flags method. This is how it goes:
#define TX_PACKET_COLUMN (4u)
#define RX_PACKET_COLUMN (5u)
Qt::ItemFlags MyStandardItemModel::flags(const QModelIndex& index) const
{
if (index.column() == TX_PACKET_COLUMN || index.column() == RX_PACKET_COLUMN)
{
return (QStandardItemModel::flags(index) & ~Qt::ItemIsEditable);
}
else
{
return QStandardItemModel::flags(index);
}
}
...
//Set model
ui->testCaseTableView->setModel(model);
Having this done, I am not able to edit the cells in the third and fourth column.
Now, I want that when I double click on these cells, a pop-up dialog comes up. I will modify some data in the editable field of that dialog, and then copy it back to the non editable cells inside the code.
I tried to just write a doubleclick() handler for the QTreeView and just copy some data to the cells just to see if it is possible to copy data to the non-editable cells.
This operation is failing, and the data is not written into the non-editable cells.
Here you can find the double click handler:
void MainWindow::on_testCaseTableView_doubleClicked(const QModelIndex &index)
{
QVariant variant;
variant.toString() = "AA";
if((index.column() == TX_PACKET_COLUMN)||(index.column() == RX_PACKET_COLUMN))
{
model->setData(index, variant); // set new value
}
}
The setData(..) operation is clearing the already written data in the cells, but string "AA" is not getting written. Please suggest how to copy some data to non-editable cells inside the code.
QVariant set is empty. Nothing needs to be wrong in your model. Error is on this line:
variant.toString() = "AA";
change to:
QVariant variant("AA"); // just for testing anyway
As I indicated in my comment, you have to fix this first issue:
instead of:
QVariant variant;
variant.toString() = "AA";
you should write
QVariant variant = QLatin1String("AA");
In general, you would look into the setData(...) implementation for such cases whether you emit the data changed signal properly and so forth, but here you are entering a prior issue which can lead to problems, so let us fix that.
Note, you should use QLatin1String to avoid the unnecessary explicit conversion from raw char* to QString. This is a good practice in general, and this is available with Qt 4 as well as Qt 5.
Although, you could also use the QStringLiteral macro for creating a QString very efficiently with template magic out of your raw literal, but that requires C++11.

QCompleter Custom Completion Rules

I'm using Qt4.6 and I have a QComboBox with a QCompleter in it.
The usual functionality is to provide completion hints (these can be in a dropdown rather than inline - which is my usage) based on a prefix. For example, given
chicken soup
chilli peppers
grilled chicken
entering ch would match chicken soup and chilli peppers but not grilled chicken.
What I want is to be able to enter ch and match all of them or, more specifically, chicken and match chicken soup and grilled chicken.
I also want to be able to assign a tag like chs to chicken soup to produce another match which is not just on the text's content. I can handle the algorithm but,
Which of QCompleter's functions do I need to override?
I'm not really sure where I should be looking...
Based on #j3frea suggestion, here is a working example (using PySide). It appears that the model needs to be set every time splitPath is called (setting the proxy once in setModel doesn't work).
combobox.setEditable(True)
combobox.setInsertPolicy(QComboBox.NoInsert)
class CustomQCompleter(QCompleter):
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
class InnerProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
index0 = self.sourceModel().index(sourceRow, 0, sourceParent)
return local_completion_prefix.lower() in self.sourceModel().data(index0).lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
completer = CustomQCompleter(combobox)
completer.setCompletionMode(QCompleter.PopupCompletion)
completer.setModel(combobox.model())
combobox.setCompleter(completer)
Building on the answer of #Bruno, I am using the standard QSortFilterProxyModel function setFilterRegExp to change the search string. In this way no sub-classing is necessary.
It also fixes a bug in #Bruno's answer, which made the suggestions vanish for some reasons once the input string got corrected with backspace while typing.
class CustomQCompleter(QtGui.QCompleter):
"""
adapted from: http://stackoverflow.com/a/7767999/2156909
"""
def __init__(self, *args):#parent=None):
super(CustomQCompleter, self).__init__(*args)
self.local_completion_prefix = ""
self.source_model = None
self.filterProxyModel = QtGui.QSortFilterProxyModel(self)
self.usingOriginalModel = False
def setModel(self, model):
self.source_model = model
self.filterProxyModel = QtGui.QSortFilterProxyModel(self)
self.filterProxyModel.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(self.filterProxyModel)
self.usingOriginalModel = True
def updateModel(self):
if not self.usingOriginalModel:
self.filterProxyModel.setSourceModel(self.source_model)
pattern = QtCore.QRegExp(self.local_completion_prefix,
QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.FixedString)
self.filterProxyModel.setFilterRegExp(pattern)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
if self.filterProxyModel.rowCount() == 0:
self.usingOriginalModel = False
self.filterProxyModel.setSourceModel(QtGui.QStringListModel([path]))
return [path]
return []
class AutoCompleteComboBox(QtGui.QComboBox):
def __init__(self, *args, **kwargs):
super(AutoCompleteComboBox, self).__init__(*args, **kwargs)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
self.comp = CustomQCompleter(self)
self.comp.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.setCompleter(self.comp)#
self.setModel(["Lola", "Lila", "Cola", 'Lothian'])
def setModel(self, strList):
self.clear()
self.insertItems(0, strList)
self.comp.setModel(self.model())
def focusInEvent(self, event):
self.clearEditText()
super(AutoCompleteComboBox, self).focusInEvent(event)
def keyPressEvent(self, event):
key = event.key()
if key == 16777220:
# Enter (if event.key() == QtCore.Qt.Key_Enter) does not work
# for some reason
# make sure that the completer does not set the
# currentText of the combobox to "" when pressing enter
text = self.currentText()
self.setCompleter(None)
self.setEditText(text)
self.setCompleter(self.comp)
return super(AutoCompleteComboBox, self).keyPressEvent(event)
Update:
I figured that my previous solution worked until the string in the combobox matched none of the list items. Then the QFilterProxyModel was empty and this in turn reseted the text of the combobox. I tried to find an elegant solution to this problem, but I ran into problems (referencing deleted object errors) whenever I tried to change something on self.filterProxyModel. So now the hack is to set the model of self.filterProxyModel everytime new when its pattern is updated. And whenever the pattern does not match anything in the model anymore, to give it a new model that just contains the current text (aka path in splitPath). This might lead to performance issues if you are dealing with very large models, but for me the hack works pretty well.
Update 2:
I realized that this is still not the perfect way to go, because if a new string is typed in the combobox and the user presses enter, the combobox is cleared again. The only way to enter a new string is to select it from the drop down menu after typing.
Update 3:
Now enter works as well. I worked around the reset of the combobox text by simply taking it off charge when the user presses enter. But I put it back in, so that the completion functionality remains in place. If the user decides to do further edits.
Use filterMode : Qt::MatchFlags property. This property holds how the filtering is performed. If filterMode is set to Qt::MatchStartsWith, only those entries that start with the typed characters will be displayed. Qt::MatchContains will display the entries that contain the typed characters, and Qt::MatchEndsWith the ones that end with the typed characters. Currently, only these three modes are implemented. Setting filterMode to any other Qt::MatchFlag will issue a warning, and no action will be performed. The default mode is Qt::MatchStartsWith.
This property was introduced in Qt 5.2.
Access functions:
Qt::MatchFlags filterMode() const
void setFilterMode(Qt::MatchFlags filterMode)
Thanks Thorbjørn,
I actually did solve the problem by inheriting from QSortFilterProxyModel.
The filterAcceptsRow method must be overwritten and then you just return true or false depending on whether or not you want that item displayed.
The problem with this solution is that it only hides items in a list and so you can never rearrange them (which is what I wanted to do to give certain items priority).
[EDIT]
I thought I'd throw this into the solution since it's [basically] what I ended up doing (because the above solution wasn't adequate). I used http://www.cppblog.com/biao/archive/2009/10/31/99873.html:
#include "locationlineedit.h"
#include <QKeyEvent>
#include <QtGui/QListView>
#include <QtGui/QStringListModel>
#include <QDebug>
LocationLineEdit::LocationLineEdit(QStringList *words, QHash<QString, int> *hash, QVector<int> *bookChapterRange, int maxVisibleRows, QWidget *parent)
: QLineEdit(parent), words(**&words), hash(**&hash)
{
listView = new QListView(this);
model = new QStringListModel(this);
listView->setWindowFlags(Qt::ToolTip);
connect(this, SIGNAL(textChanged(const QString &)), this, SLOT(setCompleter(const QString &)));
connect(listView, SIGNAL(clicked(const QModelIndex &)), this, SLOT(completeText(const QModelIndex &)));
this->bookChapterRange = new QVector<int>;
this->bookChapterRange = bookChapterRange;
this->maxVisibleRows = &maxVisibleRows;
listView->setModel(model);
}
void LocationLineEdit::focusOutEvent(QFocusEvent *e)
{
listView->hide();
QLineEdit::focusOutEvent(e);
}
void LocationLineEdit::keyPressEvent(QKeyEvent *e)
{
int key = e->key();
if (!listView->isHidden())
{
int count = listView->model()->rowCount();
QModelIndex currentIndex = listView->currentIndex();
if (key == Qt::Key_Down || key == Qt::Key_Up)
{
int row = currentIndex.row();
switch(key) {
case Qt::Key_Down:
if (++row >= count)
row = 0;
break;
case Qt::Key_Up:
if (--row < 0)
row = count - 1;
break;
}
if (listView->isEnabled())
{
QModelIndex index = listView->model()->index(row, 0);
listView->setCurrentIndex(index);
}
}
else if ((Qt::Key_Enter == key || Qt::Key_Return == key || Qt::Key_Space == key) && listView->isEnabled())
{
if (currentIndex.isValid())
{
QString text = currentIndex.data().toString();
setText(text + " ");
listView->hide();
setCompleter(this->text());
}
else if (this->text().length() > 1)
{
QString text = model->stringList().at(0);
setText(text + " ");
listView->hide();
setCompleter(this->text());
}
else
{
QLineEdit::keyPressEvent(e);
}
}
else if (Qt::Key_Escape == key)
{
listView->hide();
}
else
{
listView->hide();
QLineEdit::keyPressEvent(e);
}
}
else
{
if (key == Qt::Key_Down || key == Qt::Key_Up)
{
setCompleter(this->text());
if (!listView->isHidden())
{
int row;
switch(key) {
case Qt::Key_Down:
row = 0;
break;
case Qt::Key_Up:
row = listView->model()->rowCount() - 1;
break;
}
if (listView->isEnabled())
{
QModelIndex index = listView->model()->index(row, 0);
listView->setCurrentIndex(index);
}
}
}
else
{
QLineEdit::keyPressEvent(e);
}
}
}
void LocationLineEdit::setCompleter(const QString &text)
{
if (text.isEmpty())
{
listView->hide();
return;
}
/*
This is there in the original but it seems to be bad for performance
(keeping listview hidden unnecessarily - havn't thought about it properly though)
*/
// if ((text.length() > 1) && (!listView->isHidden()))
// {
// return;
// }
model->setStringList(filteredModelFromText(text));
if (model->rowCount() == 0)
{
return;
}
int maxVisibleRows = 10;
// Position the text edit
QPoint p(0, height());
int x = mapToGlobal(p).x();
int y = mapToGlobal(p).y() + 1;
listView->move(x, y);
listView->setMinimumWidth(width());
listView->setMaximumWidth(width());
if (model->rowCount() > maxVisibleRows)
{
listView->setFixedHeight(maxVisibleRows * (listView->fontMetrics().height() + 2) + 2);
}
else
{
listView->setFixedHeight(model->rowCount() * (listView->fontMetrics().height() + 2) + 2);
}
listView->show();
}
//Basically just a slot to connect to the listView's click event
void LocationLineEdit::completeText(const QModelIndex &index)
{
QString text = index.data().toString();
setText(text);
listView->hide();
}
QStringList LocationLineEdit::filteredModelFromText(const QString &text)
{
QStringList newFilteredModel;
//do whatever you like and fill the filteredModel
return newFilteredModel;
}
Unfortunately, the answer is currently that it's not possible. To do that you'd need to duplicate much of the functionality of QCompleter in your own application (Qt Creator does that for its Locator, see src/plugins/locator/locatorwidget.cpp for the magic if you're interested).
Meanwhile you could vote on QTBUG-7830, which is about making it possible to customize the way completion items are matched, like you want. But don't hold your breath on that one.
You can get around QTBUG-7830 as mentioned above by providing custom role and making completion on that role. In the handler of that role, you can do the trick to let QCompleter know that item is there. This will work if you also override filterAcceptsRow in your SortFilterProxy model.
Easiest solution with PyQt5 :
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QCompleter
completer = QCompleter()
completer.setFilterMode(Qt.MatchContains)
This page now has been viewed over 14k times and referenced by many other posts on SO. It seems that people are creating and setting a new proxy model every time when splitPath is called, which is completely unnecessary (and expensive for large models). We just need to set the proxy model once in setModel.
As #bruno mentioned:
It appears that the model needs to be set every time splitPath is called (setting the proxy once in setModel doesn't work).
That is because if we don't invalidate the current filtering, the proxy model won't update internally. Just make sure to invalidate any current filtering or sorting on the proxy model and then you will be able to see the updates:
def splitPath(self, path):
self.local_completion_prefix = path
self.proxyModel.invalidateFilter() # invalidate the current filtering
self.proxyModel.invalidate() # or invalidate both filtering and sorting
return ""
This is available since Qt 4.3, see https://doc.qt.io/qt-5/qsortfilterproxymodel.html#invalidateFilter

QTableView and horizontalHeader()->restoreState()

I can't narrow down this bug, however I seem to have the following problem:
saveState() of a horizontalHeader()
restart app
modify model so that it has one less column
restoreState()
Now, for some reason, the state of the headerview is totally messed up. I cannot show or hide any new columns, nor can I ever get a reasonable state back
I know, this is not very descriptive but I'm hoping others have had this problem before.
For QMainWindow, the save/restoreState takes a version number. QTableView's restoreState() does not, so you need to manage this case yourself.
If you want to restore state even if the model doesn't match, you have these options:
Store the state together with a list of the columns that existed in the model upon save, so you can avoid restoring from the data if the columns don't match, and revert to defualt case
Implement your own save/restoreState functions that handle that case (ugh)
Add a proxy model that has provides bogus/dummy columns for state that is being restored, then remove those columns just afterwards.
I personally never use saveState()/restoreState() in any Qt widget, since they just return a binary blob anyway. I want my config files to be human-readable, with simple types. That also gets rid of these kind of problems.
In addition, QHeaderView has the naughty problem that restoreState() (or equivalents) only ever worked for me when the model has already been set, and then some time. I ended up connecting to the QHeaderView::sectionCountChanged() signal and setting the state in the slot called from it.
Here is the solution I made using Boost Serialization.
It handles new and removed columns, more or less. Works for my use cases.
// Because QHeaderView sucks
struct QHeaderViewState
{
explicit QHeaderViewState(ssci::CustomTreeView const & view):
m_headers(view.header()->count())
{
QHeaderView const & headers(*view.header());
// Stored in *visual index* order
for(int vi = 0; vi < headers.count();++vi)
{
int li = headers.logicalIndex(vi);
HeaderState & header = m_headers[vi];
header.hidden = headers.isSectionHidden(li);
header.size = headers.sectionSize(li);
header.logical_index = li;
header.visual_index = vi;
header.name = view.model()->headerData(li,Qt::Horizontal).toString();
header.view = &view;
}
m_sort_indicator_shown = headers.isSortIndicatorShown();
if(m_sort_indicator_shown)
{
m_sort_indicator_section = headers.sortIndicatorSection();
m_sort_order = headers.sortIndicatorOrder();
}
}
QHeaderViewState(){}
template<typename Archive>
void serialize(Archive & ar, unsigned int)
{
ar & m_headers;
ar & m_sort_indicator_shown;
if(m_sort_indicator_shown)
{
ar & m_sort_indicator_section;
ar & m_sort_order;
}
}
void
restoreState(ssci::CustomTreeView & view) const
{
QHeaderView & headers(*view.header());
const int max_columns = std::min(headers.count(),
static_cast<int>(m_headers.size()));
std::vector<HeaderState> header_state(m_headers);
std::map<QString,HeaderState *> map;
for(std::size_t ii = 0; ii < header_state.size(); ++ii)
map[header_state[ii].name] = &header_state[ii];
// First set all sections to be hidden and update logical
// indexes
for(int li = 0; li < headers.count(); ++li)
{
headers.setSectionHidden(li,true);
std::map<QString,HeaderState *>::iterator it =
map.find(view.model()->headerData(li,Qt::Horizontal).toString());
if(it != map.end())
it->second->logical_index = li;
}
// Now restore
for(int vi = 0; vi < max_columns; ++vi)
{
HeaderState const & header = header_state[vi];
const int li = header.logical_index;
SSCI_ASSERT_BUG(vi == header.visual_index);
headers.setSectionHidden(li,header.hidden);
headers.resizeSection(li,header.size);
headers.moveSection(headers.visualIndex(li),vi);
}
if(m_sort_indicator_shown)
headers.setSortIndicator(m_sort_indicator_section,
m_sort_order);
}
struct HeaderState
{
initialize<bool,false> hidden;
initialize<int,0> size;
initialize<int,0> logical_index;
initialize<int,0> visual_index;
QString name;
CustomTreeView const *view;
HeaderState():view(0){}
template<typename Archive>
void serialize(Archive & ar, unsigned int)
{
ar & hidden & size & logical_index & visual_index & name;
}
};
std::vector<HeaderState> m_headers;
bool m_sort_indicator_shown;
int m_sort_indicator_section;
Qt::SortOrder m_sort_order; // iff m_sort_indicator_shown
};
I would expect it to break if you change the model! Those functions save and restore private class member variables directly without any sanity checks. Try restoring the state and then changing the model.
I'm attempting to fix this issue for Qt 5.6.2, after hitting the same issue. See
this link for a Qt patch under review, which makes restoreState() handle the case where the number of sections (e.g. columns) in the saved state does not match the number of sections in the current view.

Resources