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