[system/dolphin] src: KItemViews: store per-item role values in a flat SmallHash
Méven Car <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit f9488f64666466378fa8f45c125336367eff54a2 by Méven Car.
Committed on 25/07/2026 at 14:32.
Pushed by meven into branch 'master'.
KItemViews: store per-item role values in a flat SmallHash
Each item's role values were cached in a QHash<QByteArray, QVariant>. In Qt6
a QHash grabs a ~3 KB minimum span on the first insert, so one hash per item
- a handful of roles times thousands of items - dominates the model's memory
once the values are populated (scrolling a folder, or sorting by a role that
needs them).
Introduce SmallHash, a flat vector<pair<QByteArray, QVariant>> exposing the
subset of the QHash API used here, and make it the role-values type across the
item-views layer: KItemModelBase::data()/setData(), the widgets, the roles
updater and the Baloo roles provider. For the few entries per item a linear
scan is cheap and cache-friendly, and the span overhead is gone. As it is now
used beyond kitemviews it lives in src/smallhash.h.
The url is no longer stored per item (it is derivable from the KFileItem):
data() injects it and url(int) reads it straight from the item.
Measured on 100k items, per-item values drop from ~4.7 KB to ~2.1 KB (from
~3.9 KB to ~0.18 KB with only the name role); fill and sort are unchanged or
slightly faster.
M +1 -1 src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
M +1 -1 src/kitemviews/kfileitemlistview.cpp
M +1 -1 src/kitemviews/kfileitemlistwidget.cpp
M +1 -1 src/kitemviews/kfileitemlistwidget.h
M +25 -21 src/kitemviews/kfileitemmodel.cpp
M +7 -4 src/kitemviews/kfileitemmodel.h
M +19 -20 src/kitemviews/kfileitemmodelrolesupdater.cpp
M +2 -2 src/kitemviews/kfileitemmodelrolesupdater.h
M +3 -3 src/kitemviews/kitemlistwidget.cpp
M +5 -4 src/kitemviews/kitemlistwidget.h
M +1 -1 src/kitemviews/kitemmodelbase.cpp
M +3 -2 src/kitemviews/kitemmodelbase.h
M +13 -13 src/kitemviews/kstandarditemlistwidget.cpp
M +4 -4 src/kitemviews/kstandarditemlistwidget.h
M +2 -2 src/kitemviews/private/kbaloorolesprovider.cpp
M +2 -1 src/kitemviews/private/kbaloorolesprovider.h
A +117 -0 src/smallhash.h [License: GPL(v2.0+)]
M +7 -7 src/tests/kfileitemmodeltest.cpp
M +3 -3 src/tests/kitemlistselectionmanagertest.cpp
M +1 -1 src/views/dolphinfileitemlistwidget.cpp
M +2 -2 src/views/dolphinview.cpp
M +1 -1 src/views/versioncontrol/versioncontrolobserver.cpp
https://invent.kde.org/system/dolphin/-/commit/f9488f64666466378fa8f45c125336367eff54a2
diff --git a/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp b/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
index b50b71daf5..82bba1c5c6 100644
--- a/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
+++ b/src/kitemviews/accessibility/kitemlistdelegateaccessible.cpp
@@ -130,7 +130,7 @@ QRect KItemListDelegateAccessible::rect() const
QString KItemListDelegateAccessible::text(QAccessible::Text t) const
{
- const QHash<QByteArray, QVariant> data = m_view->model()->data(m_index);
+ const SmallHash data = m_view->model()->data(m_index);
switch (t) {
case QAccessible::Name: {
return data["text"].toString();
diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp
index 585a972a6e..495d6a1f30 100644
--- a/src/kitemviews/kfileitemlistview.cpp
+++ b/src/kitemviews/kfileitemlistview.cpp
@@ -204,7 +204,7 @@ void KFileItemListView::initializeItemListWidget(KItemListWidget *item)
KStandardItemListView::initializeItemListWidget(item);
// Make sure that the item has an icon.
- QHash<QByteArray, QVariant> data = item->data();
+ SmallHash data = item->data();
if (!data.contains("iconName") && data["iconPixmap"].value<QPixmap>().isNull()) {
Q_ASSERT(qobject_cast<KFileItemModel *>(model()));
KFileItemModel *fileItemModel = static_cast<KFileItemModel *>(model());
diff --git a/src/kitemviews/kfileitemlistwidget.cpp b/src/kitemviews/kfileitemlistwidget.cpp
index ccb1819af2..d2ef75dfdd 100644
--- a/src/kitemviews/kfileitemlistwidget.cpp
+++ b/src/kitemviews/kfileitemlistwidget.cpp
@@ -43,7 +43,7 @@ bool KFileItemListWidgetInformant::itemIsLink(int index, const KItemListView *vi
return item.isLink();
}
-QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs) const
+QString KFileItemListWidgetInformant::roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs) const
{
QString text;
const QVariant roleValue = values.value(role);
diff --git a/src/kitemviews/kfileitemlistwidget.h b/src/kitemviews/kfileitemlistwidget.h
index e2db43178b..3d4a78f3a8 100644
--- a/src/kitemviews/kfileitemlistwidget.h
+++ b/src/kitemviews/kfileitemlistwidget.h
@@ -29,7 +29,7 @@ protected:
QString itemText(int index, const KItemListView *view) const override;
bool itemIsLink(int index, const KItemListView *view) const override;
/** @see KStandardItemListWidget::roleText(). */
- QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const override;
+ QString roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const override;
QFont customizedFontForLinks(const QFont &baseFont) const override;
friend class KItemListDelegateAccessible;
diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp
index 0dd9f58d81..542c32ae6d 100644
--- a/src/kitemviews/kfileitemmodel.cpp
+++ b/src/kitemviews/kfileitemmodel.cpp
@@ -379,7 +379,7 @@ int KFileItemModel::count() const
return m_itemData.count();
}
-QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
+SmallHash KFileItemModel::data(int index) const
{
if (index >= 0 && index < count()) {
ItemData *data = m_itemData.at(index);
@@ -399,27 +399,32 @@ QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
}
}
- return data->values;
+ SmallHash result = data->values;
+ // The url is not stored per item (it is derivable from the KFileItem); inject it here.
+ result.insert(sharedValue("url"), data->item.url());
+ return result;
}
- return QHash<QByteArray, QVariant>();
+ return SmallHash();
+}
+
+QUrl KFileItemModel::url(int index) const
+{
+ // "url" is not stored in the per-item role cache; read it straight from the item.
+ return fileItem(index).url();
}
-bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &values)
+bool KFileItemModel::setData(int index, const SmallHash &values)
{
if (index < 0 || index >= count()) {
return false;
}
- QHash<QByteArray, QVariant> currentValues = data(index);
+ SmallHash currentValues = data(index);
// Determine which roles have been changed
QSet<QByteArray> changedRoles;
- QHashIterator<QByteArray, QVariant> it(values);
- while (it.hasNext()) {
- it.next();
- const QByteArray role = sharedValue(it.key());
- const QVariant value = it.value();
-
+ for (const auto &[key, value] : values) {
+ const QByteArray role = sharedValue(key);
if (currentValues[role] != value) {
currentValues[role] = value;
changedRoles.insert(role);
@@ -443,6 +448,8 @@ bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &value
currentValues["url"] = url;
}
}
+ // "url" is injected by data(), not stored per item.
+ currentValues.remove(sharedValue("url"));
m_itemData[index]->values = currentValues;
emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
@@ -870,7 +877,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded)
return false;
}
- QHash<QByteArray, QVariant> values;
+ SmallHash values;
values.insert(sharedValue("isExpanded"), expanded);
if (!setData(index, values)) {
return false;
@@ -1650,12 +1657,10 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &
// Keep old values as long as possible if they could not retrieved synchronously yet.
// The update of the values will be done asynchronously by KFileItemModelRolesUpdater.
ItemData *const itemData = m_itemData.at(indexForItem);
- QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, itemData->parent));
- while (it.hasNext()) {
- it.next();
- const QByteArray &role = it.key();
- if (itemData->values.value(role) != it.value()) {
- itemData->values.insert(role, it.value());
+ const SmallHash newData = retrieveData(newItem, itemData->parent);
+ for (const auto &[role, value] : newData) {
+ if (itemData->values.value(role) != value) {
+ itemData->values.insert(role, value);
changedRoles.insert(role);
}
}
@@ -2203,13 +2208,12 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const
return roles.value(roleType);
}
-QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const
+SmallHash KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const
{
// It is important to insert only roles that are fast to retrieve. E.g.
// KFileItem::iconName() can be very expensive if the MIME-type is unknown
// and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
- QHash<QByteArray, QVariant> data;
- data.insert(sharedValue("url"), item.url());
+ SmallHash data;
const bool isDir = item.isDir();
if (m_requestRole[IsDirRole] && isDir) {
diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h
index 9e30486079..dca78a263b 100644
--- a/src/kitemviews/kfileitemmodel.h
+++ b/src/kitemviews/kfileitemmodel.h
@@ -10,6 +10,7 @@
#include "dolphin_export.h"
#include "kitemviews/kitemmodelbase.h"
#include "kitemviews/private/kfileitemmodelfilter.h"
+#include "smallhash.h"
#include <KFileItem>
#include <KLazyLocalizedString>
@@ -18,6 +19,7 @@
#include <QHash>
#include <QSet>
#include <QUrl>
+#include <QVariant>
#include <functional>
@@ -77,8 +79,9 @@ public:
void cancelDirectoryLoading();
int count() const override;
- QHash<QByteArray, QVariant> data(int index) const override;
- bool setData(int index, const QHash<QByteArray, QVariant> &values) override;
+ SmallHash data(int index) const override;
+ QUrl url(int index) const override;
+ bool setData(int index, const SmallHash &values) override;
/**
* Sets a separate sorting with directories first (true) or a mixed
@@ -373,7 +376,7 @@ private:
struct ItemData {
KFileItem item;
- QHash<QByteArray, QVariant> values;
+ SmallHash values;
ItemData *parent;
};
@@ -429,7 +432,7 @@ private:
*/
QByteArray roleForType(RoleType roleType) const;
- QHash<QByteArray, QVariant> retrieveData(const KFileItem &item, const ItemData *parent) const;
+ SmallHash retrieveData(const KFileItem &item, const ItemData *parent) const;
/**
* @return True if role values benefit from natural or case insensitive sorting.
diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp
index 6f86e1b382..af6fc46c44 100644
--- a/src/kitemviews/kfileitemmodelrolesupdater.cpp
+++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp
@@ -571,7 +571,7 @@ void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPi
return;
}
- QHash<QByteArray, QVariant> data = rolesData(item, index);
+ SmallHash data = rolesData(item, index);
data.insert("iconPixmap", transformPreviewPixmap(pixmap));
data.insert("supportsSequencing", m_previewJob->handlesSequences());
@@ -592,7 +592,7 @@ void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item)
const int index = m_model->index(item);
if (index >= 0) {
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
data.insert("iconPixmap", QPixmap());
setModelData(index, data);
@@ -628,7 +628,7 @@ void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &it
return;
}
- QHash<QByteArray, QVariant> data = m_model->data(index);
+ SmallHash data = m_model->data(index);
QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
const int loadedIndex = pixmaps.size();
@@ -674,7 +674,7 @@ void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem
static const int numRetries = 2;
- QHash<QByteArray, QVariant> data = m_model->data(index);
+ SmallHash data = m_model->data(index);
QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
qCDebug(DolphinDebug).nospace() << "Failed to generate hover sequence preview #" << pixmaps.size() << " for file " << item.url().toString() << " (attempt "
@@ -788,7 +788,7 @@ void KFileItemModelRolesUpdater::resolveNextPendingRoles()
if (m_clearPreviews) {
// Only go through the list if there are items which might still have previews.
if (m_finishedItems.count() != m_model->count()) {
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
data.insert("iconPixmap", QPixmap());
data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
@@ -837,7 +837,7 @@ void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &
file.load();
const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
const auto roles = rolesProvider.roles();
for (const QByteArray &role : roles) {
@@ -847,10 +847,9 @@ void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &
data.insert(role, QVariant());
}
- QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(file, m_roles));
- while (it.hasNext()) {
- it.next();
- data.insert(it.key(), it.value());
+ const SmallHash roleValues = rolesProvider.roleValues(file, m_roles);
+ for (const auto &[key, value] : roleValues) {
+ data.insert(key, value);
}
const int index = m_model->index(item);
@@ -870,7 +869,7 @@ void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QStrin
if (getSizeRole || getIsExpandableRole) {
const int index = m_model->index(QUrl::fromLocalFile(path));
if (index >= 0) {
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
if (getSizeRole) {
data.insert("count", count);
@@ -1055,7 +1054,7 @@ void KFileItemModelRolesUpdater::loadNextHoverSequencePreview()
// We generate the next few sequence indices in advance (buffering)
const int maxSeqIdx = m_hoverSequenceIndex + 5;
- QHash<QByteArray, QVariant> data = m_model->data(index);
+ SmallHash data = m_model->data(index);
if (!data.contains("hoverSequencePixmaps")) {
// The pixmap at index 0 isn't used ("iconPixmap" will be used instead)
@@ -1180,7 +1179,7 @@ void KFileItemModelRolesUpdater::updateChangedItems()
}
}
-void KFileItemModelRolesUpdater::setModelData(int index, const QHash<QByteArray, QVariant> &data)
+void KFileItemModelRolesUpdater::setModelData(int index, const SmallHash &data)
{
const QScopedValueRollback<bool> guard(m_applyingChangesToModel, true);
m_model->setData(index, data);
@@ -1188,7 +1187,7 @@ void KFileItemModelRolesUpdater::setModelData(int index, const QHash<QByteArray,
void KFileItemModelRolesUpdater::applySortRole(int index)
{
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
const KFileItem item = m_model->fileItem(index);
if (m_model->sortRole() == "type") {
@@ -1234,7 +1233,7 @@ bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint,
return false;
}
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
if (resolveAll) {
data = rolesData(item, index);
}
@@ -1321,7 +1320,7 @@ void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &ite
auto data = m_model->data(index);
int origCount = data.value("count").toInt();
- QHash<QByteArray, QVariant> newData;
+ SmallHash newData;
QVariant expandable = data.value("isExpandable");
if (expandable.isNull() || expandable.toBool() != (entryCount > 0)) {
// if expandable has changed
@@ -1348,9 +1347,9 @@ void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &ite
m_directoryContentsCounter->scanDirectory(path, priority);
}
-QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
+SmallHash KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
{
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
const bool getSizeRole = m_roles.contains("size");
const bool getIsExpandableRole = m_roles.contains("isExpandable");
@@ -1392,7 +1391,7 @@ void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStr
return;
}
const int index = m_model->index(item);
- QHash<QByteArray, QVariant> data = m_model->data(index);
+ SmallHash data = m_model->data(index);
QStringList overlays = item.overlays();
for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
overlays.append(it->getOverlays(url));
@@ -1502,7 +1501,7 @@ void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems()
const int index = m_model->index(item);
if (index >= 0) {
- QHash<QByteArray, QVariant> data = m_model->data(index);
+ SmallHash data = m_model->data(index);
data["hoverSequencePixmaps"] = QVariant::fromValue(QVector<QPixmap>() << QPixmap());
m_model->setData(index, data);
}
diff --git a/src/kitemviews/kfileitemmodelrolesupdater.h b/src/kitemviews/kfileitemmodelrolesupdater.h
index 61e85188e5..6ecc577f3e 100644
--- a/src/kitemviews/kfileitemmodelrolesupdater.h
+++ b/src/kitemviews/kfileitemmodelrolesupdater.h
@@ -315,14 +315,14 @@ private:
enum ResolveHint { ResolveFast, ResolveAll };
bool applyResolvedRoles(int index, ResolveHint hint, const KFileItem &referenceItem = KFileItem());
- QHash<QByteArray, QVariant> rolesData(const KFileItem &item, int index);
+ SmallHash rolesData(const KFileItem &item, int index);
/**
* Sets \a data on the model item at \a index without re-entering
* slotItemsChanged() for this self-induced change (other listeners, e.g. the
* view, still get the change). Replaces a manual disconnect/setData/connect.
*/
- void setModelData(int index, const QHash<QByteArray, QVariant> &data);
+ void setModelData(int index, const SmallHash &data);
/**
* Must be invoked if a property has been changed that affects
diff --git a/src/kitemviews/kitemlistwidget.cpp b/src/kitemviews/kitemlistwidget.cpp
index e792844b03..272db71671 100644
--- a/src/kitemviews/kitemlistwidget.cpp
+++ b/src/kitemviews/kitemlistwidget.cpp
@@ -75,7 +75,7 @@ int KItemListWidget::index() const
return m_index;
}
-void KItemListWidget::setData(const QHash<QByteArray, QVariant> &data, const QSet<QByteArray> &roles)
+void KItemListWidget::setData(const SmallHash &data, const QSet<QByteArray> &roles)
{
clearHoverCache();
if (roles.isEmpty()) {
@@ -90,7 +90,7 @@ void KItemListWidget::setData(const QHash<QByteArray, QVariant> &data, const QSe
update();
}
-QHash<QByteArray, QVariant> KItemListWidget::data() const
+SmallHash KItemListWidget::data() const
{
return m_data;
}
@@ -456,7 +456,7 @@ void KItemListWidget::startActivateSoonAnimation(int timeUntilActivation)
Q_UNUSED(timeUntilActivation)
}
-void KItemListWidget::dataChanged(const QHash<QByteArray, QVariant> ¤t, const QSet<QByteArray> &roles)
+void KItemListWidget::dataChanged(const SmallHash ¤t, const QSet<QByteArray> &roles)
{
Q_UNUSED(current)
Q_UNUSED(roles)
diff --git a/src/kitemviews/kitemlistwidget.h b/src/kitemviews/kitemlistwidget.h
index 100c13466f..d4098545c1 100644
--- a/src/kitemviews/kitemlistwidget.h
+++ b/src/kitemviews/kitemlistwidget.h
@@ -11,6 +11,7 @@
#include "dolphin_export.h"
#include "kitemviews/kitemliststyleoption.h"
+#include "smallhash.h"
#include <QBitArray>
#include <QGraphicsWidget>
@@ -59,8 +60,8 @@ public:
void setIndex(int index);
int index() const;
- void setData(const QHash<QByteArray, QVariant> &data, const QSet<QByteArray> &roles = QSet<QByteArray>());
- QHash<QByteArray, QVariant> data() const;
+ void setData(const SmallHash &data, const QSet<QByteArray> &roles = QSet<QByteArray>());
+ SmallHash data() const;
QVariant value(const QByteArray &key) const;
/**
@@ -206,7 +207,7 @@ Q_SIGNALS:
void roleEditingFinished(int index, const QByteArray &role, const QVariant &value);
protected:
- virtual void dataChanged(const QHash<QByteArray, QVariant> ¤t, const QSet<QByteArray> &roles = QSet<QByteArray>());
+ virtual void dataChanged(const SmallHash ¤t, const QSet<QByteArray> &roles = QSet<QByteArray>());
virtual void visibleRolesChanged(const QList<QByteArray> ¤t, const QList<QByteArray> &previous);
virtual void columnWidthChanged(const QByteArray &role, qreal current, qreal previous);
virtual void sidePaddingChanged(qreal leftPaddingWidth, qreal rightPaddingWidth);
@@ -267,7 +268,7 @@ private:
bool m_alternateBackground;
bool m_enabledSelectionToggle;
bool m_clickHighlighted;
- QHash<QByteArray, QVariant> m_data;
+ SmallHash m_data;
QList<QByteArray> m_visibleRoles;
QHash<QByteArray, qreal> m_columnWidths;
qreal m_leftPadding;
diff --git a/src/kitemviews/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp
index 7cff2a9c48..5c384f28dd 100644
--- a/src/kitemviews/kitemmodelbase.cpp
+++ b/src/kitemviews/kitemmodelbase.cpp
@@ -26,7 +26,7 @@ KItemModelBase::KItemModelBase(const QByteArray &sortRole, QObject *parent)
KItemModelBase::~KItemModelBase() = default;
-bool KItemModelBase::setData(int index, const QHash<QByteArray, QVariant> &values)
+bool KItemModelBase::setData(int index, const SmallHash &values)
{
Q_UNUSED(index)
Q_UNUSED(values)
diff --git a/src/kitemviews/kitemmodelbase.h b/src/kitemviews/kitemmodelbase.h
index 5d968ea069..1a3d19f525 100644
--- a/src/kitemviews/kitemmodelbase.h
+++ b/src/kitemviews/kitemmodelbase.h
@@ -12,6 +12,7 @@
#include "dolphin_export.h"
#include "kitemviews/kitemrange.h"
#include "kitemviews/kitemset.h"
+#include "smallhash.h"
#include <QHash>
#include <QObject>
@@ -47,7 +48,7 @@ public:
/** @return The number of items. */
virtual int count() const = 0;
- virtual QHash<QByteArray, QVariant> data(int index) const = 0;
+ virtual SmallHash data(int index) const = 0;
/**
* Sets the data for the item at \a index to the given \a values. Returns true
@@ -56,7 +57,7 @@ public:
* The default implementation does not set the data, and will always return
* false.
*/
- virtual bool setData(int index, const QHash<QByteArray, QVariant> &values);
+ virtual bool setData(int index, const SmallHash &values);
/**
* Enables/disables the grouped sorting. The method KItemModelBase::onGroupedSortingChanged() will be
diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp
index edc8bfa789..88d2ddad64 100644
--- a/src/kitemviews/kstandarditemlistwidget.cpp
+++ b/src/kitemviews/kstandarditemlistwidget.cpp
@@ -63,7 +63,7 @@ void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector<std::pair<
qreal KStandardItemListWidgetInformant::preferredRoleColumnWidth(const QByteArray &role, int index, const KItemListView *view) const
{
- const QHash<QByteArray, QVariant> values = view->model()->data(index);
+ const SmallHash values = view->model()->data(index);
const KItemListStyleOption &option = view->styleOption();
const QString text = roleText(role, values);
@@ -108,7 +108,7 @@ bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView
return false;
}
-QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs) const
+QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs) const
{
if (role == "rating") {
if (forUsageAs == ForUsageAs::DisplayedText) {
@@ -221,7 +221,7 @@ void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVect
if (showOnlyTextRole) {
maximumRequiredWidth = fontMetrics.horizontalAdvance(itemText(index, view));
} else {
- const QHash<QByteArray, QVariant> &values = view->model()->data(index);
+ const SmallHash &values = view->model()->data(index);
for (const QByteArray &role : visibleRoles) {
const QString &text = roleText(role, values);
const qreal requiredWidth = fontMetrics.horizontalAdvance(text);
@@ -628,7 +628,7 @@ void KStandardItemListWidget::startActivateSoonAnimation(int timeUntilActivation
wantedIconName = "folder-open";
}
- QHash<QByteArray, QVariant> itemData{data()};
+ SmallHash itemData{data()};
if (itemData["iconName"] != wantedIconName) {
itemData.insert("iconName", wantedIconName);
setData(itemData);
@@ -637,7 +637,7 @@ void KStandardItemListWidget::startActivateSoonAnimation(int timeUntilActivation
});
connect(m_activateSoonAnimation, &QObject::destroyed, this, [originalIconName, this]() {
- QHash<QByteArray, QVariant> itemData{data()};
+ SmallHash itemData{data()};
if (itemData["iconName"] == "folder-open") {
itemData.insert("iconName", originalIconName);
setData(itemData);
@@ -740,12 +740,12 @@ QHash<Qt::Corner, QString> KStandardItemListWidget::overlays() const
return m_overlays;
}
-QString KStandardItemListWidget::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const
+QString KStandardItemListWidget::roleText(const QByteArray &role, const SmallHash &values) const
{
return static_cast<const KStandardItemListWidgetInformant *>(informant())->roleText(role, values);
}
-void KStandardItemListWidget::dataChanged(const QHash<QByteArray, QVariant> ¤t, const QSet<QByteArray> &roles)
+void KStandardItemListWidget::dataChanged(const SmallHash ¤t, const QSet<QByteArray> &roles)
{
Q_UNUSED(current)
@@ -1027,7 +1027,7 @@ void KStandardItemListWidget::triggerCacheRefreshing()
refreshCache();
- const QHash<QByteArray, QVariant> values = data();
+ const SmallHash values = data();
m_isExpandable = m_supportsItemExpanding && values["isExpandable"].toBool();
m_isHidden = isHidden();
m_customizedFont = customizedFont(styleOption().font);
@@ -1049,7 +1049,7 @@ void KStandardItemListWidget::triggerCacheRefreshing()
void KStandardItemListWidget::updateExpansionArea()
{
if (m_supportsItemExpanding) {
- const QHash<QByteArray, QVariant> values = data();
+ const SmallHash values = data();
const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
if (expandedParentsCount >= 0) {
const int widgetIconSize = styleOption().iconSize;
@@ -1086,7 +1086,7 @@ void KStandardItemListWidget::updatePixmapCache()
const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : widgetIconSize;
const int maxIconHeight = widgetIconSize;
- const QHash<QByteArray, QVariant> values = data();
+ const SmallHash values = data();
bool updatePixmap = (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight);
if (!updatePixmap && m_dirtyContent) {
@@ -1322,7 +1322,7 @@ void KStandardItemListWidget::updateIconsLayoutTextCache()
// Additional role 1
// Additional role 2
- const QHash<QByteArray, QVariant> values = data();
+ const SmallHash values = data();
const KItemListStyleOption &option = styleOption();
const qreal padding = option.padding;
@@ -1441,7 +1441,7 @@ void KStandardItemListWidget::updateCompactLayoutTextCache()
// | Icon | Additional role 1
// +------+ Additional role 2
- const QHash<QByteArray, QVariant> values = data();
+ const SmallHash values = data();
const KItemListStyleOption &option = styleOption();
const qreal widgetHeight = size().height();
@@ -1493,7 +1493,7 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache()
m_textRect = QRectF();
const KItemListStyleOption &option = styleOption();
- const QHash<QByteArray, QVariant> values = data();
+ const SmallHash values = data();
const qreal widgetHeight = size().height();
const int fontHeight = m_customizedFontMetrics.height();
diff --git a/src/kitemviews/kstandarditemlistwidget.h b/src/kitemviews/kstandarditemlistwidget.h
index e973169192..1f92998a69 100644
--- a/src/kitemviews/kstandarditemlistwidget.h
+++ b/src/kitemviews/kstandarditemlistwidget.h
@@ -42,7 +42,7 @@ protected:
* @return The value of the "text" role. The default implementation returns
* view->model()->data(index)["text"]. If a derived class can
* prevent the (possibly expensive) construction of the
- * QHash<QByteArray, QVariant> returned by KItemModelBase::data(int),
+ * SmallHash returned by KItemModelBase::data(int),
* it can reimplement this function.
*/
virtual QString itemText(int index, const KItemListView *view) const;
@@ -66,7 +66,7 @@ protected:
* a role might depend on other roles, so the values of all roles
* are passed as parameter.
*/
- virtual QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const;
+ virtual QString roleText(const QByteArray &role, const SmallHash &values, ForUsageAs forUsageAs = ForUsageAs::DisplayedText) const;
/**
* @return A font based on baseFont which is customized for symlinks.
@@ -164,7 +164,7 @@ protected:
/**
* @see KStandardItemListWidgetInformant::roleText().
*/
- QString roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const;
+ QString roleText(const QByteArray &role, const SmallHash &values) const;
static int numberOfUnicodeCharactersIn(const QString &text);
@@ -173,7 +173,7 @@ protected:
*/
virtual int selectionLength(const QString &text) const;
- void dataChanged(const QHash<QByteArray, QVariant> ¤t, const QSet<QByteArray> &roles = QSet<QByteArray>()) override;
+ void dataChanged(const SmallHash ¤t, const QSet<QByteArray> &roles = QSet<QByteArray>()) override;
void visibleRolesChanged(const QList<QByteArray> ¤t, const QList<QByteArray> &previous) override;
void columnWidthChanged(const QByteArray &role, qreal current, qreal previous) override;
void sidePaddingChanged(qreal leftPaddingWidth, qreal rightPaddingWidth) override;
diff --git a/src/kitemviews/private/kbaloorolesprovider.cpp b/src/kitemviews/private/kbaloorolesprovider.cpp
index 9e396ede51..030f0ed378 100644
--- a/src/kitemviews/private/kbaloorolesprovider.cpp
+++ b/src/kitemviews/private/kbaloorolesprovider.cpp
@@ -85,9 +85,9 @@ QSet<QByteArray> KBalooRolesProvider::roles() const
return m_roles;
}
-QHash<QByteArray, QVariant> KBalooRolesProvider::roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const
+SmallHash KBalooRolesProvider::roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const
{
- QHash<QByteArray, QVariant> values;
+ SmallHash values;
using entry = std::pair<const KFileMetaData::Property::Property &, const QVariant &>;
diff --git a/src/kitemviews/private/kbaloorolesprovider.h b/src/kitemviews/private/kbaloorolesprovider.h
index 5ff33e1c61..5e690afab5 100644
--- a/src/kitemviews/private/kbaloorolesprovider.h
+++ b/src/kitemviews/private/kbaloorolesprovider.h
@@ -9,6 +9,7 @@
#define KBALOO_ROLESPROVIDER_H
#include "dolphin_export.h"
+#include "smallhash.h"
#include <QHash>
#include <QSet>
@@ -40,7 +41,7 @@ public:
* @return Values for the roles \a roles that can be determined from the file
* with the URL \a url.
*/
- QHash<QByteArray, QVariant> roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const;
+ SmallHash roleValues(const Baloo::File &file, const QSet<QByteArray> &roles) const;
protected:
KBalooRolesProvider();
diff --git a/src/smallhash.h b/src/smallhash.h
new file mode 100644
index 0000000000..db8a2d870b
--- /dev/null
+++ b/src/smallhash.h
@@ -0,0 +1,117 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Méven Car <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef SMALLHASH_H
+#define SMALLHASH_H
+
+#include <QByteArray>
+#include <QHash>
+#include <QVariant>
+
+#include <utility>
+#include <vector>
+
+/**
+ * A minimal QHash-like map backed by a flat vector<pair<QByteArray, QVariant>>.
+ *
+ * Qt6's QHash allocates a ~3 KB minimum span on the first insert, so one QHash
+ * per item (a handful of entries times N items) dominates memory; a flat vector
+ * holds the same few entries in a fraction of that, with O(n) lookups that are
+ * cheap for the small n involved. Only the subset of the QHash API the callers
+ * need is provided, plus toHash()/fromHash() to bridge to code expecting a QHash.
+ */
+class SmallHash
+{
+public:
+ bool isEmpty() const
+ {
+ return m_data.empty();
+ }
+ int count() const
+ {
+ return static_cast<int>(m_data.size());
+ }
+ bool contains(const QByteArray &key) const
+ {
+ return indexOf(key) >= 0;
+ }
+ QVariant value(const QByteArray &key, const QVariant &defaultValue = QVariant()) const
+ {
+ const int i = indexOf(key);
+ return i >= 0 ? m_data[i].second : defaultValue;
+ }
+ void insert(const QByteArray &key, const QVariant &value)
+ {
+ (*this)[key] = value;
+ }
+ QVariant operator[](const QByteArray &key) const
+ {
+ return value(key);
+ }
+ // Returns a modifiable reference to the value for key, inserting a default
+ // one if absent (like QHash). The reference is valid until the next insert.
+ QVariant &operator[](const QByteArray &key)
+ {
+ const int i = indexOf(key);
+ if (i >= 0) {
+ return m_data[i].second;
+ }
+ m_data.emplace_back(key, QVariant());
+ return m_data.back().second;
+ }
+ void remove(const QByteArray &key)
+ {
+ const int i = indexOf(key);
+ if (i >= 0) {
+ m_data.erase(m_data.begin() + i);
+ }
+ }
+ void clear()
+ {
+ m_data.clear();
+ m_data.shrink_to_fit();
+ }
+ QHash<QByteArray, QVariant> toHash() const
+ {
+ QHash<QByteArray, QVariant> hash;
+ hash.reserve(static_cast<int>(m_data.size()));
+ for (const auto &[key, value] : m_data) {
+ hash.insert(key, value);
+ }
+ return hash;
+ }
+ static SmallHash fromHash(const QHash<QByteArray, QVariant> &hash)
+ {
+ SmallHash values;
+ values.m_data.reserve(hash.size());
+ for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
+ values.m_data.emplace_back(it.key(), it.value());
+ }
+ return values;
+ }
+ std::vector<std::pair<QByteArray, QVariant>>::const_iterator begin() const
+ {
+ return m_data.begin();
+ }
+ std::vector<std::pair<QByteArray, QVariant>>::const_iterator end() const
+ {
+ return m_data.end();
+ }
+
+private:
+ int indexOf(const QByteArray &key) const
+ {
+ for (size_t i = 0; i < m_data.size(); ++i) {
+ if (m_data[i].first == key) {
+ return static_cast<int>(i);
+ }
+ }
+ return -1;
+ }
+ std::vector<std::pair<QByteArray, QVariant>> m_data;
+};
+
+#endif // SMALLHASH_H
diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp
index 81540a1f1d..46e4e47083 100644
--- a/src/tests/kfileitemmodeltest.cpp
+++ b/src/tests/kfileitemmodeltest.cpp
@@ -261,7 +261,7 @@ void KFileItemModelTest::testSetData()
m_model->loadDirectory(m_testDir->url());
QVERIFY(itemsInsertedSpy.wait());
- QHash<QByteArray, QVariant> values;
+ SmallHash values;
values.insert("customRole1", "Test1");
values.insert("customRole2", "Test2");
@@ -327,15 +327,15 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole()
// b.txt -> 4
// c.txt -> 6
- QHash<QByteArray, QVariant> ratingA;
+ SmallHash ratingA;
ratingA.insert("rating", 2);
m_model->setData(0, ratingA);
- QHash<QByteArray, QVariant> ratingB;
+ SmallHash ratingB;
ratingB.insert("rating", 4);
m_model->setData(1, ratingB);
- QHash<QByteArray, QVariant> ratingC;
+ SmallHash ratingC;
ratingC.insert("rating", 6);
m_model->setData(2, ratingC);
@@ -345,7 +345,7 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole()
// Now change the rating from a.txt. This usually results
// in reordering of the items.
- QHash<QByteArray, QVariant> rating;
+ SmallHash rating;
rating.insert("rating", changedRating);
m_model->setData(changedIndex, rating);
@@ -427,7 +427,7 @@ void KFileItemModelTest::testResortAfterChangingName()
// We rename a.txt to d.txt. Even though the size has not changed at all,
// the model must re-sort the items.
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
data.insert("text", "d.txt");
m_model->setData(0, data);
@@ -2170,7 +2170,7 @@ void KFileItemModelTest::testNameRoleGroups()
QCOMPARE(m_model->groups(), expectedGroups);
// Rename d.txt to a.txt.
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
data.insert("text", "a.txt");
m_model->setData(2, data);
QVERIFY(itemsMovedSpy.wait());
diff --git a/src/tests/kitemlistselectionmanagertest.cpp b/src/tests/kitemlistselectionmanagertest.cpp
index da761d9a74..d727cbf3c0 100644
--- a/src/tests/kitemlistselectionmanagertest.cpp
+++ b/src/tests/kitemlistselectionmanagertest.cpp
@@ -19,7 +19,7 @@ public:
DummyModel();
void setCount(int count);
int count() const override;
- QHash<QByteArray, QVariant> data(int index) const override;
+ SmallHash data(int index) const override;
private:
int m_count;
@@ -41,10 +41,10 @@ int DummyModel::count() const
return m_count;
}
-QHash<QByteArray, QVariant> DummyModel::data(int index) const
+SmallHash DummyModel::data(int index) const
{
Q_UNUSED(index)
- return QHash<QByteArray, QVariant>();
+ return SmallHash();
}
class KItemListSelectionManagerTest : public QObject
diff --git a/src/views/dolphinfileitemlistwidget.cpp b/src/views/dolphinfileitemlistwidget.cpp
index daa1add81e..0163faac58 100644
--- a/src/views/dolphinfileitemlistwidget.cpp
+++ b/src/views/dolphinfileitemlistwidget.cpp
@@ -21,7 +21,7 @@ DolphinFileItemListWidget::~DolphinFileItemListWidget() = default;
void DolphinFileItemListWidget::refreshCache()
{
QColor color;
- const QHash<QByteArray, QVariant> values = data();
+ const SmallHash values = data();
QHash<Qt::Corner, QString> overlays;
if (values.contains("version")) {
// The item is under version control. Apply the text color corresponding
diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp
index ac69a408ee..68f9e0bc15 100644
--- a/src/views/dolphinview.cpp
+++ b/src/views/dolphinview.cpp
@@ -878,7 +878,7 @@ void DolphinView::renameSelectedItems()
for (int i = 0; i < items.count(); ++i) {
const int index = m_model->index(items[i]);
if (index >= 0) {
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
data.insert("text", urls[i].fileName());
m_model->setData(index, data);
}
@@ -2163,7 +2163,7 @@ void DolphinView::slotRenamingResult(KJob *job)
const QUrl oldUrl = copyJob->srcUrls().at(0);
const int index = m_model->index(newUrl);
if (m_model->index(oldUrl) == index) {
- QHash<QByteArray, QVariant> data;
+ SmallHash data;
data.insert("text", newUrl.fileName());
m_model->setData(index, data);
}
diff --git a/src/views/versioncontrol/versioncontrolobserver.cpp b/src/views/versioncontrol/versioncontrolobserver.cpp
index de81156c5f..b993656b14 100644
--- a/src/views/versioncontrol/versioncontrolobserver.cpp
+++ b/src/views/versioncontrol/versioncontrolobserver.cpp
@@ -210,7 +210,7 @@ void VersionControlObserver::slotThreadFinished()
for (const ItemState &item : items) {
const KFileItem &fileItem = item.first;
const KVersionControlPlugin::ItemVersion version = item.second;
- QHash<QByteArray, QVariant> values;
+ SmallHash values;
values.insert("version", QVariant(version));
m_model->setData(m_model->index(fileItem), values);
}