* Copyright © 2003-2007 Fredrik Höglund <fredrik@kde.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include <config-X11.h>
#include <KConfig>
#include <KConfigGroup>
#include <KStandardDirs>
#include <KGlobalSettings>
#include <KMessageBox>
#include <KUrlRequesterDialog>
#include <KIO/Job>
#include <KIO/DeleteJob>
#include <kio/netaccess.h>
#include <KNewStuff3/KNS3/DownloadDialog>
#include <KLocalizedString>
#include <KTar>
#include <klauncher_iface.h>
#include "../../krdb/krdb.h"
#include <QWidget>
#include <QPushButton>
#include <QDir>
#include <QX11Info>
#include "themepage.h"
#include "thememodel.h"
#include "itemdelegate.h"
#include "sortproxymodel.h"
#include "cursortheme.h"
#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>
#ifdef HAVE_XFIXES
# include <X11/extensions/Xfixes.h>
#endif
ThemePage::ThemePage(QWidget *parent)
: QWidget(parent)
{
setupUi(this);
installKnsButton->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff")));
installButton->setIcon(QIcon::fromTheme(QStringLiteral("document-import")));
removeButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
model = new CursorThemeModel(this);
proxy = new SortProxyModel(this);
proxy->setSourceModel(model);
proxy->setFilterCaseSensitivity(Qt::CaseSensitive);
proxy->sort(NameColumn, Qt::AscendingOrder);
int size = style()->pixelMetric(QStyle::PM_LargeIconSize);
view->setModel(proxy);
view->setItemDelegate(new ItemDelegate(this));
view->setIconSize(QSize(size, size));
view->setSelectionMode(QAbstractItemView::SingleSelection);
connect(view->selectionModel(),
&QItemSelectionModel::selectionChanged,
this, &ThemePage::selectionChanged);
connect(sizeComboBox,
SIGNAL(currentIndexChanged(int)),
SLOT(sizeChanged()));
connect(sizeComboBox,
SIGNAL(activated(int)),
SLOT(preferredSizeChanged()));
if (!model->searchPaths().contains(QDir::homePath() + "/.icons") || !iconsIsWritable()) {
installButton->setEnabled(false);
installKnsButton->setEnabled(false);
}
connect(installKnsButton, &QAbstractButton::clicked, this, &ThemePage::getNewClicked);
connect(installButton, &QAbstractButton::clicked, this, &ThemePage::installClicked);
connect(removeButton, &QAbstractButton::clicked, this, &ThemePage::removeClicked);
}
ThemePage::~ThemePage()
{
}
bool ThemePage::iconsIsWritable() const
{
const QFileInfo icons = QFileInfo(QDir::homePath() + "/.icons");
const QFileInfo home = QFileInfo(QDir::homePath());
return ((icons.exists() && icons.isDir() && icons.isWritable()) ||
(!icons.exists() && home.isWritable()));
}
void ThemePage::updateSizeComboBox()
{
sizeComboBox->clear();
QModelIndex selected = selectedIndex();
int maxIconWidth = 0;
int maxIconHeight = 0;
if (selected.isValid())
{
const CursorTheme *theme = proxy->theme(selected);
const QList<int> sizes = theme->availableSizes();
QIcon m_icon;
if (sizes.size() > 1)
{
int i;
QList<int> comboBoxList;
QPixmap m_pixmap;
m_pixmap = theme->createIcon(0);
if (m_pixmap.width() > maxIconWidth)
maxIconWidth = m_pixmap.width();
if (m_pixmap.height() > maxIconHeight)
maxIconHeight = m_pixmap.height();
sizeComboBox->addItem(
QIcon(m_pixmap),
i18nc("@item:inlistbox size", "Resolution dependent"),
0);
comboBoxList << 0;
foreach (i, sizes)
{
m_pixmap = theme->createIcon(i);
if (m_pixmap.width() > maxIconWidth)
maxIconWidth = m_pixmap.width();
if (m_pixmap.height() > maxIconHeight)
maxIconHeight = m_pixmap.height();
sizeComboBox->addItem(QIcon(m_pixmap), QString::number(i), i);
comboBoxList << i;
};
int selectItem = comboBoxList.indexOf(preferredSize);
if (selectItem < 0)
{
is ignored. (If preferredSize would have been 0, then we
would had found it yet. As preferredSize is not 0, we won't
default to "automatic size".)*/
int j;
int distance;
int smallestDistance;
selectItem = 1;
j = comboBoxList.value(selectItem);
smallestDistance = j < preferredSize ? preferredSize - j : j - preferredSize;
for (int i = 2; i < comboBoxList.size(); ++i)
{
j = comboBoxList.value(i);
distance = j < preferredSize ? preferredSize - j : j - preferredSize;
if (distance < smallestDistance || (distance == smallestDistance && j > preferredSize))
{
smallestDistance = distance;
selectItem = i;
};
}
};
sizeComboBox->setCurrentIndex(selectItem);
};
};
sizeComboBox->setIconSize(QSize(maxIconWidth, maxIconHeight));
KConfig c(QStringLiteral("kcminputrc"));
KConfigGroup cg(&c, "Mouse");
if (cg.isEntryImmutable("cursorSize")) {
sizeComboBox->setEnabled(false);
} else {
sizeComboBox->setEnabled(sizeComboBox->count() > 0);
};
}
int ThemePage::selectedSize() const
{
if (sizeComboBox->isEnabled() && sizeComboBox->currentIndex() >= 0)
return sizeComboBox->itemData(sizeComboBox->currentIndex(), Qt::UserRole).toInt();
return 0;
}
void ThemePage::updatePreview()
{
QModelIndex selected = selectedIndex();
if (selected.isValid()) {
const CursorTheme *theme = proxy->theme(selected);
preview->setTheme(theme, selectedSize());
removeButton->setEnabled(theme->isWritable());
} else {
preview->setTheme(NULL, 0);
removeButton->setEnabled(false);
};
}
bool ThemePage::applyTheme(const CursorTheme *theme, const int size)
{
#if HAVE_XFIXES && XFIXES_MAJOR >= 2 && XCURSOR_LIB_VERSION >= 10105
if (!theme)
return false;
const bool isX11 = QX11Info::isPlatformX11();
if (isX11 && !CursorTheme::haveXfixes())
return false;
QByteArray themeName = QFile::encodeName(theme->name());
OrgKdeKLauncherInterface klauncher(QStringLiteral("org.kde.klauncher5"),
QStringLiteral("/KLauncher"),
QDBusConnection::sessionBus());
klauncher.setLaunchEnv(QStringLiteral("XCURSOR_THEME"), themeName);
if (isX11) {
runRdb(0);
}
KGlobalSettings::self()->emitChange(KGlobalSettings::CursorChanged);
if (!isX11) {
return false;
}
QStringList names;
names << QStringLiteral("left_ptr") << QStringLiteral("up_arrow") << QStringLiteral("cross") << QStringLiteral("wait")
<< QStringLiteral("left_ptr_watch") << QStringLiteral("ibeam") << QStringLiteral("size_ver") << QStringLiteral("size_hor")
<< QStringLiteral("size_bdiag") << QStringLiteral("size_fdiag") << QStringLiteral("size_all") << QStringLiteral("split_v")
<< QStringLiteral("split_h") << QStringLiteral("pointing_hand") << QStringLiteral("openhand")
<< QStringLiteral("closedhand") << QStringLiteral("forbidden") << QStringLiteral("whats_this") << QStringLiteral("copy") << QStringLiteral("move") << QStringLiteral("link");
names << QStringLiteral("X_cursor") << QStringLiteral("right_ptr") << QStringLiteral("hand1")
<< QStringLiteral("hand2") << QStringLiteral("watch") << QStringLiteral("xterm")
<< QStringLiteral("crosshair") << QStringLiteral("left_ptr_watch") << QStringLiteral("center_ptr")
<< QStringLiteral("sb_h_double_arrow") << QStringLiteral("sb_v_double_arrow") << QStringLiteral("fleur")
<< QStringLiteral("top_left_corner") << QStringLiteral("top_side") << QStringLiteral("top_right_corner")
<< QStringLiteral("right_side") << QStringLiteral("bottom_right_corner") << QStringLiteral("bottom_side")
<< QStringLiteral("bottom_left_corner") << QStringLiteral("left_side") << QStringLiteral("question_arrow")
<< QStringLiteral("pirate");
foreach (const QString &name, names)
{
XFixesChangeCursorByName(QX11Info::display(), theme->loadCursor(name, size), QFile::encodeName(name));
}
return true;
#else
Q_UNUSED(theme)
return false;
#endif
}
void ThemePage::save()
{
const CursorTheme *theme = selectedIndex().isValid() ? proxy->theme(selectedIndex()) : NULL;
const int size = selectedSize();
KConfig config(QStringLiteral("kcminputrc"));
KConfigGroup c(&config, "Mouse");
if (theme)
{
c.writeEntry("cursorTheme", theme->name());
};
c.writeEntry("cursorSize", size);
preferredSize = size;
c.sync();
if (!applyTheme(theme, size))
{
KMessageBox::information(this,
i18n("You have to restart the Plasma session for these changes to take effect."),
i18n("Cursor Settings Changed"), QStringLiteral("CursorSettingsChanged"));
}
appliedIndex = selectedIndex();
appliedSize = size;
}
void ThemePage::load()
{
view->selectionModel()->clear();
QString currentTheme;
if (QX11Info::isPlatformX11()) {
currentTheme = XcursorGetTheme(QX11Info::display());
}
KConfig c(QStringLiteral("kcminputrc"));
KConfigGroup cg(&c, "Mouse");
currentTheme = cg.readEntry("cursorTheme", currentTheme);
if (!currentTheme.isEmpty())
appliedIndex = proxy->findIndex(currentTheme);
else
appliedIndex = proxy->defaultIndex();
if (cg.isEntryImmutable("cursorTheme"))
{
view->setEnabled(false);
installButton->setEnabled(false);
removeButton->setEnabled(false);
}
int size = cg.readEntry("cursorSize", 0);
if (size <= 0)
preferredSize = 0;
else
preferredSize = size;
updateSizeComboBox();
appliedSize = size;
const CursorTheme *theme = proxy->theme(appliedIndex);
if (appliedIndex.isValid())
{
view->setCurrentIndex(appliedIndex);
view->scrollTo(appliedIndex, QListView::PositionAtCenter);
preview->setTheme(theme, size);
}
if (!theme || !theme->isWritable())
removeButton->setEnabled(false);
}
void ThemePage::defaults()
{
view->selectionModel()->clear();
QModelIndex defaultIndex = proxy->findIndex(QStringLiteral("breeze_cursors"));
view->setCurrentIndex(defaultIndex);
preferredSize = 0;
updateSizeComboBox();
}
void ThemePage::selectionChanged()
{
updateSizeComboBox();
updatePreview();
emit changed(appliedIndex != selectedIndex());
}
QModelIndex ThemePage::selectedIndex() const
{
QModelIndexList selection = view->selectionModel()->selectedIndexes();
if (!selection.isEmpty()) {
return (selection.at(0));
}
return QModelIndex();
}
void ThemePage::sizeChanged()
{
updatePreview();
emit changed(selectedSize() != appliedSize);
}
void ThemePage::preferredSizeChanged()
{
int index = sizeComboBox->currentIndex();
if (index >= 0)
preferredSize = sizeComboBox->itemData(index, Qt::UserRole).toInt();
else
preferredSize = 0;
}
void ThemePage::getNewClicked()
{
KNS3::DownloadDialog dialog(QStringLiteral("xcursor.knsrc"), this);
if (dialog.exec()) {
KNS3::Entry::List list = dialog.changedEntries();
if (list.count() > 0)
model->refreshList();
}
}
void ThemePage::installClicked()
{
QUrl url = KUrlRequesterDialog::getUrl(QUrl(), this, i18n("Drag or Type Theme URL"));
if (url.isEmpty())
return;
QString tempFile;
if (!KIO::NetAccess::download(url, tempFile, this))
{
QString text;
if (url.isLocalFile())
text = i18n("Unable to find the cursor theme archive %1.",
url.toDisplayString());
else
text = i18n("Unable to download the cursor theme archive; "
"please check that the address %1 is correct.",
url.toDisplayString());
KMessageBox::sorry(this, text);
return;
}
if (!installThemes(tempFile))
KMessageBox::error(this, i18n("The file %1 does not appear to be a valid "
"cursor theme archive.", url.fileName()));
KIO::NetAccess::removeTempFile(tempFile);
}
void ThemePage::removeClicked()
{
const CursorTheme *theme = proxy->theme(selectedIndex());
if (selectedIndex() == appliedIndex) {
KMessageBox::sorry(this, i18n("<qt>You cannot delete the theme you are currently "
"using.<br />You have to switch to another theme first.</qt>"));
return;
}
QString question = i18n("<qt>Are you sure you want to remove the "
"<i>%1</i> cursor theme?<br />"
"This will delete all the files installed by this theme.</qt>",
theme->title());
int answer = KMessageBox::warningContinueCancel(this, question,
i18n("Confirmation"), KStandardGuiItem::del());
if (answer != KMessageBox::Continue)
return;
KIO::del(QUrl::fromLocalFile(theme->path()));
proxy->removeTheme(selectedIndex());
}
bool ThemePage::installThemes(const QString &file)
{
KTar archive(file);
if (!archive.open(QIODevice::ReadOnly))
return false;
const KArchiveDirectory *archiveDir = archive.directory();
QStringList themeDirs;
foreach(const QString &name, archiveDir->entries())
{
const KArchiveEntry *entry = archiveDir->entry(name);
if (entry->isDirectory() && entry->name().toLower() != QLatin1String("default"))
{
const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(entry);
if (dir->entry(QStringLiteral("index.theme")) && dir->entry(QStringLiteral("cursors")))
themeDirs << dir->name();
}
}
if (themeDirs.isEmpty())
return false;
QString destDir = QDir::homePath() + "/.icons/";
QDir().mkpath(destDir);
foreach (const QString &dirName, themeDirs)
{
QDir dest(destDir + dirName);
if (dest.exists())
{
QString question = i18n("A theme named %1 already exists in your icon "
"theme folder. Do you want replace it with this one?", dirName);
int answer = KMessageBox::warningContinueCancel(this, question,
i18n("Overwrite Theme?"),
KStandardGuiItem::overwrite());
if (answer != KMessageBox::Continue)
continue;
}
const KArchiveDirectory *dir = static_cast<const KArchiveDirectory*>
(archiveDir->entry(dirName));
dir->copyTo(dest.path());
model->addTheme(dest);
}
archive.close();
return true;
}