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>
24 #include <KStandardDirs>
26 #include <KGlobalSettings>
27 #include <KToolInvocation>
29 #include <KMessageBox>
30 #include <KUrlRequesterDialog>
33 #include <KIO/DeleteJob>
34 #include <KIO/NetAccess>
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
)
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()
114 int event_base
, error_base
;
115 if (XFixesQueryExtension(QX11Info::display(), &event_base
, &error_base
))
118 XFixesQueryVersion(QX11Info::display(), &major
, &minor
);
119 result
= (major
>= 2);
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
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
144 // Notify all applications that the cursor theme has changed
145 KGlobalSettings::self()->emitChange(KGlobalSettings::CursorChanged
);
147 // Reload the standard 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";
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"
167 foreach (const QString
&name
, names
)
169 QCursor cursor
= theme
->loadCursor(name
);
170 XFixesChangeCursorByName(x11Info().display(), cursor
.handle(), QFile::encodeName(name
));
181 void ThemePage::save()
183 if (appliedIndex
== view
->currentIndex() || !view
->currentIndex().isValid())
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
);
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
¤t
, const QModelIndex
&previous
)
264 if (current
.isValid())
266 const CursorTheme
*theme
= proxy
->theme(current
);
267 preview
->setTheme(theme
);
268 removeButton
->setEnabled(theme
->isWritable());
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"));
285 if (!KIO::NetAccess::download(url
, tempFile
, this))
289 if (url
.isLocalFile())
290 text
= i18n("Unable to find the cursor theme archive %1.",
293 text
= i18n("Unable to download the cursor theme archive; "
294 "please check that the address %1 is correct.",
297 KMessageBox::sorry(this, text
);
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>"));
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>",
328 int answer
= KMessageBox::warningContinueCancel(this, question
,
329 i18n("Confirmation"), KStandardGuiItem::del());
331 if (answer
!= KMessageBox::Continue
)
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());
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
)
354 if (!archive
.open(QIODevice::ReadOnly
))
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())
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
);
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
)
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
);