diff --git a/kcms/cursortheme/kcmcursortheme.cpp b/kcms/cursortheme/kcmcursortheme.cpp index 229c5fa36..5e7c6e2a2 100644 --- a/kcms/cursortheme/kcmcursortheme.cpp +++ b/kcms/cursortheme/kcmcursortheme.cpp @@ -1,609 +1,620 @@ /* * Copyright © 2003-2007 Fredrik Höglund * * 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 #include "kcmcursortheme.h" #include "xcursor/thememodel.h" #include "xcursor/sortproxymodel.h" #include "xcursor/cursortheme.h" #include "xcursor/previewwidget.h" #include "../krdb/krdb.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_XFIXES # include #endif K_PLUGIN_FACTORY_WITH_JSON(CursorThemeConfigFactory, "kcm_cursortheme.json", registerPlugin();) CursorThemeConfig::CursorThemeConfig(QObject *parent, const QVariantList &args) : KQuickAddons::ConfigModule(parent, args), m_appliedSize(0), m_preferredSize(0), m_selectedThemeRow(-1), m_selectedSizeRow(-1), m_originalSelectedThemeRow(-1), m_canInstall(true), m_canResize(true), m_canConfigure(true) { qmlRegisterType("org.kde.private.kcm_cursortheme", 1, 0, "PreviewWidget"); qmlRegisterType(); KAboutData* aboutData = new KAboutData(QStringLiteral("kcm_cursortheme"), i18n("Choose the mouse cursor theme"), QStringLiteral("1.0"), QString(), KAboutLicense::GPL, i18n("(c) 2003-2007 Fredrik Höglund")); aboutData->addAuthor(i18n("Fredrik Höglund")); aboutData->addAuthor(i18n("Marco Martin")); setAboutData(aboutData); m_model = new CursorThemeModel(this); m_proxyModel = new SortProxyModel(this); m_proxyModel->setSourceModel(m_model); m_proxyModel->setFilterCaseSensitivity(Qt::CaseSensitive); m_proxyModel->sort(NameColumn, Qt::AscendingOrder); m_sizesModel = new QStandardItemModel(this); // Disable the install button if we can't install new themes to ~/.icons, // or Xcursor isn't set up to look for cursor themes there. if (!m_model->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable()) { setCanInstall(false); } } CursorThemeConfig::~CursorThemeConfig() { /* */ } void CursorThemeConfig::setCanInstall(bool can) { if (m_canInstall == can) { return; } m_canInstall = can; emit canInstallChanged(); } bool CursorThemeConfig::canInstall() const { return m_canInstall; } void CursorThemeConfig::setCanResize(bool can) { if (m_canResize == can) { return; } m_canResize = can; emit canResizeChanged(); } bool CursorThemeConfig::canResize() const { return m_canResize; } void CursorThemeConfig::setCanConfigure(bool can) { if (m_canConfigure == can) { return; } m_canConfigure = can; emit canConfigureChanged(); } bool CursorThemeConfig::canConfigure() const { return m_canConfigure; } void CursorThemeConfig::setSelectedThemeRow(int row) { if (m_selectedThemeRow == row) { return; } m_selectedThemeRow = row; emit selectedThemeRowChanged(); updateSizeComboBox(); QModelIndex selected = selectedIndex(); if (selected.isValid()) { const CursorTheme *theme = m_proxyModel->theme(selected); } setNeedsSave(m_originalSelectedThemeRow != m_selectedThemeRow || m_originalPreferredSize != m_preferredSize); } int CursorThemeConfig::selectedThemeRow() const { return m_selectedThemeRow; } void CursorThemeConfig::setSelectedSizeRow(int row) { Q_ASSERT (row < m_sizesModel->rowCount() && row >= 0); // we don't return early if m_selectedSizeRow == row as this is called after the model is changed m_selectedSizeRow = row; emit selectedSizeRowChanged(); int size = m_sizesModel->item(row)->data().toInt(); m_preferredSize = size; setNeedsSave(m_originalSelectedThemeRow != m_selectedThemeRow || m_originalPreferredSize != m_preferredSize); } int CursorThemeConfig::selectedSizeRow() const { return m_selectedSizeRow; } +bool CursorThemeConfig::downloadingFile() const +{ + return m_tempCopyJob; +} + QAbstractItemModel *CursorThemeConfig::cursorsModel() { return m_proxyModel; } QAbstractItemModel *CursorThemeConfig::sizesModel() { return m_sizesModel; } bool CursorThemeConfig::iconsIsWritable() const { const QFileInfo icons = QFileInfo(QDir::homePath() + "/.icons"); const QFileInfo home = QFileInfo(QDir::homePath()); return ((icons.exists() && icons.isDir() && icons.isWritable()) || (!icons.exists() && home.isWritable())); } void CursorThemeConfig::updateSizeComboBox() { // clear the combo box m_sizesModel->clear(); // refill the combo box and adopt its icon size QModelIndex selected = selectedIndex(); int maxIconWidth = 0; int maxIconHeight = 0; if (selected.isValid()) { const CursorTheme *theme = m_proxyModel->theme(selected); const QList sizes = theme->availableSizes(); QIcon m_icon; // only refill the combobox if there is more that 1 size if (sizes.size() > 1) { int i; QList comboBoxList; QPixmap m_pixmap; // insert the items m_pixmap = theme->createIcon(0); if (m_pixmap.width() > maxIconWidth) { maxIconWidth = m_pixmap.width(); } if (m_pixmap.height() > maxIconHeight) { maxIconHeight = m_pixmap.height(); } QStandardItem *item = new QStandardItem(QIcon(m_pixmap), i18nc("@item:inlistbox size", "Resolution dependent")); item->setData(0); m_sizesModel->appendRow(item); comboBoxList << 0; foreach (i, sizes) { m_pixmap = theme->createIcon(i); if (m_pixmap.width() > maxIconWidth) { maxIconWidth = m_pixmap.width(); } if (m_pixmap.height() > maxIconHeight) { maxIconHeight = m_pixmap.height(); } QStandardItem *item = new QStandardItem(QIcon(m_pixmap), QString::number(i)); item->setData(i); m_sizesModel->appendRow(item); comboBoxList << i; }; // select an item int selectItem = comboBoxList.indexOf(m_preferredSize); // m_preferredSize not available for this theme if (selectItem < 0) { /* Search the value next to m_preferredSize. The first entry (0) is ignored. (If m_preferredSize would have been 0, then we would had found it yet. As m_preferredSize is not 0, we won't default to "automatic size".)*/ int j; int distance; int smallestDistance; selectItem = 1; j = comboBoxList.value(selectItem); smallestDistance = j < m_preferredSize ? m_preferredSize - j : j - m_preferredSize; for (int i = 2; i < comboBoxList.size(); ++i) { j = comboBoxList.value(i); distance = j < m_preferredSize ? m_preferredSize - j : j - m_preferredSize; if (distance < smallestDistance || (distance == smallestDistance && j > m_preferredSize)) { smallestDistance = distance; selectItem = i; }; } }; if (selectItem < 0) { selectItem = 0; } setSelectedSizeRow(selectItem); }; }; // enable or disable the combobox KConfig c("kcminputrc"); KConfigGroup cg(&c, "Mouse"); if (cg.isEntryImmutable("cursorSize")) { setCanResize(false); } else { setCanResize(m_sizesModel->rowCount() > 0); }; } bool CursorThemeConfig::applyTheme(const CursorTheme *theme, const int size) { // Require the Xcursor version that shipped with X11R6.9 or greater, since // in previous versions the Xfixes code wasn't enabled due to a bug in the // build system (freedesktop bug #975). #if HAVE_XFIXES && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105 if (!theme) { return false; } if (!CursorTheme::haveXfixes()) { return false; } QByteArray themeName = QFile::encodeName(theme->name()); // Set up the proper launch environment for newly started apps OrgKdeKLauncherInterface klauncher(QStringLiteral("org.kde.klauncher5"), QStringLiteral("/KLauncher"), QDBusConnection::sessionBus()); klauncher.setLaunchEnv(QStringLiteral("XCURSOR_THEME"), themeName); // Update the Xcursor X resources runRdb(0); // Notify all applications that the cursor theme has changed KGlobalSettings::self()->emitChange(KGlobalSettings::CursorChanged); // Reload the standard cursors QStringList names; // Qt cursors names << "left_ptr" << "up_arrow" << "cross" << "wait" << "left_ptr_watch" << "ibeam" << "size_ver" << "size_hor" << "size_bdiag" << "size_fdiag" << "size_all" << "split_v" << "split_h" << "pointing_hand" << "openhand" << "closedhand" << "forbidden" << "whats_this" << "copy" << "move" << "link"; // X core cursors names << "X_cursor" << "right_ptr" << "hand1" << "hand2" << "watch" << "xterm" << "crosshair" << "left_ptr_watch" << "center_ptr" << "sb_h_double_arrow" << "sb_v_double_arrow" << "fleur" << "top_left_corner" << "top_side" << "top_right_corner" << "right_side" << "bottom_right_corner" << "bottom_side" << "bottom_left_corner" << "left_side" << "question_arrow" << "pirate"; foreach (const QString &name, names) { XFixesChangeCursorByName(QX11Info::display(), theme->loadCursor(name, size), QFile::encodeName(name)); } return true; #else Q_UNUSED(theme) return false; #endif } void CursorThemeConfig::save() { const CursorTheme *theme = selectedIndex().isValid() ? m_proxyModel->theme(selectedIndex()) : nullptr; KConfig config("kcminputrc"); KConfigGroup c(&config, "Mouse"); if (theme) { c.writeEntry("cursorTheme", theme->name()); }; c.writeEntry("cursorSize", m_preferredSize); c.sync(); if (!applyTheme(theme, m_preferredSize)) { emit showInfoMessage(i18n("You have to restart the Plasma session for these changes to take effect.")); } m_appliedIndex = selectedIndex(); m_appliedSize = m_preferredSize; m_originalSelectedThemeRow = m_selectedThemeRow; m_originalPreferredSize = m_preferredSize; setNeedsSave(false); } void CursorThemeConfig::load() { // Get the name of the theme libXcursor currently uses QString currentTheme; if (QX11Info::isPlatformX11()) { currentTheme = XcursorGetTheme(QX11Info::display()); } // Get the name of the theme KDE is configured to use KConfig c("kcminputrc"); KConfigGroup cg(&c, "Mouse"); currentTheme = cg.readEntry("cursorTheme", currentTheme); // Find the theme in the listview if (!currentTheme.isEmpty()) { m_appliedIndex = m_proxyModel->findIndex(currentTheme); } else { m_appliedIndex = m_proxyModel->defaultIndex(); } // Disable the listview and the buttons if we're in kiosk mode if (cg.isEntryImmutable("cursorTheme")) { setCanConfigure(false); setCanInstall(false); } const CursorTheme *theme = m_proxyModel->theme(m_appliedIndex); setSelectedThemeRow(m_appliedIndex.row()); m_originalSelectedThemeRow = m_selectedThemeRow; m_originalPreferredSize = m_preferredSize; // Load cursor size int size = cg.readEntry("cursorSize", 0); if (size <= 0) { m_preferredSize = 0; } else { m_preferredSize = size; } updateSizeComboBox(); // This handles also the kiosk mode m_appliedSize = size; setNeedsSave(false); } void CursorThemeConfig::defaults() { QModelIndex defaultIndex = m_proxyModel->findIndex("breeze_cursors"); setSelectedThemeRow(defaultIndex.row()); m_preferredSize = 0; updateSizeComboBox(); setNeedsSave(m_originalSelectedThemeRow != m_selectedThemeRow || m_originalPreferredSize != m_preferredSize); } void CursorThemeConfig::selectionChanged() { updateSizeComboBox(); setNeedsSave(m_originalSelectedThemeRow != m_selectedThemeRow || m_originalPreferredSize != m_preferredSize); //setNeedsSave(m_appliedIndex != selectedIndex()); } QModelIndex CursorThemeConfig::selectedIndex() const { return m_proxyModel->index(m_selectedThemeRow, 0); } void CursorThemeConfig::getNewClicked() { KNS3::DownloadDialog dialog("xcursor.knsrc", nullptr); if (dialog.exec()) { KNS3::Entry::List list = dialog.changedEntries(); if (!list.isEmpty()) { m_model->refreshList(); } } } void CursorThemeConfig::installThemeFromFile(const QUrl &url) { if (url.isLocalFile()) { installThemeFile(url.toLocalFile()); return; } + if (m_tempCopyJob) { + 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()), + m_tempCopyJob = KIO::file_copy(url,QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); - job->uiDelegate()->setAutoErrorHandlingEnabled(true); + m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + emit downloadingFileChanged(); - connect(job, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { + connect(m_tempCopyJob, &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(); }); + connect(m_tempCopyJob, &QObject::destroyed, this, &CursorThemeConfig::downloadingFileChanged); } void CursorThemeConfig::installThemeFile(const QString &path) { KTar archive(path); archive.open(QIODevice::ReadOnly); const KArchiveDirectory *archiveDir = archive.directory(); QStringList themeDirs; // Extract the dir names of the cursor themes in the archive, and // append them to themeDirs foreach(const QString &name, archiveDir->entries()) { const KArchiveEntry *entry = archiveDir->entry(name); if (entry->isDirectory() && entry->name().toLower() != "default") { const KArchiveDirectory *dir = static_cast(entry); if (dir->entry("index.theme") && dir->entry("cursors")) { themeDirs << dir->name(); } } } if (themeDirs.isEmpty()) { emit showErrorMessage(i18n("The file is not a valid icon theme archive.")); return; } // The directory we'll install the themes to QString destDir = QDir::homePath() + "/.icons/"; if (!QDir().mkpath(destDir)) { emit showErrorMessage(i18n("Failed to create 'icons' folder.")); return; }; // Process each cursor theme in the archive foreach (const QString &dirName, themeDirs) { QDir dest(destDir + dirName); if (dest.exists()) { QString question = i18n("A theme named %1 already exists in your icon " "theme folder. Do you want replace it with this one?", dirName); int answer = KMessageBox::warningContinueCancel(nullptr, question, i18n("Overwrite Theme?"), KStandardGuiItem::overwrite()); if (answer != KMessageBox::Continue) { continue; } // ### If the theme that's being replaced is the current theme, it // will cause cursor inconsistencies in newly started apps. } // ### Should we check if a theme with the same name exists in a global theme dir? // If that's the case it will effectively replace it, even though the global theme // won't be deleted. Checking for this situation is easy, since the global theme // will be in the listview. Maybe this should never be allowed since it might // result in strange side effects (from the average users point of view). OTOH // a user might want to do this 'upgrade' a global theme. const KArchiveDirectory *dir = static_cast (archiveDir->entry(dirName)); dir->copyTo(dest.path()); m_model->addTheme(dest); } archive.close(); emit showSuccessMessage(i18n("Theme installed successfully.")); m_model->refreshList(); } void CursorThemeConfig::removeTheme(int row) { QModelIndex idx = m_proxyModel->index(row, 0); if (!idx.isValid()) { return; } const CursorTheme *theme = m_proxyModel->theme(idx); // Don't let the user delete the currently configured theme if (idx == m_appliedIndex) { KMessageBox::sorry(nullptr, i18n("You cannot delete the theme you are currently " "using.
You have to switch to another theme first.
")); return; } // Get confirmation from the user QString question = i18n("Are you sure you want to remove the " "%1 cursor theme?
" "This will delete all the files installed by this theme.
", theme->title()); int answer = KMessageBox::warningContinueCancel(nullptr, question, i18n("Confirmation"), KStandardGuiItem::del()); if (answer != KMessageBox::Continue) { return; } // Delete the theme from the harddrive KIO::del(QUrl::fromLocalFile(theme->path())); // async // Remove the theme from the model m_proxyModel->removeTheme(idx); // TODO: // Since it's possible to substitute cursors in a system theme by adding a local // theme with the same name, we shouldn't remove the theme from the list if it's // still available elsewhere. We could add a // bool CursorThemeModel::tryAddTheme(const QString &name), and call that, but // since KIO::del() is an asynchronos operation, the theme we're deleting will be // readded to the list again before KIO has removed it. } #include "kcmcursortheme.moc" diff --git a/kcms/cursortheme/kcmcursortheme.h b/kcms/cursortheme/kcmcursortheme.h index 4a9021e47..1f8e45de7 100644 --- a/kcms/cursortheme/kcmcursortheme.h +++ b/kcms/cursortheme/kcmcursortheme.h @@ -1,139 +1,150 @@ /* * Copyright © 2003-2007 Fredrik Höglund * * 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. */ #ifndef KCMCURSORTHEME_H #define KCMCURSORTHEME_H #include #include class QStandardItemModel; class QTemporaryFile; class CursorThemeModel; class SortProxyModel; class CursorTheme; +namespace KIO +{ + class FileCopyJob; +} + class CursorThemeConfig : public KQuickAddons::ConfigModule { Q_OBJECT Q_PROPERTY(bool canInstall READ canInstall WRITE setCanInstall NOTIFY canInstallChanged) Q_PROPERTY(bool canResize READ canResize WRITE setCanResize NOTIFY canResizeChanged) Q_PROPERTY(bool canConfigure READ canConfigure WRITE setCanConfigure NOTIFY canConfigureChanged) Q_PROPERTY(QAbstractItemModel *cursorsModel READ cursorsModel CONSTANT) Q_PROPERTY(QAbstractItemModel *sizesModel READ sizesModel CONSTANT) Q_PROPERTY(int selectedThemeRow READ selectedThemeRow WRITE setSelectedThemeRow NOTIFY selectedThemeRowChanged) Q_PROPERTY(int selectedSizeRow READ selectedSizeRow WRITE setSelectedSizeRow NOTIFY selectedSizeRowChanged) + Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) + public: CursorThemeConfig(QObject *parent, const QVariantList &); ~CursorThemeConfig() override; public: void load() override; void save() override; void defaults() override; //for QML properties bool canInstall() const; void setCanInstall(bool can); bool canResize() const; void setCanResize(bool can); bool canConfigure() const; void setCanConfigure(bool can); int selectedThemeRow() const; void setSelectedThemeRow(int row); int selectedSizeRow() const; void setSelectedSizeRow(int row); + bool downloadingFile() const; + QAbstractItemModel *cursorsModel(); QAbstractItemModel *sizesModel(); Q_SIGNALS: void canInstallChanged(); void canResizeChanged(); void canConfigureChanged(); void selectedThemeRowChanged(); void selectedSizeRowChanged(); + void downloadingFileChanged(); void showSuccessMessage(const QString &message); void showInfoMessage(const QString &message); void showErrorMessage(const QString &message); public Q_SLOTS: void getNewClicked(); void installThemeFromFile(const QUrl &url); void removeTheme(int row); private Q_SLOTS: void selectionChanged(); /** Updates the size combo box. It loads the size list of the selected cursor theme with the corresponding icons and chooses an appropriate entry. It enables the combo box and the label if the theme provides more than one size, otherwise it disables it. If the size setting is looked in kiosk mode, it stays always disabled. */ void updateSizeComboBox(); private: QModelIndex selectedIndex() const; void installThemeFile(const QString &path); /** Applies a given theme, using XFixes, XCursor and KGlobalSettings. @param theme The cursor theme to be applied. It is save to pass 0 here (will result in \e false as return value). @param size The size hint that is used to select the cursor size. @returns If the changes could be applied. Will return \e false if \e theme is 0 or if the XFixes and XCursor libraries aren't available in the required version, otherwise returns \e true. */ bool applyTheme(const CursorTheme *theme, const int size); bool iconsIsWritable() const; CursorThemeModel *m_model; SortProxyModel *m_proxyModel; QStandardItemModel *m_sizesModel; int m_appliedSize; // This index refers to the CursorThemeModel, not the proxy or the view QPersistentModelIndex m_appliedIndex; /** Holds the last size that was chosen by the user. Example: The user chooses theme1 which provides the sizes 24 and 36. He chooses 36. preferredSize gets set to 36. Now, he switches to theme2 which provides the sizes 30 and 40. preferredSize still is 36, so the UI will default to 40, which is next to 36. Now, he chooses theme3 which provides the sizes 34 and 44. preferredSize is still 36, so the UI defaults to 34. Now the user changes manually to 44. This will also change preferredSize. */ int m_preferredSize; int m_originalPreferredSize; int m_selectedThemeRow; int m_selectedSizeRow; int m_originalSelectedThemeRow; bool m_canInstall; bool m_canResize; bool m_canConfigure; QScopedPointer m_tempInstallFile; + QPointer m_tempCopyJob; }; #endif diff --git a/kcms/cursortheme/package/contents/ui/main.qml b/kcms/cursortheme/package/contents/ui/main.qml index 9171db9d0..93e92e0b4 100644 --- a/kcms/cursortheme/package/contents/ui/main.qml +++ b/kcms/cursortheme/package/contents/ui/main.qml @@ -1,186 +1,188 @@ /* Copyright (c) 2015 Marco Martin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Window 2.2 // for Screen import QtQuick.Layouts 1.1 import QtQuick.Controls 2.2 as QtControls import QtQuick.Dialogs 1.1 as QtDialogs import org.kde.kirigami 2.4 as Kirigami import org.kde.kconfig 1.0 // for KAuthorized import org.kde.kcm 1.1 as KCM import org.kde.private.kcm_cursortheme 1.0 KCM.GridViewKCM { KCM.ConfigModule.quickHelp: i18n("This module lets you choose the mouse cursor theme.") view.model: kcm.cursorsModel view.delegate: Delegate {} view.onCurrentIndexChanged: { kcm.selectedThemeRow = view.currentIndex; view.positionViewAtIndex(view.currentIndex, view.GridView.Beginning); } + enabled: !kcm.downloadingFile + DropArea { anchors.fill: parent onEntered: { if (!drag.hasUrls) { drag.accepted = false; } } onDropped: kcm.installThemeFromFile(drop.urls[0]) } Connections { target: kcm onSelectedThemeRowChanged: view.currentIndex = kcm.selectedThemeRow; } footer: ColumnLayout { id: footerLayout Kirigami.InlineMessage { id: infoLabel Layout.fillWidth: true showCloseButton: true Connections { target: kcm onShowSuccessMessage: { infoLabel.type = Kirigami.MessageType.Positive; infoLabel.text = message; infoLabel.visible = true; } onShowInfoMessage: { infoLabel.type = Kirigami.MessageType.Information; infoLabel.text = message; infoLabel.visible = true; } onShowErrorMessage: { infoLabel.type = Kirigami.MessageType.Error; infoLabel.text = message; infoLabel.visible = true; } } } RowLayout { id: row1 //spacer Item { Layout.fillWidth: true Layout.fillHeight: true } RowLayout { id: comboLayout enabled: kcm.canResize QtControls.Label { text: i18n("Size:") } QtControls.ComboBox { id: sizeCombo model: kcm.sizesModel textRole: "display" onActivated: { kcm.selectedSizeRow = sizeCombo.currentIndex } Connections { target: kcm onSelectedSizeRowChanged: { sizeCombo.currentIndex = kcm.selectedSizeRow } } delegate: QtControls.ItemDelegate { id: sizeComboDelegate readonly property int size: parseInt(model.display) width: parent.width highlighted: ListView.isCurrentItem text: model.display contentItem: RowLayout { Kirigami.Icon { source: model.decoration smooth: true width: sizeComboDelegate.size / Screen.devicePixelRatio height: sizeComboDelegate.size / Screen.devicePixelRatio visible: valid && sizeComboDelegate.size > 0 } QtControls.Label { Layout.fillWidth: true color: sizeComboDelegate.highlighted ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor text: model[sizeCombo.textRole] } } } } } RowLayout { parent: footerLayout.x + footerLayout.width - comboLayout.width > width ? row1 : row2 QtControls.Button { icon.name: "document-import" text: i18n("&Install from File...") onClicked: fileDialogLoader.active = true; enabled: kcm.canInstall } QtControls.Button { icon.name: "get-hot-new-stuff" text: i18n("&Get New Cursors...") onClicked: kcm.getNewClicked(); enabled: kcm.canInstall visible: KAuthorized.authorize("ghns") } } } RowLayout { id: row2 visible: children.length > 1 //spacer Item { Layout.fillWidth: true Layout.fillHeight: true } } } Loader { id: fileDialogLoader active: false sourceComponent: QtDialogs.FileDialog { title: i18n("Open Theme") folder: shortcuts.home nameFilters: [ i18n("Cursor Theme Files (*.tar.gz *.tar.bz2)") ] Component.onCompleted: open() onAccepted: { kcm.installThemeFromFile(fileUrls[0]) fileDialogLoader.active = false } onRejected: { fileDialogLoader.active = false } } } } diff --git a/kcms/icons/main.cpp b/kcms/icons/main.cpp index d589e26a5..d07dcd758 100644 --- a/kcms/icons/main.cpp +++ b/kcms/icons/main.cpp @@ -1,591 +1,602 @@ /* * 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; } +bool IconModule::downloadingFile() const +{ + return m_tempCopyJob; +} + 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; } + if (m_tempCopyJob) { + 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()), + m_tempCopyJob = KIO::file_copy(url,QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); - job->uiDelegate()->setAutoErrorHandlingEnabled(true); + m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + emit downloadingFileChanged(); - connect(job, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { + connect(m_tempCopyJob, &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(); }); + connect(m_tempCopyJob, &QObject::destroyed, this, &IconModule::downloadingFileChanged); } 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, int limit) { 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")} }; // created on-demand as it is quite expensive to do and we don't want to do it every loop iteration either QScopedPointer theme; QVariantList pixmaps; for (const QStringList &iconNames : s_previewIcons) { const QString cacheKey = themeName + QLatin1Char('@') + QString::number(size) + QLatin1Char('@') + QString::number(dpr,'f',1) + QLatin1Char('@') + iconNames.join(QLatin1Char(',')); QPixmap pix; if (!QPixmapCache::find(cacheKey, pix)) { if (!theme) { theme.reset(new KIconTheme(themeName)); } pix = getBestIcon(*theme.data(), iconNames, size, dpr); // Inserting a pixmap even if null so we know whether we searched for it already QPixmapCache::insert(cacheKey, pix); } 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); return pixmap; } //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); } if (path.isEmpty()) { continue; } if (!renderer.load(path)) { continue; } 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 QPixmap(); } #include "main.moc" diff --git a/kcms/icons/main.h b/kcms/icons/main.h index 116ae7fb2..1959442ce 100644 --- a/kcms/icons/main.h +++ b/kcms/icons/main.h @@ -1,116 +1,127 @@ /* * 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; +namespace KIO +{ +class FileCopyJob; +} + class IconsModel; class IconModule : public KQuickAddons::ConfigModule { Q_OBJECT Q_PROPERTY(IconsModel *iconsModel READ iconsModel CONSTANT) Q_PROPERTY(QStringList iconGroups READ iconGroups CONSTANT) + Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) + public: IconModule(QObject *parent, const QVariantList &args); ~IconModule() override; enum Roles { ThemeNameRole = Qt::UserRole + 1, DescriptionRole, RemovableRole, PendingDeletionRole }; IconsModel *iconsModel() const; QStringList iconGroups() const; + bool downloadingFile() 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; // 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 downloadingFileChanged(); 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_tempCopyJob; QPointer m_newStuffDialog; }; diff --git a/kcms/icons/package/contents/ui/main.qml b/kcms/icons/package/contents/ui/main.qml index 96372d338..dd44a4183 100644 --- a/kcms/icons/package/contents/ui/main.qml +++ b/kcms/icons/package/contents/ui/main.qml @@ -1,285 +1,287 @@ /* * 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.kconfig 1.0 // for KAuthorized 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 + enabled: !kcm.downloadingFile + DropArea { anchors.fill: parent onEntered: { if (!drag.hasUrls) { drag.accepted = false; } } onDropped: kcm.installThemeFromFile(drop.urls[0]) } 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() { // 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 Icons...") icon.name: "get-hot-new-stuff" onClicked: kcm.getNewStuff(this) visible: KAuthorized.authorize("ghns") } } } 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 } } } }