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());
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
);
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
¤t
, const QModelIndex
&previous
)
265 if (current
.isValid())
267 const CursorTheme
*theme
= proxy
->theme(current
);
268 preview
->setTheme(theme
);
269 removeButton
->setEnabled(theme
->isWritable());
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"));
286 if (!KIO::NetAccess::download(url
, tempFile
, this))
290 if (url
.isLocalFile())
291 text
= i18n("Unable to find the cursor theme archive %1.",
294 text
= i18n("Unable to download the cursor theme archive; "
295 "please check that the address %1 is correct.",
298 KMessageBox::sorry(this, text
);
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>"));
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>",
329 int answer
= KMessageBox::warningContinueCancel(this, question
,
330 i18n("Confirmation"), KStandardGuiItem::del());
332 if (answer
!= KMessageBox::Continue
)
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());
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
)
355 if (!archive
.open(QIODevice::ReadOnly
))
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())
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
);
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
)
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
);