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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user