kcmcursortheme.cpp 19.8 KB
Newer Older
1
2
/*
 *  Copyright © 2003-2007 Fredrik Höglund <fredrik@kde.org>
Benjamin Port's avatar
Benjamin Port committed
3
 *  Copyright © 2019 Benjamin Port <benjamin.port@enioka.com>
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 *
 *  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.
 */

20
21
#include <config-X11.h>

22
23
#include "kcmcursortheme.h"

24
25
26
27
28
29
#include "xcursor/thememodel.h"
#include "xcursor/sortproxymodel.h"
#include "xcursor/cursortheme.h"
#include "xcursor/previewwidget.h"
#include "../krdb/krdb.h"

30
31
32
#include <KAboutData>
#include <KPluginFactory>
#include <KLocalizedString>
33
34
#include <KMessageBox>
#include <KUrlRequesterDialog>
35
#include <KIO/CopyJob>
36
#include <KIO/DeleteJob>
37
38
#include <KIO/Job>
#include <KIO/JobUiDelegate>
39
40
#include <KTar>
#include <KGlobalSettings>
41

42
#include <knewstuffcore_version.h>
43
#if KNEWSTUFFCORE_VERSION_MAJOR==5 && KNEWSTUFFCORE_VERSION_MINOR>=68
44
45
#include <KNSCore/EntryWrapper>
#endif
Marco Martin's avatar
Marco Martin committed
46

47
#include <QQmlListReference>
48
49
#include <QX11Info>
#include <QStandardItemModel>
50

51
52
#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>
53

Benjamin Port's avatar
Benjamin Port committed
54
#include "cursorthemesettings.h"
55
56
57
58
59
60
61
#include <klauncher_iface.h>

#ifdef HAVE_XFIXES
#  include <X11/extensions/Xfixes.h>
#endif

K_PLUGIN_FACTORY_WITH_JSON(CursorThemeConfigFactory, "kcm_cursortheme.json", registerPlugin<CursorThemeConfig>();)
62

63
CursorThemeConfig::CursorThemeConfig(QObject *parent, const QVariantList &args)
64
65
    : KQuickAddons::ManagedConfigModule(parent, args),
      m_settings(new CursorThemeSettings(this)),
66
67
68
69
      m_canInstall(true),
      m_canResize(true),
      m_canConfigure(true)
{
70
    m_preferredSize = m_settings->cursorSize();
71
    connect(m_settings, &CursorThemeSettings::cursorThemeChanged, this, &CursorThemeConfig::updateSizeComboBox);
72
73
    qmlRegisterType<PreviewWidget>("org.kde.private.kcm_cursortheme", 1, 0, "PreviewWidget");
    qmlRegisterType<SortProxyModel>();
74
    qmlRegisterType<CursorThemeSettings>();
75
    KAboutData* aboutData = new KAboutData(QStringLiteral("kcm_cursortheme"), i18n("Cursors"),
Björn Feber's avatar
Björn Feber committed
76
        QStringLiteral("1.0"), QString(), KAboutLicense::GPL, i18n("(c) 2003-2007 Fredrik Höglund"));
77
    aboutData->addAuthor(i18n("Fredrik Höglund"));
78
    aboutData->addAuthor(i18n("Marco Martin"));
79
    setAboutData(aboutData);
80

81
    m_themeModel = new CursorThemeModel(this);
82

83
84
85
86
    m_themeProxyModel = new SortProxyModel(this);
    m_themeProxyModel->setSourceModel(m_themeModel);
    m_themeProxyModel->setFilterCaseSensitivity(Qt::CaseSensitive);
    m_themeProxyModel->sort(NameColumn, Qt::AscendingOrder);
87
88
89
90
91

    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.
92
    if (!m_themeModel->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable()) {
93
94
        setCanInstall(false);
    }
95
96
97
98
99
100
101
}

CursorThemeConfig::~CursorThemeConfig()
{
    /* */
}

102
103
104
105
106
CursorThemeSettings *CursorThemeConfig::cursorThemeSettings() const
{
    return m_settings;
}

107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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();
}

147
int CursorThemeConfig::preferredSize() const
148
{
149
    return m_preferredSize;
150
151
}

152
void CursorThemeConfig::setPreferredSize(int size)
153
{
154
    if (m_preferredSize == size) {
155
156
157
        return;
    }
    m_preferredSize = size;
158
    emit preferredSizeChanged();
159
160
}

161
bool CursorThemeConfig::canConfigure() const
162
{
163
    return m_canConfigure;
164
165
}

166
167
168
169
170
bool CursorThemeConfig::downloadingFile() const
{
    return m_tempCopyJob;
}

171
172
QAbstractItemModel *CursorThemeConfig::cursorsModel()
{
173
    return m_themeProxyModel;
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
}

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
197
198
    int row = cursorThemeIndex(m_settings->cursorTheme());
    QModelIndex selected = m_themeProxyModel->index(row, 0);
199
200
201
    int maxIconWidth = 0;
    int maxIconHeight = 0;
    if (selected.isValid()) {
202
        const CursorTheme *theme = m_themeProxyModel->theme(selected);
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
        const QList<int> sizes = theme->availableSizes();
        QIcon m_icon;
        // only refill the combobox if there is more that 1 size
        if (sizes.size() > 1) {
            int i;
            QList<int> 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;
236
            }
237
238

            // select an item
239
240
241
242
            int size = m_preferredSize;
            int selectItem = comboBoxList.indexOf(size);

            // cursor size not available for this theme
243
            if (selectItem < 0) {
244
245
246
                /* 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
247
248
249
250
251
252
                   default to "automatic size".)*/
                int j;
                int distance;
                int smallestDistance;
                selectItem = 1;
                j = comboBoxList.value(selectItem);
253
254
                size = j;
                smallestDistance = qAbs(m_preferredSize - j);
255
256
                for (int i = 2; i < comboBoxList.size(); ++i) {
                    j = comboBoxList.value(i);
257
                    distance = qAbs(m_preferredSize - j);
258
259
260
                    if (distance < smallestDistance || (distance == smallestDistance && j > m_preferredSize)) {
                        smallestDistance = distance;
                        selectItem = i;
261
                        size = j;
262
                    }
263
                }
264
            }
265
            m_settings->setCursorSize(size);
266
267
        }
    }
268
269

    // enable or disable the combobox
Benjamin Port's avatar
Benjamin Port committed
270
    if (m_settings->isImmutable("cursorSize")) {
271
272
273
        setCanResize(false);
    } else {
        setCanResize(m_sizesModel->rowCount() > 0);
274
    }
275
276
    // We need to emit a cursorSizeChanged in all case to refresh UI
    emit m_settings->cursorSizeChanged();
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
}

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);

    // 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));
    }
327
    updateSizeComboBox();
328
    emit themeApplied();
329
330
331
332
333
334
335
    return true;
#else
    Q_UNUSED(theme)
    return false;
#endif
}

336
337
338
339
340
341
342
343
344
345
346
347
348
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;
}
349

350
int CursorThemeConfig::cursorSizeFromIndex(int index)
351
{
352
    Q_ASSERT (index < m_sizesModel->rowCount() && index >= 0);
353

354
355
356
357
358
359
360
361
362
363
364
365
366
367
    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();
}
Benjamin Port's avatar
Benjamin Port committed
368

369
370
void CursorThemeConfig::save()
{
371
372
    ManagedConfigModule::save();
    setPreferredSize(m_settings->cursorSize());
373

374
375
376
377
    int row = cursorThemeIndex(m_settings->cursorTheme());
    QModelIndex selected = m_themeProxyModel->index(row, 0);
    const CursorTheme *theme = selected.isValid() ? m_themeProxyModel->theme(selected) : nullptr;

378
    if (!applyTheme(theme, m_settings->cursorSize())) {
379
        emit showInfoMessage(i18n("You have to restart the Plasma session for these changes to take effect."));
380
    }
381
382

    KGlobalSettings::self()->emitChange(KGlobalSettings::CursorChanged);
383
384
}

385
386
387

void CursorThemeConfig::load()
{
388
389
390
    ManagedConfigModule::load();
    setPreferredSize(m_settings->cursorSize());
    // Get the name of the theme KDE is configured to use
Benjamin Port's avatar
Benjamin Port committed
391
    QString currentTheme = m_settings->cursorTheme();
392
393

    // Disable the listview and the buttons if we're in kiosk mode
Benjamin Port's avatar
Benjamin Port committed
394
    if (m_settings->isImmutable( QStringLiteral( "cursorTheme" ))) {
395
396
397
398
399
400
401
402
403
404
          setCanConfigure(false);
          setCanInstall(false);
    }

    updateSizeComboBox(); // This handles also the kiosk mode

    setNeedsSave(false);
}


405
406
void CursorThemeConfig::defaults()
{
407
    ManagedConfigModule::defaults();
408
    m_preferredSize = m_settings->cursorSize();
409
410
}

411
void CursorThemeConfig::ghnsEntriesChanged(const QQmlListReference &changedEntries)
412
{
413
#if KNEWSTUFFCORE_VERSION_MAJOR==5 && KNEWSTUFFCORE_VERSION_MINOR>=68
414
415
416
417
418
419
420
421
    for (int i = 0; i < changedEntries.count(); ++i) {
        KNSCore::EntryWrapper* entry = qobject_cast<KNSCore::EntryWrapper*>(changedEntries.at(i));
        if (entry) {
            if (entry->entry().status() == KNS3::Entry::Deleted) {
                for (const QString& deleted : entry->entry().uninstalledFiles()) {
                    QVector<QStringRef> list = deleted.splitRef(QLatin1Char('/'));
                    if (list.last() == QLatin1Char('*')) {
                        list.takeLast();
422
                    }
423
424
425
                    QModelIndex idx = m_themeModel->findIndex(list.last().toString());
                    if (idx.isValid()) {
                        m_themeModel->removeTheme(idx);
426
427
                    }
                }
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
            } else if (entry->entry().status() == KNS3::Entry::Installed) {
                for (const QString& created : entry->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('/')));
                }
443
            }
444
445
        }
    }
446
#endif
447
448
}

449
void CursorThemeConfig::installThemeFromFile(const QUrl &url)
450
{
451
452
    if (url.isLocalFile()) {
        installThemeFile(url.toLocalFile());
453
454
455
        return;
    }

456
457
458
459
    if (m_tempCopyJob) {
        return;
    }

460
461
462
463
    m_tempInstallFile.reset(new QTemporaryFile());
    if (!m_tempInstallFile->open()) {
        emit showErrorMessage(i18n("Unable to create a temporary file."));
        m_tempInstallFile.reset();
464
465
466
        return;
    }

467
    m_tempCopyJob = KIO::file_copy(url,QUrl::fromLocalFile(m_tempInstallFile->fileName()),
468
                                           -1, KIO::Overwrite);
469
470
    m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
    emit downloadingFileChanged();
471

472
    connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) {
473
474
475
476
        if (job->error() != KJob::NoError) {
            emit showErrorMessage(i18n("Unable to download the icon theme archive: %1", job->errorText()));
            return;
        }
477

478
479
480
        installThemeFile(m_tempInstallFile->fileName());
        m_tempInstallFile.reset();
    });
481
    connect(m_tempCopyJob, &QObject::destroyed, this, &CursorThemeConfig::downloadingFileChanged);
482
483
}

484
void CursorThemeConfig::installThemeFile(const QString &path)
485
{
486
487
    KTar archive(path);
    archive.open(QIODevice::ReadOnly);
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504

    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<const KArchiveDirectory *>(entry);
            if (dir->entry("index.theme") && dir->entry("cursors")) {
                themeDirs << dir->name();
            }
        }
    }

    if (themeDirs.isEmpty()) {
505
506
        emit showErrorMessage(i18n("The file is not a valid icon theme archive."));
        return;
507
508
509
510
    }

    // The directory we'll install the themes to
    QString destDir = QDir::homePath() + "/.icons/";
511
512
513
    if (!QDir().mkpath(destDir)) {
        emit showErrorMessage(i18n("Failed to create 'icons' folder."));
        return;
514
    }
515
516
517
518
519
520
521
522

    // 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);

523
            int answer = KMessageBox::warningContinueCancel(nullptr, question,
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
                                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<const KArchiveDirectory*>
                        (archiveDir->entry(dirName));
        dir->copyTo(dest.path());
545
        m_themeModel->addTheme(dest);
546
547
548
    }

    archive.close();
549
550
551

    emit showSuccessMessage(i18n("Theme installed successfully."));

552
    m_themeModel->refreshList();
553
554
555
556
}

void CursorThemeConfig::removeTheme(int row)
{
557
    QModelIndex idx = m_themeProxyModel->index(row, 0);
558
559
560
561
    if (!idx.isValid()) {
        return;
    }

562
    const CursorTheme *theme = m_themeProxyModel->theme(idx);
563
564

    // Don't let the user delete the currently configured theme
565
    if (theme->name() == m_settings->cursorTheme()) {
566
        KMessageBox::sorry(nullptr, i18n("<qt>You cannot delete the theme you are currently "
567
568
569
570
571
572
573
574
575
576
                "using.<br />You have to switch to another theme first.</qt>"));
        return;
    }

    // Get confirmation from the user
    QString question = i18n("<qt>Are you sure you want to remove the "
            "<i>%1</i> cursor theme?<br />"
            "This will delete all the files installed by this theme.</qt>",
            theme->title());

577
    int answer = KMessageBox::warningContinueCancel(nullptr, question,
578
579
580
581
582
583
584
585
586
587
            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
588
    m_themeProxyModel->removeTheme(idx);
589
590
591
592
593
594
595
596

    // 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.
597
598
}

599
#include "kcmcursortheme.moc"