Make a branch to make krunner Good Enough For Aaron™.
[kdebase/uwolfer.git] / workspace / kcontrol / input / xcursor / themepage.cpp
blobc25ae202d143ea63b72bee20ec7cd7af3eeabcc5
1 /*
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>
21 #include <KConfig>
22 #include <KLocale>
23 #include <KAboutData>
24 #include <KStandardDirs>
26 #include <KGlobalSettings>
27 #include <KToolInvocation>
28 #include <KDialog>
29 #include <KMessageBox>
30 #include <KUrlRequesterDialog>
32 #include <KIO/Job>
33 #include <KIO/DeleteJob>
34 #include <KIO/NetAccess>
35 #include <KTar>
37 #include <klauncher_iface.h>
38 #include "../../krdb/krdb.h"
40 #include <QWidget>
41 #include <QPushButton>
42 #include <QDir>
43 #include <QX11Info>
45 #include "themepage.h"
46 #include "themepage.moc"
48 #include "thememodel.h"
49 #include "itemdelegate.h"
50 #include "sortproxymodel.h"
51 #include "cursortheme.h"
53 #include <X11/Xlib.h>
54 #include <X11/Xcursor/Xcursor.h>
56 #ifdef HAVE_XFIXES
57 # include <X11/extensions/Xfixes.h>
58 #endif
61 ThemePage::ThemePage(QWidget *parent)
62 : QWidget(parent)
64 setupUi(this);
66 model = new CursorThemeModel(this);
68 proxy = new SortProxyModel(this);
69 proxy->setSourceModel(model);
70 proxy->setFilterCaseSensitivity(Qt::CaseSensitive);
71 proxy->sort(NameColumn, Qt::AscendingOrder);
73 int size = style()->pixelMetric(QStyle::PM_LargeIconSize);
75 view->setModel(proxy);
76 view->setItemDelegate(new ItemDelegate(this));
77 view->setIconSize(QSize(size, size));
79 // Make sure we find out about selection changes
80 connect(view->selectionModel(),
81 SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)),
82 SLOT(currentChanged(const QModelIndex &, const QModelIndex &)));
84 // Disable the install button if we can't install new themes to ~/.icons,
85 // or Xcursor isn't set up to look for cursor themes there.
86 if (!model->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable())
87 installButton->setEnabled(false);
89 connect(installButton, SIGNAL(clicked()), SLOT(installClicked()));
90 connect(removeButton, SIGNAL(clicked()), SLOT(removeClicked()));
94 ThemePage::~ThemePage()
99 bool ThemePage::iconsIsWritable() const
101 const QFileInfo icons = QFileInfo(QDir::homePath() + "/.icons");
102 const QFileInfo home = QFileInfo(QDir::homePath());
104 return ((icons.exists() && icons.isDir() && icons.isWritable()) ||
105 (!icons.exists() && home.isWritable()));
109 bool ThemePage::haveXfixes()
111 bool result = false;
113 #ifdef HAVE_XFIXES
114 int event_base, error_base;
115 if (XFixesQueryExtension(QX11Info::display(), &event_base, &error_base))
117 int major, minor;
118 XFixesQueryVersion(QX11Info::display(), &major, &minor);
119 result = (major >= 2);
121 #endif
123 return result;
127 bool ThemePage::applyTheme(const CursorTheme *theme)
129 // Require the Xcursor version that shipped with X11R6.9 or greater, since
130 // in previous versions the Xfixes code wasn't enabled due to a bug in the
131 // build system (freedesktop bug #975).
132 #if HAVE_XFIXES && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105
133 if (!haveXfixes())
134 return false;
136 QByteArray themeName = QFile::encodeName(theme->name());
138 // Set up the proper launch environment for newly started apps
139 KToolInvocation::klauncher()->setLaunchEnv("XCURSOR_THEME", themeName);
141 // Update the Xcursor X resources
142 runRdb(0);
144 // Notify all applications that the cursor theme has changed
145 KGlobalSettings::self()->emitChange(KGlobalSettings::CursorChanged);
147 // Reload the standard cursors
148 QStringList names;
150 // Qt cursors
151 names << "left_ptr" << "up_arrow" << "cross" << "wait"
152 << "left_ptr_watch" << "ibeam" << "size_ver" << "size_hor"
153 << "size_bdiag" << "size_fdiag" << "size_all" << "split_v"
154 << "split_h" << "pointing_hand" << "openhand"
155 << "closedhand" << "forbidden" << "whats_this";
157 // X core cursors
158 names << "X_cursor" << "right_ptr" << "hand1"
159 << "hand2" << "watch" << "xterm"
160 << "crosshair" << "left_ptr_watch" << "center_ptr"
161 << "sb_h_double_arrow" << "sb_v_double_arrow" << "fleur"
162 << "top_left_corner" << "top_side" << "top_right_corner"
163 << "right_side" << "bottom_right_corner" << "bottom_side"
164 << "bottom_left_corner" << "left_side" << "question_arrow"
165 << "pirate";
167 foreach (const QString &name, names)
169 QCursor cursor = theme->loadCursor(name);
170 XFixesChangeCursorByName(x11Info().display(), cursor.handle(), QFile::encodeName(name));
173 return true;
174 #else
175 Q_UNUSED(theme)
176 return false;
177 #endif
181 void ThemePage::save()
183 if (appliedIndex == view->currentIndex() || !view->currentIndex().isValid())
184 return;
186 const CursorTheme *theme = proxy->theme(view->currentIndex());
188 KConfig config("kcminputrc");
189 KConfigGroup c(&config, "Mouse");
190 c.writeEntry("cursorTheme", theme->name());
192 if (!applyTheme(theme))
194 KMessageBox::information(this,
195 i18n("You have to restart KDE for these changes to take effect."),
196 i18n("Cursor Settings Changed"), "CursorSettingsChanged");
199 appliedIndex = view->currentIndex();
203 void ThemePage::load()
205 // Get the name of the theme libXcursor currently uses
206 QString currentTheme = XcursorGetTheme(x11Info().display());
208 // Get the name of the theme KDE is configured to use
209 KConfig c("kcminputrc");
210 KConfigGroup cg(&c, "Mouse");
211 currentTheme = cg.readEntry("cursorTheme", currentTheme);
213 // Find the theme in the listview
214 if (!currentTheme.isEmpty())
215 appliedIndex = proxy->findIndex(currentTheme);
216 else
217 appliedIndex = proxy->defaultIndex();
219 // Disable the listview and the buttons if we're in kiosk mode
220 if (cg.isEntryImmutable("cursorTheme"))
222 view->setEnabled(false);
223 installButton->setEnabled(false);
224 removeButton->setEnabled(false);
227 const CursorTheme *theme = proxy->theme(appliedIndex);
229 if (appliedIndex.isValid())
231 // Select the current theme
232 selectRow(appliedIndex);
233 view->scrollTo(appliedIndex, QListView::PositionAtCenter);
235 // Update the preview widget as well
236 preview->setTheme(theme);
239 if (!theme || !theme->isWritable())
240 removeButton->setEnabled(false);
244 void ThemePage::defaults()
249 void ThemePage::selectRow(int row) const
251 // Create a selection that stretches across all columns
252 QModelIndex from = proxy->index(row, 0);
253 QModelIndex to = proxy->index(row, model->columnCount() - 1);
254 QItemSelection selection(from, to);
256 view->selectionModel()->select(selection, QItemSelectionModel::Select);
260 void ThemePage::currentChanged(const QModelIndex &current, const QModelIndex &previous)
262 Q_UNUSED(previous)
264 if (current.isValid())
266 const CursorTheme *theme = proxy->theme(current);
267 preview->setTheme(theme);
268 removeButton->setEnabled(theme->isWritable());
269 } else
270 preview->setTheme(NULL);
272 emit changed(appliedIndex != current);
276 void ThemePage::installClicked()
278 // Get the URL for the theme we're going to install
279 KUrl url = KUrlRequesterDialog::getUrl(QString(), this, i18n("Drag or Type Theme URL"));
281 if (url.isEmpty())
282 return;
284 QString tempFile;
285 if (!KIO::NetAccess::download(url, tempFile, this))
287 QString text;
289 if (url.isLocalFile())
290 text = i18n("Unable to find the cursor theme archive %1.",
291 url.prettyUrl());
292 else
293 text = i18n("Unable to download the cursor theme archive; "
294 "please check that the address %1 is correct.",
295 url.prettyUrl());
297 KMessageBox::sorry(this, text);
298 return;
301 if (!installThemes(tempFile))
302 KMessageBox::error(this, i18n("The file %1 does not appear to be a valid "
303 "cursor theme archive.", url.fileName()));
305 KIO::NetAccess::removeTempFile(tempFile);
309 void ThemePage::removeClicked()
311 // We don't have to check if the current index is valid, since
312 // the remove button will be disabled when there's no selection.
313 const CursorTheme *theme = model->theme(view->currentIndex());
315 // Don't let the user delete the currently configured theme
316 if (view->currentIndex() == appliedIndex) {
317 KMessageBox::sorry(this, i18n("<qt>You cannot delete the theme you are currently "
318 "using.<br />You have to switch to another theme first.</qt>"));
319 return;
322 // Get confirmation from the user
323 QString question = i18n("<qt>Are you sure you want to remove the "
324 "<i>%1</i> cursor theme?<br />"
325 "This will delete all the files installed by this theme.</qt>",
326 theme->title());
328 int answer = KMessageBox::warningContinueCancel(this, question,
329 i18n("Confirmation"), KStandardGuiItem::del());
331 if (answer != KMessageBox::Continue)
332 return;
334 // Delete the theme from the harddrive
335 KIO::del(KUrl(theme->path())); // async
337 // Remove the theme from the model
338 proxy->removeTheme(view->currentIndex());
340 // TODO:
341 // Since it's possible to substitute cursors in a system theme by adding a local
342 // theme with the same name, we shouldn't remove the theme from the list if it's
343 // still available elsewhere. We could add a
344 // bool CursorThemeModel::tryAddTheme(const QString &name), and call that, but
345 // since KIO::del() is an asynchronos operation, the theme we're deleting will be
346 // readded to the list again before KIO has removed it.
350 bool ThemePage::installThemes(const QString &file)
352 KTar archive(file);
354 if (!archive.open(QIODevice::ReadOnly))
355 return false;
357 const KArchiveDirectory *archiveDir = archive.directory();
358 QStringList themeDirs;
360 // Extract the dir names of the cursor themes in the archive, and
361 // append them to themeDirs
362 foreach(const QString &name, archiveDir->entries())
364 const KArchiveEntry *entry = archiveDir->entry(name);
365 if (entry->isDirectory() && entry->name().toLower() != "default")
367 const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(entry);
368 if (dir->entry("index.theme") && dir->entry("cursors"))
369 themeDirs << dir->name();
373 if (themeDirs.isEmpty())
374 return false;
376 // The directory we'll install the themes to
377 QString destDir = QDir::homePath() + "/.icons/";
378 KStandardDirs::makeDir(destDir); // Make sure the directory exists
380 // Process each cursor theme in the archive
381 foreach (const QString &dirName, themeDirs)
383 QDir dest(destDir + dirName);
384 if (dest.exists())
386 QString question = i18n("A theme named %1 already exists in your icon "
387 "theme folder. Do you want replace it with this one?", dirName);
389 int answer = KMessageBox::warningContinueCancel(this, question,
390 i18n("Overwrite Theme?"),
391 KStandardGuiItem::overwrite());
393 if (answer != KMessageBox::Continue)
394 continue;
396 // ### If the theme that's being replaced is the current theme, it
397 // will cause cursor inconsistencies in newly started apps.
400 // ### Should we check if a theme with the same name exists in a global theme dir?
401 // If that's the case it will effectively replace it, even though the global theme
402 // won't be deleted. Checking for this situation is easy, since the global theme
403 // will be in the listview. Maybe this should never be allowed since it might
404 // result in strange side effects (from the average users point of view). OTOH
405 // a user might want to do this 'upgrade' a global theme.
407 const KArchiveDirectory *dir = static_cast<const KArchiveDirectory*>
408 (archiveDir->entry(dirName));
409 dir->copyTo(dest.path());
410 model->addTheme(dest);
413 archive.close();
414 return true;