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

0001 /*
0002  *   SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
0003  *
0004  *   SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "thememodel.h"
0008 #include "coloreditor.h"
0009 #include "themelistmodel.h"
0010 #include <QByteArray>
0011 #include <QDebug>
0012 #include <QDir>
0013 #include <QDirIterator>
0014 #include <QFile>
0015 #include <QIcon>
0016 #include <QStandardPaths>
0017 
0018 #include <QXmlDefaultHandler>
0019 #include <QXmlInputSource>
0020 #include <QXmlSimpleReader>
0021 
0022 #include <KAboutData>
0023 #include <KCompressionDevice>
0024 #include <KConfigGroup>
0025 #include <KIO/FileCopyJob>
0026 #include <KIO/MkdirJob>
0027 #include <KProcess>
0028 
0029 #include <Plasma/Theme>
0030 
0031 class IconsParserHandler : public QXmlDefaultHandler
0032 {
0033 public:
0034     IconsParserHandler();
0035     bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts) override;
0036     QStringList m_ids;
0037     QStringList m_prefixes;
0038 };
0039 
0040 IconsParserHandler::IconsParserHandler()
0041     : QXmlDefaultHandler()
0042 {
0043 }
0044 
0045 bool IconsParserHandler::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &atts)
0046 {
0047     Q_UNUSED(namespaceURI)
0048     Q_UNUSED(localName)
0049     Q_UNUSED(qName)
0050 
0051     const QString id = atts.value("id");
0052     // qWarning() << "Start Element:"<<id;
0053 
0054     if (!id.isEmpty() && !id.contains(QRegularExpression("\\d\\d$")) && id != "base" && !id.contains("layer")) {
0055         m_ids << id;
0056     }
0057     if (id.endsWith(QLatin1String("-center")) && !id.contains("hint-")) {
0058         // remove -center
0059         m_prefixes << id.mid(0, id.length() - 7);
0060     }
0061     return true;
0062 }
0063 
0064 ThemeModel::ThemeModel(const KPackage::Package &package, QObject *parent)
0065     : QAbstractListModel(parent)
0066     , m_theme(new Plasma::Theme)
0067     , m_themeName(QStringLiteral("default"))
0068     , m_package(package)
0069     , m_themeListModel(new ThemeListModel(this))
0070     , m_colorEditor(new ColorEditor(this))
0071 {
0072     m_theme->setUseGlobalSettings(false);
0073     m_theme->setThemeName(m_themeName);
0074 
0075     m_roleNames.insert(ImagePath, "imagePath");
0076     m_roleNames.insert(Description, "description");
0077     m_roleNames.insert(Delegate, "delegate");
0078     m_roleNames.insert(UsesFallback, "usesFallback");
0079     m_roleNames.insert(SvgAbsolutePath, "svgAbsolutePath");
0080     m_roleNames.insert(IsWritable, "isWritable");
0081     m_roleNames.insert(IconElements, "iconElements");
0082     m_roleNames.insert(FrameSvgPrefixes, "frameSvgPrefixes");
0083 
0084     load();
0085 }
0086 
0087 ThemeModel::~ThemeModel()
0088 {
0089 }
0090 
0091 ThemeListModel *ThemeModel::themeList()
0092 {
0093     return m_themeListModel;
0094 }
0095 
0096 ColorEditor *ThemeModel::colorEditor()
0097 {
0098     return m_colorEditor;
0099 }
0100 
0101 QHash<int, QByteArray> ThemeModel::roleNames() const
0102 {
0103     return m_roleNames;
0104 }
0105 
0106 int ThemeModel::rowCount(const QModelIndex &parent) const
0107 {
0108     Q_UNUSED(parent)
0109     return m_jsonDoc.array().size();
0110 }
0111 
0112 QVariant ThemeModel::data(const QModelIndex &index, int role) const
0113 {
0114     if (!index.isValid() || index.row() < 0 || index.row() > m_jsonDoc.array().size()) {
0115         return QVariant();
0116     }
0117 
0118     const QVariantMap value = m_jsonDoc.array().at(index.row()).toObject().toVariantMap();
0119 
0120     switch (role) {
0121     case ImagePath:
0122         return value.value("imagePath");
0123     case Description:
0124         return value.value("description");
0125     case Delegate:
0126         return value.value("delegate");
0127     case UsesFallback:
0128         return !m_theme->currentThemeHasImage(value.value("imagePath").toString());
0129     case SvgAbsolutePath: {
0130         QString path = m_theme->imagePath(value.value("imagePath").toString());
0131         if (!value.value("imagePath").toString().contains("translucent")) {
0132             path = path.replace("translucent/", "");
0133         }
0134         return path;
0135     }
0136     case IsWritable:
0137         return QFile::exists(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/plasma/desktoptheme/" + m_themeName);
0138     case IconElements:
0139     case FrameSvgPrefixes: {
0140         QString path = m_theme->imagePath(value.value("imagePath").toString());
0141         if (!value.value("imagePath").toString().contains("translucent")) {
0142             path = path.replace("translucent/", "");
0143         }
0144         KCompressionDevice file(path, KCompressionDevice::GZip);
0145         if (!file.open(QIODevice::ReadOnly)) {
0146             return QVariant();
0147         }
0148 
0149         QXmlSimpleReader reader;
0150         IconsParserHandler handler;
0151         reader.setContentHandler(&handler);
0152         QXmlInputSource source(&file);
0153         reader.parse(&source);
0154 
0155         if (role == IconElements) {
0156             return handler.m_ids;
0157         } else {
0158             return handler.m_prefixes;
0159         }
0160     }
0161     }
0162 
0163     return QVariant();
0164 }
0165 
0166 void ThemeModel::load()
0167 {
0168     beginResetModel();
0169     qDebug() << "Loading theme description file" << m_package.filePath("data", "themeDescription.json");
0170 
0171     QFile jsonFile(m_package.filePath("data", "themeDescription.json"));
0172     jsonFile.open(QIODevice::ReadOnly);
0173 
0174     QJsonParseError error;
0175     m_jsonDoc = QJsonDocument::fromJson(jsonFile.readAll(), &error);
0176 
0177     if (error.error != QJsonParseError::NoError) {
0178         qWarning() << "Error parsing Json" << error.errorString();
0179     }
0180 
0181     endResetModel();
0182 }
0183 
0184 QString ThemeModel::theme() const
0185 {
0186     return m_themeName;
0187 }
0188 
0189 QString ThemeModel::author() const
0190 {
0191     const QList<KAboutPerson> authors = m_theme->metadata().authors();
0192     return authors.isEmpty() ? authors.at(0).name() : QString();
0193 }
0194 
0195 QString ThemeModel::email() const
0196 {
0197     const QList<KAboutPerson> authors = m_theme->metadata().authors();
0198     return authors.isEmpty() ? authors.at(0).emailAddress() : QString();
0199 }
0200 
0201 QString ThemeModel::license() const
0202 {
0203     return m_theme->metadata().license();
0204 }
0205 
0206 QString ThemeModel::website() const
0207 {
0208     return m_theme->metadata().website();
0209 }
0210 
0211 void ThemeModel::setTheme(const QString &theme)
0212 {
0213     if (theme == m_themeName) {
0214         return;
0215     }
0216 
0217     m_themeName = theme;
0218     m_theme->setThemeName(theme);
0219     load();
0220     m_colorEditor->setTheme(theme);
0221     emit themeChanged();
0222 }
0223 
0224 void ThemeModel::editElement(const QString &imagePath)
0225 {
0226     QString file = m_theme->imagePath(imagePath);
0227     if (!file.contains("translucent")) {
0228         file = file.replace("translucent/", "");
0229     }
0230 
0231     QString finalFile;
0232 
0233     if (m_theme->currentThemeHasImage(imagePath)) {
0234         finalFile = file;
0235     } else {
0236         finalFile = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/plasma/desktoptheme/" + m_themeName + "/" + imagePath + ".svgz";
0237         const QString dirPath = QFileInfo(finalFile).absoluteDir().absolutePath();
0238         KIO::mkdir(QUrl::fromLocalFile(dirPath))->exec();
0239 
0240         KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file), QUrl::fromLocalFile(finalFile));
0241         if (!job->exec()) {
0242             qWarning() << "Error copying" << file << "to" << finalFile;
0243         }
0244     }
0245 
0246     // QProcess::startDetached("inkscape", QStringList() << finalFile);
0247     KProcess *process = new KProcess();
0248     // TODO: don't use the script to not depend from bash/linux?
0249     process->setProgram("bash", QStringList() << m_package.filePath("scripts", "openInEditor.sh") << finalFile);
0250 
0251     connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &ThemeModel::processFinished);
0252     process->start();
0253 }
0254 
0255 void ThemeModel::processFinished()
0256 {
0257     /*We increment the microversion of the theme: keeps track and will force the cache to be
0258       discarded in order to reload immediately the graphics*/
0259     const QString metadataPath(
0260         QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("plasma/desktoptheme/") % m_themeName % QLatin1String("/metadata.desktop")));
0261     KConfig c(metadataPath);
0262     KConfigGroup cg(&c, "Desktop Entry");
0263 
0264     QStringList version = cg.readEntry("X-KDE-PluginInfo-Version", "0.0").split('.');
0265     if (version.length() < 2) {
0266         version << QLatin1String("0");
0267     }
0268     if (version.length() < 3) {
0269         version << QLatin1String("0");
0270     }
0271 
0272     cg.writeEntry("X-KDE-PluginInfo-Version",
0273                   QString(version.first() + QLatin1String(".") + version[1] + QLatin1String(".") + QString::number(version.last().toInt() + 1)));
0274     cg.sync();
0275 }
0276 
0277 void ThemeModel::editThemeMetaData(const QString &name, const QString &author, const QString &email, const QString &license, const QString &website)
0278 {
0279     QString compactName = name.toLower();
0280     compactName.replace(' ', QString());
0281     const QString metadataPath(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) % QLatin1String("/plasma/desktoptheme/") % compactName
0282                                % QLatin1String("/metadata.desktop"));
0283     KConfig c(metadataPath);
0284 
0285     KConfigGroup cg(&c, "Desktop Entry");
0286     cg.writeEntry("X-KDE-PluginInfo-Name", name);
0287     cg.writeEntry("X-KDE-PluginInfo-Author", author);
0288     cg.writeEntry("X-KDE-PluginInfo-Email", email);
0289     cg.writeEntry("X-KDE-PluginInfo-Website", website);
0290     cg.writeEntry("X-KDE-PluginInfo-Category", "Plasma Theme");
0291     cg.writeEntry("X-KDE-PluginInfo-License", license);
0292     cg.writeEntry("X-KDE-PluginInfo-EnabledByDefault", "true");
0293     cg.writeEntry("X-Plasma-API", "5.0");
0294     cg.writeEntry("X-KDE-PluginInfo-Version", "0.1");
0295     cg.sync();
0296 
0297     KConfigGroup cg2(&c, "ContrastEffect");
0298     cg2.writeEntry("enabled", "true");
0299     cg2.writeEntry("contrast", "0.2");
0300     cg2.writeEntry("intensity", "2.0");
0301     cg2.writeEntry("saturation", "1.7");
0302     cg2.sync();
0303 }
0304 
0305 void ThemeModel::createNewTheme(const QString &name, const QString &author, const QString &email, const QString &license, const QString &website)
0306 {
0307     editThemeMetaData(name, author, email, license, website);
0308 
0309     QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, +"/plasma/desktoptheme/default/colors");
0310 
0311     QString compactName = name.toLower();
0312     compactName.replace(' ', QString());
0313     QString finalFile = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/plasma/desktoptheme/" + compactName + "/colors";
0314 
0315     KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(file), QUrl::fromLocalFile(finalFile));
0316     if (!job->exec()) {
0317         qWarning() << "Error copying" << file << "to" << finalFile;
0318     }
0319 
0320     m_themeListModel->reload();
0321 }
0322 
0323 QString ThemeModel::themeFolder()
0324 {
0325     return QStandardPaths::locate(QStandardPaths::GenericDataLocation, +"plasma/desktoptheme/" + m_themeName, QStandardPaths::LocateDirectory);
0326 }
0327 
0328 #include "moc_thememodel.cpp"