diff --git a/kcms/icons/main.cpp b/kcms/icons/main.cpp index e44763043..d589e26a5 100644 --- a/kcms/icons/main.cpp +++ b/kcms/icons/main.cpp @@ -1,559 +1,591 @@ /* * main.cpp * * Copyright (c) 1999 Matthias Hoelzer-Kluepfel * Copyright (c) 2000 Antonio Larrosa * Copyright (C) 2000 Geert Jansen * KDE Frameworks 5 port Copyright (C) 2013 Jonathan Riddell * Copyright (C) 2018 Kai Uwe Broulik * * Requires the Qt widget libraries, available at no cost at * http://www.troll.no/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "main.h" #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for unlink #include "iconsmodel.h" #include "config.h" // for CMAKE_INSTALL_FULL_LIBEXECDIR static const QVector s_defaultIconSizes = { 32, 22, 22, 16, 48, 32 }; // we try to use KIconTheme::defaultThemeName() but that could be "hicolor" which isn't a "real" theme static const QString s_defaultThemeName = QStringLiteral("breeze"); K_PLUGIN_FACTORY_WITH_JSON(IconsFactory, "kcm_icons.json", registerPlugin();) IconModule::IconModule(QObject *parent, const QVariantList &args) : KQuickAddons::ConfigModule(parent, args) , m_model(new IconsModel(this)) , m_iconGroups{ QStringLiteral("Desktop"), QStringLiteral("Toolbar"), QStringLiteral("MainToolbar"), QStringLiteral("Small"), QStringLiteral("Panel"), QStringLiteral("Dialog") } { qmlRegisterType(); // to be able to access its enums qmlRegisterUncreatableType("org.kde.private.kcms.icons", 1, 0, "KIconLoader", QString()); KAboutData* about = new KAboutData(QStringLiteral("kcm5_icons"), i18n("Icons"), QStringLiteral("1.0"), i18n("Icons Control Panel Module"), KAboutLicense::GPL, i18n("(c) 2000-2003 Geert Jansen")); about->addAuthor(i18n("Geert Jansen"), QString(), QStringLiteral("jansen@kde.org")); about->addAuthor(i18n("Antonio Larrosa Jimenez"), QString(), QStringLiteral("larrosa@kde.org")); about->addCredit(i18n("Torsten Rahn"), QString(), QStringLiteral("torsten@kde.org")); about->addAuthor(i18n("Jonathan Riddell"), QString(), QStringLiteral("jr@jriddell.org")); about->addAuthor(i18n("Kai Uwe Broulik"), QString(), QStringLiteral("kde@privat.broulik.de>")); setAboutData(about); setButtons(Apply | Default); connect(m_model, &IconsModel::selectedThemeChanged, this, [this] { m_selectedThemeDirty = true; setNeedsSave(true); }); connect(m_model, &IconsModel::pendingDeletionsChanged, this, [this] { setNeedsSave(true); }); + + // When user has a lot of themes installed, preview pixmaps might get evicted prematurely + QPixmapCache::setCacheLimit(50 * 1024); // 50 MiB } IconModule::~IconModule() { } IconsModel *IconModule::iconsModel() const { return m_model; } QStringList IconModule::iconGroups() const { return m_iconGroups; } int IconModule::iconSize(int group) const { return m_iconSizes[group]; } void IconModule::setIconSize(int group, int size) { if (iconSize(group) == size) { return; } m_iconSizes[group] = size; setNeedsSave(true); m_iconSizesDirty = true; emit iconSizesChanged(); } QList IconModule::availableIconSizes(int group) const { return KIconLoader::global()->theme()->querySizes(static_cast(group)); } void IconModule::load() { m_model->load(); loadIconSizes(); m_model->setSelectedTheme(KIconTheme::current()); setNeedsSave(false); m_selectedThemeDirty = false; m_iconSizesDirty = false; } void IconModule::save() { if (m_selectedThemeDirty) { QProcess::startDetached(CMAKE_INSTALL_FULL_LIBEXECDIR "/plasma-changeicons", {m_model->selectedTheme()}); } if (m_iconSizesDirty || m_revertIconEffects) { auto cfg = KSharedConfig::openConfig(); for (int i = 0; i < m_iconGroups.count(); ++i) { const QString &group = m_iconGroups.at(i); KConfigGroup cg(cfg, group + QLatin1String("Icons")); cg.writeEntry("Size", m_iconSizes.at(i), KConfig::Normal | KConfig::Global); if (m_revertIconEffects) { cg.revertToDefault("Animated"); const QStringList states = { QStringLiteral("Default"), QStringLiteral("Active"), QStringLiteral("Disabled") }; const QStringList keys = { QStringLiteral("Effect"), QStringLiteral("Value"), QStringLiteral("Color"), QStringLiteral("Color2"), QStringLiteral("SemiTransparent") }; for (const QString &state : states) { for (const QString &key : keys) { cg.revertToDefault(state + key); } } } } cfg->sync(); } if (m_selectedThemeDirty || m_iconSizesDirty || m_revertIconEffects) { exportToKDE4(); } processPendingDeletions(); KIconLoader::global()->newIconLoader(); setNeedsSave(false); m_selectedThemeDirty = false; m_iconSizesDirty = false; m_revertIconEffects = false; } void IconModule::processPendingDeletions() { const QStringList pendingDeletions = m_model->pendingDeletions(); for (const QString &themeName : pendingDeletions) { Q_ASSERT(themeName != m_model->selectedTheme()); KIconTheme theme(themeName); auto *job = KIO::del(QUrl::fromLocalFile(theme.dir()), KIO::HideProgressInfo); // needs to block for it to work on "OK" where the dialog (kcmshell) closes job->exec(); } m_model->removeItemsPendingDeletion(); } void IconModule::defaults() { if (m_iconSizes != s_defaultIconSizes) { m_iconSizes = s_defaultIconSizes; emit iconSizesChanged(); } auto setThemeIfAvailable = [this](const QString &themeName) { const auto results = m_model->match(m_model->index(0, 0), ThemeNameRole, themeName); if (results.isEmpty()) { return false; } m_model->setSelectedTheme(themeName); return true; }; if (!setThemeIfAvailable(KIconTheme::defaultThemeName())) { setThemeIfAvailable(QStringLiteral("breeze")); } m_revertIconEffects = true; setNeedsSave(true); } void IconModule::loadIconSizes() { auto cfg = KSharedConfig::openConfig(); QVector iconSizes(6, 0); // why doesn't KIconLoader::LastGroup - 1 work here?! int i = KIconLoader::FirstGroup; for (const QString &group : qAsConst(m_iconGroups)) { int size = KIconLoader::global()->theme()->defaultSize(static_cast(i)); KConfigGroup iconGroup(cfg, group + QLatin1String("Icons")); size = iconGroup.readEntry("Size", size); iconSizes[i] = size; ++i; } if (m_iconSizes != iconSizes) { m_iconSizes = iconSizes; emit iconSizesChanged(); } } void IconModule::getNewStuff(QQuickItem *ctx) { if (!m_newStuffDialog) { m_newStuffDialog = new KNS3::DownloadDialog(QStringLiteral("icons.knsrc")); m_newStuffDialog->setWindowTitle(i18n("Download New Icon Themes")); m_newStuffDialog->setWindowModality(Qt::WindowModal); m_newStuffDialog->winId(); // so it creates the windowHandle(); // TODO would be lovely to scroll to and select the newly installed scheme, if any connect(m_newStuffDialog.data(), &KNS3::DownloadDialog::accepted, this, [this] { if (m_newStuffDialog->changedEntries().isEmpty()) { return; } // reload the display icontheme items KIconLoader::global()->newIconLoader(); m_model->load(); + QPixmapCache::clear(); }); } if (ctx && ctx->window()) { m_newStuffDialog->windowHandle()->setTransientParent(ctx->window()); } m_newStuffDialog->show(); } void IconModule::installThemeFromFile(const QUrl &url) { if (url.isLocalFile()) { installThemeFile(url.toLocalFile()); return; } m_tempInstallFile.reset(new QTemporaryFile()); if (!m_tempInstallFile->open()) { emit showErrorMessage(i18n("Unable to create a temporary file.")); m_tempInstallFile.reset(); return; } KIO::FileCopyJob *job = KIO::file_copy(url,QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); job->uiDelegate()->setAutoErrorHandlingEnabled(true); connect(job, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { if (job->error() != KJob::NoError) { emit showErrorMessage(i18n("Unable to download the icon theme archive: %1", job->errorText())); return; } installThemeFile(m_tempInstallFile->fileName()); m_tempInstallFile.reset(); }); } void IconModule::installThemeFile(const QString &path) { const QStringList themesNames = findThemeDirs(path); if (themesNames.isEmpty()) { emit showErrorMessage(i18n("The file is not a valid icon theme archive.")); return; } if (!installThemes(themesNames, path)) { emit showErrorMessage(i18n("A problem occurred during the installation process; however, most of the themes in the archive have been installed")); return; } emit showSuccessMessage(i18n("Theme installed successfully.")); KIconLoader::global()->newIconLoader(); m_model->load(); } void IconModule::exportToKDE4() { //TODO: killing the kde4 icon cache: possible? (kde4migration doesn't let access the cache folder) Kdelibs4Migration migration; QString configFilePath = migration.saveLocation("config"); if (configFilePath.isEmpty()) { return; } configFilePath += QLatin1String("kdeglobals"); KSharedConfigPtr kglobalcfg = KSharedConfig::openConfig(QStringLiteral("kdeglobals")); KConfig kde4config(configFilePath, KConfig::SimpleConfig); KConfigGroup kde4IconGroup(&kde4config, "Icons"); kde4IconGroup.writeEntry("Theme", m_model->selectedTheme()); //Synchronize icon effects for (const QString &group : qAsConst(m_iconGroups)) { const QString groupName = group + QLatin1String("Icons"); KConfigGroup cg(kglobalcfg, groupName); KConfigGroup kde4Cg(&kde4config, groupName); // HACK copyTo only copies keys, it doesn't replace the entire group // which means if we removed the effects in our config it won't remove // them from the kde4 config, hence revert all of them prior to copying const QStringList keys = cg.keyList() + kde4Cg.keyList(); for (const QString &key : keys) { kde4Cg.revertToDefault(key); } // now copy over the new values cg.copyTo(&kde4Cg); } kde4config.sync(); QProcess *cachePathProcess = new QProcess(this); connect(cachePathProcess, QOverload::of(&QProcess::finished), this, [cachePathProcess](int exitCode, QProcess::ExitStatus status) { if (status == QProcess::NormalExit && exitCode == 0) { QString path = cachePathProcess->readAllStandardOutput().trimmed(); path.append(QLatin1String("icon-cache.kcache")); QFile::remove(path); } //message kde4 apps that icon theme is changed for (int i = 0; i < KIconLoader::LastGroup; i++) { KIconLoader::emitChange(KIconLoader::Group(i)); QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KGlobalSettings"), QStringLiteral("org.kde.KGlobalSettings"), QStringLiteral("notifyChange")); message.setArguments({ 4, // KGlobalSettings::IconChanged KIconLoader::Group(i) }); QDBusConnection::sessionBus().send(message); } cachePathProcess->deleteLater(); }); cachePathProcess->start(QStringLiteral("kde4-config --path cache")); } QStringList IconModule::findThemeDirs(const QString &archiveName) { QStringList foundThemes; KTar archive(archiveName); archive.open(QIODevice::ReadOnly); const KArchiveDirectory *themeDir = archive.directory(); KArchiveEntry *possibleDir = nullptr; KArchiveDirectory *subDir = nullptr; // iterate all the dirs looking for an index.theme or index.desktop file const QStringList entries = themeDir->entries(); for (const QString &entry : entries) { possibleDir = const_cast(themeDir->entry(entry)); if (!possibleDir->isDirectory()) { continue; } subDir = dynamic_cast(possibleDir); if (!subDir) { continue; } if (subDir->entry(QStringLiteral("index.theme")) || subDir->entry(QStringLiteral("index.desktop"))) { foundThemes.append(subDir->name()); } } archive.close(); return foundThemes; } bool IconModule::installThemes(const QStringList &themes, const QString &archiveName) { bool everythingOk = true; const QString localThemesDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/icons/./")); emit showProgress(i18n("Installing icon themes...")); KTar archive(archiveName); archive.open(QIODevice::ReadOnly); qApp->processEvents(QEventLoop::ExcludeUserInputEvents); const KArchiveDirectory *rootDir = archive.directory(); KArchiveDirectory *currentTheme = nullptr; for (const QString &theme : themes) { emit showProgress(i18n("Installing %1 theme...", theme)); qApp->processEvents(QEventLoop::ExcludeUserInputEvents); currentTheme = dynamic_cast(const_cast(rootDir->entry(theme))); if (!currentTheme) { // we tell back that something went wrong, but try to install as much // as possible everythingOk = false; continue; } currentTheme->copyTo(localThemesDir + theme); } archive.close(); emit hideProgress(); return everythingOk; } -QVariantList IconModule::previewIcons(const QString &themeName, int size, qreal dpr) const +QVariantList IconModule::previewIcons(const QString &themeName, int size, qreal dpr, int limit) { - KIconTheme theme(themeName); - QSvgRenderer renderer; + static QVector s_previewIcons{ + {QStringLiteral("system-run"), QStringLiteral("exec")}, + {QStringLiteral("folder")}, + {QStringLiteral("document"), QStringLiteral("text-x-generic")}, + {QStringLiteral("user-trash"), QStringLiteral("user-trash-empty")}, + {QStringLiteral("system-help"), QStringLiteral("help-about"), QStringLiteral("help-contents")}, + {QStringLiteral("preferences-system"), QStringLiteral("systemsettings"), QStringLiteral("configure")}, + + {QStringLiteral("text-html")}, + {QStringLiteral("image-x-generic"), QStringLiteral("image-png"), QStringLiteral("image-jpeg")}, + {QStringLiteral("video-x-generic"), QStringLiteral("video-x-theora+ogg"), QStringLiteral("video-mp4")}, + {QStringLiteral("x-office-document")}, + {QStringLiteral("x-office-spreadsheet")}, + {QStringLiteral("x-office-presentation"), QStringLiteral("application-presentation")}, + + {QStringLiteral("user-home")}, + {QStringLiteral("user-desktop"), QStringLiteral("desktop")}, + {QStringLiteral("folder-image"), QStringLiteral("folder-images"), QStringLiteral("folder-pictures"), QStringLiteral("folder-picture")}, + {QStringLiteral("folder-documents")}, + {QStringLiteral("folder-download"), QStringLiteral("folder-downloads")}, + {QStringLiteral("folder-video"), QStringLiteral("folder-videos")} + }; - auto getBestIcon = [&](const QStringList &iconNames) { - const int iconSize = size * dpr; + // created on-demand as it is quite expensive to do and we don't want to do it every loop iteration either + QScopedPointer theme; - // not using initializer list as we want to unwrap inherits() - const QStringList themes = QStringList() << theme.internalName() << theme.inherits(); - for (const QString &themeName : themes) { - KIconTheme theme(themeName); + QVariantList pixmaps; - for (const QString &iconName : iconNames) { - QString path = theme.iconPath(QStringLiteral("%1.png").arg(iconName), iconSize, KIconLoader::MatchBest); - if (!path.isEmpty()) { - QPixmap pixmap(path); - pixmap.setDevicePixelRatio(dpr); - return pixmap; - } + for (const QStringList &iconNames : s_previewIcons) { + const QString cacheKey = themeName + QLatin1Char('@') + QString::number(size) + QLatin1Char('@') + + QString::number(dpr,'f',1) + QLatin1Char('@') + iconNames.join(QLatin1Char(',')); - //could not find the .png, try loading the .svg or .svgz - path = theme.iconPath(QStringLiteral("%1.svg").arg(iconName), iconSize, KIconLoader::MatchBest); - if (path.isEmpty()) { - path = theme.iconPath(QStringLiteral("%1.svgz").arg(iconName), iconSize, KIconLoader::MatchBest); - } + QPixmap pix; + if (!QPixmapCache::find(cacheKey, pix)) { + if (!theme) { + theme.reset(new KIconTheme(themeName)); + } - if (path.isEmpty()) { - continue; - } + pix = getBestIcon(*theme.data(), iconNames, size, dpr); - if (!renderer.load(path)) { - continue; - } + // Inserting a pixmap even if null so we know whether we searched for it already + QPixmapCache::insert(cacheKey, pix); + } - QPixmap pixmap(iconSize, iconSize); + if (pix.isNull()) { + continue; + } + + pixmaps.append(pix); + + if (limit > -1 && pixmaps.count() >= limit) { + break; + } + } + + return pixmaps; +} + +QPixmap IconModule::getBestIcon(KIconTheme &theme, const QStringList &iconNames, int size, qreal dpr) +{ + QSvgRenderer renderer; + + const int iconSize = size * dpr; + + // not using initializer list as we want to unwrap inherits() + const QStringList themes = QStringList() << theme.internalName() << theme.inherits(); + for (const QString &themeName : themes) { + KIconTheme theme(themeName); + + for (const QString &iconName : iconNames) { + QString path = theme.iconPath(QStringLiteral("%1.png").arg(iconName), iconSize, KIconLoader::MatchBest); + if (!path.isEmpty()) { + QPixmap pixmap(path); pixmap.setDevicePixelRatio(dpr); - pixmap.fill(QColor(Qt::transparent)); - QPainter p(&pixmap); - p.setViewport(0, 0, size, size); - renderer.render(&p); return pixmap; } - } - return QPixmap(); - }; + //could not find the .png, try loading the .svg or .svgz + path = theme.iconPath(QStringLiteral("%1.svg").arg(iconName), iconSize, KIconLoader::MatchBest); + if (path.isEmpty()) { + path = theme.iconPath(QStringLiteral("%1.svgz").arg(iconName), iconSize, KIconLoader::MatchBest); + } - QVariantList pixmaps{ - getBestIcon({QStringLiteral("system-run"), QStringLiteral("exec")}), - getBestIcon({QStringLiteral("folder")}), - getBestIcon({QStringLiteral("document"), QStringLiteral("text-x-generic")}), - getBestIcon({QStringLiteral("user-trash"), QStringLiteral("user-trash-empty")}), - getBestIcon({QStringLiteral("system-help"), QStringLiteral("help-about"), QStringLiteral("help-contents")}), - getBestIcon({QStringLiteral("preferences-system"), QStringLiteral("systemsettings"), QStringLiteral("configure")}), - - getBestIcon({QStringLiteral("text-html")}), - getBestIcon({QStringLiteral("image-x-generic"), QStringLiteral("image-png"), QStringLiteral("image-jpeg")}), - getBestIcon({QStringLiteral("video-x-generic"), QStringLiteral("video-x-theora+ogg"), QStringLiteral("video-mp4")}), - getBestIcon({QStringLiteral("x-office-document")}), - getBestIcon({QStringLiteral("x-office-spreadsheet")}), - getBestIcon({QStringLiteral("x-office-presentation"), QStringLiteral("application-presentation")}), - - getBestIcon({QStringLiteral("user-home")}), - getBestIcon({QStringLiteral("user-desktop"), QStringLiteral("desktop")}), - getBestIcon({QStringLiteral("folder-image"), QStringLiteral("folder-images"), QStringLiteral("folder-pictures"), QStringLiteral("folder-picture")}), - getBestIcon({QStringLiteral("folder-documents")}), - getBestIcon({QStringLiteral("folder-download"), QStringLiteral("folder-downloads")}), - getBestIcon({QStringLiteral("folder-video"), QStringLiteral("folder-videos")}) - }; + if (path.isEmpty()) { + continue; + } + + if (!renderer.load(path)) { + continue; + } - // remove missing icons - pixmaps.erase(std::remove_if(pixmaps.begin(), pixmaps.end(), [](const QVariant &pixmapVariant) { - return pixmapVariant.value().isNull(); - }), pixmaps.end()); + QPixmap pixmap(iconSize, iconSize); + pixmap.setDevicePixelRatio(dpr); + pixmap.fill(QColor(Qt::transparent)); + QPainter p(&pixmap); + p.setViewport(0, 0, size, size); + renderer.render(&p); + return pixmap; + } + } - return pixmaps; + return QPixmap(); } #include "main.moc" diff --git a/kcms/icons/main.h b/kcms/icons/main.h index d999d0655..116ae7fb2 100644 --- a/kcms/icons/main.h +++ b/kcms/icons/main.h @@ -1,113 +1,116 @@ /* * main.h * * Copyright (c) 1999 Matthias Hoelzer-Kluepfel * KDE Frameworks 5 port Copyright (C) 2013 Jonathan Riddell * Copyright (c) 2018 Kai Uwe Broulik * * Requires the Qt widget libraries, available at no cost at * http://www.troll.no/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #pragma once #include #include #include class KIconTheme; class QQuickItem; class QTemporaryFile; class IconsModel; class IconModule : public KQuickAddons::ConfigModule { Q_OBJECT Q_PROPERTY(IconsModel *iconsModel READ iconsModel CONSTANT) Q_PROPERTY(QStringList iconGroups READ iconGroups CONSTANT) public: IconModule(QObject *parent, const QVariantList &args); ~IconModule() override; enum Roles { ThemeNameRole = Qt::UserRole + 1, DescriptionRole, RemovableRole, PendingDeletionRole }; IconsModel *iconsModel() const; QStringList iconGroups() const; void load() override; void save() override; void defaults() override; Q_INVOKABLE void getNewStuff(QQuickItem *ctx); Q_INVOKABLE void installThemeFromFile(const QUrl &url); Q_INVOKABLE int iconSize(int group) const; Q_INVOKABLE void setIconSize(int group, int size); Q_INVOKABLE QList availableIconSizes(int group) const; - Q_INVOKABLE QVariantList/*QList*/ previewIcons(const QString &themeName, int size, qreal dpr) const; + // QML doesn't understand QList, hence wrapped in a QVariantList + Q_INVOKABLE QVariantList previewIcons(const QString &themeName, int size, qreal dpr, int limit = -1); signals: void iconSizesChanged(); void showSuccessMessage(const QString &message); void showErrorMessage(const QString &message); void showProgress(const QString &message); void hideProgress(); private: void loadIconSizes(); void processPendingDeletions(); static QStringList findThemeDirs(const QString &archiveName); bool installThemes(const QStringList &themes, const QString &archiveName); void installThemeFile(const QString &path); void exportToKDE4(); + static QPixmap getBestIcon(KIconTheme &theme, const QStringList &iconNames, int size, qreal dpr); + IconsModel *m_model; // so we avoid launching changeicon process when theme didn't change (but only e.g. pending deletions) bool m_selectedThemeDirty = false; bool m_iconSizesDirty = false; // set when user hits "Defaults" button at which point we'll remove all custom icon effects on Apply bool m_revertIconEffects = false; QVector m_iconSizes; QStringList m_iconGroups; QScopedPointer m_tempInstallFile; QPointer m_newStuffDialog; }; diff --git a/kcms/icons/package/contents/ui/main.qml b/kcms/icons/package/contents/ui/main.qml index 252a85df5..642d06636 100644 --- a/kcms/icons/package/contents/ui/main.qml +++ b/kcms/icons/package/contents/ui/main.qml @@ -1,285 +1,298 @@ /* * Copyright 2018 Kai Uwe Broulik * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.0 as QtDialogs import QtQuick.Controls 2.3 as QtControls import org.kde.kirigami 2.4 as Kirigami import org.kde.kquickcontrolsaddons 2.0 as KQCAddons import org.kde.kcm 1.1 as KCM import org.kde.private.kcms.icons 1.0 as Private KCM.GridViewKCM { KCM.ConfigModule.quickHelp: i18n("This module allows you to choose the icons for your desktop.") view.model: kcm.iconsModel view.currentIndex: kcm.iconsModel.selectedThemeIndex DropArea { anchors.fill: parent onEntered: { if (!drag.hasUrls) { drag.accepted = false; } } onDropped: kcm.installThemeFromFile(drop.urls[0]) } view.remove: Transition { ParallelAnimation { NumberAnimation { property: "scale"; to: 0.5; duration: Kirigami.Units.longDuration } NumberAnimation { property: "opacity"; to: 0.0; duration: Kirigami.Units.longDuration } } } view.removeDisplaced: Transition { SequentialAnimation { // wait for the "remove" animation to finish PauseAnimation { duration: Kirigami.Units.longDuration } NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration } } } view.delegate: KCM.GridDelegate { id: delegate text: model.display toolTip: model.description thumbnailAvailable: typeof thumbFlow.previews === "undefined" || thumbFlow.previews.length > 0 thumbnail: MouseArea { id: thumbArea anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true + clip: thumbFlow.y < 0 opacity: model.pendingDeletion ? 0.3 : 1 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration } } Timer { interval: 1000 repeat: true running: thumbArea.containsMouse onRunningChanged: { if (!running) { thumbFlow.currentPage = 0; } } onTriggered: { + if (!thumbFlow.allPreviesLoaded) { + thumbFlow.loadPreviews(-1 /*no limit*/); + thumbFlow.allPreviesLoaded = true; + } + ++thumbFlow.currentPage; if (thumbFlow.currentPage >= thumbFlow.pageCount) { stop(); } } } Flow { id: thumbFlow // undefined is "didn't load preview yet" // empty array is "no preview available" property var previews + // initially we only load 6 and when the animation starts we'll load the rest + property bool allPreviesLoaded: false property int currentPage readonly property int pageCount: Math.ceil(thumbRepeater.count / (thumbFlow.columns * thumbFlow.rows)) readonly property int iconWidth: Math.floor(thumbArea.width / thumbFlow.columns) readonly property int iconHeight: Math.round(thumbArea.height / thumbFlow.rows) readonly property int columns: 3 readonly property int rows: 2 + function loadPreviews(limit) { + previews = kcm.previewIcons(model.themeName, Math.min(thumbFlow.iconWidth, thumbFlow.iconHeight), Screen.devicePixelRatio, limit); + } + width: parent.width y: -currentPage * iconHeight * rows Behavior on y { NumberAnimation { duration: Kirigami.Units.longDuration } } Repeater { id: thumbRepeater model: thumbFlow.previews Item { width: thumbFlow.iconWidth height: thumbFlow.iconHeight KQCAddons.QPixmapItem { anchors.centerIn: parent width: Math.min(parent.width, nativeWidth) height: Math.min(parent.height, nativeHeight) // load on demand and avoid leaking a tiny corner of the icon pixmap: thumbFlow.y < 0 || index < (thumbFlow.columns * thumbFlow.rows) ? modelData : undefined smooth: true fillMode: KQCAddons.QPixmapItem.PreserveAspectFit } } } Component.onCompleted: { // avoid reloading it when icon sizes or dpr changes on startup Qt.callLater(function() { - previews = kcm.previewIcons(model.themeName, Math.min(thumbFlow.iconWidth, thumbFlow.iconHeight), Screen.devicePixelRatio) + // We show 6 icons initially (3x2 grid), only load those + thumbFlow.loadPreviews(6 /*limit*/); }); } } } actions: [ Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Remove Icon Theme") enabled: model.removable visible: !model.pendingDeletion onTriggered: model.pendingDeletion = true }, Kirigami.Action { iconName: "edit-undo" tooltip: i18n("Restore Icon Theme") visible: model.pendingDeletion onTriggered: model.pendingDeletion = false } ] onClicked: { if (!model.pendingDeletion) { kcm.iconsModel.selectedTheme = model.themeName; } view.forceActiveFocus(); } } footer: ColumnLayout { Kirigami.InlineMessage { id: infoLabel Layout.fillWidth: true showCloseButton: true Connections { target: kcm onShowSuccessMessage: { infoLabel.type = Kirigami.MessageType.Positive; infoLabel.text = message; infoLabel.visible = true; } onShowErrorMessage: { infoLabel.type = Kirigami.MessageType.Error; infoLabel.text = message; infoLabel.visible = true; } } } RowLayout { id: progressRow visible: false QtControls.BusyIndicator { id: progressBusy } QtControls.Label { id: progressLabel Layout.fillWidth: true textFormat: Text.PlainText wrapMode: Text.WordWrap } Connections { target: kcm onShowProgress: { progressLabel.text = message; progressBusy.running = true; progressRow.visible = true; } onHideProgress: { progressBusy.running = false; progressRow.visible = false; } } } RowLayout { Layout.fillWidth: true QtControls.Button { id: iconSizesButton text: i18n("Configure Icon Sizes") icon.name: "transform-scale" // proper icon? checkable: true checked: iconSizePopupLoader.item && iconSizePopupLoader.item.opened onClicked: { iconSizePopupLoader.active = true; iconSizePopupLoader.item.open(); } } Item { Layout.fillWidth: true } QtControls.Button { id: installFromFileButton text: i18n("Install from File...") icon.name: "document-import" onClicked: fileDialogLoader.active = true } QtControls.Button { text: i18n("Get New Themes...") icon.name: "get-hot-new-stuff" onClicked: kcm.getNewStuff(this) } } } Loader { id: iconSizePopupLoader active: false sourceComponent: IconSizePopup { parent: iconSizesButton y: -height } } Loader { id: fileDialogLoader active: false sourceComponent: QtDialogs.FileDialog { title: i18n("Open Theme") folder: shortcuts.home nameFilters: [ i18n("Theme Files (*.tar.gz *.tar.bz2)") ] Component.onCompleted: open() onAccepted: { kcm.installThemeFromFile(fileUrls[0]) fileDialogLoader.active = false } onRejected: { fileDialogLoader.active = false } } } }