2
* Copyright © 2003-2007 Fredrik Höglund <fredrik@kde.org>
4
* This program is free software; you can redistribute it and/or
5
* modify it under the terms of the GNU General Public
6
* License version 2 as published by the Free Software Foundation.
8
* This program is distributed in the hope that it will be useful,
9
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
* General Public License for more details.
13
* You should have received a copy of the GNU General Public License
14
* along with this program; see the file COPYING. If not, write to
15
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16
* Boston, MA 02110-1301, USA.
19
#include <config-X11.h>
23
#include <KStandardDirs>
25
#include <KGlobalSettings>
26
#include <KToolInvocation>
27
#include <KMessageBox>
28
#include <KUrlRequesterDialog>
30
#include <KIO/DeleteJob>
31
#include <KIO/NetAccess>
32
#include <knewstuff3/downloaddialog.h>
37
#include <klauncher_iface.h>
38
#include "../../krdb/krdb.h"
41
#include <QPushButton>
45
#include "themepage.h"
46
#include "themepage.moc"
48
#include "thememodel.h"
49
#include "itemdelegate.h"
50
#include "sortproxymodel.h"
51
#include "cursortheme.h"
54
#include <X11/Xcursor/Xcursor.h>
57
# include <X11/extensions/Xfixes.h>
61
ThemePage::ThemePage(QWidget *parent)
65
installKnsButton->setIcon( KIcon("get-hot-new-stuff") );
67
model = new CursorThemeModel(this);
69
proxy = new SortProxyModel(this);
70
proxy->setSourceModel(model);
71
proxy->setFilterCaseSensitivity(Qt::CaseSensitive);
72
proxy->sort(NameColumn, Qt::AscendingOrder);
74
int size = style()->pixelMetric(QStyle::PM_LargeIconSize);
76
view->setModel(proxy);
77
view->setItemDelegate(new ItemDelegate(this));
78
view->setIconSize(QSize(size, size));
79
view->setSelectionMode( QAbstractItemView::SingleSelection );
81
// Make sure we find out about selection changes
82
connect(view->selectionModel(),
83
SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)),
84
SLOT(currentChanged(const QModelIndex &, const QModelIndex &)));
86
// Disable the install button if we can't install new themes to ~/.icons,
87
// or Xcursor isn't set up to look for cursor themes there.
88
if (!model->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable()) {
89
installButton->setEnabled(false);
90
installKnsButton->setEnabled(false);
93
connect(installKnsButton, SIGNAL(clicked()), SLOT(getNewClicked()));
94
connect(installButton, SIGNAL(clicked()), SLOT(installClicked()));
95
connect(removeButton, SIGNAL(clicked()), SLOT(removeClicked()));
99
ThemePage::~ThemePage()
104
bool ThemePage::iconsIsWritable() const
106
const QFileInfo icons = QFileInfo(QDir::homePath() + "/.icons");
107
const QFileInfo home = QFileInfo(QDir::homePath());
109
return ((icons.exists() && icons.isDir() && icons.isWritable()) ||
110
(!icons.exists() && home.isWritable()));
114
bool ThemePage::haveXfixes()
119
int event_base, error_base;
120
if (XFixesQueryExtension(QX11Info::display(), &event_base, &error_base))
123
XFixesQueryVersion(QX11Info::display(), &major, &minor);
124
result = (major >= 2);
132
bool ThemePage::applyTheme(const CursorTheme *theme)
134
// Require the Xcursor version that shipped with X11R6.9 or greater, since
135
// in previous versions the Xfixes code wasn't enabled due to a bug in the
136
// build system (freedesktop bug #975).
137
#if HAVE_XFIXES && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105
141
QByteArray themeName = QFile::encodeName(theme->name());
143
// Set up the proper launch environment for newly started apps
144
KToolInvocation::klauncher()->setLaunchEnv("XCURSOR_THEME", themeName);
146
// Update the Xcursor X resources
149
// Notify all applications that the cursor theme has changed
150
KGlobalSettings::self()->emitChange(KGlobalSettings::CursorChanged);
152
// Reload the standard cursors
156
names << "left_ptr" << "up_arrow" << "cross" << "wait"
157
<< "left_ptr_watch" << "ibeam" << "size_ver" << "size_hor"
158
<< "size_bdiag" << "size_fdiag" << "size_all" << "split_v"
159
<< "split_h" << "pointing_hand" << "openhand"
160
<< "closedhand" << "forbidden" << "whats_this";
163
names << "X_cursor" << "right_ptr" << "hand1"
164
<< "hand2" << "watch" << "xterm"
165
<< "crosshair" << "left_ptr_watch" << "center_ptr"
166
<< "sb_h_double_arrow" << "sb_v_double_arrow" << "fleur"
167
<< "top_left_corner" << "top_side" << "top_right_corner"
168
<< "right_side" << "bottom_right_corner" << "bottom_side"
169
<< "bottom_left_corner" << "left_side" << "question_arrow"
172
foreach (const QString &name, names)
174
QCursor cursor = theme->loadCursor(name);
175
XFixesChangeCursorByName(x11Info().display(), cursor.handle(), QFile::encodeName(name));
186
void ThemePage::save()
188
if (appliedIndex == view->currentIndex() || !view->currentIndex().isValid())
191
const CursorTheme *theme = proxy->theme(view->currentIndex());
193
KConfig config("kcminputrc");
194
KConfigGroup c(&config, "Mouse");
195
c.writeEntry("cursorTheme", theme->name());
198
if (!applyTheme(theme))
200
KMessageBox::information(this,
201
i18n("You have to restart KDE for these changes to take effect."),
202
i18n("Cursor Settings Changed"), "CursorSettingsChanged");
205
appliedIndex = view->currentIndex();
209
void ThemePage::load()
211
view->selectionModel()->clear();
212
// Get the name of the theme libXcursor currently uses
213
QString currentTheme = XcursorGetTheme(x11Info().display());
215
// Get the name of the theme KDE is configured to use
216
KConfig c("kcminputrc");
217
KConfigGroup cg(&c, "Mouse");
218
currentTheme = cg.readEntry("cursorTheme", currentTheme);
220
// Find the theme in the listview
221
if (!currentTheme.isEmpty())
222
appliedIndex = proxy->findIndex(currentTheme);
224
appliedIndex = proxy->defaultIndex();
226
// Disable the listview and the buttons if we're in kiosk mode
227
if (cg.isEntryImmutable("cursorTheme"))
229
view->setEnabled(false);
230
installButton->setEnabled(false);
231
removeButton->setEnabled(false);
234
const CursorTheme *theme = proxy->theme(appliedIndex);
236
if (appliedIndex.isValid())
238
// Select the current theme
239
selectRow(appliedIndex);
240
view->scrollTo(appliedIndex, QListView::PositionAtCenter);
242
// Update the preview widget as well
243
preview->setTheme(theme);
246
if (!theme || !theme->isWritable())
247
removeButton->setEnabled(false);
251
void ThemePage::defaults()
253
view->selectionModel()->clear();
254
QModelIndex defaultIndex = proxy->findIndex("Oxygen_Black");
255
view->setCurrentIndex(defaultIndex);
259
void ThemePage::selectRow(int row) const
261
// Create a selection that stretches across all columns
262
QModelIndex from = proxy->index(row, 0);
263
QModelIndex to = proxy->index(row, model->columnCount() - 1);
264
QItemSelection selection(from, to);
266
view->selectionModel()->select(selection, QItemSelectionModel::Select);
270
void ThemePage::currentChanged(const QModelIndex ¤t, const QModelIndex &previous)
274
if (current.isValid())
276
const CursorTheme *theme = proxy->theme(current);
277
preview->setTheme(theme);
278
removeButton->setEnabled(theme->isWritable());
280
preview->setTheme(NULL);
282
emit changed(appliedIndex != current);
285
void ThemePage::getNewClicked()
287
KNS3::DownloadDialog dialog("xcursor.knsrc", this);
289
KNS3::Entry::List list = dialog.changedEntries();
290
if (list.count() > 0)
291
model->refreshList();
295
void ThemePage::installClicked()
297
// Get the URL for the theme we're going to install
298
KUrl url = KUrlRequesterDialog::getUrl(QString(), this, i18n("Drag or Type Theme URL"));
304
if (!KIO::NetAccess::download(url, tempFile, this))
308
if (url.isLocalFile())
309
text = i18n("Unable to find the cursor theme archive %1.",
312
text = i18n("Unable to download the cursor theme archive; "
313
"please check that the address %1 is correct.",
316
KMessageBox::sorry(this, text);
320
if (!installThemes(tempFile))
321
KMessageBox::error(this, i18n("The file %1 does not appear to be a valid "
322
"cursor theme archive.", url.fileName()));
324
KIO::NetAccess::removeTempFile(tempFile);
328
void ThemePage::removeClicked()
330
// We don't have to check if the current index is valid, since
331
// the remove button will be disabled when there's no selection.
332
const CursorTheme *theme = proxy->theme(view->currentIndex());
334
// Don't let the user delete the currently configured theme
335
if (view->currentIndex() == appliedIndex) {
336
KMessageBox::sorry(this, i18n("<qt>You cannot delete the theme you are currently "
337
"using.<br />You have to switch to another theme first.</qt>"));
341
// Get confirmation from the user
342
QString question = i18n("<qt>Are you sure you want to remove the "
343
"<i>%1</i> cursor theme?<br />"
344
"This will delete all the files installed by this theme.</qt>",
347
int answer = KMessageBox::warningContinueCancel(this, question,
348
i18n("Confirmation"), KStandardGuiItem::del());
350
if (answer != KMessageBox::Continue)
353
// Delete the theme from the harddrive
354
KIO::del(KUrl(theme->path())); // async
356
// Remove the theme from the model
357
proxy->removeTheme(view->currentIndex());
360
// Since it's possible to substitute cursors in a system theme by adding a local
361
// theme with the same name, we shouldn't remove the theme from the list if it's
362
// still available elsewhere. We could add a
363
// bool CursorThemeModel::tryAddTheme(const QString &name), and call that, but
364
// since KIO::del() is an asynchronos operation, the theme we're deleting will be
365
// readded to the list again before KIO has removed it.
369
bool ThemePage::installThemes(const QString &file)
373
if (!archive.open(QIODevice::ReadOnly))
376
const KArchiveDirectory *archiveDir = archive.directory();
377
QStringList themeDirs;
379
// Extract the dir names of the cursor themes in the archive, and
380
// append them to themeDirs
381
foreach(const QString &name, archiveDir->entries())
383
const KArchiveEntry *entry = archiveDir->entry(name);
384
if (entry->isDirectory() && entry->name().toLower() != "default")
386
const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(entry);
387
if (dir->entry("index.theme") && dir->entry("cursors"))
388
themeDirs << dir->name();
392
if (themeDirs.isEmpty())
395
// The directory we'll install the themes to
396
QString destDir = QDir::homePath() + "/.icons/";
397
KStandardDirs::makeDir(destDir); // Make sure the directory exists
399
// Process each cursor theme in the archive
400
foreach (const QString &dirName, themeDirs)
402
QDir dest(destDir + dirName);
405
QString question = i18n("A theme named %1 already exists in your icon "
406
"theme folder. Do you want replace it with this one?", dirName);
408
int answer = KMessageBox::warningContinueCancel(this, question,
409
i18n("Overwrite Theme?"),
410
KStandardGuiItem::overwrite());
412
if (answer != KMessageBox::Continue)
415
// ### If the theme that's being replaced is the current theme, it
416
// will cause cursor inconsistencies in newly started apps.
419
// ### Should we check if a theme with the same name exists in a global theme dir?
420
// If that's the case it will effectively replace it, even though the global theme
421
// won't be deleted. Checking for this situation is easy, since the global theme
422
// will be in the listview. Maybe this should never be allowed since it might
423
// result in strange side effects (from the average users point of view). OTOH
424
// a user might want to do this 'upgrade' a global theme.
426
const KArchiveDirectory *dir = static_cast<const KArchiveDirectory*>
427
(archiveDir->entry(dirName));
428
dir->copyTo(dest.path());
429
model->addTheme(dest);