File indexing completed on 2024-05-26 04:59:40

0001 /*
0002     SPDX-FileCopyrightText: 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar>
0003     SPDX-FileCopyrightText: 2010-2022 Mladen Milinkovic <max@smoothware.net>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "scriptsmanager.h"
0009 
0010 #include "appglobal.h"
0011 #include "application.h"
0012 #include "actions/useraction.h"
0013 #include "actions/useractionnames.h"
0014 #include "dialogs/textinputdialog.h"
0015 #include "helpers/common.h"
0016 #include "helpers/fileloadhelper.h"
0017 #include "helpers/filetrasher.h"
0018 #include "scripting/scripting_rangesmodule.h"
0019 #include "scripting/scripting_stringsmodule.h"
0020 #include "scripting/scripting_subtitlemodule.h"
0021 #include "scripting/scripting_subtitlelinemodule.h"
0022 
0023 #include <QAbstractItemModel>
0024 #include <QStandardPaths>
0025 #include <QDialog>
0026 #include <QFileDialog>
0027 #include <QJSEngine>
0028 #include <QMenuBar>
0029 #include <QMenu>
0030 #include <QDesktopServices>
0031 #include <QKeyEvent>
0032 #include <QStringBuilder>
0033 
0034 #include <kio_version.h>
0035 #include <KMessageBox>
0036 #if KIO_VERSION < QT_VERSION_CHECK(5, 71, 0)
0037 #include <KRun>
0038 #endif
0039 #include <KActionCollection>
0040 #include <KLocalizedString>
0041 #if KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0)
0042 #include <KIO/OpenUrlJob>
0043 #include <KIO/JobUiDelegate>
0044 #endif
0045 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0046 #include <KIO/JobUiDelegateFactory>
0047 #endif
0048 #include <kwidgetsaddons_version.h>
0049 
0050 inline static const QDir &
0051 userScriptDir()
0052 {
0053     static const QDir *dir = nullptr;
0054     if(dir == nullptr) {
0055         const QString userScriptDirName = $("scripts");
0056         static QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
0057         if(!d.exists(userScriptDirName))
0058             d.mkpath(userScriptDirName);
0059         d.cd(userScriptDirName);
0060         dir = &d;
0061     }
0062     return *dir;
0063 }
0064 
0065 namespace SubtitleComposer {
0066 class SCScript
0067 {
0068     friend class InstalledScriptsModel;
0069 
0070 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0071     template<class> friend struct QtPrivate::QGenericArrayOps;
0072     template<typename iterator, typename N>
0073     friend void QtPrivate::q_relocate_overlap_n_left_move(iterator first, N n, iterator d_first);
0074     template<typename> friend class QList;
0075 #endif
0076 
0077     SCScript(const QString &path, const QString &name)
0078         : m_name(name),
0079           m_path(path),
0080           m_isScript(name.right(3) == $(".js")),
0081           m_title(name)
0082     {
0083         if(m_isScript)
0084             initScript();
0085         else
0086             m_category = i18n("Non-Script Files");
0087     }
0088 
0089     void initScript()
0090     {
0091         const QString script = content();
0092 
0093         QRegularExpressionMatch m;
0094         staticRE$(reCat, "@category\\s+(.+)\\s*$", REm);
0095         if((m = reCat.match(script)).hasMatch())
0096             m_category = m.captured(1);
0097         staticRE$(reName, "@name\\s+(.+)\\s*$", REm);
0098         if((m = reName.match(script)).hasMatch())
0099             m_title = m.captured(1);
0100         staticRE$(reVer, "@version\\s+(.+)\\s*$", REm);
0101         if((m = reVer.match(script)).hasMatch())
0102             m_version = m.captured(1);
0103         staticRE$(reSummary, "@summary\\s+(.+)\\s*$", REm);
0104         if((m = reSummary.match(script)).hasMatch())
0105             m_description = m.captured(1);
0106         staticRE$(reAuthor, "@author\\s+(.+)\\s*$", REm);
0107         if((m = reAuthor.match(script)).hasMatch())
0108             m_author = m.captured(1);
0109     }
0110 
0111     friend QVector<SCScript>;
0112     SCScript() noexcept {}
0113     SCScript(const SCScript &) noexcept = default;
0114     SCScript & operator=(const SCScript &) noexcept = default;
0115     SCScript(SCScript &&) noexcept = default;
0116     SCScript & operator=(SCScript &&) noexcept = default;
0117 
0118 public:
0119     QString content() const
0120     {
0121         QFile jsf(m_path);
0122         if(!jsf.open(QFile::ReadOnly | QFile::Text))
0123             return QString();
0124         return QTextStream(&jsf).readAll();
0125     }
0126 
0127     inline bool isScript() const { return m_isScript; }
0128 
0129     inline const QString & name() const { return m_name; }
0130     inline const QString & path() const { return m_path; }
0131 
0132     inline const QString & title() const { return m_title; }
0133     inline const QString & version() const { return m_version; }
0134     inline const QString & category() const { return m_category; }
0135     inline const QString & description() const { return m_description; }
0136     inline const QString & author() const { return m_author; }
0137 
0138     inline int compare(const SCScript &other, Qt::CaseSensitivity cs = Qt::CaseSensitive) const
0139     { return m_title.compare(other.m_title, cs); }
0140 
0141 private:
0142     QString m_name;
0143     QString m_path;
0144 
0145     bool m_isScript;
0146     QString m_title;
0147     QString m_version;
0148     QString m_category;
0149     QString m_description;
0150     QString m_author;
0151 };
0152 
0153 class InstalledScriptsModel : public QAbstractItemModel
0154 {
0155     Q_OBJECT
0156 
0157     enum { Title, Version, Author, Path, ColumnCount };
0158 
0159 public:
0160     InstalledScriptsModel(QObject *parent = 0) : QAbstractItemModel(parent) {}
0161     ~InstalledScriptsModel() {}
0162 
0163     QVariant headerData(int section, Qt::Orientation orientation, int role) const override
0164     {
0165         if(role != Qt::DisplayRole || orientation == Qt::Vertical)
0166             return QVariant();
0167         switch(section) {
0168         case Title: return i18n("Title");
0169         case Version: return i18n("Version");
0170         case Author: return i18n("Author");
0171         case Path: return i18n("Path");
0172         default: return QString();
0173         }
0174     }
0175 
0176     Qt::ItemFlags flags(const QModelIndex &index) const override
0177     {
0178         if(index.internalId() > 0)
0179             return Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
0180         return Qt::ItemIsEnabled;
0181     }
0182 
0183     int childIndex(const QString &categoryName, int row) const
0184     {
0185         for(int i = 0, si = 0; i < m_scripts.size(); i++) {
0186             const SCScript &s = m_scripts.at(i);
0187             if(s.category() != categoryName)
0188                 continue;
0189             if(si == row)
0190                 return i;
0191             si++;
0192         }
0193         Q_ASSERT(false);
0194         return -1;
0195     }
0196 
0197     QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
0198     {
0199         if(!hasIndex(row, column, parent))
0200             return QModelIndex();
0201 
0202         if(!parent.isValid()) {
0203             // category
0204             if(row < m_categories.size())
0205                 return createIndex(row, column, quintptr(0ULL));
0206             // script (root)
0207             return createIndex(row, column, childIndex(QString(), row - m_categories.size()) + 1);
0208         } else {
0209             // script (child)
0210             Q_ASSERT(!parent.parent().isValid());
0211             Q_ASSERT(parent.row() < m_categories.size());
0212             const QString catName = m_categories.at(parent.row());
0213             return createIndex(row, column, childIndex(catName, row) + 1);
0214         }
0215     }
0216 
0217     QModelIndex parent(const QModelIndex &child) const override
0218     {
0219         if(!child.isValid() || child.internalId() == 0)
0220             return QModelIndex();
0221 
0222         // script
0223         Q_ASSERT(int(child.internalId()) <= m_scripts.size());
0224         const SCScript &s = m_scripts.at(child.internalId() - 1);
0225         const int i = m_categories.indexOf(s.category());
0226         if(i >= 0) // script (child)
0227             return createIndex(i, 0, quintptr(0ULL));
0228         return QModelIndex(); // script (root)
0229     }
0230 
0231     int scriptCount(const QString &categoryName) const
0232     {
0233         int n = 0;
0234         for(int i = 0; i < m_scripts.size(); i++) {
0235             if(m_scripts.at(i).category() == categoryName)
0236                 n++;
0237         }
0238         return n;
0239     }
0240 
0241     int rowCount(const QModelIndex &parent = QModelIndex()) const override
0242     {
0243         if(!parent.isValid())
0244             return m_categories.size() + scriptCount(QString());
0245         if(parent.row() >= m_categories.size())
0246             return 0;
0247         return scriptCount(m_categories.at(parent.row()));
0248     }
0249 
0250     int columnCount(const QModelIndex &parent = QModelIndex()) const override
0251     {
0252         Q_UNUSED(parent);
0253         return ColumnCount;
0254     }
0255 
0256     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
0257     {
0258         if(index.internalId() > 0) {
0259             if(role == Qt::DisplayRole) {
0260                 switch(index.column()) {
0261                 case Title: return m_scripts.at(index.internalId() - 1).title();
0262                 case Version: return m_scripts.at(index.internalId() - 1).version();
0263                 case Author: return m_scripts.at(index.internalId() - 1).author();
0264                 case Path: return m_scripts.at(index.internalId() - 1).path();
0265                 }
0266             } else if(role == Qt::ToolTipRole) {
0267                 if(index.column() == Path)
0268                     return m_scripts.at(index.internalId() - 1).path();
0269                 return m_scripts.at(index.internalId() - 1).description();
0270             }
0271         } else {
0272             if(role == Qt::DisplayRole && index.column() == Title) {
0273                 const QString title = m_categories.at(index.row());
0274                 return title.isEmpty() ? $("Misc") : title;
0275             }
0276         }
0277         return QVariant();
0278 
0279     }
0280 
0281     const SCScript * findByFilename(const QString &name) const
0282     {
0283         if(name.isEmpty())
0284             return nullptr;
0285         for(auto it = m_scripts.begin(); it != m_scripts.end(); ++it) {
0286             if(it->name() == name)
0287                 return &(*it);
0288         }
0289         return nullptr;
0290     }
0291 
0292     const SCScript * findByTitle(const QString &title) const
0293     {
0294         if(title.isEmpty())
0295             return nullptr;
0296         for(auto it = m_scripts.begin(); it != m_scripts.end(); ++it) {
0297             if(it->title() == title)
0298                 return &(*it);
0299         }
0300         return nullptr;
0301     }
0302 
0303     void removeAll()
0304     {
0305         beginRemoveRows(QModelIndex(), 0, m_categories.size());
0306         m_categories.clear();
0307         m_scripts.clear();
0308         endRemoveRows();
0309     }
0310 
0311     inline const SCScript * at(int index) const { return &m_scripts.at(index); }
0312 
0313     template<class T>
0314     const T * insertSorted(QVector<T> *vec, T &&el)
0315     {
0316         for(int i = 0; i < vec->size(); i++) {
0317             if(vec->at(i).compare(el, Qt::CaseInsensitive) >= 0) {
0318                 vec->insert(i, std::move(el));
0319                 return &vec->at(i);
0320             }
0321         }
0322         vec->push_back(std::move(el));
0323         return &vec->last();
0324     }
0325 
0326     const SCScript * add(const QString &path, int prefixLen)
0327     {
0328         const QString name = path.mid(prefixLen);
0329         if(findByFilename(name))
0330             return nullptr;
0331 
0332         const SCScript *script = insertSorted(&m_scripts, SCScript(path, name));
0333         const QString catTitle = script->category();
0334 
0335         int catIndex = m_categories.indexOf(catTitle);
0336         if(catIndex < 0 && !catTitle.isEmpty()) {
0337             catIndex = m_categories.size();
0338             beginInsertRows(QModelIndex(), catIndex, catIndex + 1);
0339             insertSorted(&m_categories, QString(catTitle));
0340             endInsertRows();
0341         }
0342 
0343         const int n = m_scripts.size();
0344         beginInsertRows(catTitle.isEmpty() ? QModelIndex() : createIndex(catIndex, 0, quintptr(0ULL)), n - 1, n + 1);
0345         endInsertRows();
0346         return script;
0347     }
0348 
0349 private:
0350     QVector<SCScript> m_scripts;
0351     QVector<QString> m_categories;
0352 };
0353 
0354 class Debug : public QObject
0355 {
0356     Q_OBJECT
0357 
0358 public:
0359     Debug() {}
0360     ~Debug() {}
0361 
0362 public slots:
0363     void information(const QString &message)
0364     {
0365         KMessageBox::information(app()->mainWindow(), message, i18n("Information"));
0366         qDebug() << message;
0367     }
0368 
0369     void warning(const QString &message)
0370     {
0371         KMessageBox::error(app()->mainWindow(), message, i18n("Warning"));
0372         qWarning() << message;
0373     }
0374 
0375     void error(const QString &message)
0376     {
0377         KMessageBox::error(app()->mainWindow(), message, i18n("Error"));
0378         qWarning() << message;
0379     }
0380 };
0381 }
0382 
0383 using namespace SubtitleComposer;
0384 
0385 ScriptsManager::ScriptsManager(QObject *parent)
0386     : QObject(parent)
0387 {
0388     m_dialog = new QDialog(app()->mainWindow());
0389     setupUi(m_dialog);
0390 
0391     scriptsView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
0392     scriptsView->installEventFilter(this);
0393     if(QItemSelectionModel *m = scriptsView->selectionModel())
0394         m->deleteLater();
0395     scriptsView->setModel(new InstalledScriptsModel(scriptsView));
0396     scriptsView->setSortingEnabled(false);
0397     scriptsView->expandAll();
0398     connect(scriptsView, &QTreeView::doubleClicked, this, [&](){ editScript(); });
0399     connect(scriptsView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, [&](){
0400         const SCScript *s = currentScript();
0401         const bool isScriptSelected = s && s->isScript();
0402         const bool isFileWritable = s && QFileInfo(s->path()).isWritable();
0403         btnRemove->setEnabled(isFileWritable);
0404         if(isFileWritable) {
0405             btnEdit->setText(i18n("Edit"));
0406             btnEdit->setIcon(QIcon::fromTheme($("document-edit")));
0407         } else {
0408             btnEdit->setText(i18n("View"));
0409             btnEdit->setIcon(QIcon::fromTheme($("document-open")));
0410         }
0411         btnEdit->setEnabled(isScriptSelected);
0412         btnRun->setEnabled(isScriptSelected);
0413     });
0414 
0415     connect(btnCreate, &QPushButton::clicked, this, [&](){ createScript(); });
0416     connect(btnAdd, &QPushButton::clicked, this, [&](){ addScript(); });
0417     connect(btnRemove, &QPushButton::clicked, this, [&](){ removeScript(); });
0418     connect(btnEdit, &QPushButton::clicked, this, [&](){ editScript(); });
0419     connect(btnRun, &QPushButton::clicked, this, [&](){ runScript(); });
0420     connect(btnRefresh, &QAbstractButton::clicked, this, [&](){ reloadScripts(); });
0421 }
0422 
0423 ScriptsManager::~ScriptsManager()
0424 {
0425 
0426 }
0427 
0428 void
0429 ScriptsManager::setSubtitle(Subtitle *subtitle)
0430 {
0431     btnRun->setEnabled(subtitle != 0);
0432 }
0433 
0434 void
0435 ScriptsManager::showDialog()
0436 {
0437     m_dialog->show();
0438 }
0439 
0440 const SCScript *
0441 ScriptsManager::findScript(const QString filename) const
0442 {
0443     const SCScript *s = filename.isEmpty() ? nullptr : static_cast<InstalledScriptsModel *>(scriptsView->model())->findByFilename(filename);
0444     return s ? s : currentScript();
0445 }
0446 
0447 const SCScript *
0448 ScriptsManager::currentScript() const
0449 {
0450     const QModelIndex ci = scriptsView->currentIndex();
0451     if(ci.isValid() && ci.internalId() > 0)
0452         return static_cast<InstalledScriptsModel *>(scriptsView->model())->at(ci.internalId() - 1);
0453     return nullptr;
0454 }
0455 
0456 #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 100, 0)
0457 #define questionTwoActions questionYesNo
0458 #define PrimaryAction Yes
0459 #endif
0460 
0461 void
0462 ScriptsManager::createScript(const QString &sN)
0463 {
0464     QString scriptName = sN;
0465 
0466     InstalledScriptsModel *model = static_cast<InstalledScriptsModel *>(scriptsView->model());
0467 
0468     const SCScript *script = nullptr;
0469     while(scriptName.isEmpty() || (script = model->findByTitle(scriptName))) {
0470         if(script) {
0471             QFileInfo fp(script->path());
0472             if(!fp.canonicalFilePath().startsWith(userScriptDir().canonicalPath())) {
0473                 // a system script can be overridden by user
0474                 scriptName = script->name();
0475                 break;
0476             }
0477             if(KMessageBox::questionTwoActions(app()->mainWindow(),
0478                     i18n("A script already exists with this name.\nWould you like to enter another name?"),
0479                     i18n("Duplicate filename"),
0480                     KGuiItem(i18n("Yes")), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction)
0481                 return;
0482         }
0483 
0484         TextInputDialog nameDlg(i18n("Create New Script"), i18n("Script name:"));
0485         if(nameDlg.exec() != QDialog::Accepted)
0486             return;
0487         scriptName = nameDlg.value();
0488     }
0489 
0490     if(!scriptName.endsWith($(".js")))
0491         scriptName.append($(".js"));
0492 
0493     QFile scriptFile(userScriptDir().absoluteFilePath(scriptName));
0494     if(!scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
0495         KMessageBox::error(app()->mainWindow(), i18n("There was an error creating the file <b>%1</b>.", userScriptDir().absoluteFilePath(scriptName)));
0496         return;
0497     }
0498 
0499     QTextStream outputStream(&scriptFile);
0500     outputStream << "/*\n"
0501         "\t@name " << scriptName.chopped(3) << " Title\n"
0502         "\t@version 1.0\n"
0503         "\t@summary " << scriptName << " summary/short desription.\n"
0504         "\t@author Author's Name\n"
0505         "*/\n";
0506 
0507     scriptFile.close();
0508 
0509     reloadScripts();
0510     editScript(scriptName);
0511 }
0512 
0513 const QStringList &
0514 ScriptsManager::mimeTypes()
0515 {
0516     static QStringList mimeTypes;
0517 
0518     if(mimeTypes.isEmpty())
0519         mimeTypes.append("application/javascript");
0520 
0521     return mimeTypes;
0522 }
0523 
0524 void
0525 ScriptsManager::addScript(const QUrl &sSU)
0526 {
0527     QUrl srcScriptUrl = sSU;
0528 
0529     if(srcScriptUrl.isEmpty()) {
0530         QFileDialog fileDialog(m_dialog, i18n("Select Existing Script"), QString(), QString());
0531         fileDialog.setAcceptMode(QFileDialog::AcceptOpen);
0532         fileDialog.setFileMode(QFileDialog::ExistingFile);
0533         fileDialog.setMimeTypeFilters(mimeTypes());
0534         fileDialog.setModal(true);
0535 
0536         if(fileDialog.exec() != QDialog::Accepted)
0537             return;
0538 
0539         srcScriptUrl = fileDialog.selectedUrls().constFirst();
0540     }
0541 
0542     QString scriptName = QFileInfo(srcScriptUrl.fileName()).fileName();
0543 
0544     InstalledScriptsModel *model = static_cast<InstalledScriptsModel *>(scriptsView->model());
0545     while(model->findByFilename(scriptName)) {
0546         if(KMessageBox::questionTwoActions(app()->mainWindow(),
0547                 i18n("You must enter an unused name to continue.\nWould you like to enter another name?"),
0548                 i18n("Name Already Used"),
0549                 KStandardGuiItem::cont(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction)
0550             return;
0551 
0552         TextInputDialog nameDlg(i18n("Rename Script"), i18n("Script name:"));
0553         if(nameDlg.exec() != QDialog::Accepted)
0554             return;
0555         scriptName = nameDlg.value();
0556     }
0557 
0558     FileLoadHelper fileLoadHelper(srcScriptUrl);
0559 
0560     if(!fileLoadHelper.open()) {
0561         KMessageBox::error(app()->mainWindow(), i18n("There was an error opening the file <b>%1</b>.", srcScriptUrl.toString(QUrl::PreferLocalFile)));
0562         return;
0563     }
0564 
0565     QFile dest(userScriptDir().absoluteFilePath(scriptName));
0566     if(!dest.open(QIODevice::WriteOnly | QIODevice::Truncate)
0567             || dest.write(fileLoadHelper.file()->readAll()) == -1
0568             || !dest.flush()) {
0569         KMessageBox::error(app()->mainWindow(), i18n("There was an error copying the file to <b>%1</b>.", dest.fileName()));
0570         return;
0571     }
0572 
0573     reloadScripts();
0574 }
0575 
0576 void
0577 ScriptsManager::removeScript(const QString &sN)
0578 {
0579     const SCScript *script = findScript(sN);
0580     if(!script)
0581         return;
0582 
0583     if(KMessageBox::warningContinueCancel(app()->mainWindow(),
0584             i18n("Do you really want to send file <b>%1</b> to the trash?", script->path()), i18n("Move to Trash")) != KMessageBox::Continue)
0585         return;
0586 
0587     if(!FileTrasher(script->path()).exec()) {
0588         KMessageBox::error(app()->mainWindow(), i18n("There was an error removing the file <b>%1</b>.", script->path()));
0589         return;
0590     }
0591 
0592     reloadScripts();
0593 }
0594 
0595 void
0596 ScriptsManager::editScript(const QString &sN)
0597 {
0598     const SCScript *script = findScript(sN);
0599     if(!script) {
0600         qWarning() << "unknown script specified";
0601         return;
0602     }
0603 
0604     const QUrl scriptUrl = QUrl::fromLocalFile(script->path());
0605 #ifdef SC_APPIMAGE
0606     {
0607 #elif KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0608     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(scriptUrl);
0609     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, app()->mainWindow()));
0610     if(!job->exec()) {
0611 #elif KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0)
0612     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(scriptUrl);
0613     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, app()->mainWindow()));
0614     if(!job->exec()) {
0615 #elif KIO_VERSION >= QT_VERSION_CHECK(5, 31, 0)
0616     if(!KRun::runUrl(scriptUrl, "text/plain", app()->mainWindow(), KRun::RunFlags())) {
0617 #else
0618     if(!KRun::runUrl(scriptUrl, "text/plain", app()->mainWindow(), false, false)) {
0619 #endif
0620         if(!QDesktopServices::openUrl(scriptUrl))
0621             KMessageBox::error(app()->mainWindow(), i18n("Could not launch external editor.\n"));
0622     }
0623 }
0624 
0625 void
0626 ScriptsManager::runScript(const QString &sN)
0627 {
0628     const SCScript *script = findScript(sN);
0629     if(!script || !script->isScript())
0630         return;
0631 
0632     QJSEngine jse;
0633     jse.installExtensions(QJSEngine::ConsoleExtension);
0634     jse.globalObject().setProperty("ranges", jse.newQObject(new Scripting::RangesModule));
0635     jse.globalObject().setProperty("strings", jse.newQObject(new Scripting::StringsModule));
0636     jse.globalObject().setProperty("subtitle", jse.newQObject(new Scripting::SubtitleModule));
0637     jse.globalObject().setProperty("subtitleline", jse.newQObject(new Scripting::SubtitleLineModule));
0638     jse.globalObject().setProperty("debug", jse.newQObject(new Debug()));
0639 
0640     QString scriptData = script->content();
0641     if(scriptData.isNull()) {
0642         KMessageBox::error(app()->mainWindow(), i18n("Error opening script %1.", script->path()), i18n("Error Running Script"));
0643         return;
0644     }
0645 
0646     QJSValue res;
0647     {
0648         // everything done by the script will be undoable in a single step
0649         SubtitleCompositeActionExecutor executor(appSubtitle(), script->title());
0650         res = jse.evaluate(scriptData, script->name());
0651     }
0652 
0653     if(!res.isUndefined()) {
0654         if(res.isError()) {
0655             const QString details = i18n("Path: %1", script->path()) % "\n"
0656                 % res.property($("stack")).toString();
0657             KMessageBox::detailedError(app()->mainWindow(), res.toString(), details, i18n("Error Running Script"));
0658         } else {
0659             KMessageBox::error(app()->mainWindow(), res.toString(), i18n("Error Running Script"));
0660         }
0661     }
0662 }
0663 
0664 QMenu *
0665 ScriptsManager::toolsMenu()
0666 {
0667     static QMenu *toolsMenu = 0;
0668 
0669     if(!toolsMenu) {
0670         toolsMenu = app()->mainWindow()->findChild<QMenu *>("tools");
0671         if(!toolsMenu) {
0672             toolsMenu = app()->mainWindow()->menuBar()->addMenu(i18n("Tools"));
0673             toolsMenu->setObjectName("tools");
0674         }
0675         connect(toolsMenu, &QMenu::triggered, this, &ScriptsManager::onToolsMenuActionTriggered);
0676     }
0677 
0678     return toolsMenu;
0679 }
0680 
0681 void
0682 ScriptsManager::findAllFiles(QString directory, QStringList &fileList)
0683 {
0684     QDir path(directory);
0685     const QFileInfoList files = path.entryInfoList();
0686     for(const QFileInfo &file: files) {
0687         if(file.isDir()) {
0688             if(file.fileName().at(0) != '.')
0689                 findAllFiles(file.absoluteFilePath(), fileList);
0690         } else {
0691             fileList.append(file.absoluteFilePath());
0692         }
0693     }
0694 }
0695 
0696 void
0697 ScriptsManager::reloadScripts()
0698 {
0699     QMenu *toolsMenu = ScriptsManager::toolsMenu();
0700     KActionCollection *actionCollection = app()->mainWindow()->actionCollection();
0701     UserActionManager *actionManager = UserActionManager::instance();
0702 
0703     InstalledScriptsModel *model = static_cast<InstalledScriptsModel *>(scriptsView->model());
0704 
0705     toolsMenu->clear();
0706     toolsMenu->addAction(app()->action(ACT_SCRIPTS_MANAGER));
0707     toolsMenu->addSeparator();
0708 
0709     QMap<QString, QMenu *> categoryMenus;
0710 
0711     model->removeAll();
0712 
0713     // make sure userScriptDir is first on the list so it will override system scripts
0714     const QString userDir = userScriptDir().absolutePath();
0715     QStringList scriptDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, userScriptDir().dirName(), QStandardPaths::LocateDirectory);
0716     int pos = scriptDirs.indexOf(userDir);
0717     if(pos == -1)
0718         scriptDirs.prepend(userDir);
0719     else if(pos > 0)
0720         scriptDirs.move(pos, 0);
0721 
0722     foreach(const QString &path, scriptDirs) {
0723         const int pathLen = QDir(path).absolutePath().length() + 1;
0724         QStringList scriptPaths;
0725         findAllFiles(path, scriptPaths);
0726         foreach(const QString &path, scriptPaths) {
0727             const SCScript *script = model->add(path, pathLen);
0728             if(!script)
0729                 continue;
0730 
0731             if(script->isScript()) {
0732                 QMenu *parentMenu = toolsMenu;
0733                 if(!script->category().isEmpty()) {
0734                     auto it = categoryMenus.constFind(script->category());
0735                     if(it != categoryMenus.cend()) {
0736                         parentMenu = it.value();
0737                     } else {
0738                         parentMenu = new QMenu(script->category(), toolsMenu);
0739                         categoryMenus[script->category()] = parentMenu;
0740                     }
0741                 }
0742 
0743                 QAction *scriptAction = parentMenu->addAction(script->title());
0744                 scriptAction->setObjectName(script->name());
0745                 if(!script->description().isEmpty())
0746                     scriptAction->setStatusTip(script->description());
0747                 actionCollection->addAction(script->name(), scriptAction);
0748                 actionManager->addAction(scriptAction, UserAction::SubOpened | UserAction::FullScreenOff);
0749             }
0750         }
0751     }
0752 
0753     for(auto it = categoryMenus.cbegin(); it != categoryMenus.cend(); ++it)
0754         toolsMenu->addMenu(it.value());
0755 }
0756 
0757 void
0758 ScriptsManager::onToolsMenuActionTriggered(QAction *triggeredAction)
0759 {
0760     if(triggeredAction == app()->action(ACT_SCRIPTS_MANAGER))
0761         return;
0762 
0763     runScript(triggeredAction->objectName());
0764 }
0765 
0766 bool
0767 ScriptsManager::eventFilter(QObject *object, QEvent *event)
0768 {
0769     if(object == scriptsView && event->type() == QEvent::KeyPress && static_cast<QKeyEvent *>(event)->matches(QKeySequence::Delete)) {
0770         removeScript();
0771         return true; // eat event
0772     }
0773 
0774     return QObject::eventFilter(object, event);
0775 }
0776 
0777 #include "scriptsmanager.moc"