vcl/inc/qt5/QtInstanceTreeView.hxx | 11 +++++- vcl/qt5/QtBuilder.cxx | 10 +++-- vcl/qt5/QtInstanceTreeView.cxx | 65 ++++++++++++++++++++++++------------- 3 files changed, 60 insertions(+), 26 deletions(-)
New commits: commit 3418a3648fe05169e96532039d01f20e959bacb6 Author: Michael Weghorn <m.wegh...@posteo.de> AuthorDate: Tue Apr 22 13:11:51 2025 +0200 Commit: Michael Weghorn <m.wegh...@posteo.de> CommitDate: Tue Apr 22 15:16:31 2025 +0200 tdf#130857 qt weld: Actually implement tree view sorting Just calling QTreeView::setSortingEnabled in QtInstanceTreeView::make_sorted isn't sufficient for the tree view entires to be and remain sorted. It also requires that the tree view uses a model that supports sorting. While QStandardItemModel has a QStandardItemModel::sort method that can be used to sort the model, it doesn't ensure the entries remain sorted when adding new ones, i.e. it would have to be called every time an entry is inserted. Instead, switch to using QSortFilterProxyModel for the QTreeView's model to "wrap" the QStandardItemModel, which ensures that items remain sorted in the tree view once sorting has been enabled. In addition, QSortFilterProxyModel::sort needs to be called once so that the model knows what column to sort by. Quoting the QSortFilterProxyModel::sortColumn doc [1] > Returns the column currently used for sorting > > This returns the most recently used sort column. The default > value is -1, which means that this proxy model does not sort. > > See also sort(). Use the first "normal" column (i.e. not the special toggle button column, if that one exists). A different sort column could explicitly be set by calling weld::TreeView::set_sort_column. The drawback of switching to QSortFilterProxyModel instead of using QStandardItemModel directly is that QSortFilterProxyModel only derives from QAbstractItemModel, so some QStandardItemModel/QStandardItem convenience methods cannot be used directly any more. Because of that, introduce a QtInstanceTreeView::itemFromIndex helper method instead of calling QStandardItemModel::itemFromIndex directly. In QtInstanceTreeView::swap, convert between the proxy and source model row indices and work with the underlying QStandardItemModel directly in order to be able to continue using QStandardItemModel::takeRow and QStandardItemModel::insertRow. Similar in QtInstanceTreeView::find_text, where QStandardItemModel::findItems is used. This commit e.g. causes the entries in the "Document" tab in Writer's "Insert" -> "Field" -> "More Fields..." dialog to be sorted as expected with SAL_VCL_QT_USE_WELDED_WIDGETS=1 in a WIP branch where support for native Qt widgets for that dialog is declared while they were unsorted previously despite the SwFieldDokPage ctor calling weld::TreeView::make_sorted. [1] https://doc.qt.io/qt-6/qsortfilterproxymodel.html#sortColumn Change-Id: I5849f66caf78bc23059c8b52d45742f444578452 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/184434 Tested-by: Jenkins Reviewed-by: Michael Weghorn <m.wegh...@posteo.de> diff --git a/vcl/inc/qt5/QtInstanceTreeView.hxx b/vcl/inc/qt5/QtInstanceTreeView.hxx index 86c64a606d10..cbf83c755969 100644 --- a/vcl/inc/qt5/QtInstanceTreeView.hxx +++ b/vcl/inc/qt5/QtInstanceTreeView.hxx @@ -11,6 +11,7 @@ #include "QtInstanceWidget.hxx" +#include <QtCore/QSortFilterProxyModel> #include <QtGui/QStandardItemModel> #include <QtWidgets/QTreeView> @@ -19,7 +20,14 @@ class QtInstanceTreeView : public QtInstanceWidget, public virtual weld::TreeVie Q_OBJECT QTreeView* m_pTreeView; - QStandardItemModel* m_pModel; + + /** The model displayed in the tree view. The proxy model takes care of sorting + * if sorting is enabled. */ + QSortFilterProxyModel* m_pModel; + + /** The QStandardItemModel used as the source model for `m_pModel`. */ + QStandardItemModel* m_pSourceModel; + QItemSelectionModel* m_pSelectionModel; bool m_bExtraToggleButtonColumnEnabled = false; @@ -195,6 +203,7 @@ private: QModelIndex modelIndex(int nRow, int nCol = 0) const; QModelIndex modelIndex(const weld::TreeIter& rIter, int nCol = 0) const; static int rowIndex(const weld::TreeIter& rIter); + QStandardItem* itemFromIndex(const QModelIndex& rIndex) const; QModelIndex toggleButtonModelIndex(int nRow) const; QModelIndex firstTextColumnModelIndex(int nRow) const; static QAbstractItemView::SelectionMode mapSelectionMode(SelectionMode eMode); diff --git a/vcl/qt5/QtBuilder.cxx b/vcl/qt5/QtBuilder.cxx index 564436dd307e..b96c12263cd5 100644 --- a/vcl/qt5/QtBuilder.cxx +++ b/vcl/qt5/QtBuilder.cxx @@ -20,6 +20,7 @@ #include <rtl/ustrbuf.hxx> #include <vcl/qt/QtUtils.hxx> +#include <QtCore/QSortFilterProxyModel> #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include <QtGui/QActionGroup> #endif @@ -365,7 +366,10 @@ QObject* QtBuilder::makeObject(QObject* pParent, std::u16string_view sName, std: else if (sName == u"GtkTreeView") { QTreeView* pTreeView = new QTreeView(pParentWidget); - pTreeView->setModel(new QStandardItemModel(pTreeView)); + QStandardItemModel* pItemModel = new QStandardItemModel(pTreeView); + QSortFilterProxyModel* pProxyModel = new QSortFilterProxyModel(pTreeView); + pProxyModel->setSourceModel(pItemModel); + pTreeView->setModel(pProxyModel); pTreeView->setHeaderHidden(!extractHeadersVisible(rMap)); pTreeView->setRootIsDecorated(extractShowExpanders(rMap)); pObject = pTreeView; @@ -374,8 +378,8 @@ QObject* QtBuilder::makeObject(QObject* pParent, std::u16string_view sName, std: { QTreeView* pTreeView = qobject_cast<QTreeView*>(pParentWidget); assert(pTreeView && "Tree view column doesn't have a tree view parent"); - QStandardItemModel* pModel = qobject_cast<QStandardItemModel*>(pTreeView->model()); - assert(pModel && "Tree view doesn't have QStandardItemModel set"); + QAbstractItemModel* pModel = pTreeView->model(); + assert(pModel && "Tree view doesn't have model set"); const int nCol = pModel->columnCount(); pModel->insertColumn(nCol); pModel->setHeaderData(nCol, Qt::Horizontal, toQString(extractTitle(rMap))); diff --git a/vcl/qt5/QtInstanceTreeView.cxx b/vcl/qt5/QtInstanceTreeView.cxx index 83cf9869084b..d44726b5a657 100644 --- a/vcl/qt5/QtInstanceTreeView.cxx +++ b/vcl/qt5/QtInstanceTreeView.cxx @@ -25,8 +25,11 @@ QtInstanceTreeView::QtInstanceTreeView(QTreeView* pTreeView) { assert(m_pTreeView); - m_pModel = qobject_cast<QStandardItemModel*>(m_pTreeView->model()); - assert(m_pModel && "tree view doesn't have expected item model set"); + m_pModel = qobject_cast<QSortFilterProxyModel*>(m_pTreeView->model()); + assert(m_pModel && "tree view doesn't have expected QSortFilterProxyModel set"); + + m_pSourceModel = qobject_cast<QStandardItemModel*>(m_pModel->sourceModel()); + assert(m_pSourceModel && "proxy model doesn't have expected source model"); m_pSelectionModel = m_pTreeView->selectionModel(); assert(m_pSelectionModel); @@ -57,7 +60,7 @@ void QtInstanceTreeView::insert(const weld::TreeIter* pParent, int nPos, const O m_pModel->insertRow(nPos); const QModelIndex aIndex = modelIndex(nPos); - QStandardItem* pItem = m_pModel->itemFromIndex(aIndex); + QStandardItem* pItem = itemFromIndex(aIndex); if (pStr) pItem->setText(toQString(*pStr)); if (pId) @@ -69,7 +72,7 @@ void QtInstanceTreeView::insert(const weld::TreeIter* pParent, int nPos, const O pItem->setIcon(toQPixmap(*pImageSurface)); if (m_bExtraToggleButtonColumnEnabled) - m_pModel->itemFromIndex(toggleButtonModelIndex(nPos))->setCheckable(true); + itemFromIndex(toggleButtonModelIndex(nPos))->setCheckable(true); if (pRet) static_cast<QtInstanceTreeIter*>(pRet)->setModelIndex(aIndex); @@ -91,7 +94,7 @@ OUString QtInstanceTreeView::get_selected_text() const if (aSelectedIndexes.empty()) return; - sText = toOUString(m_pModel->itemFromIndex(aSelectedIndexes.first())->text()); + sText = toOUString(itemFromIndex(aSelectedIndexes.first())->text()); }); return sText; @@ -208,7 +211,7 @@ void QtInstanceTreeView::set_sensitive(int nRow, bool bSensitive, int nCol) return; } - QStandardItem* pItem = m_pModel->item(nRow, nCol); + QStandardItem* pItem = itemFromIndex(modelIndex(nRow, nCol)); if (pItem) { if (bSensitive) @@ -225,7 +228,7 @@ bool QtInstanceTreeView::get_sensitive(int nRow, int nCol) const bool bSensitive = false; GetQtInstance().RunInMainThread([&] { - QStandardItem* pItem = m_pModel->item(nRow, nCol); + QStandardItem* pItem = itemFromIndex(modelIndex(nRow, nCol)); if (pItem) bSensitive = pItem->flags() & Qt::ItemIsEnabled; }); @@ -249,7 +252,7 @@ void QtInstanceTreeView::set_toggle(int nRow, TriState eState, int nCol) GetQtInstance().RunInMainThread([&] { QModelIndex aIndex = nCol == -1 ? toggleButtonModelIndex(nRow) : modelIndex(nRow, nCol); - m_pModel->itemFromIndex(aIndex)->setCheckState(toQtCheckState(eState)); + itemFromIndex(aIndex)->setCheckState(toQtCheckState(eState)); }); } @@ -260,7 +263,7 @@ TriState QtInstanceTreeView::get_toggle(int nRow, int nCol) const TriState eState = TRISTATE_INDET; GetQtInstance().RunInMainThread([&] { QModelIndex aIndex = nCol == -1 ? toggleButtonModelIndex(nRow) : modelIndex(nRow, nCol); - eState = toVclTriState(m_pModel->itemFromIndex(aIndex)->checkState()); + eState = toVclTriState(itemFromIndex(aIndex)->checkState()); }); return eState; @@ -333,18 +336,21 @@ void QtInstanceTreeView::swap(int nPos1, int nPos2) const bool bPos1Selected = m_pSelectionModel->isRowSelected(nPos1); const bool bPos2Selected = m_pSelectionModel->isRowSelected(nPos2); - const int nMin = std::min(nPos1, nPos2); - const int nMax = std::max(nPos1, nPos2); - QList<QStandardItem*> aMaxRow = m_pModel->takeRow(nMax); - QList<QStandardItem*> aMinRow = m_pModel->takeRow(nMin); - m_pModel->insertRow(nMin, aMaxRow); - m_pModel->insertRow(nMax, aMinRow); + const int nSourceModelPos1 = m_pModel->mapToSource(modelIndex(nPos1)).row(); + const int nSourceModelPos2 = m_pModel->mapToSource(modelIndex(nPos2)).row(); + + const int nMin = std::min(nSourceModelPos1, nSourceModelPos2); + const int nMax = std::max(nSourceModelPos1, nSourceModelPos2); + QList<QStandardItem*> aMaxRow = m_pSourceModel->takeRow(nMax); + QList<QStandardItem*> aMinRow = m_pSourceModel->takeRow(nMin); + m_pSourceModel->insertRow(nMin, aMaxRow); + m_pSourceModel->insertRow(nMax, aMinRow); // restore selection if (bPos1Selected) - select(nPos2); + select(m_pModel->mapFromSource(m_pSourceModel->index(nSourceModelPos2, 0)).row()); if (bPos2Selected) - select(nPos1); + select(m_pModel->mapFromSource(m_pSourceModel->index(nSourceModelPos1, 0)).row()); }); } @@ -413,9 +419,10 @@ int QtInstanceTreeView::find_text(const OUString& rText) const int nIndex = -1; GetQtInstance().RunInMainThread([&] { - const QList<QStandardItem*> aItems = m_pModel->findItems(toQString(rText)); + // search in underlying QStandardItemModel and map index + const QList<QStandardItem*> aItems = m_pSourceModel->findItems(toQString(rText)); if (!aItems.empty()) - nIndex = aItems.at(0)->index().row(); + nIndex = m_pModel->mapFromSource(aItems.at(0)->index()).row(); }); return nIndex; @@ -766,14 +773,22 @@ int QtInstanceTreeView::n_children() const SolarMutexGuard g; int nChildCount; - GetQtInstance().RunInMainThread( - [&] { nChildCount = m_pModel->rowCount(m_pModel->invisibleRootItem()->index()); }); + GetQtInstance().RunInMainThread([&] { + const QModelIndex aRootIndex + = m_pModel->mapFromSource(m_pSourceModel->invisibleRootItem()->index()); + nChildCount = m_pModel->rowCount(aRootIndex); + }); return nChildCount; } void QtInstanceTreeView::make_sorted() { - GetQtInstance().RunInMainThread([&] { m_pTreeView->setSortingEnabled(true); }); + GetQtInstance().RunInMainThread([&] { + m_pTreeView->setSortingEnabled(true); + // sort by first "normal" column + const int nSortColumn = m_bExtraToggleButtonColumnEnabled ? 1 : 0; + m_pModel->sort(nSortColumn); + }); } void QtInstanceTreeView::make_unsorted() @@ -961,6 +976,12 @@ int QtInstanceTreeView::rowIndex(const weld::TreeIter& rIter) return aModelIndex.row(); } +QStandardItem* QtInstanceTreeView::itemFromIndex(const QModelIndex& rIndex) const +{ + const QModelIndex aSourceIndex = m_pModel->mapToSource(rIndex); + return m_pSourceModel->itemFromIndex(aSourceIndex); +} + QModelIndex QtInstanceTreeView::toggleButtonModelIndex(int nRow) const { assert(m_bExtraToggleButtonColumnEnabled && "Special toggle button column is not enabled");