[system/dolphin] src: informationpanel: ask for a preview that fits the panel it is shown in

Méven Car <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit ff51c83685c3058d8fd501196e090370a5a46f50 by Méven Car.
Committed on 07/08/2026 at 13:54.
Pushed by meven into branch 'master'.

informationpanel: ask for a preview that fits the panel it is shown in

A preview was asked for at the size the panel happened to have when the item was
selected, and the viewer drew it centred at its own size, neither growing nor
shrinking it. A panel the user widens therefore gained empty space around a
preview that stayed as it was, and a panel that was narrowed cut it off. A pdf
stayed as small as it was when it was selected however much room it was given,
while an image that was already large enough looked right by chance.

The viewer now draws a preview larger than itself scaled to fit, and asks for one
of the size it has when it is given more room than the preview it holds was made
for. Dragging the panel wider resizes it many times over, so nothing is asked for
while a preview is being made, and the size the viewer has by the time that one
arrives is what is asked for next.

The panel used to answer for this itself, re-showing the item on a resize so that
the content would ask again 300 milliseconds later. It no longer needs to, and
that path did more than was wanted, since showing an item with nothing selected
stats the folder it is in.

Asking for a preview to be kept, which the panel has opted out of since 2009,
lets it be handed out again for this file until the file changes, which KIO tells
from the modification time and the size of the file. What that bought is gone:
emitPreview() scales a preview down to the size that was asked for whatever the
scale type, so Unscaled no longer returns the size the preview was made at.

BUG: 435226

M  +0    -4    src/panels/information/informationpanel.cpp
M  +34   -2    src/panels/information/informationpanelcontent.cpp
M  +8    -0    src/panels/information/informationpanelcontent.h
M  +17   -2    src/panels/information/pixmapviewer.cpp
M  +3    -0    src/panels/information/pixmapviewer.h
M  +13   -0    src/tests/CMakeLists.txt
A  +90   -0    src/tests/informationpanelcontenttest.cpp     [License: GPL(v2.0+)]
A  +81   -0    src/tests/pixmapviewertest.cpp     [License: GPL(v2.0+)]

https://invent.kde.org/system/dolphin/-/commit/ff51c83685c3058d8fd501196e090370a5a46f50

diff --git a/src/panels/information/informationpanel.cpp b/src/panels/information/informationpanel.cpp
index e540cf8b71..9143bc7734 100644
--- a/src/panels/information/informationpanel.cpp
+++ b/src/panels/information/informationpanel.cpp
@@ -139,10 +139,6 @@ void InformationPanel::showEvent(QShowEvent *event)
 
 void InformationPanel::resizeEvent(QResizeEvent *event)
 {
-    if (isVisible()) {
-        m_urlCandidate = m_shownUrl;
-        m_infoTimer->start();
-    }
     Panel::resizeEvent(event);
 }
 
diff --git a/src/panels/information/informationpanelcontent.cpp b/src/panels/information/informationpanelcontent.cpp
index cf49e714d0..df1303ab79 100644
--- a/src/panels/information/informationpanelcontent.cpp
+++ b/src/panels/information/informationpanelcontent.cpp
@@ -143,6 +143,7 @@ InformationPanelContent::InformationPanelContent(QWidget *parent)
     grabGesture(Qt::TapAndHoldGesture);
 
     parent->installEventFilter(this);
+    m_preview->installEventFilter(this);
 }
 
 InformationPanelContent::~InformationPanelContent()
@@ -182,8 +183,11 @@ void InformationPanelContent::refreshPixmapView()
 
     const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
     const QStringList plugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins());
-    m_previewJob = new KIO::PreviewJob(KFileItemList() << m_item, QSize(m_preview->width(), m_preview->height()), &plugins);
-    m_previewJob->setScaleType(KIO::PreviewJob::Unscaled);
+    m_previewSize = m_preview->size();
+    m_previewJob = new KIO::PreviewJob(KFileItemList() << m_item, m_previewSize, &plugins);
+    // Asking for a preview to be kept lets it be handed out again for this file until the file changes,
+    // which is told from the time the file was modified and its size.
+    m_previewJob->setScaleType(KIO::PreviewJob::ScaledAndCached);
     m_previewJob->setIgnoreMaximumSize(m_item.isLocalFile() && !m_item.isSlow());
     m_previewJob->setDevicePixelRatio(devicePixelRatioF());
     if (m_previewJob->uiDelegate()) {
@@ -192,6 +196,32 @@ void InformationPanelContent::refreshPixmapView()
 
     connect(m_previewJob.data(), &KIO::PreviewJob::gotPreview, this, &InformationPanelContent::showPreview);
     connect(m_previewJob.data(), &KIO::PreviewJob::failed, this, &InformationPanelContent::showIcon);
+    connect(m_previewJob.data(), &KJob::finished, this, [this]() {
+        // The job is done with, so it is no longer what a request has to wait for.
+        m_previewJob = nullptr;
+        refreshPixmapViewForItsSize();
+    });
+}
+
+void InformationPanelContent::refreshPixmapViewForItsSize()
+{
+    if (m_item.isNull() || !m_preview->isVisible()) {
+        return;
+    }
+
+    // A preview is drawn to fit a viewer smaller than the one it was made for, so only a viewer with more
+    // room than that has anything to gain from another.
+    const QSize size = m_preview->size();
+    if (size.width() <= m_previewSize.width() && size.height() <= m_previewSize.height()) {
+        return;
+    }
+
+    if (m_previewJob) {
+        // One is being made, and the size the viewer has by then is what is asked for next.
+        return;
+    }
+
+    refreshPixmapView();
 }
 
 void InformationPanelContent::refreshPreview()
@@ -308,6 +338,8 @@ bool InformationPanelContent::eventFilter(QObject *obj, QEvent *event)
             // The size of the meta text area has changed. Adjust the fixed
             // width in a way that no horizontal scrollbar needs to be shown.
             m_metaDataWidget->setFixedWidth(resizeEvent->size().width());
+        } else if (obj == m_preview) {
+            refreshPixmapViewForItsSize();
         } else if (obj == parent()) {
             adjustWidgetSizes(resizeEvent->size().width());
         }
diff --git a/src/panels/information/informationpanelcontent.h b/src/panels/information/informationpanelcontent.h
index e305eddded..81422a8636 100644
--- a/src/panels/information/informationpanelcontent.h
+++ b/src/panels/information/informationpanelcontent.h
@@ -137,6 +137,12 @@ private:
      */
     void refreshPixmapView();
 
+    /**
+     * Asks for a preview of the size the viewer has, when it has room for a bigger one than the preview it
+     * holds was made for, and no preview is being made already.
+     */
+    void refreshPixmapViewForItsSize();
+
     bool gestureEvent(QGestureEvent *event);
 
 private:
@@ -144,6 +150,8 @@ private:
 
     QPointer<KIO::PreviewJob> m_previewJob;
     QTimer *m_outdatedPreviewTimer;
+    /** The size the preview that is shown was asked for. */
+    QSize m_previewSize;
 
     PixmapViewer *m_preview;
     MediaWidget *m_mediaWidget;
diff --git a/src/panels/information/pixmapviewer.cpp b/src/panels/information/pixmapviewer.cpp
index 4f3f9e82f2..b12dd3eae2 100644
--- a/src/panels/information/pixmapviewer.cpp
+++ b/src/panels/information/pixmapviewer.cpp
@@ -95,9 +95,24 @@ void PixmapViewer::paintEvent(QPaintEvent *event)
 
     QPainter painter(this);
 
-    if (!m_pixmap.isNull()) {
-        style()->drawItemPixmap(&painter, rect(), Qt::AlignCenter, m_pixmap);
+    if (m_pixmap.isNull()) {
+        return;
+    }
+
+    // A pixmap made for a wider panel is drawn to fit the one it is in, rather than being cut off by it.
+    const QSizeF shown = m_pixmap.deviceIndependentSize();
+    if (shown.width() > width() || shown.height() > height()) {
+        if (m_scaledFrom != m_pixmap.cacheKey() || m_scaledFor != size()) {
+            m_scaled = m_pixmap.scaled(size() * m_pixmap.devicePixelRatio(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
+            m_scaled.setDevicePixelRatio(m_pixmap.devicePixelRatio());
+            m_scaledFrom = m_pixmap.cacheKey();
+            m_scaledFor = size();
+        }
+        style()->drawItemPixmap(&painter, rect(), Qt::AlignCenter, m_scaled);
+        return;
     }
+
+    style()->drawItemPixmap(&painter, rect(), Qt::AlignCenter, m_pixmap);
 }
 
 void PixmapViewer::updateAnimatedImageFrame()
diff --git a/src/panels/information/pixmapviewer.h b/src/panels/information/pixmapviewer.h
index a71ce6677c..67b1f173f0 100644
--- a/src/panels/information/pixmapviewer.h
+++ b/src/panels/information/pixmapviewer.h
@@ -57,6 +57,9 @@ private Q_SLOTS:
 
 private:
     QPixmap m_pixmap;
+    QPixmap m_scaled;
+    qint64 m_scaledFrom = 0;
+    QSize m_scaledFor;
     QMovie *m_animatedImage;
     QSize m_sizeHint;
     bool m_hasAnimatedImage;
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 04725f990c..4cc502216a 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -127,6 +127,19 @@ ecm_add_test(placespaneltest.cpp
     TEST_NAME placespaneltest
     LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test)
 
+# The information panel is built only where Baloo is, and so are the tests of it.
+if(HAVE_BALOO)
+    # PixmapViewerTest
+    ecm_add_test(pixmapviewertest.cpp
+        TEST_NAME pixmapviewertest
+        LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test)
+
+    # InformationPanelContentTest
+    ecm_add_test(informationpanelcontenttest.cpp
+        TEST_NAME informationpanelcontenttest
+        LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test)
+endif()
+
 find_gem(test-unit)
 set_package_properties(Gem_test-unit PROPERTIES
     TYPE RECOMMENDED
diff --git a/src/tests/informationpanelcontenttest.cpp b/src/tests/informationpanelcontenttest.cpp
new file mode 100644
index 0000000000..978e657057
--- /dev/null
+++ b/src/tests/informationpanelcontenttest.cpp
@@ -0,0 +1,90 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Méven Car <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "panels/information/informationpanelcontent.h"
+#include "panels/information/pixmapviewer.h"
+
+#include <KFileItem>
+
+#include <QPainter>
+#include <QTemporaryDir>
+#include <QTest>
+#include <QVBoxLayout>
+#include <QWidget>
+
+class InformationPanelContentTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void initTestCase();
+    void testThePreviewGrowsWithThePanel();
+
+private:
+    QTemporaryDir m_dir;
+    QString m_imagePath;
+};
+
+void InformationPanelContentTest::initTestCase()
+{
+    QVERIFY(m_dir.isValid());
+    m_imagePath = m_dir.path() + QLatin1String("/big.png");
+
+    // Larger than any panel the test gives it, so what arrives is what was asked for rather than all the
+    // image has.
+    QImage image(2000, 2000, QImage::Format_RGB32);
+    image.fill(Qt::white);
+    QPainter painter(&image);
+    painter.fillRect(0, 0, 1000, 1000, Qt::red);
+    painter.end();
+    QVERIFY(image.save(m_imagePath));
+}
+
+// A panel that is made wider shows a preview made for the room it has now, rather than the one it was
+// given when the file was selected.
+void InformationPanelContentTest::testThePreviewGrowsWithThePanel()
+{
+    // The panel builds its content when it is shown, so the content is built here as it is there, while
+    // the panel it is given is on its way to the screen.
+    QWidget panel;
+    panel.resize(300, 900);
+    panel.show();
+    QVERIFY(QTest::qWaitForWindowExposed(&panel));
+
+    auto *content = new InformationPanelContent(&panel);
+    QVBoxLayout *layout = new QVBoxLayout(&panel);
+    layout->setContentsMargins(0, 0, 0, 0);
+    layout->addWidget(content);
+
+    auto *viewer = content->findChild<PixmapViewer *>();
+    QVERIFY(viewer);
+
+    content->showItem(KFileItem(QUrl::fromLocalFile(m_imagePath)));
+
+    // Every preview is made by a thumbnailer of the system, which the test cannot do without.
+    if (!QTest::qWaitFor([viewer]() {
+            return !viewer->pixmap().isNull();
+        })) {
+        QSKIP("no thumbnailer answered for a png");
+    }
+
+    const int narrow = viewer->pixmap().width();
+    QVERIFY(narrow > 0);
+
+    panel.resize(900, 900);
+    QTRY_VERIFY(viewer->pixmap().width() > narrow);
+
+    // What arrived is of the size the viewer grew to, not merely bigger than it was: it fits the viewer,
+    // and fills it in the direction that bounds it.
+    const QSize preview = viewer->pixmap().deviceIndependentSize().toSize();
+    QVERIFY(preview.width() <= viewer->width());
+    QVERIFY(preview.height() <= viewer->height());
+    QCOMPARE(qMax(preview.width(), preview.height()), qMin(viewer->width(), viewer->height()));
+}
+
+QTEST_MAIN(InformationPanelContentTest)
+
+#include "informationpanelcontenttest.moc"
diff --git a/src/tests/pixmapviewertest.cpp b/src/tests/pixmapviewertest.cpp
new file mode 100644
index 0000000000..a3e5f5dc28
--- /dev/null
+++ b/src/tests/pixmapviewertest.cpp
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Méven Car <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "panels/information/pixmapviewer.h"
+
+#include <QPainter>
+#include <QTest>
+
+// A pixmap of the given size, white but for a red corner at the top left and a blue one at the bottom
+// right, so that what became of its corners says how it was drawn.
+static QPixmap cornerMarkedPixmap(int size)
+{
+    QPixmap pixmap(size, size);
+    pixmap.fill(Qt::white);
+
+    const int corner = size / 10;
+    QPainter painter(&pixmap);
+    painter.fillRect(0, 0, corner, corner, Qt::red);
+    painter.fillRect(size - corner, size - corner, corner, corner, Qt::blue);
+    painter.end();
+    return pixmap;
+}
+
+static QImage renderOf(PixmapViewer &viewer)
+{
+    QImage image(viewer.size(), QImage::Format_ARGB32);
+    image.fill(Qt::transparent);
+    viewer.render(&image);
+    return image;
+}
+
+class PixmapViewerTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void testPixmapLargerThanTheViewerIsDrawnToFit();
+    void testPixmapSmallerThanTheViewerKeepsItsSize();
+};
+
+// A preview made for a panel wider than the one it ends up in is drawn to fit, so all of it is seen.
+void PixmapViewerTest::testPixmapLargerThanTheViewerIsDrawnToFit()
+{
+    PixmapViewer viewer(nullptr);
+    viewer.resize(300, 300);
+    // The viewer holds a minimum size of its own, so what it ended up with is what the corners are read
+    // against.
+    const QSize shown = viewer.size();
+    viewer.setPixmap(cornerMarkedPixmap(qMax(shown.width(), shown.height()) * 3));
+
+    const QImage image = renderOf(viewer);
+    QCOMPARE(image.pixelColor(2, 2), QColor(Qt::red));
+    QCOMPARE(image.pixelColor(shown.width() - 3, shown.height() - 3), QColor(Qt::blue));
+}
+
+// One that fits is left as it is, rather than being blown up to the panel.
+void PixmapViewerTest::testPixmapSmallerThanTheViewerKeepsItsSize()
+{
+    PixmapViewer viewer(nullptr);
+    viewer.resize(400, 400);
+    const QSize shown = viewer.size();
+    const int pixmapSize = 100;
+    viewer.setPixmap(cornerMarkedPixmap(pixmapSize));
+
+    const QImage image = renderOf(viewer);
+    // The pixmap is centred at its own size, so its corners are inside the viewer rather than at the
+    // corners of it, which are none of the pixmap.
+    const int left = (shown.width() - pixmapSize) / 2;
+    const int top = (shown.height() - pixmapSize) / 2;
+    QCOMPARE(image.pixelColor(left + 2, top + 2), QColor(Qt::red));
+    QCOMPARE(image.pixelColor(left + pixmapSize - 3, top + pixmapSize - 3), QColor(Qt::blue));
+    QVERIFY(image.pixelColor(2, 2) != QColor(Qt::red));
+    QVERIFY(image.pixelColor(shown.width() - 3, shown.height() - 3) != QColor(Qt::blue));
+}
+
+QTEST_MAIN(PixmapViewerTest)
+
+#include "pixmapviewertest.moc"
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.