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 }