File indexing completed on 2024-05-12 05:56:49

0001 /*
0002   SPDX-FileCopyrightText: 2008-2009 Eike Hein <hein@kde.org>
0003 
0004   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "appearancesettings.h"
0008 #include "settings.h"
0009 #include "skinlistdelegate.h"
0010 
0011 #include <KIO/CopyJob>
0012 #include <KIO/DeleteJob>
0013 #include <KIO/ListJob>
0014 #include <KJobUiDelegate>
0015 #include <KLocalizedString>
0016 #include <KMessageBox>
0017 #include <KTar>
0018 
0019 #include <QDir>
0020 #include <QDirIterator>
0021 #include <QFile>
0022 #include <QFileDialog>
0023 #include <QPointer>
0024 #include <QStandardItemModel>
0025 
0026 #include <unistd.h>
0027 
0028 AppearanceSettings::AppearanceSettings(QWidget *parent)
0029     : QWidget(parent)
0030 {
0031     setupUi(this);
0032 
0033     kcfg_Skin->hide();
0034     kcfg_SkinInstalledWithKns->hide();
0035 
0036     m_skins = new QStandardItemModel(this);
0037 
0038     m_skinListDelegate = new SkinListDelegate(this);
0039 
0040     skinList->setModel(m_skins);
0041     skinList->setItemDelegate(m_skinListDelegate);
0042 
0043     connect(skinList->selectionModel(), &QItemSelectionModel::currentChanged, this, &AppearanceSettings::updateSkinSetting);
0044     connect(skinList->selectionModel(), &QItemSelectionModel::currentChanged, this, &AppearanceSettings::updateRemoveSkinButton);
0045     connect(installButton, SIGNAL(clicked()), this, SLOT(installSkin()));
0046     connect(removeButton, &QAbstractButton::clicked, this, &AppearanceSettings::removeSelectedSkin);
0047 
0048     installButton->setIcon(QIcon::fromTheme(QStringLiteral("folder")));
0049     removeButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
0050     ghnsButton->setConfigFile(QStringLiteral("yakuake.knsrc"));
0051 
0052     connect(ghnsButton, &KNSWidgets::Button::dialogFinished, this, &AppearanceSettings::knsDialogFinished);
0053 
0054     m_selectedSkinId = Settings::skin();
0055 
0056     // Get all local skin directories.
0057     // One for manually installed skins, one for skins installed
0058     // through KNS3.
0059     m_localSkinsDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/yakuake/skins/");
0060     m_knsSkinDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/yakuake/kns_skins/");
0061 
0062     populateSkinList();
0063 }
0064 
0065 AppearanceSettings::~AppearanceSettings()
0066 {
0067 }
0068 
0069 void AppearanceSettings::showEvent(QShowEvent *event)
0070 {
0071     populateSkinList();
0072 
0073     if (skinList->currentIndex().isValid())
0074         skinList->scrollTo(skinList->currentIndex());
0075 
0076     QWidget::showEvent(event);
0077 }
0078 
0079 void AppearanceSettings::populateSkinList()
0080 {
0081     m_skins->clear();
0082 
0083     QStringList allSkinLocations;
0084     allSkinLocations << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("/yakuake/skins/"), QStandardPaths::LocateDirectory);
0085     allSkinLocations << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("/yakuake/kns_skins/"), QStandardPaths::LocateDirectory);
0086 
0087     for (const QString &skinLocation : std::as_const(allSkinLocations)) {
0088         populateSkinList(skinLocation);
0089     }
0090 
0091     m_skins->sort(0);
0092 
0093     updateRemoveSkinButton();
0094 }
0095 
0096 void AppearanceSettings::populateSkinList(const QString &installLocation)
0097 {
0098     QDirIterator it(installLocation, QDir::Dirs | QDir::NoDotAndDotDot);
0099 
0100     while (it.hasNext()) {
0101         const QDir &skinDir(it.next());
0102 
0103         if (skinDir.exists(QStringLiteral("title.skin")) && skinDir.exists(QStringLiteral("tabs.skin"))) {
0104             QStandardItem *skin = createSkinItem(skinDir.absolutePath());
0105 
0106             if (!skin)
0107                 continue;
0108 
0109             m_skins->appendRow(skin);
0110 
0111             if (skin->data(SkinId).toString() == m_selectedSkinId)
0112                 skinList->setCurrentIndex(skin->index());
0113         }
0114     }
0115 }
0116 
0117 QStandardItem *AppearanceSettings::createSkinItem(const QString &skinDir)
0118 {
0119     QString skinId = skinDir.section(QLatin1Char('/'), -1, -1);
0120     QString titleName, tabName, skinName;
0121     QString titleAuthor, tabAuthor, skinAuthor;
0122     QString titleIcon, tabIcon;
0123     QIcon skinIcon;
0124 
0125     // Check if the skin dir starts with the path where all
0126     // KNS3 skins are found in.
0127     bool isKnsSkin = skinDir.startsWith(m_knsSkinDir);
0128 
0129     KConfig titleConfig(skinDir + QStringLiteral("/title.skin"), KConfig::SimpleConfig);
0130     KConfigGroup titleDescription = titleConfig.group(QStringLiteral("Description"));
0131 
0132     KConfig tabConfig(skinDir + QStringLiteral("/tabs.skin"), KConfig::SimpleConfig);
0133     KConfigGroup tabDescription = tabConfig.group(QStringLiteral("Description"));
0134 
0135     titleName = titleDescription.readEntry("Skin", "");
0136     titleAuthor = titleDescription.readEntry("Author", "");
0137     titleIcon = skinDir + titleDescription.readEntry("Icon", "");
0138 
0139     tabName = tabDescription.readEntry("Skin", "");
0140     tabAuthor = tabDescription.readEntry("Author", "");
0141     tabIcon = skinDir + tabDescription.readEntry("Icon", "");
0142 
0143     skinName = titleName.isEmpty() ? tabName : titleName;
0144     skinAuthor = titleAuthor.isEmpty() ? tabAuthor : titleAuthor;
0145     titleIcon.isEmpty() ? skinIcon.addPixmap(tabIcon) : skinIcon.addPixmap(titleIcon);
0146 
0147     if (skinName.isEmpty() || skinAuthor.isEmpty())
0148         skinName = skinId;
0149 
0150     if (skinAuthor.isEmpty())
0151         skinAuthor = xi18nc("@item:inlistbox Unknown skin author", "Unknown");
0152 
0153     QStandardItem *skin = new QStandardItem(skinName);
0154 
0155     skin->setData(skinId, SkinId);
0156     skin->setData(skinDir, SkinDir);
0157     skin->setData(skinName, SkinName);
0158     skin->setData(skinAuthor, SkinAuthor);
0159     skin->setData(skinIcon, SkinIcon);
0160     skin->setData(isKnsSkin, SkinInstalledWithKns);
0161 
0162     return skin;
0163 }
0164 
0165 void AppearanceSettings::updateSkinSetting()
0166 {
0167     QString skinId = skinList->currentIndex().data(SkinId).toString();
0168 
0169     if (!skinId.isEmpty()) {
0170         m_selectedSkinId = skinId;
0171         kcfg_Skin->setText(skinId);
0172         kcfg_SkinInstalledWithKns->setChecked(skinList->currentIndex().data(SkinInstalledWithKns).toBool());
0173     }
0174 }
0175 
0176 void AppearanceSettings::resetSelection()
0177 {
0178     m_selectedSkinId = Settings::skin();
0179 
0180     QModelIndexList skins = m_skins->match(m_skins->index(0, 0), SkinId, Settings::skin(), 1, Qt::MatchExactly | Qt::MatchWrap);
0181 
0182     if (skins.count() > 0)
0183         skinList->setCurrentIndex(skins.at(0));
0184 }
0185 
0186 void AppearanceSettings::installSkin()
0187 {
0188     QStringList mimeTypes;
0189     mimeTypes << QStringLiteral("application/x-compressed-tar");
0190     mimeTypes << QStringLiteral("application/x-xz-compressed-tar");
0191     mimeTypes << QStringLiteral("application/x-bzip-compressed-tar");
0192     mimeTypes << QStringLiteral("application/zip");
0193     mimeTypes << QStringLiteral("application/x-tar");
0194 
0195     QFileDialog fileDialog(parentWidget());
0196     fileDialog.setWindowTitle(i18nc("@title:window", "Select the skin archive to install"));
0197     fileDialog.setMimeTypeFilters(mimeTypes);
0198     fileDialog.setFileMode(QFileDialog::ExistingFile);
0199 
0200     QUrl skinUrl;
0201     if (fileDialog.exec() && !fileDialog.selectedUrls().isEmpty())
0202         skinUrl = fileDialog.selectedUrls().at(0);
0203     else
0204         return;
0205 
0206     m_installSkinFile.open();
0207 
0208     KIO::CopyJob *job = KIO::copy(skinUrl, QUrl::fromLocalFile(m_installSkinFile.fileName()), KIO::JobFlag::HideProgressInfo | KIO::JobFlag::Overwrite);
0209     connect(job, &KIO::CopyJob::result, [=, this](KJob *job) {
0210         if (job->error()) {
0211             job->uiDelegate()->showErrorMessage();
0212             job->kill();
0213 
0214             cleanupAfterInstall();
0215 
0216             return;
0217         }
0218 
0219         installSkin(static_cast<KIO::CopyJob *>(job)->destUrl());
0220     });
0221 }
0222 
0223 void AppearanceSettings::installSkin(const QUrl &skinUrl)
0224 {
0225     QUrl skinArchiveUrl = QUrl(skinUrl);
0226     skinArchiveUrl.setScheme(QStringLiteral("tar"));
0227 
0228     KIO::ListJob *job = KIO::listRecursive(skinArchiveUrl, KIO::HideProgressInfo, KIO::ListJob::ListFlags{});
0229     connect(job, &KIO::ListJob::entries, [=, this](KIO::Job * /* job */, const KIO::UDSEntryList &list) {
0230         if (list.isEmpty())
0231             return;
0232 
0233         QListIterator<KIO::UDSEntry> i(list);
0234         while (i.hasNext())
0235             m_installSkinFileList.append(i.next().stringValue(KIO::UDSEntry::UDS_NAME));
0236     });
0237 
0238     connect(job, &KIO::ListJob::result, this, [=, this](KJob *job) {
0239         if (!job->error())
0240             checkForExistingSkin();
0241         else
0242             failInstall(xi18nc("@info", "Unable to list the skin archive contents.") + QStringLiteral("\n\n") + job->errorString());
0243     });
0244 }
0245 
0246 bool AppearanceSettings::validateSkin(const QString &skinId, bool kns)
0247 {
0248     QString dir = kns ? QStringLiteral("kns_skins/") : QStringLiteral("skins/");
0249 
0250     QString titlePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, dir + skinId + QStringLiteral("/title.skin"));
0251     QString tabsPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, dir + skinId + QStringLiteral("/tabs.skin"));
0252 
0253     return !titlePath.isEmpty() && !tabsPath.isEmpty();
0254 }
0255 
0256 void AppearanceSettings::checkForExistingSkin()
0257 {
0258     m_installSkinId = m_installSkinFileList.at(0);
0259 
0260     const QModelIndexList skins = m_skins->match(m_skins->index(0, 0), SkinId, m_installSkinId, 1, Qt::MatchExactly | Qt::MatchWrap);
0261 
0262     int exists = skins.count();
0263 
0264     for (const QModelIndex &skin : skins) {
0265         if (m_skins->item(skin.row())->data(SkinInstalledWithKns).toBool())
0266             --exists;
0267     }
0268 
0269     if (exists > 0) {
0270         QString skinDir = skins.at(0).data(SkinDir).toString();
0271         QFile skin(skinDir + QStringLiteral("titles.skin"));
0272 
0273         if (!skin.open(QIODevice::ReadWrite)) {
0274             failInstall(xi18nc("@info", "This skin appears to be already installed and you lack the required permissions to overwrite it."));
0275         } else {
0276             skin.close();
0277 
0278             int remove = KMessageBox::warningContinueCancel(parentWidget(),
0279                                                             xi18nc("@info", "This skin appears to be already installed. Do you want to overwrite it?"),
0280                                                             xi18nc("@title:window", "Skin Already Exists"),
0281                                                             KGuiItem(xi18nc("@action:button", "Reinstall Skin")));
0282 
0283             if (remove == KMessageBox::Continue)
0284                 removeSkin(skinDir, [this]() {
0285                     installSkinArchive();
0286                 });
0287             else
0288                 cleanupAfterInstall();
0289         }
0290     } else
0291         installSkinArchive();
0292 }
0293 
0294 void AppearanceSettings::removeSkin(const QString &skinDir, std::function<void()> successCallback)
0295 {
0296     KIO::DeleteJob *job = KIO::del(QUrl::fromLocalFile(skinDir), KIO::HideProgressInfo);
0297     connect(job, &KIO::DeleteJob::result, this, [=, this](KJob *deleteJob) {
0298         if (deleteJob->error()) {
0299             KMessageBox::error(parentWidget(), deleteJob->errorString(), xi18nc("@title:Window", "Could Not Delete Skin"));
0300         } else if (successCallback) {
0301             successCallback();
0302         }
0303     });
0304 }
0305 
0306 void AppearanceSettings::installSkinArchive()
0307 {
0308     KTar skinArchive(m_installSkinFile.fileName());
0309 
0310     if (skinArchive.open(QIODevice::ReadOnly)) {
0311         const KArchiveDirectory *skinDir = skinArchive.directory();
0312         skinDir->copyTo(m_localSkinsDir);
0313         skinArchive.close();
0314 
0315         if (validateSkin(m_installSkinId, false)) {
0316             populateSkinList();
0317 
0318             if (Settings::skin() == m_installSkinId)
0319                 Q_EMIT settingsChanged();
0320 
0321             cleanupAfterInstall();
0322         } else {
0323             removeSkin(m_localSkinsDir + m_installSkinId);
0324             failInstall(xi18nc("@info", "Unable to locate required files in the skin archive.<nl/><nl/>The archive appears to be invalid."));
0325         }
0326     } else
0327         failInstall(xi18nc("@info", "The skin archive file could not be opened."));
0328 }
0329 
0330 void AppearanceSettings::failInstall(const QString &error)
0331 {
0332     KMessageBox::error(parentWidget(), error, xi18nc("@title:window", "Cannot Install Skin"));
0333 
0334     cleanupAfterInstall();
0335 }
0336 
0337 void AppearanceSettings::cleanupAfterInstall()
0338 {
0339     m_installSkinId.clear();
0340     m_installSkinFileList.clear();
0341 
0342     if (m_installSkinFile.exists()) {
0343         m_installSkinFile.close();
0344         m_installSkinFile.remove();
0345     }
0346 }
0347 
0348 void AppearanceSettings::updateRemoveSkinButton()
0349 {
0350     if (m_skins->rowCount() <= 1) {
0351         removeButton->setEnabled(false);
0352         return;
0353     }
0354 
0355     const QString skinDir = skinList->currentIndex().data(SkinDir).toString();
0356     bool enabled = false;
0357     if (!skinDir.isEmpty()) {
0358         enabled = QFileInfo(skinDir + QStringLiteral("/title.skin")).isWritable();
0359     }
0360     removeButton->setEnabled(enabled);
0361 }
0362 
0363 void AppearanceSettings::removeSelectedSkin()
0364 {
0365     if (m_skins->rowCount() <= 1)
0366         return;
0367 
0368     QString skinDir = skinList->currentIndex().data(SkinDir).toString();
0369     QString skinName = skinList->currentIndex().data(SkinName).toString();
0370     QString skinAuthor = skinList->currentIndex().data(SkinAuthor).toString();
0371 
0372     if (skinDir.isEmpty())
0373         return;
0374 
0375     int remove = KMessageBox::warningContinueCancel(parentWidget(),
0376                                                     xi18nc("@info", "Do you want to remove \"%1\" by %2?", skinName, skinAuthor),
0377                                                     xi18nc("@title:window", "Remove Skin"),
0378                                                     KStandardGuiItem::del());
0379 
0380     if (remove == KMessageBox::Continue)
0381         removeSkin(skinDir, [=, this]() {
0382             QString skinId = skinList->currentIndex().data(SkinId).toString();
0383             if (skinId == Settings::skin()) {
0384                 Settings::setSkin(QStringLiteral("default"));
0385                 Settings::setSkinInstalledWithKns(false);
0386                 Settings::self()->save();
0387                 Q_EMIT settingsChanged();
0388             }
0389 
0390             resetSelection();
0391             populateSkinList();
0392         });
0393 }
0394 
0395 QSet<QString> AppearanceSettings::extractKnsSkinIds(const QStringList &fileList)
0396 {
0397     QSet<QString> skinIdList;
0398 
0399     for (const QString &file : fileList) {
0400         // We only care about files/directories which are subdirectories of our KNS skins dir.
0401         if (file.startsWith(m_knsSkinDir, Qt::CaseInsensitive)) {
0402             // Get the relative filename (this removes the KNS install dir from the filename).
0403             QString relativeName = QString(file).remove(m_knsSkinDir, Qt::CaseInsensitive);
0404 
0405             // Get everything before the first slash - that should be our skins ID.
0406             QString skinId = relativeName.section(QLatin1Char('/'), 0, 0, QString::SectionSkipEmpty);
0407 
0408             // Skip all other entries in the file list if we found what we were searching for.
0409             if (!skinId.isEmpty()) {
0410                 // First remove all remaining slashes (as there could be leading or trailing ones).
0411                 skinId.remove(QStringLiteral("/"));
0412 
0413                 skinIdList.insert(skinId);
0414             }
0415         }
0416     }
0417 
0418     return skinIdList;
0419 }
0420 
0421 void AppearanceSettings::knsDialogFinished(const QList<KNSCore::Entry> &changedEntries)
0422 {
0423     quint32 invalidEntryCount = 0;
0424     QString invalidSkinText;
0425     for (const auto &entry : changedEntries) {
0426         auto Installed = KNSCore::Entry::Installed;
0427         if (entry.status() != Installed) {
0428             continue;
0429         }
0430         bool isValid = true;
0431         const QSet<QString> &skinIdList = extractKnsSkinIds(entry.installedFiles());
0432 
0433         // Validate all skin IDs as each archive can contain multiple skins.
0434         for (const QString &skinId : skinIdList) {
0435             // Validate the current skin.
0436             if (!validateSkin(skinId, true)) {
0437                 isValid = false;
0438             }
0439         }
0440 
0441         // We'll add an error message for the whole KNS entry if
0442         // the current skin is marked as invalid.
0443         // We should not do this per skin as the user does not know that
0444         // there are more skins inside one archive.
0445         if (!isValid) {
0446             invalidEntryCount++;
0447 
0448             // The user needs to know the name of the skin which
0449             // was removed.
0450             invalidSkinText += QString(QStringLiteral("<li>%1</li>")).arg(entry.name());
0451 
0452             // Then remove the skin.
0453             const QStringList files = entry.installedFiles();
0454             for (const QString &file : files) {
0455                 QFileInfo info(QString(file).remove(QStringLiteral("/*")));
0456                 if (!info.exists()) {
0457                     continue;
0458                 }
0459                 if (info.isDir()) {
0460                     QDir(info.absoluteFilePath()).removeRecursively();
0461                 } else {
0462                     QFile::remove(info.absoluteFilePath());
0463                 }
0464             }
0465         }
0466     }
0467 
0468     // Are there any invalid entries?
0469     if (invalidEntryCount > 0) {
0470         failInstall(xi18ncp("@info",
0471                             "The following skin is missing required files. Thus it was removed:<ul>%2</ul>",
0472                             "The following skins are missing required files. Thus they were removed:<ul>%2</ul>",
0473                             invalidEntryCount,
0474                             invalidSkinText));
0475     }
0476 
0477     if (!changedEntries.isEmpty()) {
0478         // Reset the selection in case the currently selected
0479         // skin was removed.
0480         resetSelection();
0481 
0482         // Re-populate the list of skins if the user changed something.
0483         populateSkinList();
0484     }
0485 }
0486 
0487 #include "moc_appearancesettings.cpp"