kcmcursortheme.cpp 20.6 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 <KNewStuff3/KNS3/DownloadDialog>
Marco Martin's avatar
Marco Martin committed
43

44
45
#include <QX11Info>
#include <QStandardItemModel>
46

47
48
#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>
49

Benjamin Port's avatar
Benjamin Port committed
50
#include "cursorthemesettings.h"
51
52
53
54
55
56
57
#include <klauncher_iface.h>

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

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

59
60
CursorThemeConfig::CursorThemeConfig(QObject *parent, const QVariantList &args)
    : KQuickAddons::ConfigModule(parent, args),
Benjamin Port's avatar
Benjamin Port committed
61
      m_settings(new CursorThemeSettings),
62
63
64
65
      m_canInstall(true),
      m_canResize(true),
      m_canConfigure(true)
{
Benjamin Port's avatar
Benjamin Port committed
66
67
    // Unfortunately doesn't generate a ctor taking the parent as parameter
    m_settings->setParent(this);
68
69
70
71
72
73
74
    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);
75
76
    qmlRegisterType<PreviewWidget>("org.kde.private.kcm_cursortheme", 1, 0, "PreviewWidget");
    qmlRegisterType<SortProxyModel>();
77
    qmlRegisterType<CursorThemeSettings>();
78
    KAboutData* aboutData = new KAboutData(QStringLiteral("kcm_cursortheme"), i18n("Cursors"),
Björn Feber's avatar
Björn Feber committed
79
        QStringLiteral("1.0"), QString(), KAboutLicense::GPL, i18n("(c) 2003-2007 Fredrik Höglund"));
80
    aboutData->addAuthor(i18n("Fredrik Höglund"));
81
    aboutData->addAuthor(i18n("Marco Martin"));
82
    setAboutData(aboutData);
83

84
    m_themeModel = new CursorThemeModel(this);
85

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

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

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

105
106
107
108
109
CursorThemeSettings *CursorThemeConfig::cursorThemeSettings() const
{
    return m_settings;
}

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
147
148
149
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();
}

150
int CursorThemeConfig::preferredSize() const
151
{
152
    return m_preferredSize;
153
154
}

155
void CursorThemeConfig::setPreferredSize(int size)
156
{
157
    if (m_preferredSize == size) {
158
159
160
        return;
    }
    m_preferredSize = size;
161
    emit preferredSizeChanged();
162
163
}

164
bool CursorThemeConfig::canConfigure() const
165
{
166
    return m_canConfigure;
167
168
}

169
170
171
172
173
bool CursorThemeConfig::downloadingFile() const
{
    return m_tempCopyJob;
}

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

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
200
201
    int row = cursorThemeIndex(m_settings->cursorTheme());
    QModelIndex selected = m_themeProxyModel->index(row, 0);
202
203
204
    int maxIconWidth = 0;
    int maxIconHeight = 0;
    if (selected.isValid()) {
205
        const CursorTheme *theme = m_themeProxyModel->theme(selected);
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
236
237
238
        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;
239
            }
240
241

            // select an item
242
243
244
245
            int size = m_preferredSize;
            int selectItem = comboBoxList.indexOf(size);

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

    // enable or disable the combobox
Benjamin Port's avatar
Benjamin Port committed
276
    if (m_settings->isImmutable("cursorSize")) {
277
278
279
        setCanResize(false);
    } else {
        setCanResize(m_sizesModel->rowCount() > 0);
280
    }
281
282
    // We need to emit a cursorSizeChanged in all case to refresh UI
    emit m_settings->cursorSizeChanged();
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
327
328
329
330
331
332
333
334
335
}

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));
    }
336
    updateSizeComboBox();
337
    emit themeApplied();
338
339
340
341
342
343
344
    return true;
#else
    Q_UNUSED(theme)
    return false;
#endif
}

345
346
347
348
349
350
351
352
353
354
355
356
357
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;
}
358

359
int CursorThemeConfig::cursorSizeFromIndex(int index)
360
{
361
    Q_ASSERT (index < m_sizesModel->rowCount() && index >= 0);
362

363
364
365
366
367
368
369
370
371
372
373
374
375
376
    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
377

378
379
void CursorThemeConfig::save()
{
Benjamin Port's avatar
Benjamin Port committed
380
    m_settings->save();
381
382
383
    m_currentTheme = m_settings->cursorTheme();
    m_currentSize = m_settings->cursorSize();
    setPreferredSize(m_currentSize);
384

385
386
387
388
389
    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)) {
390
        emit showInfoMessage(i18n("You have to restart the Plasma session for these changes to take effect."));
391
392
    }
    setNeedsSave(false);
393
394
}

395
396
397

void CursorThemeConfig::load()
{
398
399
400
401
402
    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
Benjamin Port's avatar
Benjamin Port committed
403
    QString currentTheme = m_settings->cursorTheme();
404
405

    // Disable the listview and the buttons if we're in kiosk mode
Benjamin Port's avatar
Benjamin Port committed
406
    if (m_settings->isImmutable( QStringLiteral( "cursorTheme" ))) {
407
408
409
410
411
412
413
414
415
416
          setCanConfigure(false);
          setCanInstall(false);
    }

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

    setNeedsSave(false);
}


417
418
void CursorThemeConfig::defaults()
{
419
420
    m_settings->setDefaults();
    m_preferredSize = m_settings->cursorSize();
421
422
}

423
void CursorThemeConfig::updateNeedsSave()
424
{
425
    setNeedsSave(m_settings->cursorTheme() != m_currentTheme || m_settings->cursorSize() != m_currentSize);
426
427
428
429
}

void CursorThemeConfig::getNewClicked()
{
430
    KNS3::DownloadDialog dialog("xcursor.knsrc", nullptr);
431
432
    if (dialog.exec()) {
        KNS3::Entry::List list = dialog.changedEntries();
433
        if (!list.isEmpty()) {
434
435
436
437
438
439
440
            for (const KNS3::Entry& entry : list) {
                if (entry.status() == KNS3::Entry::Deleted) {
                    for (const QString& deleted : entry.uninstalledFiles()) {
                        QVector<QStringRef> list = deleted.splitRef(QLatin1Char('/'));
                        if (list.last() == QLatin1Char('*')) {
                            list.takeLast();
                        }
441
                        QModelIndex idx = m_themeModel->findIndex(list.last().toString());
442
                        if (idx.isValid()) {
443
                            m_themeModel->removeTheme(idx);
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
                        }
                    }
                } 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;
                        }
459
                        m_themeModel->addTheme(list.join(QLatin1Char('/')));
460
461
462
                    }
                }
            }
463
464
465
466
        }
    }
}

467
void CursorThemeConfig::installThemeFromFile(const QUrl &url)
468
{
469
470
    if (url.isLocalFile()) {
        installThemeFile(url.toLocalFile());
471
472
473
        return;
    }

474
475
476
477
    if (m_tempCopyJob) {
        return;
    }

478
479
480
481
    m_tempInstallFile.reset(new QTemporaryFile());
    if (!m_tempInstallFile->open()) {
        emit showErrorMessage(i18n("Unable to create a temporary file."));
        m_tempInstallFile.reset();
482
483
484
        return;
    }

485
    m_tempCopyJob = KIO::file_copy(url,QUrl::fromLocalFile(m_tempInstallFile->fileName()),
486
                                           -1, KIO::Overwrite);
487
488
    m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true);
    emit downloadingFileChanged();
489

490
    connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) {
491
492
493
494
        if (job->error() != KJob::NoError) {
            emit showErrorMessage(i18n("Unable to download the icon theme archive: %1", job->errorText()));
            return;
        }
495

496
497
498
        installThemeFile(m_tempInstallFile->fileName());
        m_tempInstallFile.reset();
    });
499
    connect(m_tempCopyJob, &QObject::destroyed, this, &CursorThemeConfig::downloadingFileChanged);
500
501
}

502
void CursorThemeConfig::installThemeFile(const QString &path)
503
{
504
505
    KTar archive(path);
    archive.open(QIODevice::ReadOnly);
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522

    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()) {
523
524
        emit showErrorMessage(i18n("The file is not a valid icon theme archive."));
        return;
525
526
527
528
    }

    // The directory we'll install the themes to
    QString destDir = QDir::homePath() + "/.icons/";
529
530
531
    if (!QDir().mkpath(destDir)) {
        emit showErrorMessage(i18n("Failed to create 'icons' folder."));
        return;
532
    }
533
534
535
536
537
538
539
540

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

541
            int answer = KMessageBox::warningContinueCancel(nullptr, question,
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
                                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());
563
        m_themeModel->addTheme(dest);
564
565
566
    }

    archive.close();
567
568
569

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

570
    m_themeModel->refreshList();
571
572
573
574
}

void CursorThemeConfig::removeTheme(int row)
{
575
    QModelIndex idx = m_themeProxyModel->index(row, 0);
576
577
578
579
    if (!idx.isValid()) {
        return;
    }

580
    const CursorTheme *theme = m_themeProxyModel->theme(idx);
581
582

    // Don't let the user delete the currently configured theme
583
    if (theme->name() == m_currentTheme) {
584
        KMessageBox::sorry(nullptr, i18n("<qt>You cannot delete the theme you are currently "
585
586
587
588
589
590
591
592
593
594
                "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());

595
    int answer = KMessageBox::warningContinueCancel(nullptr, question,
596
597
598
599
600
601
602
603
604
605
            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
606
    m_themeProxyModel->removeTheme(idx);
607
608
609
610
611
612
613
614

    // 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.
615
616
}

617
#include "kcmcursortheme.moc"