File indexing completed on 2024-04-21 15:55:47
0001 /************************************************************************** 0002 * Copyright (C) 2006-2020 by Michel Ludwig (michel.ludwig@kdemail.net) * 0003 ***************************************************************************/ 0004 0005 /************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or modify * 0008 * it under the terms of the GNU General Public License as published by * 0009 * the Free Software Foundation; either version 2 of the License, or * 0010 * (at your option) any later version. * 0011 * * 0012 ***************************************************************************/ 0013 0014 #include "scriptmanager.h" 0015 0016 #include <KConfig> 0017 #include <KLocalizedString> 0018 #include <KMessageBox> 0019 #include <KXMLGUIClient> 0020 #include <KXMLGUIFactory> 0021 0022 #include <QEvent> 0023 #include <QDir> 0024 #include <QDirIterator> 0025 #include <QMap> 0026 0027 #include "kiledebug.h" 0028 #include "kileconfig.h" 0029 #include "kileinfo.h" 0030 #include "kileversion.h" 0031 #include "kileviewmanager.h" 0032 #include "editorkeysequencemanager.h" 0033 #include "utilities.h" 0034 #include "scripting/script.h" 0035 0036 namespace KileScript { 0037 0038 ////////////////////////////// Manager ////////////////////////////// 0039 0040 Manager::Manager(KileInfo *kileInfo, KConfig *config, KActionCollection *actionCollection, QObject *parent, const char *name) 0041 : QObject(parent), m_jScriptDirWatch(Q_NULLPTR), m_kileInfo(kileInfo), m_config(config), m_actionCollection(actionCollection) 0042 { 0043 setObjectName(name); 0044 0045 // create a local scripts directory if it doesn't exist yet 0046 m_localScriptDir = KileUtilities::writableLocation(QStandardPaths::AppDataLocation) + "/scripts/"; 0047 QDir testDir(m_localScriptDir); 0048 if (!testDir.exists()) { 0049 testDir.mkpath(m_localScriptDir); 0050 } 0051 0052 m_jScriptDirWatch = new KDirWatch(this); 0053 m_jScriptDirWatch->setObjectName("KileScript::Manager::ScriptDirWatch"); 0054 connect(m_jScriptDirWatch, SIGNAL(dirty(QString)), this, SLOT(scanScriptDirectories())); 0055 connect(m_jScriptDirWatch, SIGNAL(created(QString)), this, SLOT(scanScriptDirectories())); 0056 connect(m_jScriptDirWatch, SIGNAL(deleted(QString)), this, SLOT(scanScriptDirectories())); 0057 m_jScriptDirWatch->startScan(); 0058 0059 // read plugin code for QScriptEngine 0060 readEnginePlugin(); 0061 m_scriptActionMap = new QMap<QString,QAction *>; 0062 0063 // init script objects 0064 m_kileScriptView = new KileScriptView(this, m_kileInfo->editorExtension()); 0065 m_kileScriptDocument = new KileScriptDocument(this, m_kileInfo, m_kileInfo->editorExtension(), m_scriptActionMap); 0066 m_kileScriptObject = new KileScriptObject(this, m_kileInfo, m_scriptActionMap); 0067 } 0068 0069 Manager::~Manager() 0070 { 0071 delete m_jScriptDirWatch; 0072 delete m_scriptActionMap; 0073 0074 delete m_kileScriptView; 0075 delete m_kileScriptDocument; 0076 delete m_kileScriptObject; 0077 0078 //still need to delete the scripts 0079 for(Script *script : qAsConst(m_jScriptList)) { 0080 delete script; 0081 } 0082 m_jScriptList.clear(); 0083 } 0084 0085 void Manager::executeScript(const Script *script) 0086 { 0087 KILE_DEBUG_MAIN << "execute script: " << script->getName(); 0088 0089 // compatibility check 0090 QString code = script->getCode(); 0091 QRegExp endOfLineExp("(\r\n)|\n|\r"); 0092 int i = code.indexOf(endOfLineExp); 0093 QString firstLine = (i >= 0 ? code.left(i) : code); 0094 QRegExp requiredVersionTagExp("(kile-version:\\s*)(\\d+\\.\\d+(.\\d+)?)"); 0095 if(requiredVersionTagExp.indexIn(firstLine) != -1) { 0096 QString requiredKileVersion = requiredVersionTagExp.cap(2); 0097 if(compareVersionStrings(requiredKileVersion, kileFullVersion) > 0) { 0098 KMessageBox::error(m_kileInfo->mainWindow(), i18n("Version %1 of Kile is at least required to execute the script \"%2\". The execution has been aborted.", 0099 requiredKileVersion, script->getName()), i18n("Version Error")); 0100 return; 0101 } 0102 } 0103 0104 // TODO only scripts with a current view can be started at this moment 0105 KTextEditor::View *view = m_kileInfo->viewManager()->currentTextView(); 0106 if(!view) { 0107 KMessageBox::error(m_kileInfo->mainWindow(), i18n("Cannot start the script: no view available"), i18n("Script Error")); 0108 return; 0109 } 0110 0111 // TODO setup script objects (with existing views at this moment) 0112 m_kileScriptView->setView(view); 0113 m_kileScriptDocument->setView(view); 0114 m_kileScriptObject->setScriptname(script->getName()); 0115 0116 // create environment for QtScript engine 0117 ScriptEnvironment env(m_kileInfo, m_kileScriptView, m_kileScriptDocument, m_kileScriptObject,m_enginePlugin); 0118 0119 env.execute(script); 0120 } 0121 0122 void Manager::executeScript(unsigned int id) 0123 { 0124 QMap<unsigned int, Script*>::iterator i = m_idScriptMap.find(id); 0125 if(i != m_idScriptMap.end()) { 0126 executeScript(*i); 0127 } 0128 } 0129 0130 const Script* Manager::getScript(unsigned int id) 0131 { 0132 QMap<unsigned int, Script*>::iterator i = m_idScriptMap.find(id); 0133 return ((i != m_idScriptMap.end()) ? (*i) : Q_NULLPTR); 0134 } 0135 0136 void Manager::scanScriptDirectories() 0137 { 0138 if(!KileConfig::scriptingEnabled()) { 0139 return; 0140 } 0141 deleteScripts(); 0142 populateDirWatch(); 0143 0144 KConfigGroup configGroup = m_config->group("Scripts"); 0145 const QList<unsigned int> idList = configGroup.readEntry("IDs", QList<unsigned int>()); 0146 unsigned int maxID = 0; 0147 QMap<QString, unsigned int> pathIDMap; 0148 QMap<unsigned int, bool> takenIDMap; 0149 for(const unsigned int i : idList) { 0150 // as of 12.07.2020, KConfigGroup::readPathEntry messes up the path if $HOME ends in / 0151 // for example, if HOME=/home/michel/, KConfigGroup::readPathEntry will return /home/michel//.local/share/kile/scripts/test.js, 0152 // resulting in the path /home/michel/.local/share/kile/scripts/test.js not being found; 0153 // we have used QDir:cleanPath to work around this 0154 QString fileName = QDir::cleanPath(configGroup.readPathEntry("Script" + QString::number(i), QString())); 0155 if(!fileName.isEmpty()) { 0156 unsigned int id = i; 0157 pathIDMap[fileName] = id; 0158 takenIDMap[id] = true; 0159 maxID = qMax(maxID, id); 0160 } 0161 } 0162 0163 // scan for *.js files 0164 QSet<QString> scriptFileNamesSet; 0165 { 0166 QSet<QString> canonicalScriptFileNamesSet; 0167 const QStringList dirs = KileUtilities::locateAll(QStandardPaths::AppDataLocation, "scripts/", QStandardPaths::LocateDirectory); 0168 for(const QString &dir : dirs) { 0169 QDirIterator it(dir, QStringList() << QStringLiteral("*.js"), QDir::Files | QDir::Readable, QDirIterator::Subdirectories); 0170 while(it.hasNext()) { 0171 const QString fileName = QDir::cleanPath(it.next()); 0172 const QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath(); 0173 0174 // filter out file paths that point to the same file (via symbolic links, for example) 0175 // but later on we work with the original file path, possibly containing symbolic links 0176 if(canonicalFilePath.isEmpty() || canonicalScriptFileNamesSet.contains(canonicalFilePath)) { 0177 continue; 0178 } 0179 canonicalScriptFileNamesSet.insert(canonicalFilePath); 0180 0181 scriptFileNamesSet.insert(fileName); 0182 } 0183 } 0184 } 0185 0186 for(const QString &scriptFileName : qAsConst(scriptFileNamesSet)) { 0187 registerScript(scriptFileName, pathIDMap, takenIDMap, maxID); 0188 } 0189 //rewrite the IDs that are currently in use 0190 writeIDs(); 0191 m_actionCollection->readSettings(); 0192 emit scriptsChanged(); 0193 } 0194 0195 void Manager::deleteScripts() 0196 { 0197 QList<Script*> scriptList = m_jScriptList; 0198 m_jScriptList.clear(); // pretend that there are no scripts left 0199 QStringList keySequenceList; 0200 for(QList<Script*>::iterator it = scriptList.begin(); it != scriptList.end(); ++it) { 0201 keySequenceList.push_back((*it)->getKeySequence()); 0202 } 0203 m_idScriptMap.clear(); 0204 m_kileInfo->editorKeySequenceManager()->removeKeySequence(keySequenceList); 0205 for(QList<Script*>::iterator it = scriptList.begin(); it != scriptList.end(); ++it) { 0206 QAction *action = (*it)->getActionObject(); 0207 if(action) { 0208 const QList<QWidget*> widgets = action->associatedWidgets(); 0209 for(QWidget *w : widgets) { 0210 w->removeAction(action); 0211 } 0212 m_actionCollection->takeAction(action); 0213 delete action; 0214 } 0215 delete *it; 0216 } 0217 emit scriptsChanged(); 0218 } 0219 0220 QList<Script*> Manager::getScripts() 0221 { 0222 return m_jScriptList; 0223 } 0224 0225 void Manager::registerScript(const QString& fileName, QMap<QString, unsigned int>& pathIDMap, QMap<unsigned int, bool>& takenIDMap, unsigned int &maxID) 0226 { 0227 unsigned int id; 0228 QMap<QString, unsigned int>::iterator it = pathIDMap.find(fileName); 0229 if(it != pathIDMap.end()) { 0230 id = *it; 0231 } 0232 else { 0233 id = findFreeID(takenIDMap, maxID); 0234 pathIDMap[fileName] = id; 0235 takenIDMap[id] = true; 0236 maxID = qMax(maxID, id); 0237 } 0238 Script* script = new Script(id, fileName); 0239 m_jScriptList.push_back(script); 0240 m_idScriptMap[id] = script; 0241 0242 // start with setting up the key sequence 0243 KConfigGroup configGroup = m_config->group("Scripts"); 0244 0245 int sequenceType = 0; 0246 QString editorKeySequence = QString(); 0247 QString seq = configGroup.readEntry("Script" + QString::number(id) + "KeySequence"); 0248 if(!seq.isEmpty()) { 0249 QRegExp re("(\\d+)-(.*)"); 0250 if(re.exactMatch(seq)) { 0251 sequenceType = re.cap(1).toInt(); 0252 if(sequenceType<Script::KEY_SEQUENCE || sequenceType>Script::KEY_SHORTCUT) { 0253 sequenceType = Script::KEY_SEQUENCE; 0254 } 0255 editorKeySequence = re.cap(2); 0256 } 0257 else { 0258 sequenceType = Script::KEY_SEQUENCE; 0259 editorKeySequence = re.cap(1); 0260 } 0261 } 0262 KILE_DEBUG_MAIN << "script type=" << sequenceType << " seq=" << editorKeySequence; 0263 0264 // now set up a regular action object 0265 ScriptExecutionAction *action = new ScriptExecutionAction(id, this, m_actionCollection); 0266 0267 // add to action collection 0268 m_actionCollection->addAction("script" + QString::number(id) + "_execution", action); 0269 m_actionCollection->setDefaultShortcut(action, QString()); 0270 script->setActionObject(action); 0271 0272 // action with shortcut? 0273 if(!editorKeySequence.isEmpty()) { 0274 script->setSequenceType(sequenceType); 0275 script->setKeySequence(editorKeySequence); 0276 if(sequenceType == Script::KEY_SEQUENCE) { 0277 m_kileInfo->editorKeySequenceManager()->addAction(editorKeySequence, new KileEditorKeySequence::ExecuteScriptAction(script, this)); 0278 } 0279 else { 0280 action->setShortcut(editorKeySequence); 0281 } 0282 } 0283 } 0284 0285 void Manager::writeConfig() 0286 { 0287 // don't delete the key sequence settings if scripting has been disabled 0288 if(!KileConfig::scriptingEnabled()) { 0289 return; 0290 } 0291 m_config->deleteGroup("Scripts"); 0292 writeIDs(); 0293 0294 // write the key sequences 0295 KConfigGroup configGroup = m_config->group("Scripts"); 0296 for(const Script *script : qAsConst(m_jScriptList)) { 0297 QString seq = script->getKeySequence(); 0298 QString sequenceEntry = (seq.isEmpty()) ? seq : QString("%1-%2").arg(QString::number(script->getSequenceType()), seq); 0299 configGroup.writeEntry("Script" + QString::number(script->getID()) + "KeySequence", sequenceEntry); 0300 } 0301 } 0302 0303 void Manager::setEditorKeySequence(Script* script, int type, const QString& keySequence) 0304 { 0305 if(keySequence.isEmpty()) { 0306 return; 0307 } 0308 if(script) { 0309 int oldType = script->getSequenceType(); 0310 QString oldSequence = script->getKeySequence(); 0311 if(oldType == type && oldSequence == keySequence) { 0312 return; 0313 } 0314 0315 if(oldType == KileScript::Script::KEY_SEQUENCE) { 0316 m_kileInfo->editorKeySequenceManager()->removeKeySequence(oldSequence); 0317 } 0318 else { 0319 script->getActionObject()->setShortcut(QKeySequence()); 0320 } 0321 script->setSequenceType(type); 0322 script->setKeySequence(keySequence); 0323 if(type == KileScript::Script::KEY_SEQUENCE) { 0324 m_kileInfo->editorKeySequenceManager()->addAction(keySequence, new KileEditorKeySequence::ExecuteScriptAction(script, this)); 0325 } 0326 else { 0327 script->getActionObject()->setShortcut(keySequence); 0328 } 0329 } 0330 } 0331 0332 void Manager::setShortcut(Script* script, const QKeySequence& keySequence) 0333 { 0334 if(keySequence.isEmpty()) { 0335 return; 0336 } 0337 if(script) { 0338 if(script->getSequenceType() == KileScript::Script::KEY_SEQUENCE) { 0339 m_kileInfo->editorKeySequenceManager()->removeKeySequence(script->getKeySequence()); 0340 } 0341 0342 script->setSequenceType(KileScript::Script::KEY_SHORTCUT); 0343 script->setKeySequence(keySequence.toString(QKeySequence::PortableText)); 0344 script->getActionObject()->setShortcut(keySequence); 0345 } 0346 } 0347 0348 void Manager::removeEditorKeySequence(Script* script) 0349 { 0350 if(script) { 0351 QString keySequence = script->getKeySequence(); 0352 if(keySequence.isEmpty()) { 0353 return; 0354 } 0355 script->setKeySequence(QString()); 0356 0357 int sequenceType = script->getSequenceType(); 0358 if(sequenceType == Script::KEY_SEQUENCE) { 0359 m_kileInfo->editorKeySequenceManager()->removeKeySequence(keySequence); 0360 } 0361 else { 0362 script->getActionObject()->setShortcut(QString()); 0363 } 0364 0365 writeConfig(); 0366 } 0367 } 0368 0369 void Manager::populateDirWatch() 0370 { 0371 const QStringList jScriptDirectories = KileUtilities::locateAll(QStandardPaths::AppDataLocation, "scripts/", QStandardPaths::LocateDirectory); 0372 for(const QString& dir : jScriptDirectories) { 0373 // FIXME: future KDE versions could support the recursive 0374 // watching of directories out of the box. 0375 addDirectoryToDirWatch(dir); 0376 } 0377 //we do not remove the directories that were once added as this apparently causes some strange 0378 //bugs (on KDE 3.5.x) 0379 } 0380 0381 QString Manager::getLocalScriptDirectory() const 0382 { 0383 return m_localScriptDir; 0384 } 0385 0386 void Manager::readConfig() { 0387 deleteScripts(); 0388 scanScriptDirectories(); 0389 } 0390 0391 unsigned int Manager::findFreeID(const QMap<unsigned int, bool>& takenIDMap, unsigned int maxID) 0392 { 0393 if(takenIDMap.size() == 0) { 0394 return 0; 0395 } 0396 // maxID should have a real meaning now 0397 for(unsigned int i = 0; i < maxID; ++i) { 0398 if(takenIDMap.find(i) == takenIDMap.end()) { 0399 return i; 0400 } 0401 } 0402 return (maxID + 1); 0403 } 0404 0405 void Manager::writeIDs() 0406 { 0407 KConfigGroup configGroup = m_config->group("Scripts"); 0408 //delete old entries 0409 QList<unsigned int> idList = configGroup.readEntry("IDs", QList<unsigned int>()); 0410 for(const int i : qAsConst(idList)) { 0411 configGroup.deleteEntry("Script" + QString::number(i)); 0412 } 0413 //write new ones 0414 idList.clear(); 0415 for(QMap<unsigned int, Script*>::iterator i = m_idScriptMap.begin(); i != m_idScriptMap.end(); ++i) { 0416 unsigned int id = i.key(); 0417 idList.push_back(id); 0418 configGroup.writePathEntry("Script" + QString::number(id), (*i)->getFileName()); 0419 } 0420 configGroup.writeEntry("IDs", idList); 0421 } 0422 0423 void Manager::addDirectoryToDirWatch(const QString& dir) 0424 { 0425 //FIXME: no recursive watching and no watching of files as it isn't implemented 0426 // yet 0427 //FIXME: check for KDE4 0428 if(!m_jScriptDirWatch->contains(dir)) { 0429 m_jScriptDirWatch->addDir(dir, KDirWatch::WatchDirOnly); 0430 } 0431 QDir qDir(dir); 0432 const QStringList list = qDir.entryList(QDir::Dirs); 0433 for(const QString& subdir : list) { 0434 if(subdir != "." && subdir != "..") { 0435 addDirectoryToDirWatch(qDir.filePath(subdir)); 0436 } 0437 } 0438 } 0439 0440 void Manager::readEnginePlugin() 0441 { 0442 // TODO error message and disable scripting if not found 0443 QString pluginUrl = KileUtilities::locate(QStandardPaths::AppDataLocation, "script-plugins/cursor-range.js"); 0444 m_enginePlugin = Script::readFile(pluginUrl); 0445 } 0446 0447 void Manager::initScriptActions() 0448 { 0449 QStringList m_scriptActionList = QStringList() 0450 << "tag_chapter" << "tag_section" << "tag_subsection" 0451 << "tag_subsubsection" << "tag_paragraph" << "tag_subparagraph" 0452 0453 << "tag_label" << "tag_ref" << "tag_pageref" 0454 << "tag_index" << "tag_footnote" << "tag_cite" 0455 0456 << "tools_comment" << "tools_uncomment" << "tools_uppercase" 0457 << "tools_lowercase" << "tools_capitalize" << "tools_join_lines" 0458 0459 << "wizard_tabular" << "wizard_array" << "wizard_tabbing" 0460 << "wizard_float" << "wizard_mathenv" 0461 << "wizard_postscript" << "wizard_pdf" 0462 ; 0463 0464 0465 const QList<KXMLGUIClient*> clients = m_kileInfo->mainWindow()->guiFactory()->clients(); 0466 for(KXMLGUIClient *client : clients) { 0467 KILE_DEBUG_MAIN << "collection count: " << client->actionCollection()->count(); 0468 0469 const QList<QAction*> actions = client->actionCollection()->actions(); 0470 for(QAction *action : actions) { 0471 QString objectname = action->objectName(); 0472 if(m_scriptActionList.indexOf(objectname) >= 0) { 0473 m_scriptActionMap->insert(objectname, action); 0474 } 0475 } 0476 } 0477 } 0478 0479 0480 0481 ////////////////////////////// ScriptExecutionAction ////////////////////////////// 0482 0483 ScriptExecutionAction::ScriptExecutionAction(unsigned int id, KileScript::Manager *manager, QObject* parent) : QAction(parent), m_manager(manager), m_id(id) 0484 { 0485 const KileScript::Script *script = m_manager->getScript(m_id); 0486 Q_ASSERT(script); 0487 setText(i18n("Execution of %1", script->getName())); 0488 connect(this, SIGNAL(triggered()), this, SLOT(executeScript())); 0489 } 0490 0491 ScriptExecutionAction::~ScriptExecutionAction() 0492 { 0493 } 0494 0495 void ScriptExecutionAction::executeScript() 0496 { 0497 m_manager->executeScript(m_id); 0498 } 0499 0500 0501 0502 } 0503