File indexing completed on 2024-03-24 05:54:48
0001 // SPDX-License-Identifier: GPL-2.0-or-later 0002 // SPDX-FileCopyrightText: 2007 Dominik Seichter <domseichter@web.de> 0003 // SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org> 0004 0005 #include "scriptplugin.h" 0006 0007 #include <kconfiggroup.h> 0008 #include <kmessagebox.h> 0009 #include <KIO/StoredTransferJob> 0010 #include <KIO/StatJob> 0011 #include <KJobWidgets> 0012 #include <kio_version.h> 0013 0014 #include <QTemporaryFile> 0015 #include <QFile> 0016 #include <QMenu> 0017 #include <QTextStream> 0018 #include <QVariant> 0019 #include <QFileDialog> 0020 #include <QDebug> 0021 #include <QThread> 0022 0023 #include "ui_scriptplugindialog.h" 0024 #include "ui_scriptpluginwidget.h" 0025 #include "batchrenamer.h" 0026 #include "krenamefile.h" 0027 0028 const char *ScriptPlugin::s_pszFileDialogLocation = "kfiledialog://krenamejscript"; 0029 const char *ScriptPlugin::s_pszVarNameIndex = "krename_index"; 0030 const char *ScriptPlugin::s_pszVarNameUrl = "krename_url"; 0031 const char *ScriptPlugin::s_pszVarNameFilename = "krename_filename"; 0032 const char *ScriptPlugin::s_pszVarNameExtension = "krename_extension"; 0033 const char *ScriptPlugin::s_pszVarNameDirectory = "krename_directory"; 0034 0035 enum EVarType { 0036 eVarType_String = 0, 0037 eVarType_Int, 0038 eVarType_Double, 0039 eVarType_Bool 0040 }; 0041 0042 // Wraps around QJSEngine::evaluate to force a timeout on it. 0043 // Doesn't require manual starting, result() takes care of that. 0044 // NB: Thy shalt not touch the engine after constructing the thread! 0045 class EvaluateThread : public QThread 0046 { 0047 public: 0048 EvaluateThread(QJSEngine *engine, const QString &script, QObject *parent = nullptr) 0049 : QThread(parent) 0050 , m_engine(engine) 0051 , m_script(script) 0052 { 0053 // Reset in case a previous eval was interrupted 0054 m_engine->setInterrupted(false); 0055 } 0056 0057 void run() override 0058 { 0059 m_value = m_engine->evaluate(m_script); 0060 } 0061 0062 // Start thread and gather a result from the engine, either because it finished or because 0063 // we forced an interrupt after timeout. 0064 QJSValue result() 0065 { 0066 start(); 0067 wait(m_timeout); 0068 if (!isFinished()) { 0069 // This function is called by another thread and potentially races here as the run() may finish 0070 // between the condition and the interrupt call. This still doesn't require locking though. 0071 // m_value is only set once and only read once whether the engine is needlessly in interruption state 0072 // has no impact anymore if evaluate() returned already. 0073 m_engine->setInterrupted(true); 0074 wait(); // this is expected to return eventually! 0075 } 0076 return m_value; 0077 } 0078 0079 private: 0080 QJSEngine *m_engine = nullptr; 0081 QString m_script; 0082 QJSValue m_value; 0083 const QDeadlineTimer m_timeout {30000 /* ms */}; 0084 }; 0085 0086 ScriptPlugin::ScriptPlugin(PluginLoader *loader) 0087 : QObject(), 0088 Plugin(loader), m_parent(nullptr) 0089 { 0090 m_name = i18n("JavaScript Plugin"); 0091 m_icon = "applications-development"; 0092 m_menu = new QMenu(); 0093 m_widget = new Ui::ScriptPluginWidget(); 0094 0095 this->addSupportedToken("js;.*"); 0096 0097 m_help.append("[js;4+5];;" + i18n("Insert a snippet of JavaScript code (4+5 in this case)")); 0098 0099 m_menu->addAction(i18n("Index of the current file"), this, &ScriptPlugin::slotInsertIndex); 0100 m_menu->addAction(i18n("URL of the current file"), this, &ScriptPlugin::slotInsertUrl); 0101 m_menu->addAction(i18n("Filename of the current file"), this, &ScriptPlugin::slotInsertFilename); 0102 m_menu->addAction(i18n("Extension of the current file"), this, &ScriptPlugin::slotInsertExtension); 0103 m_menu->addAction(i18n("Directory of the current file"), this, &ScriptPlugin::slotInsertDirectory); 0104 } 0105 0106 ScriptPlugin::~ScriptPlugin() 0107 { 0108 delete m_widget; 0109 delete m_menu; 0110 } 0111 0112 QString ScriptPlugin::processFile(BatchRenamer *b, int index, 0113 const QString &filenameOrToken, EPluginType) 0114 { 0115 QString token(filenameOrToken); 0116 QString script; 0117 QString definitions = m_widget->textCode->toPlainText(); 0118 0119 if (token.contains(";")) { 0120 script = token.section(';', 1); // all sections from 1 to the last 0121 token = token.section(';', 0, 0).toLower(); 0122 } else { 0123 token = token.toLower(); 0124 } 0125 0126 if (token == "js") { 0127 // Setup interpreter 0128 const KRenameFile &file = b->files()->at(index); 0129 initKRenameVars(file, index); 0130 0131 // Make sure definitions are executed first 0132 script = definitions + '\n' + script; 0133 0134 EvaluateThread thread(&m_engine, script); 0135 const QJSValue result = thread.result(); 0136 if (result.isError()) { 0137 qDebug() << "JavaScript Error:" << result.toString(); 0138 return QString(); 0139 } 0140 0141 return result.toString(); 0142 } 0143 0144 return QString(); 0145 } 0146 0147 const QIcon ScriptPlugin::icon() const 0148 { 0149 return QIcon::fromTheme(m_icon); 0150 } 0151 0152 void ScriptPlugin::createUI(QWidget *parent) const 0153 { 0154 QStringList labels; 0155 labels << i18n("Variable Name"); 0156 labels << i18n("Initial Value"); 0157 0158 const_cast<ScriptPlugin *>(this)->m_parent = parent; 0159 m_widget->setupUi(parent); 0160 m_widget->listVariables->setColumnCount(2); 0161 m_widget->listVariables->setHeaderLabels(labels); 0162 0163 connect(m_widget->listVariables, &QTreeWidget::itemSelectionChanged, 0164 this, &ScriptPlugin::slotEnableControls); 0165 connect(m_widget->buttonAdd, &QPushButton::clicked, 0166 this, &ScriptPlugin::slotAdd); 0167 connect(m_widget->buttonRemove, &QPushButton::clicked, 0168 this, &ScriptPlugin::slotRemove); 0169 connect(m_widget->buttonLoad, &QPushButton::clicked, 0170 this, &ScriptPlugin::slotLoad); 0171 connect(m_widget->buttonSave, &QPushButton::clicked, 0172 this, &ScriptPlugin::slotSave); 0173 connect(m_widget->textCode, &QTextEdit::textChanged, 0174 this, &ScriptPlugin::slotEnableControls); 0175 0176 const_cast<ScriptPlugin *>(this)->slotEnableControls(); 0177 0178 m_widget->buttonLoad->setIcon(QIcon::fromTheme("document-open")); 0179 m_widget->buttonSave->setIcon(QIcon::fromTheme("document-save-as")); 0180 m_widget->buttonAdd->setIcon(QIcon::fromTheme("list-add")); 0181 m_widget->buttonRemove->setIcon(QIcon::fromTheme("list-remove")); 0182 0183 m_widget->buttonInsert->setMenu(m_menu); 0184 } 0185 0186 void ScriptPlugin::initKRenameVars(const KRenameFile &file, int index) 0187 { 0188 // KRename definitions 0189 m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameIndex, index); 0190 m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameUrl, file.srcUrl().url()); 0191 m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameFilename, file.srcFilename()); 0192 m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameExtension, file.srcExtension()); 0193 m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameDirectory, file.srcDirectory()); 0194 0195 // User definitions, set them only on first file 0196 if (index != 0) { 0197 return; 0198 } 0199 0200 for (int i = 0; i < m_widget->listVariables->topLevelItemCount(); i++) { 0201 // TODO, we have to know the type of the variable! 0202 QTreeWidgetItem *item = m_widget->listVariables->topLevelItem(i); 0203 if (!item) { 0204 continue; 0205 } 0206 0207 EVarType eVarType = static_cast<EVarType>(item->data(1, Qt::UserRole).toInt()); 0208 const QString &name = item->text(0); 0209 const QString &value = item->text(1); 0210 switch (eVarType) { 0211 default: 0212 case eVarType_String: 0213 m_engine.globalObject().setProperty(name, value); 0214 break; 0215 case eVarType_Int: 0216 m_engine.globalObject().setProperty(name, value.toInt()); 0217 break; 0218 case eVarType_Double: 0219 m_engine.globalObject().setProperty(name, value.toDouble()); 0220 break; 0221 case eVarType_Bool: 0222 m_engine.globalObject().setProperty(name, (value.toLower() == "true" ? true : false)); 0223 break; 0224 } 0225 } 0226 } 0227 0228 void ScriptPlugin::insertVariable(const char *name) 0229 { 0230 m_widget->textCode->insertPlainText(QString(name)); 0231 } 0232 0233 void ScriptPlugin::slotEnableControls() 0234 { 0235 bool bEnable = !(m_widget->listVariables->selectedItems().isEmpty()); 0236 m_widget->buttonRemove->setEnabled(bEnable); 0237 0238 bEnable = !m_widget->textCode->toPlainText().isEmpty(); 0239 m_widget->buttonSave->setEnabled(bEnable); 0240 } 0241 0242 void ScriptPlugin::slotAdd() 0243 { 0244 QDialog dialog; 0245 Ui::ScriptPluginDialog dlg; 0246 0247 dlg.setupUi(&dialog); 0248 dlg.comboType->addItem(i18n("String"), eVarType_String); 0249 dlg.comboType->addItem(i18n("Int"), eVarType_Int); 0250 dlg.comboType->addItem(i18n("Double"), eVarType_Double); 0251 dlg.comboType->addItem(i18n("Boolean"), eVarType_Bool); 0252 0253 if (dialog.exec() != QDialog::Accepted) { 0254 return; 0255 } 0256 0257 QString name = dlg.lineName->text(); 0258 QString value = dlg.lineValue->text(); 0259 0260 // Build a Java script statement 0261 QString script = name + " = " + value + ';'; 0262 0263 EvaluateThread thread(&m_engine, script); 0264 const QJSValue result = thread.result(); 0265 if (result.isError()) { 0266 KMessageBox::error(m_parent, i18n("A JavaScript error has occurred: ") + result.toString(), this->name()); 0267 } else { 0268 QTreeWidgetItem *item = new QTreeWidgetItem(); 0269 item->setText(0, name); 0270 item->setText(1, value); 0271 item->setData(1, Qt::UserRole, dlg.comboType->currentData()); 0272 0273 m_widget->listVariables->addTopLevelItem(item); 0274 } 0275 } 0276 0277 void ScriptPlugin::slotRemove() 0278 { 0279 QTreeWidgetItem *item = m_widget->listVariables->currentItem(); 0280 if (item) { 0281 m_widget->listVariables->invisibleRootItem()->removeChild(item); 0282 delete item; 0283 } 0284 } 0285 0286 void ScriptPlugin::slotLoad() 0287 { 0288 if (!m_widget->textCode->toPlainText().isEmpty() && 0289 KMessageBox::questionYesNo(m_parent, 0290 i18n("All currently entered definitions will be lost. Do you want to continue?"), {}, 0291 KStandardGuiItem::cont(), 0292 KStandardGuiItem::cancel()) 0293 == KMessageBox::No) { 0294 return; 0295 } 0296 0297 QUrl url = QFileDialog::getOpenFileUrl(m_parent, i18n("Select file"), 0298 QUrl(ScriptPlugin::s_pszFileDialogLocation)); 0299 0300 if (!url.isEmpty()) { 0301 // Also support remote files 0302 KIO::StoredTransferJob *job = KIO::storedGet(url); 0303 KJobWidgets::setWindow(job, m_parent); 0304 if (job->exec()) { 0305 m_widget->textCode->setPlainText(QString::fromLocal8Bit(job->data())); 0306 } else { 0307 KMessageBox::error(m_parent, job->errorString()); 0308 } 0309 } 0310 0311 slotEnableControls(); 0312 } 0313 0314 void ScriptPlugin::slotSave() 0315 { 0316 QUrl url = QFileDialog::getSaveFileUrl(m_parent, i18n("Select file"), 0317 QUrl(ScriptPlugin::s_pszFileDialogLocation)); 0318 0319 if (!url.isEmpty()) { 0320 #if KIO_VERSION >= QT_VERSION_CHECK(5, 69, 0) 0321 KIO::StatJob *statJob = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatNoDetails); 0322 #else 0323 KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0); 0324 #endif 0325 statJob->exec(); 0326 if (statJob->error() != KIO::ERR_DOES_NOT_EXIST) { 0327 int m = KMessageBox::warningYesNo(m_parent, i18n("The file %1 already exists. " 0328 "Do you want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)), {}, 0329 KStandardGuiItem::overwrite(), 0330 KStandardGuiItem::cancel()); 0331 0332 if (m == KMessageBox::No) { 0333 return; 0334 } 0335 } 0336 0337 if (url.isLocalFile()) { 0338 QFile file(url.path()); 0339 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { 0340 QTextStream out(&file); 0341 out << m_widget->textCode->toPlainText(); 0342 out.flush(); 0343 file.close(); 0344 } else { 0345 KMessageBox::error(m_parent, i18n("Unable to open %1 for writing.", url.path())); 0346 } 0347 } else { 0348 KIO::StoredTransferJob *job = KIO::storedPut(m_widget->textCode->toPlainText().toLocal8Bit(), url, -1); 0349 KJobWidgets::setWindow(job, m_parent); 0350 job->exec(); 0351 if (job->error()) { 0352 KMessageBox::error(m_parent, job->errorString()); 0353 } 0354 } 0355 } 0356 0357 slotEnableControls(); 0358 } 0359 0360 void ScriptPlugin::slotTest() 0361 { 0362 } 0363 0364 void ScriptPlugin::slotInsertIndex() 0365 { 0366 this->insertVariable(ScriptPlugin::s_pszVarNameIndex); 0367 } 0368 0369 void ScriptPlugin::slotInsertUrl() 0370 { 0371 this->insertVariable(ScriptPlugin::s_pszVarNameUrl); 0372 } 0373 0374 void ScriptPlugin::slotInsertFilename() 0375 { 0376 this->insertVariable(ScriptPlugin::s_pszVarNameFilename); 0377 } 0378 0379 void ScriptPlugin::slotInsertExtension() 0380 { 0381 this->insertVariable(ScriptPlugin::s_pszVarNameExtension); 0382 } 0383 0384 void ScriptPlugin::slotInsertDirectory() 0385 { 0386 this->insertVariable(ScriptPlugin::s_pszVarNameDirectory); 0387 } 0388 0389 void ScriptPlugin::loadConfig(KConfigGroup &group) 0390 { 0391 QStringList variableNames; 0392 QStringList variableValues; 0393 QVariantList variableTypes; 0394 0395 variableNames = group.readEntry("JavaScriptVariableNames", variableNames); 0396 variableValues = group.readEntry("JavaScriptVariableValues", variableValues); 0397 variableTypes = group.readEntry("JavaScriptVariableTypes", variableTypes); 0398 0399 int min = qMin(variableNames.count(), variableValues.count()); 0400 min = qMin(min, variableTypes.count()); 0401 0402 for (int i = 0; i < min; i++) { 0403 QTreeWidgetItem *item = new QTreeWidgetItem(); 0404 item->setText(0, variableNames[i]); 0405 item->setText(1, variableValues[i]); 0406 item->setData(1, Qt::UserRole, variableTypes[i]); 0407 0408 m_widget->listVariables->addTopLevelItem(item); 0409 } 0410 0411 m_widget->textCode->setPlainText(group.readEntry("JavaScriptDefinitions", QString())); 0412 } 0413 0414 void ScriptPlugin::saveConfig(KConfigGroup &group) const 0415 { 0416 QStringList variableNames; 0417 QStringList variableValues; 0418 QVariantList variableTypes; 0419 0420 for (int i = 0; i < m_widget->listVariables->topLevelItemCount(); i++) { 0421 QTreeWidgetItem *item = m_widget->listVariables->topLevelItem(i); 0422 if (item) { 0423 variableNames << item->text(0); 0424 variableValues << item->text(1); 0425 variableTypes << item->data(1, Qt::UserRole); 0426 } 0427 } 0428 0429 group.writeEntry("JavaScriptVariableNames", variableNames); 0430 group.writeEntry("JavaScriptVariableValues", variableValues); 0431 group.writeEntry("JavaScriptVariableTypes", variableTypes); 0432 group.writeEntry("JavaScriptDefinitions", m_widget->textCode->toPlainText()); 0433 } 0434 0435 #include "moc_scriptplugin.cpp"