diff --git a/src/Mod/Start/Gui/FileCardDelegate.cpp b/src/Mod/Start/Gui/FileCardDelegate.cpp index 5a260753f6..7162a40710 100644 --- a/src/Mod/Start/Gui/FileCardDelegate.cpp +++ b/src/Mod/Start/Gui/FileCardDelegate.cpp @@ -35,21 +35,39 @@ #include #include #include +#include #endif #include "FileCardDelegate.h" #include "../App/DisplayedFilesModel.h" #include "App/Application.h" #include +#include +#include +#include using namespace Start; +QCache 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(_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(_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; } diff --git a/src/Mod/Start/Gui/FileCardDelegate.h b/src/Mod/Start/Gui/FileCardDelegate.h index 3da54f3c53..a072c074a4 100644 --- a/src/Mod/Start/Gui/FileCardDelegate.h +++ b/src/Mod/Start/Gui/FileCardDelegate.h @@ -25,6 +25,9 @@ #define FREECAD_START_FILECARDDELEGATE_H #include +#include +#include +#include #include #include #include @@ -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 _parameterGroup; const int margin = 11; const int textspacing = 2; QPushButton styleButton; + + static QCache _thumbnailCache; // cache key structure: "path:modtime:size" + static constexpr const int CACHE_SIZE_MB = 50; // 50MB cache limit }; diff --git a/src/Mod/Start/Gui/StartView.cpp b/src/Mod/Start/Gui/StartView.cpp index 923fabf563..5b039357e4 100644 --- a/src/Mod/Start/Gui/StartView.cpp +++ b/src/Mod/Start/Gui/StartView.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #endif #include "StartView.h" @@ -248,6 +249,7 @@ void StartView::configureFileCardWidget(QListView* fileCardWidget) { auto delegate = gsl::owner(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()) { + 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 listViews = findChildren(); + for (QListView* listView : listViews) { + listView->setUpdatesEnabled(enabled); + if (listView->viewport()) { + listView->viewport()->setUpdatesEnabled(enabled); + } + } +} + void StartView::retranslateUi() { QString title = QCoreApplication::translate("Workbench", "Start"); diff --git a/src/Mod/Start/Gui/StartView.h b/src/Mod/Start/Gui/StartView.h index f550f6700b..31397cf5f2 100644 --- a/src/Mod/Start/Gui/StartView.h +++ b/src/Mod/Start/Gui/StartView.h @@ -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;