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"