1 /******************************************************************************
2   This source file is part of the Avogadro project.
3   This source code is released under the 3-Clause BSD License, (see "LICENSE").
4 ******************************************************************************/
5 
6 #include "interfacescript.h"
7 
8 #include <avogadro/core/coordinateblockgenerator.h>
9 #include <avogadro/core/molecule.h>
10 
11 #include <avogadro/io/fileformat.h>
12 #include <avogadro/io/fileformatmanager.h>
13 
14 #include <avogadro/qtgui/generichighlighter.h>
15 #include <avogadro/qtgui/molecule.h>
16 #include <avogadro/qtgui/pythonscript.h>
17 #include <avogadro/qtgui/rwmolecule.h>
18 
19 #include <QtCore/QDebug>
20 #include <QtCore/QFile>
21 #include <QtCore/QJsonArray>
22 #include <QtCore/QJsonDocument>
23 
24 namespace Avogadro {
25 namespace QtGui {
26 
27 using QtGui::GenericHighlighter;
28 using QtGui::PythonScript;
29 
InterfaceScript(const QString & scriptFilePath_,QObject * parent_)30 InterfaceScript::InterfaceScript(const QString& scriptFilePath_,
31                                  QObject* parent_)
32   : QObject(parent_), m_interpreter(new PythonScript(scriptFilePath_, this)),
33     m_moleculeExtension(QStringLiteral("Unknown"))
34 {}
35 
InterfaceScript(QObject * parent_)36 InterfaceScript::InterfaceScript(QObject* parent_)
37   : QObject(parent_), m_interpreter(new PythonScript(this)),
38     m_moleculeExtension(QStringLiteral("Unknown"))
39 {}
40 
~InterfaceScript()41 InterfaceScript::~InterfaceScript() {}
42 
debug() const43 bool InterfaceScript::debug() const
44 {
45   return m_interpreter->debug();
46 }
47 
options() const48 QJsonObject InterfaceScript::options() const
49 {
50   m_errors.clear();
51   if (m_options.isEmpty()) {
52     qDeleteAll(m_highlightStyles.values());
53     m_highlightStyles.clear();
54 
55     // Retrieve/set options
56     QByteArray json = m_interpreter->execute(
57       QStringList() << QStringLiteral("--print-options"));
58 
59     if (m_interpreter->hasErrors()) {
60       m_errors << m_interpreter->errorList();
61       return m_options;
62     }
63 
64     QJsonDocument doc;
65     if (!parseJson(json, doc))
66       return m_options;
67 
68     if (!doc.isObject()) {
69       m_errors << tr("script --print-options output must be an JSON object "
70                      "at top level. Received:\n%1")
71                     .arg(json.constData());
72       return m_options;
73     }
74 
75     m_options = doc.object();
76 
77     if (m_options.contains(QStringLiteral("highlightStyles")) &&
78         m_options.value(QStringLiteral("highlightStyles")).isArray()) {
79       if (!parseHighlightStyles(
80             m_options.value(QStringLiteral("highlightStyles")).toArray())) {
81         qDebug() << "Failed to parse highlighting styles.";
82       }
83     }
84   }
85 
86   // Check if the generator needs to read a molecule.
87   m_moleculeExtension = QLatin1String("cjson");
88   if (m_options.contains(QStringLiteral("inputMoleculeFormat")) &&
89       m_options[QStringLiteral("inputMoleculeFormat")].isString()) {
90     m_moleculeExtension =
91       m_options[QStringLiteral("inputMoleculeFormat")].toString();
92   }
93 
94   return m_options;
95 }
96 
displayName() const97 QString InterfaceScript::displayName() const
98 {
99   m_errors.clear();
100   if (m_displayName.isEmpty()) {
101     m_displayName = QString(m_interpreter->execute(
102       QStringList() << QStringLiteral("--display-name")));
103     m_errors << m_interpreter->errorList();
104     m_displayName = m_displayName.trimmed();
105   }
106 
107   return m_displayName;
108 }
109 
menuPath() const110 QString InterfaceScript::menuPath() const
111 {
112   m_errors.clear();
113   if (m_menuPath.isEmpty()) {
114     m_menuPath = QString(
115       m_interpreter->execute(QStringList() << QStringLiteral("--menu-path")));
116     m_errors << m_interpreter->errorList();
117     m_menuPath = m_menuPath.trimmed();
118   }
119 
120   return m_menuPath;
121 }
122 
scriptFilePath() const123 QString InterfaceScript::scriptFilePath() const
124 {
125   return m_interpreter->scriptFilePath();
126 }
127 
setScriptFilePath(const QString & scriptFile)128 void InterfaceScript::setScriptFilePath(const QString& scriptFile)
129 {
130   reset();
131   m_interpreter->setScriptFilePath(scriptFile);
132 }
133 
reset()134 void InterfaceScript::reset()
135 {
136   m_interpreter->setDefaultPythonInterpretor();
137   m_interpreter->setScriptFilePath(QString());
138   m_moleculeExtension = QLatin1String("Unknown");
139   m_displayName = QString();
140   m_options = QJsonObject();
141   m_warnings.clear();
142   m_errors.clear();
143   m_filenames.clear();
144   m_mainFileName.clear();
145   m_files.clear();
146   m_fileHighlighters.clear();
147   m_highlightStyles.clear();
148 }
149 
runCommand(const QJsonObject & options_,Core::Molecule * mol)150 bool InterfaceScript::runCommand(const QJsonObject& options_,
151                                  Core::Molecule* mol)
152 {
153   m_errors.clear();
154   m_warnings.clear();
155   m_filenames.clear();
156   qDeleteAll(m_fileHighlighters.values());
157   m_fileHighlighters.clear();
158   m_mainFileName.clear();
159   m_files.clear();
160 
161   // Add the molecule file to the options
162   QJsonObject allOptions(options_);
163   if (!insertMolecule(allOptions, *mol))
164     return false;
165 
166   connect(m_interpreter, &PythonScript::finished, this, &::Avogadro::QtGui::InterfaceScript::commandFinished);
167   m_interpreter->asyncExecute(QStringList() << QStringLiteral("--run-command"),
168                               QJsonDocument(allOptions).toJson());
169   return true;
170 }
171 
commandFinished()172 void InterfaceScript::commandFinished()
173 {
174   emit finished();
175 }
176 
processCommand(Core::Molecule * mol)177 bool InterfaceScript::processCommand(Core::Molecule* mol)
178 {
179   if (m_interpreter == nullptr)
180     return false;
181 
182   QByteArray json(m_interpreter->asyncResponse());
183 
184   if (m_interpreter->hasErrors()) {
185     m_errors << m_interpreter->errorList();
186     return false;
187   }
188 
189   QJsonDocument doc;
190   if (!parseJson(json, doc)) {
191     return false;
192   }
193 
194   // Update cache
195   bool result = true;
196   if (doc.isObject()) {
197     QJsonObject obj = doc.object();
198 
199     // Check for any warnings:
200     if (obj.contains(QStringLiteral("warnings"))) {
201       if (obj[QStringLiteral("warnings")].isArray()) {
202         foreach (const QJsonValue& warning, obj["warnings"].toArray()) {
203           if (warning.isString())
204             m_warnings << warning.toString();
205           else
206             m_errors << tr("Non-string warning returned.");
207         }
208       } else {
209         m_errors << tr("'warnings' member is not an array.");
210       }
211     }
212 
213     m_moleculeExtension = "cjson";
214     if (obj.contains("moleculeFormat") && obj["moleculeFormat"].isString()) {
215       m_moleculeExtension = obj["moleculeFormat"].toString();
216     }
217 
218     Io::FileFormatManager& formats = Io::FileFormatManager::instance();
219     QScopedPointer<Io::FileFormat> format(
220       formats.newFormatFromFileExtension(m_moleculeExtension.toStdString()));
221 
222     if (format.isNull()) {
223       m_errors << tr("Error reading molecule representation: "
224                      "Unrecognized file format: %1")
225                     .arg(m_moleculeExtension);
226       return false;
227     }
228 
229     QtGui::Molecule* guiMol = static_cast<QtGui::Molecule*>(mol);
230     QtGui::Molecule newMol(guiMol->parent());
231     if (m_moleculeExtension == "cjson") {
232       // convert the "cjson" field to a string
233       QJsonObject cjsonObj = obj["cjson"].toObject();
234       QJsonDocument doc(cjsonObj);
235       QString strCJSON(doc.toJson(QJsonDocument::Compact));
236       if (!strCJSON.isEmpty()) {
237         result = format->readString(strCJSON.toStdString(), newMol);
238       }
239     } else if (obj.contains(m_moleculeExtension) &&
240                obj[m_moleculeExtension].isString()) {
241       QString strFile = obj[m_moleculeExtension].toString();
242       result = format->readString(strFile.toStdString(), newMol);
243     }
244 
245     // check if the script wants us to perceive bonds first
246     if (obj["bond"].toBool()) {
247       newMol.perceiveBondsSimple();
248     }
249 
250     if (obj["append"].toBool()) { // just append some new bits
251       guiMol->undoMolecule()->appendMolecule(newMol, m_displayName);
252     } else { // replace the whole molecule
253       Molecule::MoleculeChanges changes = (Molecule::Atoms | Molecule::Bonds |
254                                            Molecule::Added | Molecule::Removed);
255       guiMol->undoMolecule()->modifyMolecule(newMol, changes, m_displayName);
256     }
257   }
258   return result;
259 }
260 
generateInput(const QJsonObject & options_,const Core::Molecule & mol)261 bool InterfaceScript::generateInput(const QJsonObject& options_,
262                                     const Core::Molecule& mol)
263 {
264   m_errors.clear();
265   m_warnings.clear();
266   m_filenames.clear();
267   qDeleteAll(m_fileHighlighters.values());
268   m_fileHighlighters.clear();
269   m_mainFileName.clear();
270   m_files.clear();
271 
272   // Add the molecule file to the options
273   QJsonObject allOptions(options_);
274   if (!insertMolecule(allOptions, mol))
275     return false;
276 
277   QByteArray json(
278     m_interpreter->execute(QStringList() << QStringLiteral("--generate-input"),
279                            QJsonDocument(allOptions).toJson()));
280 
281   if (m_interpreter->hasErrors()) {
282     m_errors << m_interpreter->errorList();
283     return false;
284   }
285 
286   QJsonDocument doc;
287   if (!parseJson(json, doc))
288     return false;
289 
290   // Update cache
291   bool result = true;
292   if (doc.isObject()) {
293     QJsonObject obj = doc.object();
294 
295     // Check for any warnings:
296     if (obj.contains(QStringLiteral("warnings"))) {
297       if (obj[QStringLiteral("warnings")].isArray()) {
298         foreach (const QJsonValue& warning, obj["warnings"].toArray()) {
299           if (warning.isString())
300             m_warnings << warning.toString();
301           else
302             m_errors << tr("Non-string warning returned.");
303         }
304       } else {
305         m_errors << tr("'warnings' member is not an array.");
306       }
307     }
308 
309     // Extract input file text:
310     if (obj.contains(QStringLiteral("files"))) {
311       if (obj[QStringLiteral("files")].isArray()) {
312         foreach (const QJsonValue& file, obj["files"].toArray()) {
313           if (file.isObject()) {
314             QJsonObject fileObj = file.toObject();
315             if (fileObj[QStringLiteral("filename")].isString()) {
316               QString fileName = fileObj[QStringLiteral("filename")].toString();
317               QString contents;
318               if (fileObj[QStringLiteral("contents")].isString()) {
319                 contents = fileObj[QStringLiteral("contents")].toString();
320               } else if (fileObj[QStringLiteral("filePath")].isString()) {
321                 QFile refFile(fileObj[QStringLiteral("filePath")].toString());
322                 if (refFile.exists() && refFile.open(QFile::ReadOnly)) {
323                   contents = QString(refFile.readAll());
324                 } else {
325                   contents = tr("Reference file '%1' does not exist.")
326                                .arg(refFile.fileName());
327                   m_warnings << tr("Error populating file %1: %2")
328                                   .arg(fileName, contents);
329                 }
330               } else {
331                 m_errors << tr("File '%1' poorly formed. Missing string "
332                                "'contents' or 'filePath' members.")
333                               .arg(fileName);
334                 contents = m_errors.back();
335                 result = false;
336               }
337               replaceKeywords(contents, mol);
338               m_filenames << fileName;
339               m_files.insert(fileObj[QStringLiteral("filename")].toString(),
340                              contents);
341 
342               // Concatenate the requested styles for this input file.
343               if (fileObj[QStringLiteral("highlightStyles")].isArray()) {
344                 GenericHighlighter* highlighter(new GenericHighlighter(this));
345                 foreach (const QJsonValue& styleVal,
346                          fileObj["highlightStyles"].toArray()) {
347                   if (styleVal.isString()) {
348                     QString styleName(styleVal.toString());
349                     if (m_highlightStyles.contains(styleName)) {
350                       *highlighter += *m_highlightStyles[styleName];
351                     } else {
352                       qDebug() << "Cannot find highlight style '" << styleName
353                                << "' for file '" << fileName << "'";
354                     }
355                   }
356                 }
357                 if (highlighter->ruleCount() > 0)
358                   m_fileHighlighters[fileName] = highlighter;
359                 else
360                   highlighter->deleteLater();
361               }
362             } else {
363               result = false;
364               m_errors << tr("Malformed file entry: filename/contents missing"
365                              " or not strings:\n%1")
366                             .arg(QString(QJsonDocument(fileObj).toJson()));
367             } // end if/else filename and contents are strings
368           } else {
369             result = false;
370             m_errors << tr("Malformed file entry at index %1: Not an object.")
371                           .arg(m_filenames.size());
372           } // end if/else file is JSON object
373         }   // end foreach file
374       } else {
375         result = false;
376         m_errors << tr("'files' member not an array.");
377       } // end if obj["files"] is JSON array
378     } else {
379       result = false;
380       m_errors << tr("'files' member missing.");
381     } // end if obj contains "files"
382 
383     // Extract main input filename:
384     if (obj.contains(QStringLiteral("mainFile"))) {
385       if (obj[QStringLiteral("mainFile")].isString()) {
386         QString mainFile = obj[QStringLiteral("mainFile")].toString();
387         if (m_filenames.contains(mainFile)) {
388           m_mainFileName = mainFile;
389         } else {
390           result = false;
391           m_errors << tr("'mainFile' member does not refer to an entry in "
392                          "'files'.");
393         } // end if/else mainFile is known
394       } else {
395         result = false;
396         m_errors << tr("'mainFile' member must be a string.");
397       } // end if/else mainFile is string
398     } else {
399       // If no mainFile is specified and there is only one file, use it as the
400       // main file. Otherwise, don't set a main input file -- all files will
401       // be treated as supplemental input files
402       if (m_filenames.size() == 1)
403         m_mainFileName = m_filenames.first();
404     } // end if/else object contains mainFile
405   } else {
406     result = false;
407     m_errors << tr("Response must be a JSON object at top-level.");
408   }
409 
410   if (result == false)
411     m_errors << tr("Script output:\n%1").arg(QString(json));
412 
413   return result;
414 }
415 
numberOfInputFiles() const416 int InterfaceScript::numberOfInputFiles() const
417 {
418   return m_filenames.size();
419 }
420 
fileNames() const421 QStringList InterfaceScript::fileNames() const
422 {
423   return m_filenames;
424 }
425 
mainFileName() const426 QString InterfaceScript::mainFileName() const
427 {
428   return m_mainFileName;
429 }
430 
fileContents(const QString & fileName) const431 QString InterfaceScript::fileContents(const QString& fileName) const
432 {
433   return m_files.value(fileName, QString());
434 }
435 
createFileHighlighter(const QString & fileName) const436 GenericHighlighter* InterfaceScript::createFileHighlighter(
437   const QString& fileName) const
438 {
439   GenericHighlighter* toClone(m_fileHighlighters.value(fileName, nullptr));
440   return toClone ? new GenericHighlighter(*toClone) : toClone;
441 }
442 
setDebug(bool d)443 void InterfaceScript::setDebug(bool d)
444 {
445   m_interpreter->setDebug(d);
446 }
447 
parseJson(const QByteArray & json,QJsonDocument & doc) const448 bool InterfaceScript::parseJson(const QByteArray& json,
449                                 QJsonDocument& doc) const
450 {
451   QJsonParseError error;
452   doc = QJsonDocument::fromJson(json, &error);
453 
454   if (error.error != QJsonParseError::NoError) {
455     m_errors << tr("Parse error at offset %L1: '%2'\nRaw JSON:\n\n%3")
456                   .arg(error.offset)
457                   .arg(error.errorString())
458                   .arg(QString(json));
459     return false;
460   }
461   return true;
462 }
463 
insertMolecule(QJsonObject & json,const Core::Molecule & mol) const464 bool InterfaceScript::insertMolecule(QJsonObject& json,
465                                      const Core::Molecule& mol) const
466 {
467   // Update the cached options if the format is not set
468   if (m_moleculeExtension == QLatin1String("Unknown"))
469     options();
470 
471   if (m_moleculeExtension == QLatin1String("None"))
472     return true;
473 
474   // insert the selected atoms
475   QJsonArray selectedList;
476   for (Index i = 0; i < mol.atomCount(); ++i) {
477     if (mol.atomSelected(i))
478       selectedList.append(static_cast<qint64>(i));
479   }
480   json.insert("selectedatoms", selectedList);
481 
482   Io::FileFormatManager& formats = Io::FileFormatManager::instance();
483   QScopedPointer<Io::FileFormat> format(
484     formats.newFormatFromFileExtension(m_moleculeExtension.toStdString()));
485 
486   if (format.isNull()) {
487     m_errors << tr("Error writing molecule representation to string: "
488                    "Unrecognized file format: %1")
489                   .arg(m_moleculeExtension);
490     return false;
491   }
492 
493   std::string str;
494   if (!format->writeString(str, mol)) {
495     m_errors << tr("Error writing molecule representation to string: %1")
496                   .arg(QString::fromStdString(format->error()));
497     return false;
498   }
499 
500   if (m_moleculeExtension != QLatin1String("cjson")) {
501     json.insert(m_moleculeExtension, QJsonValue(QString::fromStdString(str)));
502   } else {
503     // If cjson was requested, embed the actual JSON, rather than the string.
504     QJsonParseError error;
505     QJsonDocument doc = QJsonDocument::fromJson(str.c_str(), &error);
506     if (error.error != QJsonParseError::NoError) {
507       m_errors << tr("Error generating cjson object: Parse error at offset %1: "
508                      "%2\nRaw JSON:\n\n%3")
509                     .arg(error.offset)
510                     .arg(error.errorString())
511                     .arg(QString::fromStdString(str));
512       return false;
513     }
514 
515     if (!doc.isObject()) {
516       m_errors << tr("Error generator cjson object: Parsed JSON is not an "
517                      "object:\n%1")
518                     .arg(QString::fromStdString(str));
519       return false;
520     }
521 
522     json.insert(m_moleculeExtension, doc.object());
523   }
524 
525   return true;
526 }
527 
generateCoordinateBlock(const QString & spec,const Core::Molecule & mol) const528 QString InterfaceScript::generateCoordinateBlock(
529   const QString& spec, const Core::Molecule& mol) const
530 {
531   Core::CoordinateBlockGenerator gen;
532   gen.setMolecule(&mol);
533   gen.setSpecification(spec.toStdString());
534   std::string tmp(gen.generateCoordinateBlock());
535   if (!tmp.empty())
536     tmp.resize(tmp.size() - 1); // Pop off the trailing newline
537   return QString::fromStdString(tmp);
538 }
539 
replaceKeywords(QString & str,const Core::Molecule & mol) const540 void InterfaceScript::replaceKeywords(QString& str,
541                                       const Core::Molecule& mol) const
542 {
543   // Simple keywords:
544   str.replace(QLatin1String("$$atomCount$$"), QString::number(mol.atomCount()));
545   str.replace(QLatin1String("$$bondCount$$"), QString::number(mol.bondCount()));
546 
547   // Find each coordinate block keyword in the file, then generate and replace
548   // it with the appropriate values.
549   QRegExp coordParser("\\$\\$coords:([^\\$]*)\\$\\$");
550   int ind = 0;
551   while ((ind = coordParser.indexIn(str, ind)) != -1) {
552     // Extract spec and prepare the replacement
553     const QString keyword = coordParser.cap(0);
554     const QString spec = coordParser.cap(1);
555 
556     // Replace all blocks with this signature
557     str.replace(keyword, generateCoordinateBlock(spec, mol));
558 
559   } // end for coordinate block
560 }
561 
parseHighlightStyles(const QJsonArray & json) const562 bool InterfaceScript::parseHighlightStyles(const QJsonArray& json) const
563 {
564   bool result(true);
565   foreach (QJsonValue styleVal, json) {
566     if (!styleVal.isObject()) {
567       qDebug() << "Non-object in highlightStyles array.";
568       result = false;
569       continue;
570     }
571     QJsonObject styleObj(styleVal.toObject());
572 
573     if (!styleObj.contains(QStringLiteral("style"))) {
574       qDebug() << "Style object missing 'style' member.";
575       result = false;
576       continue;
577     }
578     if (!styleObj.value(QStringLiteral("style")).isString()) {
579       qDebug() << "Style object contains non-string 'style' member.";
580       result = false;
581       continue;
582     }
583     QString styleName(styleObj.value(QStringLiteral("style")).toString());
584 
585     if (m_highlightStyles.contains(styleName)) {
586       qDebug() << "Duplicate highlight style: " << styleName;
587       result = false;
588       continue;
589     }
590 
591     if (!styleObj.contains(QStringLiteral("rules"))) {
592       qDebug() << "Style object" << styleName << "missing 'rules' member.";
593       result = false;
594       continue;
595     }
596     if (!styleObj.value(QStringLiteral("rules")).isArray()) {
597       qDebug() << "Style object" << styleName
598                << "contains non-array 'rules' member.";
599       result = false;
600       continue;
601     }
602     QJsonArray rulesArray(styleObj.value(QStringLiteral("rules")).toArray());
603 
604     GenericHighlighter* highlighter(
605       new GenericHighlighter(const_cast<InterfaceScript*>(this)));
606     if (!parseRules(rulesArray, *highlighter)) {
607       qDebug() << "Error parsing style" << styleName << endl
608                << QString(QJsonDocument(styleObj).toJson());
609       highlighter->deleteLater();
610       result = false;
611       continue;
612     }
613     m_highlightStyles.insert(styleName, highlighter);
614   }
615 
616   return result;
617 }
618 
parseRules(const QJsonArray & json,GenericHighlighter & highligher) const619 bool InterfaceScript::parseRules(const QJsonArray& json,
620                                  GenericHighlighter& highligher) const
621 {
622   bool result(true);
623   foreach (QJsonValue ruleVal, json) {
624     if (!ruleVal.isObject()) {
625       qDebug() << "Rule is not an object.";
626       result = false;
627       continue;
628     }
629     QJsonObject ruleObj(ruleVal.toObject());
630 
631     if (!ruleObj.contains(QStringLiteral("patterns"))) {
632       qDebug() << "Rule missing 'patterns' array:" << endl
633                << QString(QJsonDocument(ruleObj).toJson());
634       result = false;
635       continue;
636     }
637     if (!ruleObj.value(QStringLiteral("patterns")).isArray()) {
638       qDebug() << "Rule 'patterns' member is not an array:" << endl
639                << QString(QJsonDocument(ruleObj).toJson());
640       result = false;
641       continue;
642     }
643     QJsonArray patternsArray(
644       ruleObj.value(QStringLiteral("patterns")).toArray());
645 
646     if (!ruleObj.contains(QStringLiteral("format"))) {
647       qDebug() << "Rule missing 'format' object:" << endl
648                << QString(QJsonDocument(ruleObj).toJson());
649       result = false;
650       continue;
651     }
652     if (!ruleObj.value(QStringLiteral("format")).isObject()) {
653       qDebug() << "Rule 'format' member is not an object:" << endl
654                << QString(QJsonDocument(ruleObj).toJson());
655       result = false;
656       continue;
657     }
658     QJsonObject formatObj(ruleObj.value(QStringLiteral("format")).toObject());
659 
660     GenericHighlighter::Rule& rule = highligher.addRule();
661 
662     foreach (QJsonValue patternVal, patternsArray) {
663       QRegExp pattern;
664       if (!parsePattern(patternVal, pattern)) {
665         qDebug() << "Error while parsing pattern:" << endl
666                  << QString(QJsonDocument(patternVal.toObject()).toJson());
667         result = false;
668         continue;
669       }
670       rule.addPattern(pattern);
671     }
672 
673     QTextCharFormat format;
674     if (!parseFormat(formatObj, format)) {
675       qDebug() << "Error while parsing format:" << endl
676                << QString(QJsonDocument(formatObj).toJson());
677       result = false;
678     }
679     rule.setFormat(format);
680   }
681 
682   return result;
683 }
684 
parseFormat(const QJsonObject & json,QTextCharFormat & format) const685 bool InterfaceScript::parseFormat(const QJsonObject& json,
686                                   QTextCharFormat& format) const
687 {
688   // Check for presets first:
689   if (json.contains(QStringLiteral("preset"))) {
690     if (!json[QStringLiteral("preset")].isString()) {
691       qDebug() << "Preset is not a string.";
692       return false;
693     }
694 
695     QString preset(json[QStringLiteral("preset")].toString());
696     /// @todo Store presets in a singleton that can be configured in the GUI,
697     /// rather than hardcoding them.
698     if (preset == QLatin1String("title")) {
699       format.setFontFamily(QStringLiteral("serif"));
700       format.setForeground(Qt::darkGreen);
701       format.setFontWeight(QFont::Bold);
702     } else if (preset == QLatin1String("keyword")) {
703       format.setFontFamily(QStringLiteral("mono"));
704       format.setForeground(Qt::darkBlue);
705     } else if (preset == QLatin1String("property")) {
706       format.setFontFamily(QStringLiteral("mono"));
707       format.setForeground(Qt::darkRed);
708     } else if (preset == QLatin1String("literal")) {
709       format.setFontFamily(QStringLiteral("mono"));
710       format.setForeground(Qt::darkMagenta);
711     } else if (preset == QLatin1String("comment")) {
712       format.setFontFamily(QStringLiteral("serif"));
713       format.setForeground(Qt::darkGreen);
714       format.setFontItalic(true);
715     } else {
716       qDebug() << "Invalid style preset: " << preset;
717       return false;
718     }
719     return true;
720   }
721 
722   // Extract an RGB tuple from 'array' as a QBrush:
723   struct
724   {
725     QBrush operator()(const QJsonArray& array, bool* ok)
726     {
727       *ok = false;
728       QBrush result;
729 
730       if (array.size() != 3)
731         return result;
732 
733       int rgb[3];
734       for (int i = 0; i < 3; ++i) {
735         if (!array.at(i).isDouble())
736           return result;
737         rgb[i] = static_cast<int>(array.at(i).toDouble());
738         if (rgb[i] < 0 || rgb[i] > 255) {
739           qDebug() << "Warning: Color component value invalid: " << rgb[i]
740                    << " (Valid range is 0-255).";
741         }
742       }
743 
744       result.setColor(QColor(rgb[0], rgb[1], rgb[2]));
745       result.setStyle(Qt::SolidPattern);
746       *ok = true;
747       return result;
748     }
749   } colorParser;
750 
751   if (json.contains(QStringLiteral("foreground")) &&
752       json.value(QStringLiteral("foreground")).isArray()) {
753     QJsonArray foregroundArray(
754       json.value(QStringLiteral("foreground")).toArray());
755     bool ok;
756     format.setForeground(colorParser(foregroundArray, &ok));
757     if (!ok)
758       return false;
759   }
760 
761   if (json.contains(QStringLiteral("background")) &&
762       json.value(QStringLiteral("background")).isArray()) {
763     QJsonArray backgroundArray(
764       json.value(QStringLiteral("background")).toArray());
765     bool ok;
766     format.setBackground(colorParser(backgroundArray, &ok));
767     if (!ok)
768       return false;
769   }
770 
771   if (json.contains(QStringLiteral("attributes")) &&
772       json.value(QStringLiteral("attributes")).isArray()) {
773     QJsonArray attributesArray(
774       json.value(QStringLiteral("attributes")).toArray());
775     format.setFontWeight(attributesArray.contains(QLatin1String("bold"))
776                            ? QFont::Bold
777                            : QFont::Normal);
778     format.setFontItalic(attributesArray.contains(QLatin1String("italic")));
779     format.setFontUnderline(
780       attributesArray.contains(QLatin1String("underline")));
781   }
782 
783   if (json.contains(QStringLiteral("family")) &&
784       json.value(QStringLiteral("family")).isString()) {
785     format.setFontFamily(json.value(QStringLiteral("family")).toString());
786   }
787 
788   return true;
789 }
790 
parsePattern(const QJsonValue & json,QRegExp & pattern) const791 bool InterfaceScript::parsePattern(const QJsonValue& json,
792                                    QRegExp& pattern) const
793 {
794   if (!json.isObject())
795     return false;
796 
797   QJsonObject patternObj(json.toObject());
798 
799   if (patternObj.contains(QStringLiteral("regexp")) &&
800       patternObj.value(QStringLiteral("regexp")).isString()) {
801     pattern.setPatternSyntax(QRegExp::RegExp2);
802     pattern.setPattern(patternObj.value(QStringLiteral("regexp")).toString());
803   } else if (patternObj.contains(QStringLiteral("wildcard")) &&
804              patternObj.value(QStringLiteral("wildcard")).isString()) {
805     pattern.setPatternSyntax(QRegExp::WildcardUnix);
806     pattern.setPattern(patternObj.value(QStringLiteral("wildcard")).toString());
807   } else if (patternObj.contains(QStringLiteral("string")) &&
808              patternObj.value(QStringLiteral("string")).isString()) {
809     pattern.setPatternSyntax(QRegExp::FixedString);
810     pattern.setPattern(patternObj.value(QStringLiteral("string")).toString());
811   } else {
812     return false;
813   }
814 
815   if (patternObj.contains(QStringLiteral("caseSensitive"))) {
816     pattern.setCaseSensitivity(
817       patternObj.value(QStringLiteral("caseSensitive")).toBool(true)
818         ? Qt::CaseSensitive
819         : Qt::CaseInsensitive);
820   }
821 
822   return true;
823 }
824 
825 } // namespace QtGui
826 } // namespace Avogadro
827