diff --git a/kcms/cursortheme/kcmcursortheme.cpp b/kcms/cursortheme/kcmcursortheme.cpp index d19423f65..669bb11f9 100644 --- a/kcms/cursortheme/kcmcursortheme.cpp +++ b/kcms/cursortheme/kcmcursortheme.cpp @@ -1,616 +1,617 @@ /* * Copyright © 2003-2007 Fredrik Höglund * Copyright © 2019 Benjamin Port * * 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 "cursorthemesettings.h" #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_settings(new CursorThemeSettings), m_canInstall(true), m_canResize(true), m_canConfigure(true) { // Unfortunately doesn't generate a ctor taking the parent as parameter m_settings->setParent(this); m_currentSize = m_settings->cursorSize(); m_currentTheme = m_settings->cursorTheme(); m_preferredSize = m_currentSize; connect(m_settings, &CursorThemeSettings::configChanged, this, &CursorThemeConfig::updateNeedsSave); connect(m_settings, &CursorThemeSettings::cursorSizeChanged, this, &CursorThemeConfig::updateNeedsSave); connect(m_settings, &CursorThemeSettings::cursorThemeChanged, this, &CursorThemeConfig::updateNeedsSave); connect(m_settings, &CursorThemeSettings::cursorThemeChanged, this, &CursorThemeConfig::updateSizeComboBox); qmlRegisterType("org.kde.private.kcm_cursortheme", 1, 0, "PreviewWidget"); qmlRegisterType(); qmlRegisterType(); KAboutData* aboutData = new KAboutData(QStringLiteral("kcm_cursortheme"), i18n("Cursors"), 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_themeModel = new CursorThemeModel(this); m_themeProxyModel = new SortProxyModel(this); m_themeProxyModel->setSourceModel(m_themeModel); m_themeProxyModel->setFilterCaseSensitivity(Qt::CaseSensitive); m_themeProxyModel->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_themeModel->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable()) { setCanInstall(false); } } CursorThemeConfig::~CursorThemeConfig() { /* */ } CursorThemeSettings *CursorThemeConfig::cursorThemeSettings() const { return m_settings; } 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(); } int CursorThemeConfig::preferredSize() const { return m_preferredSize; } void CursorThemeConfig::setPreferredSize(int size) { if (m_preferredSize == size) { return; } m_preferredSize = size; emit preferredSizeChanged(); } bool CursorThemeConfig::canConfigure() const { return m_canConfigure; } bool CursorThemeConfig::downloadingFile() const { return m_tempCopyJob; } QAbstractItemModel *CursorThemeConfig::cursorsModel() { return m_themeProxyModel; } 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 int row = cursorThemeIndex(m_settings->cursorTheme()); QModelIndex selected = m_themeProxyModel->index(row, 0); int maxIconWidth = 0; int maxIconHeight = 0; if (selected.isValid()) { const CursorTheme *theme = m_themeProxyModel->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 size = m_preferredSize; int selectItem = comboBoxList.indexOf(size); // cursor size not available for this theme if (selectItem < 0) { /* Search the value next to cursor size. The first entry (0) is ignored. (If cursor size would have been 0, then we would had found it yet. As cursor size is not 0, we won't default to "automatic size".)*/ int j; int distance; int smallestDistance; selectItem = 1; j = comboBoxList.value(selectItem); size = j; smallestDistance = qAbs(m_preferredSize - j); for (int i = 2; i < comboBoxList.size(); ++i) { j = comboBoxList.value(i); distance = qAbs(m_preferredSize - j); if (distance < smallestDistance || (distance == smallestDistance && j > m_preferredSize)) { smallestDistance = distance; selectItem = i; size = j; } } } if (selectItem < 0) { selectItem = 0; } m_settings->setCursorSize(size); } } // enable or disable the combobox if (m_settings->isImmutable("cursorSize")) { setCanResize(false); } else { setCanResize(m_sizesModel->rowCount() > 0); } // We need to emit a cursorSizeChanged in all case to refresh UI emit m_settings->cursorSizeChanged(); } 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)); } updateSizeComboBox(); + emit themeApplied(); return true; #else Q_UNUSED(theme) return false; #endif } int CursorThemeConfig::cursorSizeIndex(int cursorSize) const { if (m_sizesModel->rowCount() > 0) { if (cursorSize == 0) { return 0; } const auto items = m_sizesModel->findItems(QString::number(cursorSize)); if (items.count() == 1) { return items.first()->row(); } } return -1; } int CursorThemeConfig::cursorSizeFromIndex(int index) { Q_ASSERT (index < m_sizesModel->rowCount() && index >= 0); return m_sizesModel->item(index)->data().toInt(); } int CursorThemeConfig::cursorThemeIndex(const QString &cursorTheme) const { auto results = m_themeProxyModel->findIndex(cursorTheme); return results.row(); } QString CursorThemeConfig::cursorThemeFromIndex(int index) const { QModelIndex idx = m_themeProxyModel->index(index, 0); return m_themeProxyModel->theme(idx)->name(); } void CursorThemeConfig::save() { m_settings->save(); m_currentTheme = m_settings->cursorTheme(); m_currentSize = m_settings->cursorSize(); setPreferredSize(m_currentSize); int row = cursorThemeIndex(m_settings->cursorTheme()); QModelIndex selected = m_themeProxyModel->index(row, 0); const CursorTheme *theme = selected.isValid() ? m_themeProxyModel->theme(selected) : nullptr; if (!applyTheme(theme, m_currentSize)) { emit showInfoMessage(i18n("You have to restart the Plasma session for these changes to take effect.")); } setNeedsSave(false); } void CursorThemeConfig::load() { m_settings->load(); m_currentSize = m_settings->cursorSize(); m_currentTheme = m_settings->cursorTheme(); setPreferredSize(m_currentSize); // Get the name of the theme KDE is configured to use QString currentTheme = m_settings->cursorTheme(); // Disable the listview and the buttons if we're in kiosk mode if (m_settings->isImmutable( QStringLiteral( "cursorTheme" ))) { setCanConfigure(false); setCanInstall(false); } updateSizeComboBox(); // This handles also the kiosk mode setNeedsSave(false); } void CursorThemeConfig::defaults() { m_settings->setDefaults(); m_preferredSize = m_settings->cursorSize(); } void CursorThemeConfig::updateNeedsSave() { setNeedsSave(m_settings->cursorTheme() != m_currentTheme || m_settings->cursorSize() != m_currentSize); } void CursorThemeConfig::getNewClicked() { KNS3::DownloadDialog dialog("xcursor.knsrc", nullptr); if (dialog.exec()) { KNS3::Entry::List list = dialog.changedEntries(); if (!list.isEmpty()) { for (const KNS3::Entry& entry : list) { if (entry.status() == KNS3::Entry::Deleted) { for (const QString& deleted : entry.uninstalledFiles()) { QVector list = deleted.splitRef(QLatin1Char('/')); if (list.last() == QLatin1Char('*')) { list.takeLast(); } QModelIndex idx = m_themeModel->findIndex(list.last().toString()); if (idx.isValid()) { m_themeModel->removeTheme(idx); } } } else if (entry.status() == KNS3::Entry::Installed) { for (const QString& created : entry.installedFiles()) { QStringList list = created.split(QLatin1Char('/')); if (list.last() == QLatin1Char('*')) { list.takeLast(); } // Because we sometimes get some extra slashes in the installed files list list.removeAll({}); // Because we'll also get the containing folder, if it was not already there // we need to ignore it. if (list.last() == QLatin1String(".icons")) { continue; } m_themeModel->addTheme(list.join(QLatin1Char('/'))); } } } } } } 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; } m_tempCopyJob = KIO::file_copy(url,QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); emit downloadingFileChanged(); 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_themeModel->addTheme(dest); } archive.close(); emit showSuccessMessage(i18n("Theme installed successfully.")); m_themeModel->refreshList(); } void CursorThemeConfig::removeTheme(int row) { QModelIndex idx = m_themeProxyModel->index(row, 0); if (!idx.isValid()) { return; } const CursorTheme *theme = m_themeProxyModel->theme(idx); // Don't let the user delete the currently configured theme if (theme->name() == m_currentTheme) { 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_themeProxyModel->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 2b7a693fb..c0f283e64 100644 --- a/kcms/cursortheme/kcmcursortheme.h +++ b/kcms/cursortheme/kcmcursortheme.h @@ -1,150 +1,151 @@ /* * Copyright © 2003-2007 Fredrik Höglund * Copyright © 2019 Benjamin Port * * 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; class CursorThemeSettings; namespace KIO { class FileCopyJob; } class CursorThemeConfig : public KQuickAddons::ConfigModule { Q_OBJECT Q_PROPERTY(CursorThemeSettings *cursorThemeSettings READ cursorThemeSettings CONSTANT) 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(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) Q_PROPERTY(int preferredSize READ preferredSize WRITE setPreferredSize NOTIFY preferredSizeChanged) public: CursorThemeConfig(QObject *parent, const QVariantList &); ~CursorThemeConfig() override; void load() override; void save() override; void defaults() override; //for QML properties CursorThemeSettings *cursorThemeSettings() const; bool canInstall() const; void setCanInstall(bool can); bool canResize() const; void setCanResize(bool can); bool canConfigure() const; void setCanConfigure(bool can); int preferredSize() const; void setPreferredSize(int size); bool downloadingFile() const; QAbstractItemModel *cursorsModel(); QAbstractItemModel *sizesModel(); Q_INVOKABLE int cursorSizeIndex(int cursorSize) const; Q_INVOKABLE int cursorSizeFromIndex(int index); Q_INVOKABLE int cursorThemeIndex(const QString &cursorTheme) const; Q_INVOKABLE QString cursorThemeFromIndex(int index) const; Q_SIGNALS: void canInstallChanged(); void canResizeChanged(); void canConfigureChanged(); void selectedSizeRowChanged(); void downloadingFileChanged(); void preferredSizeChanged(); + void themeApplied(); 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: /** 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: void updateNeedsSave(); 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_themeModel; SortProxyModel *m_themeProxyModel; QStandardItemModel *m_sizesModel; CursorThemeSettings *m_settings; /** 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; bool m_canInstall; bool m_canResize; bool m_canConfigure; QScopedPointer m_tempInstallFile; QPointer m_tempCopyJob; int m_currentSize; QString m_currentTheme; }; #endif diff --git a/kcms/cursortheme/package/contents/ui/Delegate.qml b/kcms/cursortheme/package/contents/ui/Delegate.qml index 12a8ca3db..314f55799 100644 --- a/kcms/cursortheme/package/contents/ui/Delegate.qml +++ b/kcms/cursortheme/package/contents/ui/Delegate.qml @@ -1,67 +1,74 @@ /* 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.1 import QtQuick.Window 2.2 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.2 as Controls import QtQuick.Templates 2.2 as T2 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.2 as Kirigami import org.kde.kcm 1.1 as KCM import org.kde.private.kcm_cursortheme 1.0 KCM.GridDelegate { id: delegate text: model.display toolTip: model.description thumbnailAvailable: true thumbnail: PreviewWidget { id: previewWidget //for cursor themes we must ignore the native scaling, //as they will be rendered by X11/KWin, ignoring whatever Qt //scaling width: parent.width * Screen.devicePixelRatio height: parent.height * Screen.devicePixelRatio x: Screen.devicePixelRatio % 1 y: Screen.devicePixelRatio % 1 transformOrigin: Item.TopLeft scale: 1 / Screen.devicePixelRatio themeModel: kcm.cursorsModel currentIndex: index currentSize: kcm.cursorThemeSettings.cursorSize } + Connections { + target: kcm + onThemeApplied: { + previewWidget.refresh(); + } + } + actions: [ Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Remove Theme") enabled: model.isWritable onTriggered: kcm.removeTheme(index); } ] onClicked: { view.forceActiveFocus(); kcm.cursorThemeSettings.cursorTheme = kcm.cursorThemeFromIndex(index); } } diff --git a/kcms/cursortheme/xcursor/previewwidget.cpp b/kcms/cursortheme/xcursor/previewwidget.cpp index 73b8d65e6..fa4bd10a8 100644 --- a/kcms/cursortheme/xcursor/previewwidget.cpp +++ b/kcms/cursortheme/xcursor/previewwidget.cpp @@ -1,317 +1,327 @@ /* * 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 version 2 as published by the Free Software Foundation. * * 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include "previewwidget.h" #include #include #include #include #include "cursortheme.h" namespace { // Preview cursors const char * const cursor_names[] = { "left_ptr", "left_ptr_watch", "wait", "pointing_hand", "whats_this", "ibeam", "size_all", "size_fdiag", "cross", "split_h", "size_ver", "size_hor", "size_bdiag", "split_v", }; const int numCursors = 9; // The number of cursors from the above list to be previewed const int cursorSpacing = 20; // Spacing between preview cursors const qreal widgetMinWidth = 10; // The minimum width of the preview widget const qreal widgetMinHeight = 48; // The minimum height of the preview widget } class PreviewCursor { public: PreviewCursor( const CursorTheme *theme, const QString &name, int size ); ~PreviewCursor(); const QPixmap &pixmap() const { return m_pixmap; } int width() const { return m_pixmap.width(); } int height() const { return m_pixmap.height(); } int boundingSize() const { return m_boundingSize; } inline QRect rect() const; void setPosition( const QPoint &p ) { m_pos = p; } void setPosition( int x, int y ) { m_pos = QPoint(x, y); } QPoint position() const { return m_pos; } operator const uint32_t () const { return m_cursor; } operator const QPixmap& () const { return pixmap(); } private: int m_boundingSize; QPixmap m_pixmap; uint32_t m_cursor; QPoint m_pos; }; PreviewCursor::PreviewCursor(const CursorTheme *theme, const QString &name, int size) : m_boundingSize(size > 0 ? size : theme->defaultCursorSize()) { // Create the preview pixmap QImage image = theme->loadImage(name, size); if (image.isNull()) return; m_pixmap = QPixmap::fromImage(image); // Load the cursor m_cursor = theme->loadCursor(name, size); // ### perhaps we should tag the cursor so it doesn't get // replaced when a new theme is applied } PreviewCursor::~PreviewCursor() { if (QX11Info::isPlatformX11() && m_cursor != XCB_CURSOR_NONE) { xcb_free_cursor(QX11Info::connection(), m_cursor); } } QRect PreviewCursor::rect() const { return QRect(m_pos, m_pixmap.size()) .adjusted(-(cursorSpacing / 2), -(cursorSpacing / 2), cursorSpacing / 2, cursorSpacing / 2); } // ------------------------------------------------------------------------------ PreviewWidget::PreviewWidget(QQuickItem *parent) : QQuickPaintedItem(parent), m_currentIndex(-1), m_currentSize(0) { setAcceptHoverEvents(true); current = nullptr; } PreviewWidget::~PreviewWidget() { qDeleteAll(list); list.clear(); } void PreviewWidget::setThemeModel(SortProxyModel *themeModel) { if (m_themeModel == themeModel) { return; } m_themeModel = themeModel; emit themeModelChanged(); } SortProxyModel *PreviewWidget::themeModel() { return m_themeModel; } void PreviewWidget::setCurrentIndex(int idx) { if (m_currentIndex == idx) { return; } m_currentIndex = idx; emit currentIndexChanged(); if (!m_themeModel) { return; } const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(idx, 0)); setTheme(theme, m_currentSize); } int PreviewWidget::currentIndex() const { return m_currentIndex; } void PreviewWidget::setCurrentSize(int size) { if (m_currentSize == size) { return; } m_currentSize = size; emit currentSizeChanged(); if (!m_themeModel) { return; } const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(m_currentIndex, 0)); setTheme(theme, size); } int PreviewWidget::currentSize() const { return m_currentSize; } +void PreviewWidget::refresh() +{ + if (!m_themeModel) { + return; + } + + const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(m_currentIndex, 0)); + setTheme(theme, m_currentSize); +} + void PreviewWidget::updateImplicitSize() { qreal totalWidth = 0; qreal maxHeight = 0; foreach (const PreviewCursor *c, list) { totalWidth += c->width(); maxHeight = qMax(c->height(), (int)maxHeight); } totalWidth += (list.count() - 1) * cursorSpacing; maxHeight = qMax(maxHeight, widgetMinHeight); setImplicitWidth(qMax(totalWidth, widgetMinWidth)); setImplicitHeight(qMax(height(), maxHeight)); } void PreviewWidget::layoutItems() { if (!list.isEmpty()) { const int spacing = 12; int nextX = spacing; int nextY = spacing; foreach (PreviewCursor *c, list) { c->setPosition(nextX, nextY); nextX += c->boundingSize() + spacing; if (nextX + c->boundingSize() > width()) { nextX = spacing; nextY += c->boundingSize() + spacing; } } } needLayout = false; } void PreviewWidget::setTheme(const CursorTheme *theme, const int size) { qDeleteAll(list); list.clear(); if (theme) { for (int i = 0; i < numCursors; i++) list << new PreviewCursor(theme, cursor_names[i], size); needLayout = true; updateImplicitSize(); } current = nullptr; update(); } void PreviewWidget::paint(QPainter *painter) { if (needLayout) layoutItems(); foreach(const PreviewCursor *c, list) { if (c->pixmap().isNull()) continue; painter->drawPixmap(c->position(), *c); } } void PreviewWidget::hoverMoveEvent(QHoverEvent *e) { if (needLayout) layoutItems(); for (const PreviewCursor *c : qAsConst(list)) { if (c->rect().contains(e->pos())) { if (c != current) { const uint32_t cursor = *c; if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(window())) { if (KWindowSystem::isPlatformX11() && cursor != XCB_CURSOR_NONE) { xcb_change_window_attributes(QX11Info::connection(), actualWindow->winId(), XCB_CW_CURSOR, &cursor); } } current = c; } return; } } setCursor(Qt::ArrowCursor); current = nullptr; } void PreviewWidget::hoverLeaveEvent(QHoverEvent *e) { if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(window())) { actualWindow->unsetCursor(); } } void PreviewWidget::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { Q_UNUSED(newGeometry) Q_UNUSED(oldGeometry) if (!list.isEmpty()) { needLayout = true; } } diff --git a/kcms/cursortheme/xcursor/previewwidget.h b/kcms/cursortheme/xcursor/previewwidget.h index 2d6e7cf9b..871ebc7cd 100644 --- a/kcms/cursortheme/xcursor/previewwidget.h +++ b/kcms/cursortheme/xcursor/previewwidget.h @@ -1,77 +1,79 @@ /* * 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 version 2 as published by the Free Software Foundation. * * 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef PREVIEWWIDGET_H #define PREVIEWWIDGET_H #include #include #include "sortproxymodel.h" class CursorTheme; class PreviewCursor; class PreviewWidget : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(SortProxyModel *themeModel READ themeModel WRITE setThemeModel NOTIFY themeModelChanged) Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) Q_PROPERTY(int currentSize READ currentSize WRITE setCurrentSize NOTIFY currentSizeChanged) public: explicit PreviewWidget(QQuickItem *parent = nullptr); ~PreviewWidget() override; void setTheme(const CursorTheme *theme, const int size); void setUseLables(bool); void updateImplicitSize(); void setThemeModel(SortProxyModel *themeModel); SortProxyModel *themeModel(); void setCurrentIndex(int idx); int currentIndex() const; void setCurrentSize(int size); int currentSize() const; + Q_INVOKABLE void refresh(); + Q_SIGNALS: void themeModelChanged(); void currentIndexChanged(); void currentSizeChanged(); protected: void paint(QPainter *) override; void hoverMoveEvent(QHoverEvent *event) override; void hoverLeaveEvent(QHoverEvent *e) override; void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; private: void layoutItems(); QList list; const PreviewCursor *current; bool needLayout:1; QPointer m_themeModel; int m_currentIndex; int m_currentSize; }; #endif // PREVIEWWIDGET_H