From f299b565b5904e39a47b6133643448e46810f0ed Mon Sep 17 00:00:00 2001 From: Mitch Curtis Date: Wed, 4 Jan 2017 16:27:52 +0100 Subject: Implement support for Scale directory key according to Icon Theme spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qt already supports high DPI icons using the “@nx” approach, where the device pixel ratio that the image was designed for is in the file name. However, our implementation of the freedekstop.org Icon Theme specification did not support the Scale directory key: https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout This meant that users creating icons via QIcon::fromTheme() did not get high DPI support. This patch fixes that. [ChangeLog][QtGui][QIcon] Implemented support for Scale directory key according to Icon Theme Spec. Icons created via QIcon::fromTheme() now have high DPI support by specifying the Scale in the appropriate entry of the relevant index.theme file. Task-number: QTBUG-49820 Change-Id: If442fbc551034166d88defe607109de1c6ca1d28 Reviewed-by: Paul Olav Tvete Reviewed-by: Shawn Rutledge Reviewed-by: Topi Reiniö Reviewed-by: Eirik Aavitsland --- src/gui/doc/qtgui.qdocconf | 3 +- src/gui/doc/src/external-resources.qdoc | 12 +- .../includes/qiconengine-virtualhookhelper.qdocinc | 3 + src/gui/image/qicon.cpp | 52 +++++- src/gui/image/qiconengine.cpp | 102 ++++++++++- src/gui/image/qiconengine.h | 12 +- src/gui/image/qiconloader.cpp | 48 +++-- src/gui/image/qiconloader_p.h | 5 +- tests/auto/gui/image/image.pro | 1 + .../testtheme/16x16/actions/appointment-new.png | Bin 0 -> 897 bytes .../testtheme/22x22/actions/appointment-new.png | Bin 0 -> 1411 bytes .../testtheme/22x22@2/actions/appointment-new.png | Bin 0 -> 1740 bytes .../image/qiconhighdpi/icons/testtheme/index.theme | 21 +++ tests/auto/gui/image/qiconhighdpi/qiconhighdpi.pro | 8 + .../gui/image/qiconhighdpi/tst_qiconhighdpi.cpp | 195 +++++++++++++++++++++ .../gui/image/qiconhighdpi/tst_qiconhighdpi.qrc | 8 + 16 files changed, 443 insertions(+), 27 deletions(-) create mode 100644 src/gui/doc/src/includes/qiconengine-virtualhookhelper.qdocinc create mode 100644 tests/auto/gui/image/qiconhighdpi/icons/testtheme/16x16/actions/appointment-new.png create mode 100644 tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22/actions/appointment-new.png create mode 100644 tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22@2/actions/appointment-new.png create mode 100644 tests/auto/gui/image/qiconhighdpi/icons/testtheme/index.theme create mode 100644 tests/auto/gui/image/qiconhighdpi/qiconhighdpi.pro create mode 100644 tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.cpp create mode 100644 tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.qrc diff --git a/src/gui/doc/qtgui.qdocconf b/src/gui/doc/qtgui.qdocconf index b07d39fa37..94574a314c 100644 --- a/src/gui/doc/qtgui.qdocconf +++ b/src/gui/doc/qtgui.qdocconf @@ -44,7 +44,8 @@ depends += \ headerdirs += .. sourcedirs += .. \ - ../../../examples/gui/doc/src + ../../../examples/gui/doc/src \ + src/includes exampledirs += ../../../examples/gui \ snippets diff --git a/src/gui/doc/src/external-resources.qdoc b/src/gui/doc/src/external-resources.qdoc index 6a423323ea..480a4057be 100644 --- a/src/gui/doc/src/external-resources.qdoc +++ b/src/gui/doc/src/external-resources.qdoc @@ -49,4 +49,14 @@ /*! \externalpage http://www.opengl.org/wiki/Tessellation_Shader \title OpenGL Tessellation Shaders -*/ \ No newline at end of file +*/ + +/*! + \externalpage https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html + \title Icon Theme Specification +*/ + +/*! + \externalpage https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout + \title Icon Theme Specification - Directory Layout +*/ diff --git a/src/gui/doc/src/includes/qiconengine-virtualhookhelper.qdocinc b/src/gui/doc/src/includes/qiconengine-virtualhookhelper.qdocinc new file mode 100644 index 0000000000..b17d2bd66f --- /dev/null +++ b/src/gui/doc/src/includes/qiconengine-virtualhookhelper.qdocinc @@ -0,0 +1,3 @@ +\note This is a helper method and the actual work is done by the +virtual_hook() method, hence this method depends on icon engine support +and may not work with all icon engines. diff --git a/src/gui/image/qicon.cpp b/src/gui/image/qicon.cpp index e8f2c878c8..17734f05f3 100644 --- a/src/gui/image/qicon.cpp +++ b/src/gui/image/qicon.cpp @@ -605,6 +605,51 @@ QFactoryLoader *qt_iconEngineFactoryLoader() \note QIcon needs a QGuiApplication instance before the icon is created. + \section1 High DPI Icons + + There are two ways that QIcon supports \l {High DPI Displays}{high DPI} + icons: via \l addFile() and \l fromTheme(). + + \l addFile() is useful if you have your own custom directory structure and do + not need to use the \l {Icon Theme Specification}{freedesktop.org Icon Theme + Specification}. Icons created via this approach use Qt's \l {High Resolution + Versions of Images}{"@nx" high DPI syntax}. + + Using \l fromTheme() is necessary if you plan on following the Icon Theme + Specification. To make QIcon use the high DPI version of an image, add an + additional entry to the appropriate \c index.theme file: + + \badcode + [Icon Theme] + Name=Test + Comment=Test Theme + + Directories=32x32/actions,32x32@2/actions + + [32x32/actions] + Size=32 + Context=Actions + Type=Fixed + + # High DPI version of the entry above. + [32x32@2/actions] + Size=32 + Scale=2 + Type=Fixed + \endcode + + Your icon theme directory would then look something like this: + + \badcode + ├── 32x32 + │ └── actions + │ └── appointment-new.png + ├── 32x32@2 + │ └── actions + │ └── appointment-new.png + └── index.theme + \endcode + \sa {fowler}{GUI Design Handbook: Iconic Label}, {Icons Example} */ @@ -847,9 +892,10 @@ QPixmap QIcon::pixmap(QWindow *window, const QSize &size, Mode mode, State state } // Try get a pixmap that is big enough to be displayed at device pixel resolution. - QPixmap pixmap = d->engine->pixmap(size * devicePixelRatio, mode, state); - pixmap.setDevicePixelRatio(d->pixmapDevicePixelRatio(devicePixelRatio, size, pixmap.size())); - return pixmap; + QIconEngine::ScaledPixmapArgument scalePixmapArg = { size * devicePixelRatio, mode, state, devicePixelRatio, QPixmap() }; + d->engine->virtual_hook(QIconEngine::ScaledPixmapHook, reinterpret_cast(&scalePixmapArg)); + scalePixmapArg.pixmap.setDevicePixelRatio(d->pixmapDevicePixelRatio(devicePixelRatio, size, scalePixmapArg.pixmap.size())); + return scalePixmapArg.pixmap; } /*! diff --git a/src/gui/image/qiconengine.cpp b/src/gui/image/qiconengine.cpp index 0ba9844f7a..1f8e5f321a 100644 --- a/src/gui/image/qiconengine.cpp +++ b/src/gui/image/qiconengine.cpp @@ -169,6 +169,12 @@ void QIconEngine::addFile(const QString &/*fileName*/, const QSize &/*size*/, QI bool that can be set to true if the icon is null. This enum value was added in Qt 5.7. + \value ScaledPixmapHook Provides a way to get a pixmap that is scaled + according to the given scale (typically equal to the \l {Glossary Of High + DPI Terms}{device pixel ratio}). The \a data argument of the virtual_hook() + function is a \l ScaledPixmapArgument pointer that contains both the input and + output arguments. This enum value was added in Qt 5.9. + \sa virtual_hook() */ @@ -207,6 +213,60 @@ void QIconEngine::addFile(const QString &/*fileName*/, const QSize &/*size*/, QI vectorial format normally return an empty list. */ +/*! + \class QIconEngine::ScaledPixmapArgument + \since 5.9 + + \inmodule QtGui + + This struct represents arguments to the virtual_hook() function when + the \a id parameter is QIconEngine::ScaledPixmapHook. + + The struct provides a way for icons created via \l QIcon::fromTheme() + to return pixmaps that are designed for the current \l {Glossary Of High + DPI Terms}{device pixel ratio}. The scale for such an icon is specified + using the \l {Icon Theme Specification - Directory Layout}{Scale directory key} + in the appropriate \c index.theme file. + + Icons created via other approaches will return the same result as a call to + \l pixmap() would, and continue to benefit from Qt's \l {High Resolution + Versions of Images}{"@nx" high DPI syntax}. + + \sa virtual_hook(), QIconEngine::IconEngineHook, {High DPI Icons} + */ + +/*! + \variable QIconEngine::ScaledPixmapArgument::size + \brief The requested size of the pixmap. +*/ + +/*! + \variable QIconEngine::ScaledPixmapArgument::mode + \brief The requested mode of the pixmap. + + \sa QIcon::Mode +*/ + +/*! + \variable QIconEngine::ScaledPixmapArgument::state + \brief The requested state of the pixmap. + + \sa QIcon::State +*/ + +/*! + \variable QIconEngine::ScaledPixmapArgument::scale + \brief The requested scale of the pixmap. +*/ + +/*! + \variable QIconEngine::ScaledPixmapArgument::pixmap + + \brief The pixmap that is the best match for the given \l size, \l mode, \l + \state, and \l scale. This is an output parameter that is set after calling + \l virtual_hook(). +*/ + /*! Returns a key that identifies this icon engine. @@ -262,6 +322,13 @@ void QIconEngine::virtual_hook(int id, void *data) arg.sizes.clear(); break; } + case QIconEngine::ScaledPixmapHook: { + // We don't have any notion of scale besides "@nx", so just call pixmap() here. + QIconEngine::ScaledPixmapArgument &arg = + *reinterpret_cast(data); + arg.pixmap = pixmap(arg.size, arg.mode, arg.state); + break; + } default: break; } @@ -273,9 +340,7 @@ void QIconEngine::virtual_hook(int id, void *data) Returns sizes of all images that are contained in the engine for the specific \a mode and \a state. - \note This is a helper method and the actual work is done by - virtual_hook() method, hence this method depends on icon engine support - and may not work with all icon engines. + \include qiconengine-virtualhookhelper.qdocinc */ QList QIconEngine::availableSizes(QIcon::Mode mode, QIcon::State state) const { @@ -291,9 +356,7 @@ QList QIconEngine::availableSizes(QIcon::Mode mode, QIcon::State state) c Returns the name used to create the engine, if available. - \note This is a helper method and the actual work is done by - virtual_hook() method, hence this method depends on icon engine support - and may not work with all icon engines. + \include qiconengine-virtualhookhelper.qdocinc */ QString QIconEngine::iconName() const { @@ -306,6 +369,8 @@ QString QIconEngine::iconName() const \since 5.7 Returns true if this icon engine represent a null QIcon. + + \include qiconengine-virtualhookhelper.qdocinc */ bool QIconEngine::isNull() const { @@ -314,4 +379,29 @@ bool QIconEngine::isNull() const return isNull; } +/*! + \since 5.9 + + Returns a pixmap for the given \a size, \a mode, \a state and \a scale. + + The \a scale argument is typically equal to the \l {Glossary Of High DPI + Terms}{device pixel ratio} of the display. + + \include qiconengine-virtualhookhelper.qdocinc + + \note Some engines may cast \a scale to an integer. + + \sa ScaledPixmapArgument +*/ +QPixmap QIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) +{ + ScaledPixmapArgument arg; + arg.size = size; + arg.mode = mode; + arg.state = state; + arg.scale = scale; + const_cast(this)->virtual_hook(QIconEngine::ScaledPixmapHook, reinterpret_cast(&arg)); + return arg.pixmap; +} + QT_END_NAMESPACE diff --git a/src/gui/image/qiconengine.h b/src/gui/image/qiconengine.h index 783770cd30..0c67ef2686 100644 --- a/src/gui/image/qiconengine.h +++ b/src/gui/image/qiconengine.h @@ -65,7 +65,7 @@ public: virtual bool read(QDataStream &in); virtual bool write(QDataStream &out) const; - enum IconEngineHook { AvailableSizesHook = 1, IconNameHook, IsNullHook }; + enum IconEngineHook { AvailableSizesHook = 1, IconNameHook, IsNullHook, ScaledPixmapHook }; struct AvailableSizesArgument { @@ -79,6 +79,16 @@ public: virtual QString iconName() const; bool isNull() const; // ### Qt6 make virtual + QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale); // ### Qt6 make virtual + + struct ScaledPixmapArgument + { + QSize size; + QIcon::Mode mode; + QIcon::State state; + qreal scale; + QPixmap pixmap; + }; virtual void virtual_hook(int id, void *data); diff --git a/src/gui/image/qiconloader.cpp b/src/gui/image/qiconloader.cpp index 324f13a17b..d72c05a3c5 100644 --- a/src/gui/image/qiconloader.cpp +++ b/src/gui/image/qiconloader.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -347,6 +348,10 @@ QIconTheme::QIconTheme(const QString &themeName) dirInfo.maxSize = indexReader.value(directoryKey + QLatin1String("/MaxSize"), size).toInt(); + + dirInfo.scale = indexReader.value(directoryKey + + QLatin1String("/Scale"), + 1).toInt(); m_keyList.append(dirInfo); } } @@ -553,8 +558,11 @@ void QIconLoaderEngine::paint(QPainter *painter, const QRect &rect, * This algorithm is defined by the freedesktop spec: * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html */ -static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize) +static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize, int iconscale) { + if (dir.scale != iconscale) + return false; + if (dir.type == QIconDirInfo::Fixed) { return dir.size == iconsize; @@ -575,24 +583,25 @@ static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize) * This algorithm is defined by the freedesktop spec: * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html */ -static int directorySizeDistance(const QIconDirInfo &dir, int iconsize) +static int directorySizeDistance(const QIconDirInfo &dir, int iconsize, int iconscale) { + const int scaledIconSize = iconsize * iconscale; if (dir.type == QIconDirInfo::Fixed) { - return qAbs(dir.size - iconsize); + return qAbs(dir.size * dir.scale - scaledIconSize); } else if (dir.type == QIconDirInfo::Scalable) { - if (iconsize < dir.minSize) - return dir.minSize - iconsize; - else if (iconsize > dir.maxSize) - return iconsize - dir.maxSize; + if (scaledIconSize < dir.minSize * dir.scale) + return dir.minSize * dir.scale - scaledIconSize; + else if (scaledIconSize > dir.maxSize * dir.scale) + return scaledIconSize - dir.maxSize * dir.scale; else return 0; } else if (dir.type == QIconDirInfo::Threshold) { - if (iconsize < dir.size - dir.threshold) - return dir.minSize - iconsize; - else if (iconsize > dir.size + dir.threshold) - return iconsize - dir.maxSize; + if (scaledIconSize < (dir.size - dir.threshold) * dir.scale) + return dir.minSize * dir.scale - scaledIconSize; + else if (scaledIconSize > (dir.size + dir.threshold) * dir.scale) + return scaledIconSize - dir.maxSize * dir.scale; else return 0; } @@ -600,7 +609,7 @@ static int directorySizeDistance(const QIconDirInfo &dir, int iconsize) return INT_MAX; } -QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QSize &size) +QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QSize &size, int scale) { int iconsize = qMin(size.width(), size.height()); @@ -612,7 +621,7 @@ QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QSize &size) // Search for exact matches first for (int i = 0; i < numEntries; ++i) { QIconLoaderEngineEntry *entry = m_info.entries.at(i); - if (directoryMatchesSize(entry->dir, iconsize)) { + if (directoryMatchesSize(entry->dir, iconsize, scale)) { return entry; } } @@ -622,7 +631,7 @@ QIconLoaderEngineEntry *QIconLoaderEngine::entryForSize(const QSize &size) QIconLoaderEngineEntry *closestMatch = 0; for (int i = 0; i < numEntries; ++i) { QIconLoaderEngineEntry *entry = m_info.entries.at(i); - int distance = directorySizeDistance(entry->dir, iconsize); + int distance = directorySizeDistance(entry->dir, iconsize, scale); if (distance < minimalSize) { minimalSize = distance; closestMatch = entry; @@ -665,6 +674,8 @@ QPixmap PixmapEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State st basePixmap.load(filename); QSize actualSize = basePixmap.size(); + // If the size of the best match we have (basePixmap) is larger than the + // requested size, we downscale it to match. if (!actualSize.isNull() && (actualSize.width() > size.width() || actualSize.height() > size.height())) actualSize.scale(size, Qt::KeepAspectRatio); @@ -748,6 +759,15 @@ void QIconLoaderEngine::virtual_hook(int id, void *data) *reinterpret_cast(data) = m_info.entries.isEmpty(); } break; + case QIconEngine::ScaledPixmapHook: + { + QIconEngine::ScaledPixmapArgument &arg = *reinterpret_cast(data); + // QIcon::pixmap() multiplies size by the device pixel ratio. + const int integerScale = qCeil(arg.scale); + QIconLoaderEngineEntry *entry = entryForSize(arg.size / integerScale, integerScale); + arg.pixmap = entry ? entry->pixmap(arg.size, arg.mode, arg.state) : QPixmap(); + } + break; default: QIconEngine::virtual_hook(id, data); } diff --git a/src/gui/image/qiconloader_p.h b/src/gui/image/qiconloader_p.h index ed7b7ff7ae..5f3a3ef948 100644 --- a/src/gui/image/qiconloader_p.h +++ b/src/gui/image/qiconloader_p.h @@ -76,12 +76,14 @@ struct QIconDirInfo maxSize(0), minSize(0), threshold(0), + scale(1), type(Threshold) {} QString path; short size; short maxSize; short minSize; short threshold; + short scale; Type type; }; Q_DECLARE_TYPEINFO(QIconDirInfo, Q_MOVABLE_TYPE); @@ -135,7 +137,8 @@ private: bool hasIcon() const; void ensureLoaded(); void virtual_hook(int id, void *data) Q_DECL_OVERRIDE; - QIconLoaderEngineEntry *entryForSize(const QSize &size); + QIconLoaderEngineEntry *entryForSize(const QSize &size, int scale = 1); + QIconLoaderEngine(const QIconLoaderEngine &other); QThemeIconInfo m_info; QString m_iconName; diff --git a/tests/auto/gui/image/image.pro b/tests/auto/gui/image/image.pro index 5a74df4c78..f4e2ab20ba 100644 --- a/tests/auto/gui/image/image.pro +++ b/tests/auto/gui/image/image.pro @@ -10,6 +10,7 @@ SUBDIRS=\ qmovie \ qpicture \ qicon \ + qiconhighdpi !qtHaveModule(network): SUBDIRS -= \ qimagereader diff --git a/tests/auto/gui/image/qiconhighdpi/icons/testtheme/16x16/actions/appointment-new.png b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/16x16/actions/appointment-new.png new file mode 100644 index 0000000000..18b7c6781e Binary files /dev/null and b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/16x16/actions/appointment-new.png differ diff --git a/tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22/actions/appointment-new.png b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22/actions/appointment-new.png new file mode 100644 index 0000000000..d676ffd463 Binary files /dev/null and b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22/actions/appointment-new.png differ diff --git a/tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22@2/actions/appointment-new.png b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22@2/actions/appointment-new.png new file mode 100644 index 0000000000..6d094d7b54 Binary files /dev/null and b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/22x22@2/actions/appointment-new.png differ diff --git a/tests/auto/gui/image/qiconhighdpi/icons/testtheme/index.theme b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/index.theme new file mode 100644 index 0000000000..6ab6c15c42 --- /dev/null +++ b/tests/auto/gui/image/qiconhighdpi/icons/testtheme/index.theme @@ -0,0 +1,21 @@ +[Icon Theme] +Name=Test +Comment=Test Theme + +Directories=16x16/actions,22x22/actions,22x22@2/actions + +[16x16/actions] +Size=16 +Context=Actions +Type=Fixed + +[22x22/actions] +Size=22 +Context=Actions +Type=Fixed + +[22x22@2/actions] +Size=22 +Context=Actions +Scale=2 +Type=Fixed diff --git a/tests/auto/gui/image/qiconhighdpi/qiconhighdpi.pro b/tests/auto/gui/image/qiconhighdpi/qiconhighdpi.pro new file mode 100644 index 0000000000..17553158bc --- /dev/null +++ b/tests/auto/gui/image/qiconhighdpi/qiconhighdpi.pro @@ -0,0 +1,8 @@ +CONFIG += testcase +TARGET = tst_qicon + +QT += testlib +SOURCES += tst_qiconhighdpi.cpp +RESOURCES = tst_qiconhighdpi.qrc + +TESTDATA += icons/* diff --git a/tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.cpp b/tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.cpp new file mode 100644 index 0000000000..ce7f68a0a6 --- /dev/null +++ b/tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.cpp @@ -0,0 +1,195 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + +class tst_QIconHighDpi : public QObject +{ + Q_OBJECT +public: + tst_QIconHighDpi(); + +private slots: + void initTestCase(); + void fromTheme_data(); + void fromTheme(); +}; + +tst_QIconHighDpi::tst_QIconHighDpi() +{ +} + +void tst_QIconHighDpi::initTestCase() +{ +} + +void tst_QIconHighDpi::fromTheme_data() +{ + QTest::addColumn("requestedSize"); + QTest::addColumn("expectedSize"); + QTest::addColumn("expectedDpr"); + + // The pixmaps that we have available can be found in tst_qiconhighdpi.qrc. + // Currently we only have @1 and @2 icons available. + const int dpr = qCeil(qApp->devicePixelRatio()); + + // We have an @2 high DPI version of the 22x22 size of this icon. + switch (dpr) { + case 1: QTest::newRow("22x22,dpr=1") << 22 << 22 << 1.0; break; + case 2: QTest::newRow("22x22,dpr=2") << 22 << 44 << 2.0; break; + case 3: QTest::newRow("22x22,dpr=3") << 22 << 44 << 2.0; break; + } + + // We don't have a high DPI version of the 16x16 size of this icon, + // so directoryMatchesSize() will return false for all directories. + // directorySizeDistance() is then called to find the best match. + // The table below illustrates the results for our available images at various DPRs: + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 16 * 1 = 28 + // 22 * 1 - 16 * 1 = 6 + // 16 * 1 - 16 * 1 = 0 < (16x16) + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 16 * 2 = 12 + // 22 * 1 - 16 * 2 = 10 < (22x22) + // 16 * 1 - 16 * 2 = 16 + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 16 * 3 = 4 < (22x22@2) + // 22 * 1 - 16 * 3 = 26 + // 16 * 1 - 16 * 3 = 32 + // Both of these functions are implementations of the freedesktop icon theme spec, + // which dictates that if there is no matching scale, directorySizeDistance() determines + // the winner, regardless of whether or not the scale is too low for the requested scale. + switch (dpr) { + case 1: QTest::newRow("16x16,dpr=1") << 16 << 16 << 1.0; break; + // PixmapEntry::pixmap() will only downscale the pixmap if actualSize.width() > size.width(). + // In this case, 22 > 32 is false, so a 22x22 pixmap is returned. + case 2: QTest::newRow("16x16,dpr=2") << 16 << 22 << 1.375; break; + case 3: QTest::newRow("16x16,dpr=3") << 16 << 44 << 2.75; break; + } + + // We don't have an 8x8 size of this icon, so: + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 8 * 1 = 36 + // 22 * 1 - 8 * 1 = 14 + // 16 * 1 - 8 * 1 = 8 < (16x16) + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 8 * 2 = 28 + // 22 * 1 - 8 * 2 = 6 + // 16 * 1 - 8 * 2 = 0 < (16x16) + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 8 * 3 = 20 + // 22 * 1 - 8 * 3 = 2 < (22x22) + // 16 * 1 - 8 * 3 = 8 + switch (dpr) { + case 1: QTest::newRow("8x8,dpr=1") << 8 << 8 << 1.0; break; + case 2: QTest::newRow("8x8,dpr=2") << 8 << 16 << 2.0; break; + case 3: QTest::newRow("8x8,dpr=3") << 8 << 22 << 2.75; break; + } + + // We don't have a 44x44 size of this icon, so: + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 44 * 1 = 0 < (22x22@2) + // 22 * 1 - 44 * 1 = 22 + // 16 * 1 - 44 * 1 = 28 + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 44 * 2 = 44 < (22x22@2) + // 22 * 1 - 44 * 2 = 66 + // 16 * 1 - 44 * 2 = 72 + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 44 * 3 = 88 < (22x22@2) + // 22 * 1 - 44 * 3 = 110 + // 16 * 1 - 44 * 3 = 116 + switch (dpr) { + case 1: QTest::newRow("44x44,dpr=1") << 44 << 44 << 1.0; break; + case 2: QTest::newRow("44x44,dpr=2") << 44 << 44 << 1.0; break; + case 3: QTest::newRow("44x44,dpr=3") << 44 << 44 << 1.0; break; + } + + // We don't have a 20x20 size of this icon, so: + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 20 * 1 = 24 + // 22 * 1 - 20 * 1 = 2 < (22x22) + // 16 * 1 - 20 * 1 = 4 + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 20 * 2 = 4 < (22x22@2) + // 22 * 1 - 20 * 2 = 18 + // 16 * 1 - 20 * 2 = 24 + // Available size | Available scale | Requested size | Requested scale | Distance + // 22 * 2 - 20 * 3 = 16 < (22x22@2) + // 22 * 1 - 20 * 3 = 38 + // 16 * 1 - 20 * 3 = 44 + switch (dpr) { + case 1: QTest::newRow("20x20,dpr=1") << 20 << 20 << 1.0; break; + // PixmapEntry::pixmap() will only downscale the pixmap if actualSize.width() > size.width(). + // In this case, 44 > 40 is true, so the 44x44 pixmap is downscaled to 40x40. + case 2: QTest::newRow("20x20,dpr=2") << 20 << 40 << 2.0; break; + case 3: QTest::newRow("20x20,dpr=3") << 20 << 44 << 2.2; break; + } +} + +void tst_QIconHighDpi::fromTheme() +{ + QFETCH(int, requestedSize); + QFETCH(int, expectedSize); + QFETCH(qreal, expectedDpr); + + QString searchPath = QLatin1String(":/icons"); + QIcon::setThemeSearchPaths(QStringList() << searchPath); + QCOMPARE(QIcon::themeSearchPaths().size(), 1); + QCOMPARE(searchPath, QIcon::themeSearchPaths()[0]); + + QString themeName("testtheme"); + QIcon::setThemeName(themeName); + QCOMPARE(QIcon::themeName(), themeName); + + QIcon appointmentIcon = QIcon::fromTheme("appointment-new"); + QVERIFY(!appointmentIcon.isNull()); + QVERIFY(!appointmentIcon.availableSizes(QIcon::Normal, QIcon::Off).isEmpty()); + QVERIFY(appointmentIcon.availableSizes().contains(QSize(16, 16))); + QVERIFY(appointmentIcon.availableSizes().contains(QSize(22, 22))); + + const QPixmap pixmap = appointmentIcon.pixmap(requestedSize); + QCOMPARE(pixmap.size(), QSize(expectedSize, expectedSize)); + // We should get the high DPI version of an image if it exists in the correct directory. + // Note that we didn't pass the QWindow to QIcon::pixmap(), + // because QGuiApplication::devicePixelRatio() will be used if no window was given. + QCOMPARE(pixmap.devicePixelRatio(), expectedDpr); +} + +int main(int argc, char *argv[]) +{ + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QGuiApplication app(argc, argv); + Q_UNUSED(app); + tst_QIconHighDpi test; + QTEST_SET_MAIN_SOURCE_PATH + return QTest::qExec(&test, argc, argv); +} + +#include "tst_qiconhighdpi.moc" diff --git a/tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.qrc b/tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.qrc new file mode 100644 index 0000000000..80b5e38ee6 --- /dev/null +++ b/tests/auto/gui/image/qiconhighdpi/tst_qiconhighdpi.qrc @@ -0,0 +1,8 @@ + + + icons/testtheme/16x16/actions/appointment-new.png + icons/testtheme/22x22/actions/appointment-new.png + icons/testtheme/index.theme + icons/testtheme/22x22@2/actions/appointment-new.png + + -- cgit v1.2.3