File indexing completed on 2024-05-19 05:42:27

0001 // ct_lvtqtw_plugineditor.cpp                                             -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 
0020 #include <ct_lvtqtw_plugineditor.h>
0021 #include <result/result.hpp>
0022 
0023 // KDE
0024 #include <KMessageBox>
0025 #ifndef KDE_FRAMEWORKS_IS_OLD
0026 #include <KNSWidgets/Button>
0027 #endif
0028 #include <KStandardGuiItem>
0029 #include <KTextEditor/Document>
0030 #include <KTextEditor/Editor>
0031 #include <KTextEditor/View>
0032 #include <kwidgetsaddons_version.h>
0033 
0034 // Qt
0035 #include <QApplication>
0036 #include <QBoxLayout>
0037 #include <QFileDialog>
0038 #include <QInputDialog>
0039 #include <QStandardPaths>
0040 #include <QTabWidget>
0041 #include <QToolBar>
0042 #include <QToolButton>
0043 
0044 // Own
0045 #include <ct_lvtplg_pluginmanager.h>
0046 
0047 using namespace Codethink::lvtqtw;
0048 
0049 struct PluginEditor::Private {
0050     KTextEditor::Document *docReadme = nullptr;
0051     KTextEditor::View *viewReadme = nullptr;
0052 
0053     KTextEditor::Document *docPlugin = nullptr;
0054     KTextEditor::View *viewPlugin = nullptr;
0055 
0056     KTextEditor::Document *docLicense = nullptr;
0057     KTextEditor::View *viewLicense = nullptr;
0058 
0059     KTextEditor::Document *docMetadata = nullptr;
0060     KTextEditor::View *viewMetadata = nullptr;
0061 
0062     QTabWidget *documentViews = nullptr;
0063 
0064     // Those QActions will probably move to something else when we use KXmlGui
0065     QAction *openFile = nullptr;
0066     QAction *newPlugin = nullptr;
0067     QAction *savePlugin = nullptr;
0068     QAction *reloadPlugin = nullptr;
0069     QAction *closePlugin = nullptr;
0070 
0071     lvtplg::PluginManager *pluginManager = nullptr;
0072 
0073     QString currentPluginFolder;
0074     QString basePluginPath = QDir::homePath() + "/lks-plugins/";
0075 
0076     bool hasPlugin = false;
0077 };
0078 
0079 PluginEditor::PluginEditor(QWidget *parent): QWidget(parent), d(std::make_unique<PluginEditor::Private>())
0080 {
0081     d->documentViews = new QTabWidget();
0082 
0083     auto *editor = KTextEditor::Editor::instance();
0084     if (!editor) {
0085         std::cout << "Error getting the ktexteditor\n";
0086         abort();
0087     }
0088 
0089     using RangeType = std::initializer_list<std::pair<KTextEditor::Document **, KTextEditor::View **>>;
0090     const auto elements = RangeType{{&d->docReadme, &d->viewReadme},
0091                                     {&d->docPlugin, &d->viewPlugin},
0092                                     {&d->docLicense, &d->viewLicense},
0093                                     {&d->docMetadata, &d->viewMetadata}};
0094 
0095     for (const auto& [doc, view] : elements) {
0096         (*doc) = editor->createDocument(this);
0097         (*view) = (*doc)->createView(d->documentViews);
0098         (*view)->setContextMenu((*view)->defaultContextMenu());
0099     }
0100 
0101     d->docReadme->setHighlightingMode("Markdown");
0102     d->docPlugin->setHighlightingMode("Python");
0103     d->docMetadata->setHighlightingMode("json");
0104 
0105     d->openFile = new QAction(tr("Open Python Plugin"));
0106     d->openFile->setIcon(QIcon::fromTheme("document-open"));
0107     connect(d->openFile, &QAction::triggered, this, &PluginEditor::load);
0108 
0109     d->newPlugin = new QAction(tr("New Plugin"));
0110     d->newPlugin->setIcon(QIcon::fromTheme("document-new"));
0111     connect(d->newPlugin, &QAction::triggered, this, &PluginEditor::requestCreatePythonPlugin);
0112 
0113     d->savePlugin = new QAction(tr("Save plugin"));
0114     d->savePlugin->setIcon(QIcon::fromTheme("document-save"));
0115     connect(d->savePlugin, &QAction::triggered, this, &PluginEditor::save);
0116 
0117     d->closePlugin = new QAction(tr("Close plugin"));
0118     d->closePlugin->setIcon(QIcon::fromTheme("document-close"));
0119     connect(d->closePlugin, &QAction::triggered, this, &PluginEditor::close);
0120 
0121     d->reloadPlugin = new QAction(tr("Reload Script"));
0122     d->reloadPlugin->setIcon(QIcon::fromTheme("system-run"));
0123     connect(d->reloadPlugin, &QAction::triggered, this, &PluginEditor::reloadPlugin);
0124 
0125     auto *toolBar = new QToolBar(this);
0126     toolBar->addActions({d->newPlugin, d->openFile, d->savePlugin, d->closePlugin, d->reloadPlugin});
0127 
0128     d->documentViews->addTab(d->viewReadme, QStringLiteral("README.md"));
0129     d->documentViews->addTab(d->viewLicense, QStringLiteral("LICENSE"));
0130     d->documentViews->addTab(d->viewMetadata, QStringLiteral("metadata.json"));
0131     d->documentViews->addTab(d->viewPlugin, QString());
0132 
0133     auto *l = new QBoxLayout(QBoxLayout::TopToBottom);
0134 
0135     d->documentViews->setEnabled(false);
0136 
0137     l->addWidget(toolBar);
0138     l->addWidget(d->documentViews);
0139     l->setContentsMargins(0, 0, 0, 0);
0140     l->setSpacing(0);
0141 
0142     setLayout(l);
0143 }
0144 
0145 PluginEditor::~PluginEditor() = default;
0146 
0147 QDir PluginEditor::basePluginPath()
0148 {
0149     return d->basePluginPath;
0150 }
0151 
0152 void PluginEditor::requestCreatePythonPlugin()
0153 {
0154     const QString newPluginPath = QFileDialog::getExistingDirectory();
0155     if (newPluginPath.isEmpty()) {
0156         return;
0157     }
0158 
0159     createPythonPlugin(newPluginPath);
0160 }
0161 
0162 void PluginEditor::setBasePluginPath(const QString& path)
0163 {
0164     d->basePluginPath = path;
0165 }
0166 
0167 void PluginEditor::reloadPlugin()
0168 {
0169     // Implementation notes:
0170     // Reloading a single plugin will either lose it's state or put it in a bad state (depending on how the plugin is
0171     // implemented). Main issue that I'm thinking is the plugin data (registerPluginData and getPluginData). Need to
0172     // check if we'll re-run the setup hooks or just leave the plugin in the last-state. Perhaps having a dedicated
0173     // button to "reload" and another to "restart" plugin (Restart would run the setup hook and cleanup data
0174     // structures?)
0175     if (!d->pluginManager) {
0176         sendErrorMsg(tr("%1 was build without plugin support.").arg(qApp->applicationName()));
0177         return;
0178     }
0179 
0180     auto res = save();
0181     if (res.has_error()) {
0182         // no need to try to reload anything that has errors.
0183         return;
0184     }
0185 
0186     d->pluginManager->reloadPlugin(d->currentPluginFolder);
0187 }
0188 
0189 void PluginEditor::setPluginManager(lvtplg::PluginManager *manager)
0190 {
0191     d->pluginManager = manager;
0192 }
0193 
0194 void PluginEditor::close()
0195 {
0196     if (!d->pluginManager) {
0197         sendErrorMsg(tr("%1 was build without plugin support.").arg(qApp->applicationName()));
0198         return;
0199     }
0200 
0201     if (d->docPlugin->isModified() || d->docReadme->isModified()) {
0202 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0203         auto saveAction = KGuiItem(tr("Save Plugins"));
0204         auto discardAction = KGuiItem(tr("Discard Changes"));
0205 
0206         const bool saveThings = KMessageBox::questionTwoActions(this,
0207                                                                 tr("Plugin has changed, Save it?"),
0208                                                                 tr("Save plugins?"),
0209                                                                 saveAction,
0210                                                                 discardAction)
0211             == KMessageBox::ButtonCode::PrimaryAction;
0212 
0213         if (saveThings) {
0214             d->docPlugin->save();
0215             d->docReadme->save();
0216         }
0217 #else
0218         d->docPlugin->save();
0219         d->docReadme->save();
0220 #endif
0221     }
0222     d->docPlugin->closeUrl();
0223     d->docReadme->closeUrl();
0224     d->documentViews->setEnabled(false);
0225 }
0226 
0227 void PluginEditor::createPythonPlugin(const QString& pluginDir)
0228 {
0229     if (!d->pluginManager) {
0230         sendErrorMsg(tr("%1 was build without plugin support.").arg(qApp->applicationName()));
0231         qDebug() << QStringLiteral("%1 was build without plugin support.").arg(qApp->applicationName());
0232         return;
0233     }
0234 
0235     // User canceled.
0236     if (pluginDir.isEmpty()) {
0237         return;
0238     }
0239 
0240     QDir pluginPath(pluginDir);
0241     if (!pluginPath.isEmpty()) {
0242         sendErrorMsg(tr("Please select an empty folder for the new python plugin"));
0243         qDebug() << "Please select an empty folder for the new python plugin";
0244         return;
0245     }
0246 
0247     const QString absPluginPath = pluginPath.absolutePath();
0248 
0249     const bool success = pluginPath.mkpath(pluginPath.path());
0250     if (!success) {
0251         sendErrorMsg(tr("Error creating the Plugin folder"));
0252         qDebug() << "Error creating the Plugin folder";
0253         return;
0254     }
0255 
0256     const auto files = std::initializer_list<std::pair<QString, QString>>{
0257         {QStringLiteral("LICENSE"), QStringLiteral("LICENSE")},
0258         {QStringLiteral("METADATA"), QStringLiteral("metadata.json")},
0259         {QStringLiteral("PLUGIN"), absPluginPath.split("/").last() + QStringLiteral(".py")},
0260         {QStringLiteral("README"), QStringLiteral("README.md")},
0261     };
0262 
0263     for (const auto& [qrc, local] : files) {
0264         if (!QFile::copy(QStringLiteral(":/python_templates/%1").arg(qrc), absPluginPath + QDir::separator() + local)) {
0265             qDebug() << "Error creating the README file";
0266             return;
0267         }
0268 
0269         QFile localFile(absPluginPath + QDir::separator() + local);
0270         if (!localFile.exists()) {
0271             qDebug() << "File does not exists on disk";
0272         }
0273 
0274         if (!localFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner)) {
0275             qDebug() << "Error setting file permissions";
0276             return;
0277         }
0278     }
0279 
0280     qDebug() << "Plugin files created successfully, loading the plugin";
0281     loadByPath(pluginDir);
0282 }
0283 
0284 cpp::result<void, PluginEditor::Error> PluginEditor::save()
0285 {
0286     if (!d->pluginManager) {
0287         sendErrorMsg(tr("%1 was build without plugin support.").arg(qApp->applicationName()));
0288         return cpp::fail(
0289             Error{e_Errors::Error,
0290                   QStringLiteral("%1 was build without plugin support.").arg(qApp->applicationName()).toStdString()});
0291     }
0292 
0293     for (auto *doc : {d->docPlugin, d->docLicense, d->docMetadata, d->docReadme}) {
0294         if (doc->url().isEmpty()) {
0295             continue;
0296         }
0297         if (!doc->save()) {
0298             sendErrorMsg(tr("Error saving plugin."));
0299             std::cout << "Error saving " << doc->url().toString().toStdString() << "\n";
0300             return cpp::fail(Error{e_Errors::Error, "Error saving plugin"});
0301         }
0302     }
0303 
0304     return {};
0305 }
0306 
0307 void PluginEditor::load()
0308 {
0309     if (!d->pluginManager) {
0310         sendErrorMsg(tr("%1 was build without plugin support.").arg(qApp->applicationName()));
0311         return;
0312     }
0313 
0314     const QString fName = QFileDialog::getExistingDirectory(this, tr("Python Script File"), QDir::homePath());
0315 
0316     // User clicked "cancel", not an error.
0317     if (fName.isEmpty()) {
0318         qDebug() << "Error name is empty";
0319         return;
0320     }
0321 
0322     loadByPath(fName);
0323 }
0324 
0325 void PluginEditor::loadByPath(const QString& pluginPath)
0326 {
0327     if (!d->pluginManager) {
0328         sendErrorMsg(tr("%1 was build without plugin support.").arg(qApp->applicationName()));
0329         return;
0330     }
0331 
0332     QFileInfo readmeInfo(pluginPath, QStringLiteral("README.md"));
0333     QFileInfo licenseInfo(pluginPath, QStringLiteral("LICENSE"));
0334     QFileInfo metadataInfo(pluginPath, QStringLiteral("metadata.json"));
0335     QFileInfo pluginInfo(pluginPath, pluginPath.split(QDir::separator()).last() + QStringLiteral(".py"));
0336 
0337     const QString errMsg = tr("Missing required file: %1");
0338     if (!readmeInfo.exists()) {
0339         qDebug() << pluginPath << readmeInfo.absoluteFilePath() << "Error 1";
0340         sendErrorMsg(errMsg.arg(QStringLiteral("README.md")));
0341         return;
0342     }
0343 
0344     if (!pluginInfo.exists()) {
0345         qDebug() << pluginInfo.absoluteFilePath() << "Error 2";
0346         sendErrorMsg(errMsg.arg(pluginInfo.fileName()));
0347         return;
0348     }
0349 
0350     d->docReadme->openUrl(QUrl::fromLocalFile(readmeInfo.absoluteFilePath()));
0351     d->docPlugin->openUrl(QUrl::fromLocalFile(pluginInfo.absoluteFilePath()));
0352     d->docLicense->openUrl(QUrl::fromLocalFile(metadataInfo.absoluteFilePath()));
0353     d->docMetadata->openUrl(QUrl::fromLocalFile(pluginInfo.absoluteFilePath()));
0354 
0355     d->documentViews->setTabText(3, pluginInfo.fileName());
0356 
0357     d->documentViews->setEnabled(true);
0358     d->currentPluginFolder = pluginPath;
0359 
0360     qDebug() << "Opened sucessfully";
0361 }