Start: Add caching for performance for thumbnails on start page (#23186)

* Start: Add caching for performance for thumbnails on start page

So currently we can have a problem where we are trying to load whole
image as a thumbnail, which can result in over 256MB~ of internal buffer
memory. Also, even if we load smaller size - every now and then start
page gets refreshed, so to check if any file got modified and refresh it
in recent files. This is okay, but with large files it loads them over
and over, resulting in start page lagging.

Solution for that is first - load image thumbnails as scaled, small
images instead of full image.

Second - for performance, use caching by using `path:modtime:size` key.
If the item fits this key, it means it didn't change, so just proceed
further and get this item from the cache. If the key is different, it
means it has been changed on the disk, so reload it.

* Start: Deactivate Start page if it loses focus to stop receiving events

As the title says. This prevents Start page from processing unnecessary
events (mostly paint ones that were causing extreme lag previously) if
it is not opened.

* Start: Preserve aspect ratio of the image for the thumbnail

* Start: use brace initialization when returning QString

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
tetektoza
2025-08-22 05:06:07 +02:00
committed by GitHub
parent 034ec47e33
commit a2250dd85d
4 changed files with 142 additions and 26 deletions

View File

@@ -35,21 +35,39 @@
#include <QApplication>
#include <QPushButton>
#include <QString>
#include <QAbstractItemView>
#endif
#include "FileCardDelegate.h"
#include "../App/DisplayedFilesModel.h"
#include "App/Application.h"
#include <Base/Color.h>
#include <Base/Console.h>
#include <Gui/Application.h>
#include <Gui/MainWindow.h>
using namespace Start;
QCache<QString, QPixmap> FileCardDelegate::_thumbnailCache;
FileCardDelegate::FileCardDelegate(QObject* parent)
: QStyledItemDelegate(parent)
{
_parameterGroup = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Start");
setObjectName(QStringLiteral("thumbnailWidget"));
// Initialize cache size based on thumbnail size (only once)
if (_thumbnailCache.maxCost() == 0) {
int thumbnailSize =
static_cast<int>(_parameterGroup->GetInt("FileThumbnailIconsSize", 128));
int thumbnailMemory = thumbnailSize * thumbnailSize * 4; // rgba
int maxCacheItems = (CACHE_SIZE_MB * 1024 * 1024) / thumbnailMemory;
_thumbnailCache.setMaxCost(maxCacheItems);
Base::Console().log("FileCardDelegate: Initialized thumbnail cache for %d items (%d MB)\n",
maxCacheItems,
CACHE_SIZE_MB);
}
}
void FileCardDelegate::paint(QPainter* painter,
@@ -135,49 +153,93 @@ QSize FileCardDelegate::sizeHint(const QStyleOptionViewItem& option, const QMode
return {cardWidth, cardHeight};
}
namespace
{
QPixmap pixmapToSizedQImage(const QImage& pixmap, int size)
{
return QPixmap::fromImage(pixmap).scaled(size,
size,
Qt::AspectRatioMode::KeepAspectRatio,
Qt::TransformationMode::SmoothTransformation);
}
} // namespace
QPixmap FileCardDelegate::generateThumbnail(const QString& path) const
{
auto thumbnailSize =
static_cast<int>(_parameterGroup->GetInt("FileThumbnailIconsSize", 128)); // NOLINT
// check if we have this thumbnail already inside cache, don't load it once again
QString cacheKey = getCacheKey(path, thumbnailSize);
if (!cacheKey.isEmpty()) {
if (QPixmap* cachedThumbnail = _thumbnailCache.object(cacheKey)) {
return *cachedThumbnail; // cache hit - we bail out
}
}
// cache miss - go and load the thumbnail as it could be changed
return loadAndCacheThumbnail(path, thumbnailSize);
}
QString FileCardDelegate::getCacheKey(const QString& path, int thumbnailSize) const
{
QFileInfo fileInfo(path);
if (!fileInfo.exists()) {
return {};
}
// create cache key: path:modtime:size
QString modTime = QString::number(fileInfo.lastModified().toSecsSinceEpoch());
return QStringLiteral("%1:%2:%3").arg(path, modTime, QString::number(thumbnailSize));
}
QPixmap FileCardDelegate::loadAndCacheThumbnail(const QString& path, int thumbnailSize) const
{
QPixmap thumbnail;
if (path.endsWith(QLatin1String(".fcstd"), Qt::CaseSensitivity::CaseInsensitive)) {
// This is a fallback, the model will have pulled the thumbnail out of the FCStd file if it
// existed.
QImageReader reader(QLatin1String(":/icons/freecad-doc.svg"));
reader.setScaledSize({thumbnailSize, thumbnailSize});
return QPixmap::fromImage(reader.read());
reader.setScaledSize(QSize(thumbnailSize, thumbnailSize));
thumbnail = QPixmap::fromImage(reader.read());
}
if (path.endsWith(QLatin1String(".fcmacro"), Qt::CaseSensitivity::CaseInsensitive)) {
else if (path.endsWith(QLatin1String(".fcmacro"), Qt::CaseSensitivity::CaseInsensitive)) {
QImageReader reader(QLatin1String(":/icons/MacroEditor.svg"));
reader.setScaledSize({thumbnailSize, thumbnailSize});
return QPixmap::fromImage(reader.read());
reader.setScaledSize(QSize(thumbnailSize, thumbnailSize));
thumbnail = QPixmap::fromImage(reader.read());
}
if (!QImageReader::imageFormat(path).isEmpty()) {
else if (!QImageReader::imageFormat(path).isEmpty()) {
// It is an image: it can be its own thumbnail
QImageReader reader(path);
// get original size to calculate proper aspect-preserving scaled size
QSize originalSize = reader.size();
if (originalSize.isValid()) {
QSize scaledSize =
originalSize.scaled(thumbnailSize, thumbnailSize, Qt::KeepAspectRatio);
reader.setScaledSize(scaledSize);
}
auto image = reader.read();
if (!image.isNull()) {
return pixmapToSizedQImage(image, thumbnailSize);
thumbnail = QPixmap::fromImage(image);
}
else {
Base::Console().log("FileCardDelegate: Failed to load image %s: %s\n",
path.toStdString().c_str(),
reader.errorString().toStdString().c_str());
}
}
QIcon icon = QFileIconProvider().icon(QFileInfo(path));
if (!icon.isNull()) {
QPixmap pixmap = icon.pixmap(thumbnailSize);
if (!pixmap.isNull()) {
return pixmap;
// fallback to system icon if no thumbnail was generated
if (thumbnail.isNull()) {
QIcon icon = QFileIconProvider().icon(QFileInfo(path));
if (!icon.isNull()) {
thumbnail = icon.pixmap(thumbnailSize);
}
else {
thumbnail = QPixmap(thumbnailSize, thumbnailSize);
thumbnail.fill();
}
}
QPixmap pixmap = QPixmap(thumbnailSize, thumbnailSize);
pixmap.fill();
return pixmap;
// cache the thumbnail if valid
if (!thumbnail.isNull()) {
QString cacheKey = getCacheKey(path, thumbnailSize);
if (!cacheKey.isEmpty()) {
_thumbnailCache.insert(cacheKey, new QPixmap(thumbnail), 1);
}
}
return thumbnail;
}

View File

@@ -25,6 +25,9 @@
#define FREECAD_START_FILECARDDELEGATE_H
#include <Base/Parameter.h>
#include <QCache>
#include <QEvent>
#include <QFileInfo>
#include <QImage>
#include <QPushButton>
#include <QStyledItemDelegate>
@@ -45,10 +48,16 @@ protected:
QPixmap generateThumbnail(const QString& path) const;
private:
QString getCacheKey(const QString& path, int thumbnailSize) const;
QPixmap loadAndCacheThumbnail(const QString& path, int thumbnailSize) const;
Base::Reference<ParameterGrp> _parameterGroup;
const int margin = 11;
const int textspacing = 2;
QPushButton styleButton;
static QCache<QString, QPixmap> _thumbnailCache; // cache key structure: "path:modtime:size"
static constexpr const int CACHE_SIZE_MB = 50; // 50MB cache limit
};

View File

@@ -37,6 +37,7 @@
#include <QTimer>
#include <QWidget>
#include <QStackedWidget>
#include <QShowEvent>
#endif
#include "StartView.h"
@@ -248,6 +249,7 @@ void StartView::configureFileCardWidget(QListView* fileCardWidget)
{
auto delegate = gsl::owner<FileCardDelegate*>(new FileCardDelegate(fileCardWidget));
fileCardWidget->setItemDelegate(delegate);
fileCardWidget->setMinimumWidth(fileCardWidget->parentWidget()->width());
// fileCardWidget->setGridSize(
// fileCardWidget->itemDelegate()->sizeHint(QStyleOptionViewItem(),
@@ -442,12 +444,48 @@ void StartView::changeEvent(QEvent* event)
}
}
}
if (event->type() == QEvent::LanguageChange) {
this->retranslateUi();
}
Gui::MDIView::changeEvent(event);
}
void StartView::showEvent(QShowEvent* event)
{
if (auto mainWindow = Gui::getMainWindow()) {
if (auto mdiArea = mainWindow->findChild<QMdiArea*>()) {
connect(mdiArea,
&QMdiArea::subWindowActivated,
this,
&StartView::onMdiSubWindowActivated,
Qt::UniqueConnection);
}
}
Gui::MDIView::showEvent(event);
}
void StartView::onMdiSubWindowActivated(QMdiSubWindow* subWindow)
{
// check if start view is activated subwindow if yes, then enable updates
// so we can once again receive paint events
bool isOurWindow = subWindow && subWindow->isAncestorOf(this);
setListViewUpdatesEnabled(isOurWindow);
}
void StartView::setListViewUpdatesEnabled(bool enabled)
{
// disable updates on all QListView widgets when inactive to prevent unnecessary paint events
QList<QListView*> listViews = findChildren<QListView*>();
for (QListView* listView : listViews) {
listView->setUpdatesEnabled(enabled);
if (listView->viewport()) {
listView->viewport()->setUpdatesEnabled(enabled);
}
}
}
void StartView::retranslateUi()
{
QString title = QCoreApplication::translate("Workbench", "Start");

View File

@@ -38,6 +38,7 @@ class QEvent;
class QGridLayout;
class QLabel;
class QListView;
class QMdiSubWindow;
class QScrollArea;
class QStackedWidget;
class QPushButton;
@@ -82,6 +83,7 @@ public:
protected:
void changeEvent(QEvent* e) override;
void showEvent(QShowEvent* event) override;
void configureNewFileButtons(QLayout* layout) const;
static void configureFileCardWidget(QListView* fileCardWidget);
@@ -98,8 +100,13 @@ protected:
QString fileCardStyle() const;
private Q_SLOTS:
void onMdiSubWindowActivated(QMdiSubWindow* subWindow);
private:
void retranslateUi();
void setListViewUpdatesEnabled(bool enabled);
QStackedWidget* _contents = nullptr;
Start::RecentFilesModel _recentFilesModel;
Start::ExamplesModel _examplesModel;