not quite so much needs to be delayed to the init() function
[personal-kdebase.git] / workspace / kcontrol / input / xcursor / themepage.cpp
blob25ca938d12ddd7c2b3689883ac019cfd17c411fc
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());
191 c.sync();
193 if (!applyTheme(theme))
195 KMessageBox::information(this,
196 i18n("You have to restart KDE for these changes to take effect."),
197 i18n("Cursor Settings Changed"), "CursorSettingsChanged");
200 appliedIndex = view->currentIndex();
204 void ThemePage::load()
206 // Get the name of the theme libXcursor currently uses
207 QString currentTheme = XcursorGetTheme(x11Info().display());
209 // Get the name of the theme KDE is configured to use
210 KConfig c("kcminputrc");
211 KConfigGroup cg(&c, "Mouse");
212 currentTheme = cg.readEntry("cursorTheme", currentTheme);
214 // Find the theme in the listview
215 if (!currentTheme.isEmpty())
216 appliedIndex = proxy->findIndex(currentTheme);
217 else
218 appliedIndex = proxy->defaultIndex();
220 // Disable the listview and the buttons if we're in kiosk mode
221 if (cg.isEntryImmutable("cursorTheme"))
223 view->setEnabled(false);
224 installButton->setEnabled(false);
225 removeButton->setEnabled(false);
228 const CursorTheme *theme = proxy->theme(appliedIndex);
230 if (appliedIndex.isValid())
232 // Select the current theme
233 selectRow(appliedIndex);
234 view->scrollTo(appliedIndex, QListView::PositionAtCenter);
236 // Update the preview widget as well
237 preview->setTheme(theme);
240 if (!theme || !theme->isWritable())
241 removeButton->setEnabled(false);
245 void ThemePage::defaults()
250 void ThemePage::selectRow(int row) const
252 // Create a selection that stretches across all columns
253 QModelIndex from = proxy->index(row, 0);
254 QModelIndex to = proxy->index(row, model->columnCount() - 1);
255 QItemSelection selection(from, to);
257 view->selectionModel()->select(selection, QItemSelectionModel::Select);
261 void ThemePage::currentChanged(const QModelIndex &current, const QModelIndex &previous)
263 Q_UNUSED(previous)
265 if (current.isValid())
267 const CursorTheme *theme = proxy->theme(current);
268 preview->setTheme(theme);
269 removeButton->setEnabled(theme->isWritable());
270 } else
271 preview->setTheme(NULL);
273 emit changed(appliedIndex != current);
277 void ThemePage::installClicked()
279 // Get the URL for the theme we're going to install
280 KUrl url = KUrlRequesterDialog::getUrl(QString(), this, i18n("Drag or Type Theme URL"));
282 if (url.isEmpty())
283 return;
285 QString tempFile;
286 if (!KIO::NetAccess::download(url, tempFile, this))
288 QString text;
290 if (url.isLocalFile())
291 text = i18n("Unable to find the cursor theme archive %1.",
292 url.prettyUrl());
293 else
294 text = i18n("Unable to download the cursor theme archive; "
295 "please check that the address %1 is correct.",
296 url.prettyUrl());
298 KMessageBox::sorry(this, text);
299 return;
302 if (!installThemes(tempFile))
303 KMessageBox::error(this, i18n("The file %1 does not appear to be a valid "
304 "cursor theme archive.", url.fileName()));
306 KIO::NetAccess::removeTempFile(tempFile);
310 void ThemePage::removeClicked()
312 // We don't have to check if the current index is valid, since
313 // the remove button will be disabled when there's no selection.
314 const CursorTheme *theme = model->theme(view->currentIndex());
316 // Don't let the user delete the currently configured theme
317 if (view->currentIndex() == appliedIndex) {
318 KMessageBox::sorry(this, i18n("<qt>You cannot delete the theme you are currently "
319 "using.<br />You have to switch to another theme first.</qt>"));
320 return;
323 // Get confirmation from the user
324 QString question = i18n("<qt>Are you sure you want to remove the "
325 "<i>%1</i> cursor theme?<br />"
326 "This will delete all the files installed by this theme.</qt>",
327 theme->title());
329 int answer = KMessageBox::warningContinueCancel(this, question,
330 i18n("Confirmation"), KStandardGuiItem::del());
332 if (answer != KMessageBox::Continue)
333 return;
335 // Delete the theme from the harddrive
336 KIO::del(KUrl(theme->path())); // async
338 // Remove the theme from the model
339 proxy->removeTheme(view->currentIndex());
341 // TODO:
342 // Since it's possible to substitute cursors in a system theme by adding a local
343 // theme with the same name, we shouldn't remove the theme from the list if it's
344 // still available elsewhere. We could add a
345 // bool CursorThemeModel::tryAddTheme(const QString &name), and call that, but
346 // since KIO::del() is an asynchronos operation, the theme we're deleting will be
347 // readded to the list again before KIO has removed it.
351 bool ThemePage::installThemes(const QString &file)
353 KTar archive(file);
355 if (!archive.open(QIODevice::ReadOnly))
356 return false;
358 const KArchiveDirectory *archiveDir = archive.directory();
359 QStringList themeDirs;
361 // Extract the dir names of the cursor themes in the archive, and
362 // append them to themeDirs
363 foreach(const QString &name, archiveDir->entries())
365 const KArchiveEntry *entry = archiveDir->entry(name);
366 if (entry->isDirectory() && entry->name().toLower() != "default")
368 const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(entry);
369 if (dir->entry("index.theme") && dir->entry("cursors"))
370 themeDirs << dir->name();
374 if (themeDirs.isEmpty())
375 return false;
377 // The directory we'll install the themes to
378 QString destDir = QDir::homePath() + "/.icons/";
379 KStandardDirs::makeDir(destDir); // Make sure the directory exists
381 // Process each cursor theme in the archive
382 foreach (const QString &dirName, themeDirs)
384 QDir dest(destDir + dirName);
385 if (dest.exists())
387 QString question = i18n("A theme named %1 already exists in your icon "
388 "theme folder. Do you want replace it with this one?", dirName);
390 int answer = KMessageBox::warningContinueCancel(this, question,
391 i18n("Overwrite Theme?"),
392 KStandardGuiItem::overwrite());
394 if (answer != KMessageBox::Continue)
395 continue;
397 // ### If the theme that's being replaced is the current theme, it
398 // will cause cursor inconsistencies in newly started apps.
401 // ### Should we check if a theme with the same name exists in a global theme dir?
402 // If that's the case it will effectively replace it, even though the global theme
403 // won't be deleted. Checking for this situation is easy, since the global theme
404 // will be in the listview. Maybe this should never be allowed since it might
405 // result in strange side effects (from the average users point of view). OTOH
406 // a user might want to do this 'upgrade' a global theme.
408 const KArchiveDirectory *dir = static_cast<const KArchiveDirectory*>
409 (archiveDir->entry(dirName));
410 dir->copyTo(dest.path());
411 model->addTheme(dest);
414 archive.close();
415 return true;