From 0ea466579d96472c47f76b480be6d860e01e6115 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 5 Oct 2016 11:56:16 -0400 Subject: [PATCH 1/7] Add support for float/double input fields for JSON dialogs. --- avogadro/molequeue/inputgeneratorwidget.cpp | 30 +++++++++++++++++++++ avogadro/molequeue/inputgeneratorwidget.h | 1 + 2 files changed, 31 insertions(+) diff --git a/avogadro/molequeue/inputgeneratorwidget.cpp b/avogadro/molequeue/inputgeneratorwidget.cpp index 2bbaeb1cb..e01d8a90b 100644 --- a/avogadro/molequeue/inputgeneratorwidget.cpp +++ b/avogadro/molequeue/inputgeneratorwidget.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -835,6 +836,8 @@ QWidget *InputGeneratorWidget::createOptionWidget(const QJsonValue &option) return createFilePathWidget(obj); else if (type == "integer") return createIntegerWidget(obj); + else if (type == "float") + return createFloatWidget(obj); else if (type == "boolean") return createBooleanWidget(obj); @@ -906,6 +909,33 @@ QWidget *InputGeneratorWidget::createIntegerWidget(const QJsonObject &obj) return spin; } +QWidget *InputGeneratorWidget::createFloatWidget(const QJsonObject &obj) +{ + QDoubleSpinBox *spin = new QDoubleSpinBox(this); + if (obj.contains("minimum") && + obj.value("minimum").isDouble()) { + spin->setMinimum(obj["minimum"].toDouble()); + } + if (obj.contains("maximum") && + obj.value("maximum").isDouble()) { + spin->setMaximum(obj["maximum"].toDouble()); + } + if (obj.contains("precision") && + obj.value("precision").isDouble()) { + spin->setDecimals(static_cast(obj["precision"].toDouble())); + } + if (obj.contains("prefix") && + obj.value("prefix").isString()) { + spin->setPrefix(obj["prefix"].toString()); + } + if (obj.contains("suffix") && + obj.value("suffix").isString()) { + spin->setSuffix(obj["suffix"].toString()); + } + connect(spin, SIGNAL(valueChanged(double)), SLOT(updatePreviewText())); + return spin; +} + QWidget *InputGeneratorWidget::createBooleanWidget(const QJsonObject &obj) { Q_UNUSED(obj); diff --git a/avogadro/molequeue/inputgeneratorwidget.h b/avogadro/molequeue/inputgeneratorwidget.h index f392cf775..9a46932cd 100644 --- a/avogadro/molequeue/inputgeneratorwidget.h +++ b/avogadro/molequeue/inputgeneratorwidget.h @@ -242,6 +242,7 @@ private slots: QWidget* createStringWidget(const QJsonObject &obj); QWidget* createFilePathWidget(const QJsonObject &obj); QWidget* createIntegerWidget(const QJsonObject &obj); + QWidget* createFloatWidget(const QJsonObject &obj); QWidget* createBooleanWidget(const QJsonObject &obj); /**@}*/ From 0c40d5624461ce02ddf22991ffef8c59b910882d Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Sun, 20 Nov 2016 19:48:46 -0500 Subject: [PATCH 2/7] Fix hydrogentools macro declaration, now that it's in QtGUI directory. --- avogadro/qtgui/hydrogentools.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avogadro/qtgui/hydrogentools.h b/avogadro/qtgui/hydrogentools.h index f0adb8374..44fd70267 100644 --- a/avogadro/qtgui/hydrogentools.h +++ b/avogadro/qtgui/hydrogentools.h @@ -29,7 +29,7 @@ namespace QtGui { class RWAtom; class RWMolecule; -class AVOGADROCORE_EXPORT HydrogenTools +class AVOGADROQTGUI_EXPORT HydrogenTools { public: From 7f772f617c73745cacec3deba2d27bf241f0bfe9 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Fri, 21 Oct 2016 20:50:45 -0400 Subject: [PATCH 3/7] Refactor InputGenerator to QtGUI to share with multiple interfaces. Now named InterfaceScript and InterfaceWidget Adds a Workflow plugin class that allows scripts to modify the current molecule, including a flexible menu path (e.g., workflows can go anywhere in the menu) --- avogadro/molequeue/CMakeLists.txt | 2 - avogadro/qtgui/CMakeLists.txt | 4 + .../interfacescript.cpp} | 145 +++- .../interfacescript.h} | 55 +- avogadro/qtgui/interfacewidget.cpp | 732 ++++++++++++++++++ avogadro/qtgui/interfacewidget.h | 188 +++++ avogadro/qtplugins/CMakeLists.txt | 1 + avogadro/qtplugins/workflows/CMakeLists.txt | 34 + avogadro/qtplugins/workflows/scripts/scale.py | 97 +++ avogadro/qtplugins/workflows/scripts/test.py | 171 ++++ avogadro/qtplugins/workflows/workflow.cpp | 317 ++++++++ avogadro/qtplugins/workflows/workflow.h | 100 +++ 12 files changed, 1796 insertions(+), 50 deletions(-) rename avogadro/{molequeue/inputgenerator.cpp => qtgui/interfacescript.cpp} (83%) rename avogadro/{molequeue/inputgenerator.h => qtgui/interfacescript.h} (92%) create mode 100644 avogadro/qtgui/interfacewidget.cpp create mode 100644 avogadro/qtgui/interfacewidget.h create mode 100644 avogadro/qtplugins/workflows/CMakeLists.txt create mode 100755 avogadro/qtplugins/workflows/scripts/scale.py create mode 100755 avogadro/qtplugins/workflows/scripts/test.py create mode 100644 avogadro/qtplugins/workflows/workflow.cpp create mode 100644 avogadro/qtplugins/workflows/workflow.h diff --git a/avogadro/molequeue/CMakeLists.txt b/avogadro/molequeue/CMakeLists.txt index 43a919549..e65bc39d0 100644 --- a/avogadro/molequeue/CMakeLists.txt +++ b/avogadro/molequeue/CMakeLists.txt @@ -11,7 +11,6 @@ find_package(Qt5 COMPONENTS Widgets Network REQUIRED) set(HEADERS batchjob.h - inputgenerator.h inputgeneratordialog.h inputgeneratorwidget.h molequeuedialog.h @@ -22,7 +21,6 @@ set(HEADERS set(SOURCES batchjob.cpp - inputgenerator.cpp inputgeneratordialog.cpp inputgeneratorwidget.cpp molequeuedialog.cpp diff --git a/avogadro/qtgui/CMakeLists.txt b/avogadro/qtgui/CMakeLists.txt index d33fbc792..016f3ed64 100644 --- a/avogadro/qtgui/CMakeLists.txt +++ b/avogadro/qtgui/CMakeLists.txt @@ -36,6 +36,8 @@ set(HEADERS fileformatdialog.h generichighlighter.h hydrogentools.h + interfacescript.h + interfacewidget.h meshgenerator.h molecule.h moleculemodel.h @@ -63,6 +65,8 @@ set(SOURCES fileformatdialog.cpp generichighlighter.cpp hydrogentools.cpp + interfacescript.cpp + interfacewidget.cpp meshgenerator.cpp molecule.cpp moleculemodel.cpp diff --git a/avogadro/molequeue/inputgenerator.cpp b/avogadro/qtgui/interfacescript.cpp similarity index 83% rename from avogadro/molequeue/inputgenerator.cpp rename to avogadro/qtgui/interfacescript.cpp index 51f53d3ee..450e72d0a 100644 --- a/avogadro/molequeue/inputgenerator.cpp +++ b/avogadro/qtgui/interfacescript.cpp @@ -14,16 +14,19 @@ ******************************************************************************/ -#include "inputgenerator.h" +#include "interfacescript.h" #include #include #include #include +#include #include #include +#include +#include #include #include @@ -31,35 +34,35 @@ #include namespace Avogadro { -namespace MoleQueue { +namespace QtGui { using QtGui::PythonScript; using QtGui::GenericHighlighter; -InputGenerator::InputGenerator(const QString &scriptFilePath_, QObject *parent_) +InterfaceScript::InterfaceScript(const QString &scriptFilePath_, QObject *parent_) : QObject(parent_), m_interpreter(new PythonScript(scriptFilePath_, this)), - m_moleculeExtension("Unknown") + m_moleculeExtension("cjson") { } -InputGenerator::InputGenerator(QObject *parent_) +InterfaceScript::InterfaceScript(QObject *parent_) : QObject(parent_), m_interpreter(new PythonScript(this)), - m_moleculeExtension("Unknown") + m_moleculeExtension("cjson") { } -InputGenerator::~InputGenerator() +InterfaceScript::~InterfaceScript() { } -bool InputGenerator::debug() const +bool InterfaceScript::debug() const { return m_interpreter->debug(); } -QJsonObject InputGenerator::options() const +QJsonObject InterfaceScript::options() const { m_errors.clear(); if (m_options.isEmpty()) { @@ -88,7 +91,7 @@ QJsonObject InputGenerator::options() const m_options = doc.object(); // Check if the generator needs to read a molecule. - m_moleculeExtension = "None"; + m_moleculeExtension = "cjson"; if (m_options.contains("inputMoleculeFormat") && m_options["inputMoleculeFormat"].isString()) { m_moleculeExtension = m_options["inputMoleculeFormat"].toString(); @@ -105,7 +108,7 @@ QJsonObject InputGenerator::options() const return m_options; } -QString InputGenerator::displayName() const +QString InterfaceScript::displayName() const { m_errors.clear(); if (m_displayName.isEmpty()) { @@ -118,18 +121,31 @@ QString InputGenerator::displayName() const return m_displayName; } -QString InputGenerator::scriptFilePath() const +QString InterfaceScript::menuPath() const +{ + m_errors.clear(); + if (m_menuPath.isEmpty()) { + m_menuPath = QString(m_interpreter->execute( + QStringList() << "--menu-path")); + m_errors << m_interpreter->errorList(); + m_menuPath = m_menuPath.trimmed(); + } + + return m_menuPath; +} + +QString InterfaceScript::scriptFilePath() const { return m_interpreter->scriptFilePath(); } -void InputGenerator::setScriptFilePath(const QString &scriptFile) +void InterfaceScript::setScriptFilePath(const QString &scriptFile) { reset(); m_interpreter->setScriptFilePath(scriptFile); } -void InputGenerator::reset() +void InterfaceScript::reset() { m_interpreter->setDefaultPythonInterpretor(); m_interpreter->setScriptFilePath(QString()); @@ -145,7 +161,74 @@ void InputGenerator::reset() m_highlightStyles.clear(); } -bool InputGenerator::generateInput(const QJsonObject &options_, +bool InterfaceScript::runWorkflow(const QJsonObject &options_, + Core::Molecule &mol) +{ + m_errors.clear(); + m_warnings.clear(); + m_filenames.clear(); + qDeleteAll(m_fileHighlighters.values()); + m_fileHighlighters.clear(); + m_mainFileName.clear(); + m_files.clear(); + + // Add the molecule file to the options + QJsonObject allOptions(options_); + if (!insertMolecule(allOptions, mol)) + return false; + + QByteArray json(m_interpreter->execute(QStringList() << "--run-workflow", + QJsonDocument(allOptions).toJson())); + + if (m_interpreter->hasErrors()) { + m_errors << m_interpreter->errorList(); + return false; + } + + QJsonDocument doc; + if (!parseJson(json, doc)) + return false; + + // Update cache + bool result = true; + if (doc.isObject()) { + QJsonObject obj = doc.object(); + + // Check for any warnings: + if (obj.contains("warnings")) { + if (obj["warnings"].isArray()) { + foreach (const QJsonValue &warning, obj["warnings"].toArray()) { + if (warning.isString()) + m_warnings << warning.toString(); + else + m_errors << tr("Non-string warning returned."); + } + } + else { + m_errors << tr("'warnings' member is not an array."); + } + } + + // TODO: add undo / redo and smart updates + Io::FileFormatManager &formats = Io::FileFormatManager::instance(); + QScopedPointer format(formats.newFormatFromFileExtension( + "cjson")); + // convert the "cjson" field to a string + QJsonObject cjsonObj = obj["cjson"].toObject(); + QJsonDocument doc(cjsonObj); + QString strCJSON(doc.toJson(QJsonDocument::Compact)); + if (!strCJSON.isEmpty()) { + result = format->readString(strCJSON.toStdString(), mol); + // TODO: need to indicate changes to the QtGui Molecule + Molecule::MoleculeChanges changes = + (Molecule::Atoms | Molecule::Bonds | Molecule::Added | Molecule::Removed); + // guiMol.undoMolecule()->modifyMolecule(newMol, changes, "Run Script"); + } + } + return result; +} + +bool InterfaceScript::generateInput(const QJsonObject &options_, const Core::Molecule &mol) { m_errors.clear(); @@ -312,39 +395,39 @@ bool InputGenerator::generateInput(const QJsonObject &options_, return result; } -int InputGenerator::numberOfInputFiles() const +int InterfaceScript::numberOfInputFiles() const { return m_filenames.size(); } -QStringList InputGenerator::fileNames() const +QStringList InterfaceScript::fileNames() const { return m_filenames; } -QString InputGenerator::mainFileName() const +QString InterfaceScript::mainFileName() const { return m_mainFileName; } -QString InputGenerator::fileContents(const QString &fileName) const +QString InterfaceScript::fileContents(const QString &fileName) const { return m_files.value(fileName, QString()); } GenericHighlighter * -InputGenerator::createFileHighlighter(const QString &fileName) const +InterfaceScript::createFileHighlighter(const QString &fileName) const { GenericHighlighter *toClone(m_fileHighlighters.value(fileName, NULL)); return toClone ? new GenericHighlighter(*toClone) : toClone; } -void InputGenerator::setDebug(bool d) +void InterfaceScript::setDebug(bool d) { m_interpreter->setDebug(d); } -bool InputGenerator::parseJson(const QByteArray &json, QJsonDocument &doc) const +bool InterfaceScript::parseJson(const QByteArray &json, QJsonDocument &doc) const { QJsonParseError error; doc = QJsonDocument::fromJson(json, &error); @@ -357,7 +440,7 @@ bool InputGenerator::parseJson(const QByteArray &json, QJsonDocument &doc) const return true; } -bool InputGenerator::insertMolecule(QJsonObject &json, +bool InterfaceScript::insertMolecule(QJsonObject &json, const Core::Molecule &mol) const { // Update the cached options if the format is not set @@ -410,7 +493,7 @@ bool InputGenerator::insertMolecule(QJsonObject &json, return true; } -QString InputGenerator::generateCoordinateBlock(const QString &spec, +QString InterfaceScript::generateCoordinateBlock(const QString &spec, const Core::Molecule &mol) const { Core::CoordinateBlockGenerator gen; @@ -422,7 +505,7 @@ QString InputGenerator::generateCoordinateBlock(const QString &spec, return QString::fromStdString(tmp); } -void InputGenerator::replaceKeywords(QString &str, +void InterfaceScript::replaceKeywords(QString &str, const Core::Molecule &mol) const { // Simple keywords: @@ -444,7 +527,7 @@ void InputGenerator::replaceKeywords(QString &str, } // end for coordinate block } -bool InputGenerator::parseHighlightStyles(const QJsonArray &json) const +bool InterfaceScript::parseHighlightStyles(const QJsonArray &json) const { bool result(true); foreach (QJsonValue styleVal, json) { @@ -487,7 +570,7 @@ bool InputGenerator::parseHighlightStyles(const QJsonArray &json) const QJsonArray rulesArray(styleObj.value("rules").toArray()); GenericHighlighter *highlighter(new GenericHighlighter( - const_cast(this))); + const_cast(this))); if (!parseRules(rulesArray, *highlighter)) { qDebug() << "Error parsing style" << styleName << endl << QString(QJsonDocument(styleObj).toJson()); @@ -501,7 +584,7 @@ bool InputGenerator::parseHighlightStyles(const QJsonArray &json) const return result; } -bool InputGenerator::parseRules(const QJsonArray &json, +bool InterfaceScript::parseRules(const QJsonArray &json, GenericHighlighter &highligher) const { bool result(true); @@ -566,7 +649,7 @@ bool InputGenerator::parseRules(const QJsonArray &json, return result; } -bool InputGenerator::parseFormat(const QJsonObject &json, +bool InterfaceScript::parseFormat(const QJsonObject &json, QTextCharFormat &format) const { // Check for presets first: @@ -672,7 +755,7 @@ bool InputGenerator::parseFormat(const QJsonObject &json, return true; } -bool InputGenerator::parsePattern(const QJsonValue &json, +bool InterfaceScript::parsePattern(const QJsonValue &json, QRegExp &pattern) const { if (!json.isObject()) @@ -707,5 +790,5 @@ bool InputGenerator::parsePattern(const QJsonValue &json, return true; } -} // namespace MoleQueue +} // namespace QtGui } // namespace Avogadro diff --git a/avogadro/molequeue/inputgenerator.h b/avogadro/qtgui/interfacescript.h similarity index 92% rename from avogadro/molequeue/inputgenerator.h rename to avogadro/qtgui/interfacescript.h index 143370f21..883b64c11 100644 --- a/avogadro/molequeue/inputgenerator.h +++ b/avogadro/qtgui/interfacescript.h @@ -14,11 +14,12 @@ ******************************************************************************/ -#ifndef AVOGADRO_MOLEQUEUE_INPUTGENERATOR_H -#define AVOGADRO_MOLEQUEUE_INPUTGENERATOR_H +#ifndef AVOGADRO_QTGUI_INTERFACESCRIPT_H +#define AVOGADRO_QTGUI_INTERFACESCRIPT_H #include -#include "avogadromolequeueexport.h" + +#include "avogadroqtguiexport.h" #include @@ -39,14 +40,11 @@ class Molecule; namespace QtGui { class GenericHighlighter; class PythonScript; -} -namespace MoleQueue { /** - * @class InputGenerator inputgenerator.h - * @brief The InputGenerator class provides an interface to input generator - * scripts. - * @sa InputGeneratorWidget + * @class InterfaceScript interfacescript.h + * @brief The Interface class provides an interface to external scripts + * @sa InterfaceWidget * * The QuantumInput extension provides a scriptable method for users to add * custom input generators to Avogadro. By writing an executable that implements @@ -415,7 +413,7 @@ namespace MoleQueue { * ================================ * * The generation of molecular geometry descriptions may be skipped in the - * script and deferred to the InputGenerator class by use of a special keyword. + * script and deferred to the InterfaceScript class by use of a special keyword. * The "contents" string may contain a keyword of the form ~~~ $$coords:[coordSpec]$$ @@ -448,7 +446,7 @@ namespace MoleQueue { * qDebug() stream from within avogadro. The script is free to handle the * debug flag as the author wishes. */ -class AVOGADROMOLEQUEUE_EXPORT InputGenerator : public QObject +class AVOGADROQTGUI_EXPORT InterfaceScript : public QObject { Q_OBJECT public: @@ -456,10 +454,10 @@ class AVOGADROMOLEQUEUE_EXPORT InputGenerator : public QObject * Constructor * @param scriptFilePath_ Absolute path to generator script. */ - explicit InputGenerator(const QString &scriptFilePath_, + explicit InterfaceScript(const QString &scriptFilePath_, QObject *parent_ = NULL); - explicit InputGenerator(QObject *parent_ = NULL); - ~InputGenerator(); + explicit InterfaceScript(QObject *parent_ = NULL); + ~InterfaceScript(); /** * @return True if debugging is enabled. @@ -492,6 +490,16 @@ class AVOGADROMOLEQUEUE_EXPORT InputGenerator : public QObject */ QString displayName() const; + /** + * Query the script for the menu path (--menu-path). + * @note The results will be cached the first time this function is called + * and reused in subsequent calls. + * @note If an error occurs, the error string will be set. Call hasErrors() + * to check for success, and errorString() or errorList() to get a + * user-friendly description of the error. + */ + QString menuPath() const; + /** * @return The path to the generator file. */ @@ -508,6 +516,18 @@ class AVOGADROMOLEQUEUE_EXPORT InputGenerator : public QObject */ void reset(); + /** + * Request input files from the script using the supplied options object and + * molecule. See the class documentation for details on the @p options_ + * object format. + * + * @return true on success and false on failure. + * @note If an error occurs, the error string will be set. Call hasErrors() + * to check for success, and errorString() or errorList() to get a + * user-friendly description of the error. + */ + bool runWorkflow(const QJsonObject &options_, Core::Molecule &mol); + /** * Request input files from the script using the supplied options object and * molecule. See the class documentation for details on the @p options_ @@ -610,6 +630,7 @@ public slots: // File extension of requested molecule format mutable QString m_moleculeExtension; mutable QString m_displayName; + mutable QString m_menuPath; mutable QJsonObject m_options; mutable QStringList m_warnings; mutable QStringList m_errors; @@ -623,13 +644,13 @@ public slots: }; -inline bool InputGenerator::isValid() const +inline bool InterfaceScript::isValid() const { displayName(); return !hasErrors(); } -} // namespace MoleQueue +} // namespace QtGui } // namespace Avogadro -#endif // AVOGADRO_MOLEQUEUE_INPUTGENERATOR_H +#endif // AVOGADRO_MOLEQUEUE_INTERFACESCRIPT_H diff --git a/avogadro/qtgui/interfacewidget.cpp b/avogadro/qtgui/interfacewidget.cpp new file mode 100644 index 000000000..32db6e58a --- /dev/null +++ b/avogadro/qtgui/interfacewidget.cpp @@ -0,0 +1,732 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2013 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#include "interfacewidget.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace Avogadro { +namespace QtGui { + +InterfaceWidget::InterfaceWidget(const QString &scriptFilePath, + QWidget *parent_) : + QWidget(parent_), + m_molecule(NULL), + m_interfaceScript(QString()) +{ + this->setInterfaceScript(scriptFilePath); +} + +InterfaceWidget::~InterfaceWidget() +{ +} + +void InterfaceWidget::setInterfaceScript(const QString &scriptFile) +{ + m_interfaceScript.setScriptFilePath(scriptFile); + m_options = m_interfaceScript.options(); + updateOptions(); +} + +void InterfaceWidget::setMolecule(QtGui::Molecule *mol) +{ + if (mol == m_molecule) + return; + + if (m_molecule) + m_molecule->disconnect(this); + + m_molecule = mol; +} + +void InterfaceWidget::defaultsClicked() +{ + setOptionDefaults(); +} + +void InterfaceWidget::setWarningText(const QString &warn) +{ + qWarning() << tr("Script returns warnings:\n") << warn; +} + +QString InterfaceWidget::warningText() const +{ + return QString(); +} + +void InterfaceWidget::showError(const QString &err) +{ + qWarning() << err; + + QWidget *theParent = this->isVisible() ? this + : qobject_cast(parent()); + QDialog dlg(theParent); + QVBoxLayout *vbox = new QVBoxLayout(); + QLabel *label = new QLabel(tr("An error has occurred:")); + vbox->addWidget(label); + QTextBrowser *textBrowser = new QTextBrowser(); + + // adjust the size of the text browser to ~80 char wide, ~20 lines high + QSize theSize = textBrowser->sizeHint(); + QFontMetrics metrics(textBrowser->currentFont()); + int charWidth = metrics.width("i7OPlmWn9/") / 10; + int charHeight = metrics.lineSpacing(); + theSize.setWidth(80 * charWidth); + theSize.setHeight(20 * charHeight); + textBrowser->setMinimumSize(theSize); + textBrowser->setText(err); + vbox->addWidget(textBrowser); + dlg.setLayout(vbox); + + dlg.exec(); +} + +QString InterfaceWidget::settingsKey(const QString &identifier) const +{ + return QString("scriptPlugin/%1/%2").arg(m_interfaceScript.displayName(), + identifier); +} + +QString InterfaceWidget::lookupOptionType(const QString &name) const +{ + if (!m_options.contains("userOptions") || + !m_options["userOptions"].isObject()) { + qWarning() << tr("'userOptions' missing, or not an object."); + return QString(); + } + + QJsonObject userOptions = m_options["userOptions"].toObject(); + + if (!userOptions.contains(name)) { + qWarning() << tr("Option '%1' not found in userOptions.").arg(name); + return QString(); + } + + if (!userOptions.value(name).isObject()) { + qWarning() << tr("Option '%1' does not refer to an object."); + return QString(); + } + + QJsonObject obj = userOptions[name].toObject(); + + if (!obj.contains("type") || + !obj.value("type").isString()) { + qWarning() << tr("'type' is not a string for option '%1'.").arg(name); + return QString(); + } + + return obj["type"].toString(); +} + +void InterfaceWidget::updateOptions() +{ + // Create the widgets, etc for the gui + buildOptionGui(); + setOptionDefaults(); +} + +void InterfaceWidget::buildOptionGui() +{ + // Clear old widgets from the layout + m_widgets.clear(); + delete layout(); // kill my layout + QFormLayout *form = new QFormLayout; + setLayout(form); + + if (!m_options.contains("userOptions") || + !m_options["userOptions"].isObject()) { + showError(tr("'userOptions' missing, or not an object:\n%1") + .arg(QString(QJsonDocument(m_options).toJson()))); + return; + } + + QJsonObject userOptions = m_options.value("userOptions").toObject(); + + // Title first + if (userOptions.contains("Title")) + addOptionRow(tr("Title"), userOptions.take("Title")); + + // File basename next: + if (userOptions.contains("Filename Base")) + addOptionRow(tr("Filename Base"), userOptions.take("Filename Base")); + + // Number of cores next: + if (userOptions.contains("Processor Cores")) + addOptionRow(tr("Processor Cores"), userOptions.take("Processor Cores")); + + // Calculation Type next: + if (userOptions.contains("Calculation Type")) + addOptionRow(tr("Calculation Type"), userOptions.take("Calculation Type")); + + // Theory/basis next. Combine into one row if both present. + bool hasTheory = userOptions.contains("Theory"); + bool hasBasis = userOptions.contains("Basis"); + if (hasTheory && hasBasis) { + QWidget *theoryWidget = createOptionWidget(userOptions.take("Theory")); + QWidget *basisWidget = createOptionWidget(userOptions.take("Basis")); + QHBoxLayout *hbox = new QHBoxLayout; + if (theoryWidget) { + theoryWidget->setObjectName("Theory"); + hbox->addWidget(theoryWidget); + m_widgets.insert("Theory", theoryWidget); + } + if (basisWidget) { + basisWidget->setObjectName("Basis"); + hbox->addWidget(basisWidget); + m_widgets.insert("Basis", basisWidget); + } + hbox->addStretch(); + + form->addRow(tr("Theory:"), hbox); + } + else { + if (hasTheory) + addOptionRow(tr("Theory"), userOptions.take("Theory")); + if (hasBasis) + addOptionRow(tr("Basis"), userOptions.take("Basis")); + } + + // Other special cases: + if (userOptions.contains("Charge")) + addOptionRow(tr("Charge"), userOptions.take("Charge")); + if (userOptions.contains("Multiplicity")) + addOptionRow(tr("Multiplicity"), userOptions.take("Multiplicity")); + + // Add remaining keys at bottom. + for (QJsonObject::const_iterator it = userOptions.constBegin(), + itEnd = userOptions.constEnd(); it != itEnd; ++it) { + addOptionRow(it.key(), it.value()); + } + + // Make connections for standard options: + if (QComboBox *combo = qobject_cast( + m_widgets.value("Calculation Type", NULL))) { + connect(combo, SIGNAL(currentIndexChanged(int)), + SLOT(updateTitlePlaceholder())); + } + if (QComboBox *combo = qobject_cast( + m_widgets.value("Theory", NULL))) { + connect(combo, SIGNAL(currentIndexChanged(int)), + SLOT(updateTitlePlaceholder())); + } + if (QComboBox *combo = qobject_cast( + m_widgets.value("Basis", NULL))) { + connect(combo, SIGNAL(currentIndexChanged(int)), + SLOT(updateTitlePlaceholder())); + } +} + +void InterfaceWidget::addOptionRow(const QString &label, + const QJsonValue &option) +{ + QWidget *widget = createOptionWidget(option); + if (!widget) + return; + + QFormLayout *form = qobject_cast(this->layout()); + if (!form) { + qWarning() << "Cannot add option" << label + << "to GUI -- layout is not a form."; + widget->deleteLater(); + return; + } + + // For lookups during unit testing: + widget->setObjectName(label); + + form->addRow(label + ":", widget); + m_widgets.insert(label, widget); +} + +QWidget *InterfaceWidget::createOptionWidget(const QJsonValue &option) +{ + if (!option.isObject()) + return NULL; + + QJsonObject obj = option.toObject(); + + if (!obj.contains("type") || + !obj.value("type").isString()) + return NULL; + + QString type = obj["type"].toString(); + + if (type == "stringList") + return createStringListWidget(obj); + else if (type == "string") + return createStringWidget(obj); + else if (type == "filePath") + return createFilePathWidget(obj); + else if (type == "integer") + return createIntegerWidget(obj); + else if (type == "float") + return createFloatWidget(obj); + else if (type == "boolean") + return createBooleanWidget(obj); + + qDebug() << "Unrecognized option type:" << type; + return NULL; +} + +QWidget *InterfaceWidget::createStringListWidget(const QJsonObject &obj) +{ + if (!obj.contains("values") || !obj["values"].isArray()) { + qDebug() << "QuantumInputDialog::createStringListWidget()" + "values missing, or not array!"; + return NULL; + } + + QJsonArray valueArray = obj["values"].toArray(); + + QComboBox *combo = new QComboBox(this); + for (QJsonArray::const_iterator vit = valueArray.constBegin(), + vitEnd = valueArray.constEnd(); vit != vitEnd; ++vit) { + if ((*vit).isString()) + combo->addItem((*vit).toString()); + else + qDebug() << "Cannot convert value to string for stringList:" << *vit; + } + connect(combo, SIGNAL(currentIndexChanged(int)), SLOT(updatePreviewText())); + + if (obj.contains("toolTip") && + obj.value("toolTip").isString()) { + combo->setToolTip(obj["toolTip"].toString()); + } + + return combo; +} + +QWidget *InterfaceWidget::createStringWidget(const QJsonObject &obj) +{ + QLineEdit *edit = new QLineEdit(this); +// connect(edit, SIGNAL(textChanged(QString)), SLOT(updatePreviewText())); + if (obj.contains("toolTip") && + obj.value("toolTip").isString()) { + edit->setToolTip(obj["toolTip"].toString()); + } + + return edit; +} + +QWidget *InterfaceWidget::createFilePathWidget(const QJsonObject &obj) +{ + QtGui::FileBrowseWidget *fileBrowse = new QtGui::FileBrowseWidget(this); + connect(fileBrowse, SIGNAL(fileNameChanged(QString)), + SLOT(updatePreviewText())); + + if (obj.contains("toolTip") && + obj.value("toolTip").isString()) { + fileBrowse->setToolTip(obj["toolTip"].toString()); + } + return fileBrowse; +} + +QWidget *InterfaceWidget::createIntegerWidget(const QJsonObject &obj) +{ + QSpinBox *spin = new QSpinBox(this); + if (obj.contains("minimum") && + obj.value("minimum").isDouble()) { + spin->setMinimum(static_cast(obj["minimum"].toDouble() + 0.5)); + } + if (obj.contains("maximum") && + obj.value("maximum").isDouble()) { + spin->setMaximum(static_cast(obj["maximum"].toDouble() + 0.5)); + } + if (obj.contains("prefix") && + obj.value("prefix").isString()) { + spin->setPrefix(obj["prefix"].toString()); + } + if (obj.contains("suffix") && + obj.value("suffix").isString()) { + spin->setSuffix(obj["suffix"].toString()); + } + if (obj.contains("toolTip") && + obj.value("toolTip").isString()) { + spin->setToolTip(obj["toolTip"].toString()); + } + connect(spin, SIGNAL(valueChanged(int)), SLOT(updatePreviewText())); + return spin; +} + +QWidget *InterfaceWidget::createFloatWidget(const QJsonObject &obj) +{ + QDoubleSpinBox *spin = new QDoubleSpinBox(this); + if (obj.contains("minimum") && + obj.value("minimum").isDouble()) { + spin->setMinimum(obj["minimum"].toDouble()); + } + if (obj.contains("maximum") && + obj.value("maximum").isDouble()) { + spin->setMaximum(obj["maximum"].toDouble()); + } + if (obj.contains("precision") && + obj.value("precision").isDouble()) { + spin->setDecimals(static_cast(obj["precision"].toDouble())); + } + if (obj.contains("prefix") && + obj.value("prefix").isString()) { + spin->setPrefix(obj["prefix"].toString()); + } + if (obj.contains("suffix") && + obj.value("suffix").isString()) { + spin->setSuffix(obj["suffix"].toString()); + } + if (obj.contains("toolTip") && + obj.value("toolTip").isString()) { + spin->setToolTip(obj["toolTip"].toString()); + } + connect(spin, SIGNAL(valueChanged(double)), SLOT(updatePreviewText())); + return spin; +} + +QWidget *InterfaceWidget::createBooleanWidget(const QJsonObject &obj) +{ + QCheckBox *checkBox = new QCheckBox(this); + connect(checkBox, SIGNAL(toggled(bool)), SLOT(updatePreviewText())); + + if (obj.contains("toolTip") && + obj.value("toolTip").isString()) { + checkBox->setToolTip(obj["toolTip"].toString()); + } + return checkBox; +} + +void InterfaceWidget::setOptionDefaults() +{ + if (!m_options.contains("userOptions") || + !m_options["userOptions"].isObject()) { + showError(tr("'userOptions' missing, or not an object:\n%1") + .arg(QString(QJsonDocument(m_options).toJson()))); + return; + } + + QJsonObject userOptions = m_options["userOptions"].toObject(); + + for (QJsonObject::ConstIterator it = userOptions.constBegin(), + itEnd = userOptions.constEnd(); it != itEnd; ++it) { + QString label = it.key(); + QJsonValue val = it.value(); + + if (!val.isObject()) { + qWarning() << tr("Error: value must be object for key '%1'.") + .arg(label); + continue; + } + + QJsonObject obj = val.toObject(); + if (obj.contains("default")) + setOption(label, obj["default"]); + else if (m_interfaceScript.debug()) + qWarning() << tr("Default value missing for option '%1'.").arg(label); + } +} + +void InterfaceWidget::setOption(const QString &name, + const QJsonValue &defaultValue) +{ + QString type = lookupOptionType(name); + + if (type == "stringList") + return setStringListOption(name, defaultValue); + else if (type == "string") + return setStringOption(name, defaultValue); + else if (type == "filePath") + return setFilePathOption(name, defaultValue); + else if (type == "integer") + return setIntegerOption(name, defaultValue); + else if (type == "float") + return setFloatOption(name, defaultValue); + else if (type == "boolean") + return setBooleanOption(name, defaultValue); + + qWarning() << tr("Unrecognized option type '%1' for option '%2'.") + .arg(type).arg(name); + return; +} + +void InterfaceWidget::setStringListOption(const QString &name, + const QJsonValue &value) +{ + QComboBox *combo = qobject_cast(m_widgets.value(name, NULL)); + if (!combo) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad widget type.") + .arg(name); + return; + } + + if (!value.isDouble() && !value.isString()) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad default value:") + .arg(name) + << value; + return; + } + + int index = -1; + if (value.isDouble()) + index = static_cast(value.toDouble() + 0.5); + else if (value.isString()) + index = combo->findText(value.toString()); + + if (index < 0) { + qWarning() << tr("Error setting default for option '%1'. " + "Could not find valid combo entry index from value:") + .arg(name) + << value; + return; + } + + combo->setCurrentIndex(index); +} + +void InterfaceWidget::setStringOption(const QString &name, + const QJsonValue &value) +{ + QLineEdit *lineEdit = qobject_cast(m_widgets.value(name, NULL)); + if (!lineEdit) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad widget type.") + .arg(name); + return; + } + + if (!value.isString()) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad default value:") + .arg(name) + << value; + return; + } + + lineEdit->setText(value.toString()); +} + +void InterfaceWidget::setFilePathOption(const QString &name, + const QJsonValue &value) +{ + QtGui::FileBrowseWidget *fileBrowse = + qobject_cast(m_widgets.value(name, NULL)); + if (!fileBrowse) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad widget type.") + .arg(name); + return; + } + + if (!value.isString()) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad default value:") + .arg(name) + << value; + return; + } + + fileBrowse->setFileName(value.toString()); +} + +void InterfaceWidget::setIntegerOption(const QString &name, + const QJsonValue &value) +{ + QSpinBox *spin = qobject_cast(m_widgets.value(name, NULL)); + if (!spin) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad widget type.") + .arg(name); + return; + } + + if (!value.isDouble()) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad default value:") + .arg(name) + << value; + return; + } + + int intVal = static_cast(value.toDouble() + 0.5); + spin->setValue(intVal); +} + +void InterfaceWidget::setFloatOption(const QString &name, + const QJsonValue &value) +{ + QDoubleSpinBox *spin = qobject_cast(m_widgets.value(name, NULL)); + if (!spin) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad widget type.") + .arg(name); + return; + } + + if (!value.isDouble()) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad default value:") + .arg(name) + << value; + return; + } + + spin->setValue(value.toDouble()); +} + +void InterfaceWidget::setBooleanOption(const QString &name, + const QJsonValue &value) +{ + QCheckBox *checkBox = qobject_cast(m_widgets.value(name, NULL)); + if (!checkBox) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad widget type.") + .arg(name); + return; + } + + if (!value.isBool()) { + qWarning() << tr("Error setting default for option '%1'. " + "Bad default value:") + .arg(name) + << value; + return; + } + + checkBox->setChecked(value.toBool()); +} + +bool InterfaceWidget::optionString(const QString &option, + QString &value) const +{ + QWidget *widget = m_widgets.value(option, NULL); + bool retval = false; + value.clear(); + + if (QLineEdit *edit = qobject_cast(widget)) { + retval = true; + value = edit->text(); + } + else if (QComboBox *combo = qobject_cast(widget)) { + retval = true; + value = combo->currentText(); + } + else if (QSpinBox *spinbox = qobject_cast(widget)) { + retval = true; + value = QString::number(spinbox->value()); + } + else if (QDoubleSpinBox *dspinbox = qobject_cast(widget)) { + retval = true; + value = QString::number(dspinbox->value()); + } + else if (QtGui::FileBrowseWidget *fileBrowse + = qobject_cast(widget)) { + retval = true; + value = fileBrowse->fileName(); + } + + return retval; +} + +QJsonObject InterfaceWidget::collectOptions() const +{ + QJsonObject ret; + + foreach (QString label, m_widgets.keys()) { + QWidget *widget = m_widgets.value(label, NULL); + if (QComboBox *combo = qobject_cast(widget)) { + ret.insert(label, combo->currentText()); + } + else if (QLineEdit *lineEdit = qobject_cast(widget)) { + QString value(lineEdit->text()); + if (value.isEmpty() && label == "Title") + value = generateJobTitle(); + ret.insert(label, value); + } + else if (QSpinBox *spinBox = qobject_cast(widget)) { + ret.insert(label, spinBox->value()); + } + else if (QDoubleSpinBox *spinBox = qobject_cast(widget)) { + ret.insert(label, spinBox->value()); + } + else if (QCheckBox *checkBox = qobject_cast(widget)) { + ret.insert(label, checkBox->isChecked()); + } + else if (QtGui::FileBrowseWidget *fileBrowse + = qobject_cast(widget)) { + ret.insert(label, fileBrowse->fileName()); + } + else { + qWarning() << tr("Unhandled widget in collectOptions for option '%1'.") + .arg(label); + } + } + + return ret; +} + +void InterfaceWidget::applyOptions(const QJsonObject &opts) +{ + foreach (const QString &label, opts.keys()) + setOption(label, opts[label]); +} + +QString InterfaceWidget::generateJobTitle() const +{ + QString calculation; + bool haveCalculation(optionString("Calculation Type", calculation)); + + QString theory; + bool haveTheory(optionString("Theory", theory)); + + QString basis; + bool haveBasis(optionString("Basis", basis)); + + // Merge theory/basis into theory + if (haveBasis) { + if (haveTheory) + theory += "/"; + theory += basis; + theory.replace(QRegExp("\\s+"), ""); + haveTheory = true; + } + + QString formula(m_molecule ? QString::fromStdString(m_molecule->formula()) + : tr("[no molecule]")); + + return QString("%1%2%3").arg(formula) + .arg(haveCalculation ? " | " + calculation : QString()) + .arg(haveTheory ? " | " + theory : QString()); + } + +} // namespace QtGui +} // namespace Avogadro diff --git a/avogadro/qtgui/interfacewidget.h b/avogadro/qtgui/interfacewidget.h new file mode 100644 index 000000000..029f2d6ab --- /dev/null +++ b/avogadro/qtgui/interfacewidget.h @@ -0,0 +1,188 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2013 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#ifndef AVOGADRO_QTGUI_INTERFACEWIDGET_H +#define AVOGADRO_QTGUI_INTERFACEWIDGET_H + +#include "avogadroqtguiexport.h" + +#include +#include +#include + +#include "interfacescript.h" + +class QJsonValue; +class QTextEdit; +class QWidget; + +namespace Avogadro { +namespace QtGui { +class Molecule; + +/** + * @class InterfaceWidget interfacewidget.h + * + * @brief The InterfaceWidget class provides a user interface for + * running external scripts + * @sa InterfaceScript + * + * The InterfaceWidget creates a GUI to represent the options given by an + * script, turning JSON from the script into a form and passing the results + * back to the script via command-line + */ +class AVOGADROQTGUI_EXPORT InterfaceWidget : public QWidget +{ + Q_OBJECT + +public: + /** + * Construct a widget that dynamically generates a GUI to configure the + * script specified by scriptFilePath. + */ + explicit InterfaceWidget(const QString &scriptFilePath, QWidget *parent_ = 0); + ~InterfaceWidget(); + + /** + * Use the script pointed to by scriptFilePath. + * @param scriptFilePath Absolute path to script. + */ + void setInterfaceScript(const QString &scriptFilePath); + + /** + * Set the molecule used in the simulation. + */ + void setMolecule(QtGui::Molecule *mol); + + /** + * Access to the underlying input generator object. + */ + const QtGui::InterfaceScript &interfaceScript() const { return m_interfaceScript; } + + /** + * Collect all of the user-specified options into a JSON object, to be sent + * to the generator script. + */ + QJsonObject collectOptions() const; + + /** + * Apply the options in the passed QJsonObject to the GUI. Any widgets changed + * by this method will have their signals blocked while modifying their + * values. + */ + void applyOptions(const QJsonObject &opts); + +private slots: + /** + * Triggered when the user resets the default values. + */ + void defaultsClicked(); + + /** + * Show the user an warning. These are messages returned by the input + * script. + */ + void setWarningText(const QString &warn); + + /** + * Show the warning text. + */ + QString warningText() const; + + /** + * Show the user an error message. These are errors that have occurred + * in this extension, not necessarily in the input generator script. + */ + void showError(const QString &err); + +private: + /** + * Generate a QSettings key with the given identifier that is unique to this + * input generator's display name. + * @param identifier Setting key, e.g. "outputPath" + * @return Script-specific key, e.g. "quantumInput/GAMESS/outputPath" + * @todo Display names are not necessarily unique, but paths are too long. + * Maybe add a namespace qualifier to the script display names? + */ + QString settingsKey(const QString &identifier) const; + + /** + * Given the name of a user-option in m_options, return the type string. + * If an error occurs, an empty string will be returned. + */ + QString lookupOptionType(const QString &name) const; + + /** + * Used to construct the script-specific GUI. + * @{ + */ + void updateOptions(); + void buildOptionGui(); + void addOptionRow(const QString &label, const QJsonValue &option); + + QWidget* createOptionWidget(const QJsonValue &option); + QWidget* createStringListWidget(const QJsonObject &obj); + QWidget* createStringWidget(const QJsonObject &obj); + QWidget* createFilePathWidget(const QJsonObject &obj); + QWidget* createIntegerWidget(const QJsonObject &obj); + QWidget* createFloatWidget(const QJsonObject &obj); + QWidget* createBooleanWidget(const QJsonObject &obj); + /**@}*/ + + /** + * Set the simulation settings to their default values. + * @{ + */ + void setOptionDefaults(); + void setOption(const QString &name, const QJsonValue &defaultValue); + void setStringListOption(const QString &name, const QJsonValue &value); + void setStringOption(const QString &name, const QJsonValue &value); + void setFilePathOption(const QString &name, const QJsonValue &value); + void setIntegerOption(const QString &name, const QJsonValue &value); + void setFloatOption(const QString &name, const QJsonValue &value); + void setBooleanOption(const QString &name, const QJsonValue &value); + /**@}*/ + + /** + * @brief Search for an option named @a option and convert its value to a + * string. + * @param option The name of the option. + * @param value String to overwrite with option value. + * @return True if value is overwritten, false if the option is not found or + * cannot be converted to a string. + */ + bool optionString(const QString &option, QString &value) const; + + /** + * Update the autogenerated job title in the GUI. + */ + QString generateJobTitle() const; + + QtGui::Molecule *m_molecule; + QJsonObject m_options; + QJsonObject m_optionCache; // For reverting changes + QList m_dirtyTextEdits; + + QMap m_widgets; + QMap m_textEdits; + + QtGui::InterfaceScript m_interfaceScript; +}; + +} // namespace QtGui +} // namespace Avogadro + +#endif // AVOGADRO_QTGUI_INTERFACEWIDGET_H diff --git a/avogadro/qtplugins/CMakeLists.txt b/avogadro/qtplugins/CMakeLists.txt index f3575ed76..acbda2d35 100644 --- a/avogadro/qtplugins/CMakeLists.txt +++ b/avogadro/qtplugins/CMakeLists.txt @@ -117,6 +117,7 @@ add_subdirectory(povray) add_subdirectory(selectiontool) add_subdirectory(spacegroup) add_subdirectory(spectra) +add_subdirectory(workflows) if(USE_MOLEQUEUE) add_subdirectory(apbs) diff --git a/avogadro/qtplugins/workflows/CMakeLists.txt b/avogadro/qtplugins/workflows/CMakeLists.txt new file mode 100644 index 000000000..f4e5bd1a8 --- /dev/null +++ b/avogadro/qtplugins/workflows/CMakeLists.txt @@ -0,0 +1,34 @@ +# Needed to find avogadroioexport.h: +include_directories("${AvogadroLibs_BINARY_DIR}/avogadro/io/") + +# Extension +set(workflow_srcs + workflow.cpp +) + +avogadro_plugin(Workflows + "Script workflows" + ExtensionPlugin + workflow.h + Workflow + "${workflow_srcs}" +) + +target_link_libraries(Workflows LINK_PRIVATE AvogadroIO) + +# Bundled workflow scripts +set(workflows + scripts/scale.py +) + +option(INSTALL_TEST_WORKFLOWS + "Install a dummy workflow that is to test scripts." + OFF +) + +if(INSTALL_TEST_WORKFLOWS) + list(APPEND workflows scripts/test.py) +endif() + +install(PROGRAMS ${input_generators} + DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/workflows/") diff --git a/avogadro/qtplugins/workflows/scripts/scale.py b/avogadro/qtplugins/workflows/scripts/scale.py new file mode 100755 index 000000000..a3eee92a9 --- /dev/null +++ b/avogadro/qtplugins/workflows/scripts/scale.py @@ -0,0 +1,97 @@ +""" +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2016 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ +""" + +import argparse +import json +import sys + +# Some globals: +debug = True + + +def getOptions(): + userOptions = {} + + userOptions['X Scale'] = {} + userOptions['X Scale']['type'] = 'float' + userOptions['X Scale']['default'] = 1.0 + userOptions['X Scale']['precision'] = 3 + userOptions['X Scale']['toolTip'] = 'Multiplier for X coordinates' + + userOptions['Y Scale'] = {} + userOptions['Y Scale']['type'] = 'float' + userOptions['Y Scale']['default'] = 1.0 + userOptions['Y Scale']['precision'] = 3 + userOptions['Y Scale']['toolTip'] = 'Multiplier for Y coordinates' + + userOptions['Z Scale'] = {} + userOptions['Z Scale']['type'] = 'float' + userOptions['Z Scale']['default'] = 1.0 + userOptions['Z Scale']['precision'] = 3 + userOptions['Z Scale']['toolTip'] = 'Multiplier for Z coordinates' + + opts = {'userOptions': userOptions} + + return opts + + +def scale(opts, mol): + xScale = float(opts['X Scale']) + yScale = float(opts['Y Scale']) + zScale = float(opts['Z Scale']) + + coords = mol['atoms']['coords']['3d'] + for i in range(0, len(coords), 3): + coords[i] = coords[i] * xScale + coords[i+1] = coords[i+1] * yScale + coords[i+2] = coords[i+2] * zScale + + return mol + + +def runWorkflow(): + # Read options from stdin + stdinStr = sys.stdin.read() + + # Parse the JSON strings + opts = json.loads(stdinStr) + + # Prepare the result + result = {} + result['cjson'] = scale(opts, opts['cjson']) + return result + +if __name__ == "__main__": + parser = argparse.ArgumentParser('Scale molecular coordinates.') + parser.add_argument('--debug', action='store_true') + parser.add_argument('--print-options', action='store_true') + parser.add_argument('--run-workflow', action='store_true') + parser.add_argument('--display-name', action='store_true') + parser.add_argument('--menu-path', action='store_true') + args = vars(parser.parse_args()) + + debug = args['debug'] + + if args['display_name']: + print("Scale Coordinates...") + if args['menu_path']: + print("&Extensions") + if args['print_options']: + print(json.dumps(getOptions())) + elif args['run_workflow']: + print(json.dumps(runWorkflow())) diff --git a/avogadro/qtplugins/workflows/scripts/test.py b/avogadro/qtplugins/workflows/scripts/test.py new file mode 100755 index 000000000..86fa10558 --- /dev/null +++ b/avogadro/qtplugins/workflows/scripts/test.py @@ -0,0 +1,171 @@ +""" +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2013 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ +""" + +import argparse +import json +import sys + +# Some globals: +debug = False + +def getOptions(): + userOptions = {} + + userOptions['Test StringList'] = {} + userOptions['Test StringList']['type'] = 'stringList' + userOptions['Test StringList']['default'] = 0 + userOptions['Test StringList']['values'] = \ + ['Option 1', 'Option 2', 'Option 3'] + + userOptions['Test String'] = {} + userOptions['Test String']['type'] = 'string' + userOptions['Test String']['default'] = 'default value' + + userOptions['Test Integer'] = {} + userOptions['Test Integer']['type'] = 'integer' + userOptions['Test Integer']['default'] = 5 + userOptions['Test Integer']['minimium'] = 0 + userOptions['Test Integer']['maximum'] = 10 + userOptions['Test Integer']['prefix'] = 'Throw ' + userOptions['Test Integer']['suffix'] = ' warnings' + + userOptions['Test Boolean'] = {} + userOptions['Test Boolean']['type'] = 'boolean' + userOptions['Test Boolean']['default'] = False + + userOptions['Test FilePath'] = {} + userOptions['Test FilePath']['type'] = 'filePath' + userOptions['Test FilePath']['default'] = '' + + # special parameters -- these should be moved to the top of the widget + userOptions['Title'] = {} + userOptions['Title']['type'] = 'string' + userOptions['Title']['default'] = '' + + userOptions['Filename Base'] = {} + userOptions['Filename Base']['type'] = 'string' + userOptions['Filename Base']['default'] = 'job' + + userOptions['Processor Cores'] = {} + userOptions['Processor Cores']['type'] = 'integer' + userOptions['Processor Cores']['default'] = 4 + + userOptions['Calculation Type'] = {} + userOptions['Calculation Type']['type'] = "stringList" + userOptions['Calculation Type']['default'] = 1 + userOptions['Calculation Type']['values'] = \ + ['Single Point', 'Equilibrium Geometry', 'Frequencies'] + + userOptions['Theory'] = {} + userOptions['Theory']['type'] = "stringList" + userOptions['Theory']['default'] = 1 + userOptions['Theory']['values'] = ['RHF', 'B3LYP', 'MP2', 'CCSD'] + + userOptions['Basis'] = {} + userOptions['Basis']['type'] = "stringList" + userOptions['Basis']['default'] = 2 + userOptions['Basis']['values'] = \ + ['STO-3G', '3-21 G', '6-31 G(d)', '6-31 G(d,p)', '6-31+ G(d)', \ + '6-311 G(d)', 'cc-pVDZ', 'cc-pVTZ', 'LANL2DZ'] + + userOptions['Multiplicity'] = {} + userOptions['Multiplicity']['type'] = "integer" + userOptions['Multiplicity']['default'] = 1 + userOptions['Multiplicity']['minimum'] = 1 + userOptions['Multiplicity']['maximum'] = 5 + + userOptions['Charge'] = {} + userOptions['Charge']['type'] = "integer" + userOptions['Charge']['default'] = 0 + userOptions['Charge']['minimum'] = -9 + userOptions['Charge']['maximum'] = 9 + + opts = {} + opts['userOptions'] = userOptions + + return opts + +def generateInputFile(opts): + output = '' + for key in opts: + output += '%s: %s\n'%(key, opts[key]) + + output += '\n\nCurrent molecule:\n$$coords:SZx1y1z0N$$\n' + + return output + +def generateInput(): + # Read options from stdin + stdinStr = sys.stdin.read() + + # Parse the JSON strings + opts = json.loads(stdinStr) + + # Generate the input file + inp = generateInputFile(opts['options']) + + # Basename for input files: + baseName = opts['options']['Filename Base'] + + # Test for warnings: + numWarnings = opts['options']['Test Integer'] + + # Test filePath: + filePath = opts['options']['Test FilePath'] + + # Prepare the result + result = {} + # Input file text -- will appear in the same order in the GUI as they are + # listed in the array: + files = [] + files.append({'filename': '%s.opts'%baseName, + 'contents': inp}) + files.append({'filename': '%s.testFilePath'%baseName, + 'filePath': filePath}) + + if debug: + files.append({'filename': 'debug_info', 'contents': stdinStr}) + + result['files'] = files + + # Specify the main input file. This will be used by MoleQueue to determine + # the value of the $$inputFileName$$ and $$inputFileBaseName$$ keywords. + result['mainFile'] = '%s.opts'%baseName + + result['warnings'] = [] + for i in range(numWarnings): + result['warnings'].append('Warning number %d...'%(i+1)) + + return result + +if __name__ == "__main__": + parser = argparse.ArgumentParser('Generate a NWChem input file.') + parser.add_argument('--debug', action='store_true') + parser.add_argument('--print-options', action='store_true') + parser.add_argument('--generate-input', action='store_true') + parser.add_argument('--display-name', action='store_true') + args = vars(parser.parse_args()) + + debug = args['debug'] + + if args['display_name']: + print("Input Generator Test") + if args['print_options']: + print(json.dumps(getOptions())) + elif args['generate_input']: + print(json.dumps(generateInput())) diff --git a/avogadro/qtplugins/workflows/workflow.cpp b/avogadro/qtplugins/workflows/workflow.cpp new file mode 100644 index 000000000..3f1e67dec --- /dev/null +++ b/avogadro/qtplugins/workflows/workflow.cpp @@ -0,0 +1,317 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2016 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#include "workflow.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Avogadro { +namespace QtPlugins { + +using Avogadro::QtGui::InterfaceScript; +using Avogadro::QtGui::InterfaceWidget; + +Workflow::Workflow(QObject *parent_) : + ExtensionPlugin(parent_), + m_molecule(NULL), + m_currentDialog(NULL), + m_currentInterface(NULL), + m_outputFormat(NULL) +{ + refreshScripts(); +} + +Workflow::~Workflow() +{ + qDeleteAll(m_dialogs.values()); + m_dialogs.clear(); +} + +QList Workflow::actions() const +{ + return m_actions; +} + +QStringList Workflow::menuPath(QAction *action) const +{ + QString scriptFileName = action->data().toString(); + QStringList path; + + // if we're passed the "Set Python" action + if (scriptFileName.isEmpty()) { + path << tr("&Extensions") << tr("Scripts"); + return path; + } + + // otherwise, we have a script name, so ask it + InterfaceScript gen(scriptFileName); + path = gen.menuPath().split('|'); + if (gen.hasErrors()) { + path << tr("&Extensions") << tr("Scripts"); + qWarning() << "Workflow::queryProgramName: Unable to retrieve program " + "name for" << scriptFileName << "." + << gen.errorList().join("\n\n"); + return path; + } + return path; +} + +void Workflow::setMolecule(QtGui::Molecule *mol) +{ + if (m_molecule == mol) + return; + + m_molecule = mol; + + foreach (InterfaceWidget *dlg, m_dialogs.values()) + dlg->setMolecule(mol); +} + +bool Workflow::readMolecule(QtGui::Molecule &mol) +{ + Io::FileFormat *reader = m_outputFormat->newInstance(); + bool success = reader->readFile(m_outputFileName.toStdString(), mol); + if (!success) { + QMessageBox::information(qobject_cast(parent()), + tr("Error"), + tr("Error reading output file '%1':\n%2") + .arg(m_outputFileName) + .arg(QString::fromStdString(reader->error()))); + } + + m_outputFormat = NULL; + m_outputFileName.clear(); + + return success; +} + +void Workflow::refreshScripts() +{ + updateScripts(); + updateActions(); +} + +void Workflow::menuActivated() +{ + QAction *theSender = qobject_cast(sender()); + if (!theSender) + return; + + QString scriptFileName = theSender->data().toString(); + QWidget *theParent = qobject_cast(parent()); + InterfaceWidget *widget = m_dialogs.value(scriptFileName, NULL); + + if (!widget) { + widget = new InterfaceWidget(scriptFileName, theParent); + m_dialogs.insert(scriptFileName, widget); + } + widget->setMolecule(m_molecule); + + if (!m_currentDialog) { + m_currentDialog = new QDialog(theParent); + } else { + delete m_currentDialog->layout(); + } + QString title; + queryProgramName(scriptFileName, title); + m_currentDialog->setWindowTitle(title); + + QVBoxLayout *vbox = new QVBoxLayout(); + vbox->addWidget(widget); + m_currentInterface = widget; // remember this when we get the run() signal + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok + | QDialogButtonBox::Cancel); + + connect(buttonBox, SIGNAL(accepted()), this, SLOT(run())); + connect(buttonBox, SIGNAL(rejected()), m_currentDialog, SLOT(reject())); + vbox->addWidget(buttonBox); + m_currentDialog->setLayout(vbox); + m_currentDialog->exec(); +} + +void Workflow::run() +{ + if (m_currentDialog) + m_currentDialog->accept(); + + if (m_currentInterface) { + QJsonObject options = m_currentInterface->collectOptions(); + QString scriptFilePath = m_currentInterface->interfaceScript().scriptFilePath(); + InterfaceScript gen(scriptFilePath); + gen.runWorkflow(options, *m_molecule); + // collect errors + } +} + +void Workflow::configurePython() +{ + // Create objects + QSettings settings; + QDialog dlg(qobject_cast(parent())); + QLabel *label = new QLabel; + QVBoxLayout *layout = new QVBoxLayout; + QtGui::FileBrowseWidget *browser = new QtGui::FileBrowseWidget; + QDialogButtonBox *buttonBox = new QDialogButtonBox; + + // Configure objects + // Check for python interpreter in env var + QString pythonInterp = QString::fromLocal8Bit( + qgetenv("AVO_PYTHON_INTERPRETER")); + if (pythonInterp.isEmpty()) { + // Check settings + pythonInterp = settings.value("interpreters/python", + QString()).toString(); + } + // Use compile-time default if still not found. + if (pythonInterp.isEmpty()) + pythonInterp = QString(pythonInterpreterPath); + browser->setMode(QtGui::FileBrowseWidget::ExecutableFile); + browser->setFileName(pythonInterp); + + buttonBox->setStandardButtons(QDialogButtonBox::Ok + | QDialogButtonBox::Cancel); + + dlg.setWindowTitle(tr("Set path to Python interpreter:")); + label->setText(tr("Select the python interpreter to run external scripts.\n" + "Avogadro must be restarted for any changes to take effect.")); + + // Build layout + layout->addWidget(label); + layout->addWidget(browser); + layout->addWidget(buttonBox); + dlg.setLayout(layout); + + // Connect + connect(buttonBox, SIGNAL(accepted()), &dlg, SLOT(accept())); + connect(buttonBox, SIGNAL(rejected()), &dlg, SLOT(reject())); + + // Show dialog + QDialog::DialogCode response = static_cast(dlg.exec()); + if (response != QDialog::Accepted) + return; + + // Handle response + settings.setValue("interpreters/python", browser->fileName()); +} + +void Workflow::updateScripts() +{ + m_workflowScripts.clear(); + + // List of directories to check. + /// @todo Custom script locations + QStringList dirs; + + // add the default paths + QStringList stdPaths + = QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation); + foreach (const QString &dirStr, stdPaths) { + QString path = dirStr + "/scripts/workflows"; + QDir dir( path ); + qDebug() << "Checking for generator scripts in" << path; + if (dir.exists() && dir.isReadable()) + dirs << path; + } + + dirs << QCoreApplication::applicationDirPath() + "/../" + + QtGui::Utilities::libraryDirectory() + + "/avogadro2/scripts/workflows"; + + foreach (const QString &dirStr, dirs) { + qDebug() << "Checking for generator scripts in" << dirStr; + QDir dir(dirStr); + if (dir.exists() && dir.isReadable()) { + foreach (const QFileInfo &file, dir.entryInfoList(QDir::Files | + QDir::NoDotAndDotDot)) { + QString filePath = file.absoluteFilePath(); + QString displayName; + if (queryProgramName(filePath, displayName)) + m_workflowScripts.insert(displayName, filePath); + } + } + } +} + +void Workflow::updateActions() +{ + m_actions.clear(); + + QAction *action = new QAction(tr("Set Python Path..."), this); + connect(action, SIGNAL(triggered()), SLOT(configurePython())); + m_actions << action; + + foreach (const QString &programName, m_workflowScripts.uniqueKeys()) { + QStringList scripts = m_workflowScripts.values(programName); + // Include the full path if there are multiple generators with the same name. + if (scripts.size() == 1) { + addAction(programName, scripts.first()); + } + else { + foreach (const QString &filePath, scripts) { + addAction(QString("%1 (%2)").arg(programName, filePath), filePath); + } + } + } +} + +void Workflow::addAction(const QString &label, + const QString &scriptFilePath) +{ + QAction *action = new QAction(label, this); + action->setData(scriptFilePath); + action->setEnabled(true); + connect(action, SIGNAL(triggered()), SLOT(menuActivated())); + m_actions << action; +} + +bool Workflow::queryProgramName(const QString &scriptFilePath, + QString &displayName) +{ + InterfaceScript gen(scriptFilePath); + displayName = gen.displayName(); + if (gen.hasErrors()) { + displayName.clear(); + qWarning() << "Workflow::queryProgramName: Unable to retrieve program " + "name for" << scriptFilePath << ";" + << gen.errorList().join("\n\n"); + return false; + } + return true; +} + +} +} diff --git a/avogadro/qtplugins/workflows/workflow.h b/avogadro/qtplugins/workflows/workflow.h new file mode 100644 index 000000000..a98217773 --- /dev/null +++ b/avogadro/qtplugins/workflows/workflow.h @@ -0,0 +1,100 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2016 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#ifndef AVOGADRO_QTPLUGINS_WORKFLOW_H +#define AVOGADRO_QTPLUGINS_WORKFLOW_H + +#include + +#include +#include + +class QAction; +class QDialog; + +namespace Avogadro { +namespace Io { + class FileFormat; +} + +namespace QtGui { + class InterfaceScript; + class InterfaceWidget; +} + +namespace QtPlugins { + +/** + * @brief The Workflow class implements the extension interface for + * external (script) workflows + * @author Geoffrey R. Hutchison + */ +class Workflow : public QtGui::ExtensionPlugin +{ + Q_OBJECT + +public: + explicit Workflow(QObject *parent = 0); + ~Workflow(); + + QString name() const { return tr("Workflow scripts"); } + + QString description() const { return tr("Run external workflow commands"); } + + QList actions() const; + + QStringList menuPath(QAction *) const; + + void setMolecule(QtGui::Molecule *mol); + +public slots: + /** + * Scan for new scripts in the workflow directories. + */ + void refreshScripts(); + + void run(); + + bool readMolecule(QtGui::Molecule &mol); + +private slots: + void menuActivated(); + void configurePython(); + +private: + void updateScripts(); + void updateActions(); + void addAction(const QString &label, const QString &scriptFilePath); + bool queryProgramName(const QString &scriptFilePath, QString &displayName); + + QList m_actions; + QtGui::Molecule *m_molecule; + // keyed on script file path + QMultiMap m_dialogs; + QDialog *m_currentDialog; + QtGui::InterfaceWidget *m_currentInterface; + + // maps program name --> script file path + QMultiMap m_workflowScripts; + + const Io::FileFormat *m_outputFormat; + QString m_outputFileName; +}; + +} +} + +#endif // AVOGADRO_QTPLUGINS_WORKFLOW_H From d791505cdd284114b21c07e8b5799d0bda5c7193 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Tue, 27 Dec 2016 15:23:01 -0500 Subject: [PATCH 4/7] Make sure to cast to a QtGui::Molecule to allow updates and undo/redo --- avogadro/qtgui/interfacescript.cpp | 10 ++++++---- avogadro/qtgui/interfacescript.h | 2 +- avogadro/qtplugins/workflows/workflow.cpp | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/avogadro/qtgui/interfacescript.cpp b/avogadro/qtgui/interfacescript.cpp index 450e72d0a..1c6bfd976 100644 --- a/avogadro/qtgui/interfacescript.cpp +++ b/avogadro/qtgui/interfacescript.cpp @@ -162,7 +162,7 @@ void InterfaceScript::reset() } bool InterfaceScript::runWorkflow(const QJsonObject &options_, - Core::Molecule &mol) + Core::Molecule *mol) { m_errors.clear(); m_warnings.clear(); @@ -174,7 +174,7 @@ bool InterfaceScript::runWorkflow(const QJsonObject &options_, // Add the molecule file to the options QJsonObject allOptions(options_); - if (!insertMolecule(allOptions, mol)) + if (!insertMolecule(allOptions, *mol)) return false; QByteArray json(m_interpreter->execute(QStringList() << "--run-workflow", @@ -218,11 +218,13 @@ bool InterfaceScript::runWorkflow(const QJsonObject &options_, QJsonDocument doc(cjsonObj); QString strCJSON(doc.toJson(QJsonDocument::Compact)); if (!strCJSON.isEmpty()) { - result = format->readString(strCJSON.toStdString(), mol); + QtGui::Molecule *guiMol = static_cast(mol); + QtGui::Molecule newMol(guiMol->parent()); + result = format->readString(strCJSON.toStdString(), newMol); // TODO: need to indicate changes to the QtGui Molecule Molecule::MoleculeChanges changes = (Molecule::Atoms | Molecule::Bonds | Molecule::Added | Molecule::Removed); - // guiMol.undoMolecule()->modifyMolecule(newMol, changes, "Run Script"); + guiMol->undoMolecule()->modifyMolecule(newMol, changes, "Run Script"); } } return result; diff --git a/avogadro/qtgui/interfacescript.h b/avogadro/qtgui/interfacescript.h index 883b64c11..2d9978deb 100644 --- a/avogadro/qtgui/interfacescript.h +++ b/avogadro/qtgui/interfacescript.h @@ -526,7 +526,7 @@ class AVOGADROQTGUI_EXPORT InterfaceScript : public QObject * to check for success, and errorString() or errorList() to get a * user-friendly description of the error. */ - bool runWorkflow(const QJsonObject &options_, Core::Molecule &mol); + bool runWorkflow(const QJsonObject &options_, Core::Molecule *mol); /** * Request input files from the script using the supplied options object and diff --git a/avogadro/qtplugins/workflows/workflow.cpp b/avogadro/qtplugins/workflows/workflow.cpp index 3f1e67dec..1855f4a68 100644 --- a/avogadro/qtplugins/workflows/workflow.cpp +++ b/avogadro/qtplugins/workflows/workflow.cpp @@ -172,7 +172,7 @@ void Workflow::run() QJsonObject options = m_currentInterface->collectOptions(); QString scriptFilePath = m_currentInterface->interfaceScript().scriptFilePath(); InterfaceScript gen(scriptFilePath); - gen.runWorkflow(options, *m_molecule); + gen.runWorkflow(options, m_molecule); // collect errors } } From 36de6b8e3f4b777439eb22f7c5b827afd80cd1f0 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 28 Dec 2016 12:19:36 -0500 Subject: [PATCH 5/7] Allow return JSON to include an "append" flag --- avogadro/qtgui/interfacescript.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/avogadro/qtgui/interfacescript.cpp b/avogadro/qtgui/interfacescript.cpp index 1c6bfd976..92d9b068d 100644 --- a/avogadro/qtgui/interfacescript.cpp +++ b/avogadro/qtgui/interfacescript.cpp @@ -209,7 +209,7 @@ bool InterfaceScript::runWorkflow(const QJsonObject &options_, } } - // TODO: add undo / redo and smart updates + // TODO: add smart updates Io::FileFormatManager &formats = Io::FileFormatManager::instance(); QScopedPointer format(formats.newFormatFromFileExtension( "cjson")); @@ -221,10 +221,14 @@ bool InterfaceScript::runWorkflow(const QJsonObject &options_, QtGui::Molecule *guiMol = static_cast(mol); QtGui::Molecule newMol(guiMol->parent()); result = format->readString(strCJSON.toStdString(), newMol); - // TODO: need to indicate changes to the QtGui Molecule - Molecule::MoleculeChanges changes = - (Molecule::Atoms | Molecule::Bonds | Molecule::Added | Molecule::Removed); - guiMol->undoMolecule()->modifyMolecule(newMol, changes, "Run Script"); + + if (obj["append"].toBool()) { // just append some new bits + guiMol->undoMolecule()->appendMolecule(newMol, m_displayName); + } else { // replace the whole molecule + Molecule::MoleculeChanges changes = + (Molecule::Atoms | Molecule::Bonds | Molecule::Added | Molecule::Removed); + guiMol->undoMolecule()->modifyMolecule(newMol, changes, m_displayName); + } } } return result; From e1b8a6936f53e4d215e3bc92aa584f003acfe56c Mon Sep 17 00:00:00 2001 From: "Marcus D. Hanwell" Date: Wed, 28 Dec 2016 13:42:31 -0500 Subject: [PATCH 6/7] Corrected CMake variable name for workflows --- avogadro/qtplugins/workflows/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avogadro/qtplugins/workflows/CMakeLists.txt b/avogadro/qtplugins/workflows/CMakeLists.txt index f4e5bd1a8..7b7a309fb 100644 --- a/avogadro/qtplugins/workflows/CMakeLists.txt +++ b/avogadro/qtplugins/workflows/CMakeLists.txt @@ -30,5 +30,5 @@ if(INSTALL_TEST_WORKFLOWS) list(APPEND workflows scripts/test.py) endif() -install(PROGRAMS ${input_generators} +install(PROGRAMS ${workflows} DESTINATION "${INSTALL_LIBRARY_DIR}/avogadro2/scripts/workflows/") From b9b789b56e096945aa87cb6719480381c52d9cb4 Mon Sep 17 00:00:00 2001 From: Geoff Hutchison Date: Wed, 28 Dec 2016 14:37:40 -0500 Subject: [PATCH 7/7] Revert accidentally missing molequeue pieces for input generators --- avogadro/molequeue/CMakeLists.txt | 2 + avogadro/molequeue/inputgenerator.cpp | 711 ++++++++++++++++++++++++++ avogadro/molequeue/inputgenerator.h | 635 +++++++++++++++++++++++ 3 files changed, 1348 insertions(+) create mode 100644 avogadro/molequeue/inputgenerator.cpp create mode 100644 avogadro/molequeue/inputgenerator.h diff --git a/avogadro/molequeue/CMakeLists.txt b/avogadro/molequeue/CMakeLists.txt index e65bc39d0..43a919549 100644 --- a/avogadro/molequeue/CMakeLists.txt +++ b/avogadro/molequeue/CMakeLists.txt @@ -11,6 +11,7 @@ find_package(Qt5 COMPONENTS Widgets Network REQUIRED) set(HEADERS batchjob.h + inputgenerator.h inputgeneratordialog.h inputgeneratorwidget.h molequeuedialog.h @@ -21,6 +22,7 @@ set(HEADERS set(SOURCES batchjob.cpp + inputgenerator.cpp inputgeneratordialog.cpp inputgeneratorwidget.cpp molequeuedialog.cpp diff --git a/avogadro/molequeue/inputgenerator.cpp b/avogadro/molequeue/inputgenerator.cpp new file mode 100644 index 000000000..51f53d3ee --- /dev/null +++ b/avogadro/molequeue/inputgenerator.cpp @@ -0,0 +1,711 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2013 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#include "inputgenerator.h" + +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace Avogadro { +namespace MoleQueue { + +using QtGui::PythonScript; +using QtGui::GenericHighlighter; + +InputGenerator::InputGenerator(const QString &scriptFilePath_, QObject *parent_) + : QObject(parent_), + m_interpreter(new PythonScript(scriptFilePath_, this)), + m_moleculeExtension("Unknown") +{ +} + +InputGenerator::InputGenerator(QObject *parent_) + : QObject(parent_), + m_interpreter(new PythonScript(this)), + m_moleculeExtension("Unknown") +{ +} + +InputGenerator::~InputGenerator() +{ +} + +bool InputGenerator::debug() const +{ + return m_interpreter->debug(); +} + +QJsonObject InputGenerator::options() const +{ + m_errors.clear(); + if (m_options.isEmpty()) { + qDeleteAll(m_highlightStyles.values()); + m_highlightStyles.clear(); + + // Retrieve/set options + QByteArray json = m_interpreter->execute( + QStringList() << "--print-options"); + + if (m_interpreter->hasErrors()) { + m_errors << m_interpreter->errorList(); + return m_options; + } + + QJsonDocument doc; + if (!parseJson(json, doc)) + return m_options; + + if (!doc.isObject()) { + m_errors << tr("script --print-options output must be an JSON object " + "at top level. Received:\n%1").arg(json.constData()); + return m_options; + } + + m_options = doc.object(); + + // Check if the generator needs to read a molecule. + m_moleculeExtension = "None"; + if (m_options.contains("inputMoleculeFormat") && + m_options["inputMoleculeFormat"].isString()) { + m_moleculeExtension = m_options["inputMoleculeFormat"].toString(); + } + + if (m_options.contains("highlightStyles") + && m_options.value("highlightStyles").isArray()) { + if (!parseHighlightStyles(m_options.value("highlightStyles").toArray())) { + qDebug() << "Failed to parse highlighting styles."; + } + } + } + + return m_options; +} + +QString InputGenerator::displayName() const +{ + m_errors.clear(); + if (m_displayName.isEmpty()) { + m_displayName = QString(m_interpreter->execute( + QStringList() << "--display-name")); + m_errors << m_interpreter->errorList(); + m_displayName = m_displayName.trimmed(); + } + + return m_displayName; +} + +QString InputGenerator::scriptFilePath() const +{ + return m_interpreter->scriptFilePath(); +} + +void InputGenerator::setScriptFilePath(const QString &scriptFile) +{ + reset(); + m_interpreter->setScriptFilePath(scriptFile); +} + +void InputGenerator::reset() +{ + m_interpreter->setDefaultPythonInterpretor(); + m_interpreter->setScriptFilePath(QString()); + m_moleculeExtension = "Unknown"; + m_displayName = QString(); + m_options = QJsonObject(); + m_warnings.clear(); + m_errors.clear(); + m_filenames.clear(); + m_mainFileName.clear(); + m_files.clear(); + m_fileHighlighters.clear(); + m_highlightStyles.clear(); +} + +bool InputGenerator::generateInput(const QJsonObject &options_, + const Core::Molecule &mol) +{ + m_errors.clear(); + m_warnings.clear(); + m_filenames.clear(); + qDeleteAll(m_fileHighlighters.values()); + m_fileHighlighters.clear(); + m_mainFileName.clear(); + m_files.clear(); + + // Add the molecule file to the options + QJsonObject allOptions(options_); + if (!insertMolecule(allOptions, mol)) + return false; + + QByteArray json(m_interpreter->execute(QStringList() << "--generate-input", + QJsonDocument(allOptions).toJson())); + + if (m_interpreter->hasErrors()) { + m_errors << m_interpreter->errorList(); + return false; + } + + QJsonDocument doc; + if (!parseJson(json, doc)) + return false; + + // Update cache + bool result = true; + if (doc.isObject()) { + QJsonObject obj = doc.object(); + + // Check for any warnings: + if (obj.contains("warnings")) { + if (obj["warnings"].isArray()) { + foreach (const QJsonValue &warning, obj["warnings"].toArray()) { + if (warning.isString()) + m_warnings << warning.toString(); + else + m_errors << tr("Non-string warning returned."); + } + } + else { + m_errors << tr("'warnings' member is not an array."); + } + } + + // Extract input file text: + if (obj.contains("files")) { + if (obj["files"].isArray()) { + foreach (const QJsonValue &file, obj["files"].toArray()) { + if (file.isObject()) { + QJsonObject fileObj = file.toObject(); + if (fileObj["filename"].isString()) { + QString fileName = fileObj["filename"].toString(); + QString contents; + if (fileObj["contents"].isString()) { + contents = fileObj["contents"].toString(); + } + else if (fileObj["filePath"].isString()) { + QFile refFile(fileObj["filePath"].toString()); + if (refFile.exists() && refFile.open(QFile::ReadOnly)) { + contents = QString(refFile.readAll()); + } + else { + contents = tr("Reference file '%1' does not exist.") + .arg(refFile.fileName()); + m_warnings << tr("Error populating file %1: %2") + .arg(fileName, contents); + } + } + else { + m_errors << tr("File '%1' poorly formed. Missing string " + "'contents' or 'filePath' members.") + .arg(fileName); + contents = m_errors.back(); + result = false; + } + replaceKeywords(contents, mol); + m_filenames << fileName; + m_files.insert(fileObj["filename"].toString(), contents); + + // Concatenate the requested styles for this input file. + if (fileObj["highlightStyles"].isArray()) { + GenericHighlighter *highlighter(new GenericHighlighter(this)); + foreach (const QJsonValue &styleVal, + fileObj["highlightStyles"].toArray()) { + if (styleVal.isString()) { + QString styleName(styleVal.toString()); + if (m_highlightStyles.contains(styleName)) { + *highlighter += *m_highlightStyles[styleName]; + } + else { + qDebug() << "Cannot find highlight style '" + << styleName << "' for file '" + << fileName << "'"; + } + } + } + if (highlighter->ruleCount() > 0) + m_fileHighlighters[fileName] = highlighter; + else + highlighter->deleteLater(); + } + } + else { + result = false; + m_errors << tr("Malformed file entry: filename/contents missing" + " or not strings:\n%1") + .arg(QString(QJsonDocument(fileObj).toJson())); + } // end if/else filename and contents are strings + } + else { + result = false; + m_errors << tr("Malformed file entry at index %1: Not an object.") + .arg(m_filenames.size()); + } // end if/else file is JSON object + } // end foreach file + } + else { + result = false; + m_errors << tr("'files' member not an array."); + } // end if obj["files"] is JSON array + } + else { + result = false; + m_errors << tr("'files' member missing."); + } // end if obj contains "files" + + // Extract main input filename: + if (obj.contains("mainFile")) { + if (obj["mainFile"].isString()) { + QString mainFile = obj["mainFile"].toString(); + if (m_filenames.contains(mainFile)) { + m_mainFileName = mainFile; + } + else { + result = false; + m_errors << tr("'mainFile' member does not refer to an entry in " + "'files'."); + } // end if/else mainFile is known + } + else { + result = false; + m_errors << tr("'mainFile' member must be a string."); + } // end if/else mainFile is string + } + else { + // If no mainFile is specified and there is only one file, use it as the + // main file. Otherwise, don't set a main input file -- all files will + // be treated as supplemental input files + if (m_filenames.size() == 1) + m_mainFileName = m_filenames.first(); + } // end if/else object contains mainFile + } + else { + result = false; + m_errors << tr("Response must be a JSON object at top-level."); + } + + if (result == false) + m_errors << tr("Script output:\n%1").arg(QString(json)); + + return result; +} + +int InputGenerator::numberOfInputFiles() const +{ + return m_filenames.size(); +} + +QStringList InputGenerator::fileNames() const +{ + return m_filenames; +} + +QString InputGenerator::mainFileName() const +{ + return m_mainFileName; +} + +QString InputGenerator::fileContents(const QString &fileName) const +{ + return m_files.value(fileName, QString()); +} + +GenericHighlighter * +InputGenerator::createFileHighlighter(const QString &fileName) const +{ + GenericHighlighter *toClone(m_fileHighlighters.value(fileName, NULL)); + return toClone ? new GenericHighlighter(*toClone) : toClone; +} + +void InputGenerator::setDebug(bool d) +{ + m_interpreter->setDebug(d); +} + +bool InputGenerator::parseJson(const QByteArray &json, QJsonDocument &doc) const +{ + QJsonParseError error; + doc = QJsonDocument::fromJson(json, &error); + + if (error.error != QJsonParseError::NoError) { + m_errors << tr("Parse error at offset %L1: '%2'\nRaw JSON:\n\n%3") + .arg(error.offset).arg(error.errorString()).arg(QString(json)); + return false; + } + return true; +} + +bool InputGenerator::insertMolecule(QJsonObject &json, + const Core::Molecule &mol) const +{ + // Update the cached options if the format is not set + if (m_moleculeExtension == "Unknown") + options(); + + if (m_moleculeExtension == "None") + return true; + + Io::FileFormatManager &formats = Io::FileFormatManager::instance(); + QScopedPointer format(formats.newFormatFromFileExtension( + m_moleculeExtension.toStdString())); + + if (format.isNull()) { + m_errors << tr("Error writing molecule representation to string: " + "Unrecognized file format: %1").arg(m_moleculeExtension); + return false; + } + + std::string str; + if (!format->writeString(str, mol)) { + m_errors << tr("Error writing molecule representation to string: %1") + .arg(QString::fromStdString(format->error())); + return false; + } + + if (m_moleculeExtension != "cjson") { + json.insert(m_moleculeExtension, QJsonValue(QString::fromStdString(str))); + } + else { + // If cjson was requested, embed the actual JSON, rather than the string. + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(str.c_str(), &error); + if (error.error != QJsonParseError::NoError) { + m_errors << tr("Error generating cjson object: Parse error at offset %1: " + "%2\nRaw JSON:\n\n%3").arg(error.offset) + .arg(error.errorString()).arg(QString::fromStdString(str)); + return false; + } + + if (!doc.isObject()) { + m_errors << tr("Error generator cjson object: Parsed JSON is not an " + "object:\n%1").arg(QString::fromStdString(str)); + return false; + } + + json.insert(m_moleculeExtension, doc.object()); + } + + return true; +} + +QString InputGenerator::generateCoordinateBlock(const QString &spec, + const Core::Molecule &mol) const +{ + Core::CoordinateBlockGenerator gen; + gen.setMolecule(&mol); + gen.setSpecification(spec.toStdString()); + std::string tmp(gen.generateCoordinateBlock()); + if (!tmp.empty()) + tmp.resize(tmp.size() - 1); // Pop off the trailing newline + return QString::fromStdString(tmp); +} + +void InputGenerator::replaceKeywords(QString &str, + const Core::Molecule &mol) const +{ + // Simple keywords: + str.replace("$$atomCount$$", QString::number(mol.atomCount())); + str.replace("$$bondCount$$", QString::number(mol.bondCount())); + + // Find each coordinate block keyword in the file, then generate and replace + // it with the appropriate values. + QRegExp coordParser("\\$\\$coords:([^\\$]*)\\$\\$"); + int ind = 0; + while ((ind = coordParser.indexIn(str, ind)) != -1) { + // Extract spec and prepare the replacement + const QString keyword = coordParser.cap(0); + const QString spec = coordParser.cap(1); + + // Replace all blocks with this signature + str.replace(keyword, generateCoordinateBlock(spec, mol)); + + } // end for coordinate block +} + +bool InputGenerator::parseHighlightStyles(const QJsonArray &json) const +{ + bool result(true); + foreach (QJsonValue styleVal, json) { + if (!styleVal.isObject()) { + qDebug() << "Non-object in highlightStyles array."; + result = false; + continue; + } + QJsonObject styleObj(styleVal.toObject()); + + if (!styleObj.contains("style")) { + qDebug() << "Style object missing 'style' member."; + result = false; + continue; + } + if (!styleObj.value("style").isString()) { + qDebug() << "Style object contains non-string 'style' member."; + result = false; + continue; + } + QString styleName(styleObj.value("style").toString()); + + if (m_highlightStyles.contains(styleName)) { + qDebug() << "Duplicate highlight style: " << styleName; + result = false; + continue; + } + + if (!styleObj.contains("rules")) { + qDebug() << "Style object" << styleName << "missing 'rules' member."; + result = false; + continue; + } + if (!styleObj.value("rules").isArray()) { + qDebug() << "Style object" << styleName + << "contains non-array 'rules' member."; + result = false; + continue; + } + QJsonArray rulesArray(styleObj.value("rules").toArray()); + + GenericHighlighter *highlighter(new GenericHighlighter( + const_cast(this))); + if (!parseRules(rulesArray, *highlighter)) { + qDebug() << "Error parsing style" << styleName << endl + << QString(QJsonDocument(styleObj).toJson()); + highlighter->deleteLater(); + result = false; + continue; + } + m_highlightStyles.insert(styleName, highlighter); + } + + return result; +} + +bool InputGenerator::parseRules(const QJsonArray &json, + GenericHighlighter &highligher) const +{ + bool result(true); + foreach (QJsonValue ruleVal, json) { + if (!ruleVal.isObject()) { + qDebug() << "Rule is not an object."; + result = false; + continue; + } + QJsonObject ruleObj(ruleVal.toObject()); + + if (!ruleObj.contains("patterns")) { + qDebug() << "Rule missing 'patterns' array:" << endl + << QString(QJsonDocument(ruleObj).toJson()); + result = false; + continue; + } + if (!ruleObj.value("patterns").isArray()) { + qDebug() << "Rule 'patterns' member is not an array:" << endl + << QString(QJsonDocument(ruleObj).toJson()); + result = false; + continue; + } + QJsonArray patternsArray(ruleObj.value("patterns").toArray()); + + if (!ruleObj.contains("format")) { + qDebug() << "Rule missing 'format' object:" << endl + << QString(QJsonDocument(ruleObj).toJson()); + result = false; + continue; + } + if (!ruleObj.value("format").isObject()) { + qDebug() << "Rule 'format' member is not an object:" << endl + << QString(QJsonDocument(ruleObj).toJson()); + result = false; + continue; + } + QJsonObject formatObj(ruleObj.value("format").toObject()); + + GenericHighlighter::Rule &rule = highligher.addRule(); + + foreach (QJsonValue patternVal, patternsArray) { + QRegExp pattern; + if (!parsePattern(patternVal, pattern)) { + qDebug() << "Error while parsing pattern:" << endl + << QString(QJsonDocument(patternVal.toObject()).toJson()); + result = false; + continue; + } + rule.addPattern(pattern); + } + + QTextCharFormat format; + if (!parseFormat(formatObj, format)) { + qDebug() << "Error while parsing format:" << endl + << QString(QJsonDocument(formatObj).toJson()); + result = false; + } + rule.setFormat(format); + } + + return result; +} + +bool InputGenerator::parseFormat(const QJsonObject &json, + QTextCharFormat &format) const +{ + // Check for presets first: + if (json.contains("preset")) { + if (!json["preset"].isString()) { + qDebug() << "Preset is not a string."; + return false; + } + + QString preset(json["preset"].toString()); + /// @todo Store presets in a singleton that can be configured in the GUI, + /// rather than hardcoding them. + if (preset == "title") { + format.setFontFamily("serif"); + format.setForeground(Qt::darkGreen); + format.setFontWeight(QFont::Bold); + } + else if (preset == "keyword") { + format.setFontFamily("mono"); + format.setForeground(Qt::darkBlue); + } + else if (preset == "property") { + format.setFontFamily("mono"); + format.setForeground(Qt::darkRed); + } + else if (preset == "literal") { + format.setFontFamily("mono"); + format.setForeground(Qt::darkMagenta); + } + else if (preset == "comment") { + format.setFontFamily("serif"); + format.setForeground(Qt::darkGreen); + format.setFontItalic(true); + } + else { + qDebug() << "Invalid style preset: " << preset; + return false; + } + return true; + } + + // Extract an RGB tuple from 'array' as a QBrush: + struct { + QBrush operator()(const QJsonArray &array, bool *ok) + { + *ok = false; + QBrush result; + + if (array.size() != 3) + return result; + + int rgb[3]; + for (int i = 0; i < 3; ++i) { + if (!array.at(i).isDouble()) + return result; + rgb[i] = static_cast(array.at(i).toDouble()); + if (rgb[i] < 0 || rgb[i] > 255) { + qDebug() << "Warning: Color component value invalid: " << rgb[i] + << " (Valid range is 0-255)."; + } + } + + result.setColor(QColor(rgb[0], rgb[1], rgb[2])); + result.setStyle(Qt::SolidPattern); + *ok = true; + return result; + } + } colorParser; + + if (json.contains("foreground") + && json.value("foreground").isArray()) { + QJsonArray foregroundArray(json.value("foreground").toArray()); + bool ok; + format.setForeground(colorParser(foregroundArray, &ok)); + if (!ok) + return false; + } + + if (json.contains("background") + && json.value("background").isArray()) { + QJsonArray backgroundArray(json.value("background").toArray()); + bool ok; + format.setBackground(colorParser(backgroundArray, &ok)); + if (!ok) + return false; + } + + if (json.contains("attributes") + && json.value("attributes").isArray()) { + QJsonArray attributesArray(json.value("attributes").toArray()); + format.setFontWeight(attributesArray.contains(QLatin1String("bold")) + ? QFont::Bold : QFont::Normal); + format.setFontItalic(attributesArray.contains(QLatin1String("italic"))); + format.setFontUnderline( + attributesArray.contains(QLatin1String("underline"))); + } + + if (json.contains("family") + && json.value("family").isString()) { + format.setFontFamily(json.value("family").toString()); + } + + return true; +} + +bool InputGenerator::parsePattern(const QJsonValue &json, + QRegExp &pattern) const +{ + if (!json.isObject()) + return false; + + QJsonObject patternObj(json.toObject()); + + if (patternObj.contains("regexp") + && patternObj.value("regexp").isString()) { + pattern.setPatternSyntax(QRegExp::RegExp2); + pattern.setPattern(patternObj.value("regexp").toString()); + } + else if (patternObj.contains("wildcard") + && patternObj.value("wildcard").isString()) { + pattern.setPatternSyntax(QRegExp::WildcardUnix); + pattern.setPattern(patternObj.value("wildcard").toString()); + } + else if (patternObj.contains("string") + && patternObj.value("string").isString()) { + pattern.setPatternSyntax(QRegExp::FixedString); + pattern.setPattern(patternObj.value("string").toString()); + } + else { + return false; + } + + if (patternObj.contains("caseSensitive")) { + pattern.setCaseSensitivity(patternObj.value("caseSensitive").toBool(true) + ? Qt::CaseSensitive : Qt::CaseInsensitive); + } + + return true; +} + +} // namespace MoleQueue +} // namespace Avogadro diff --git a/avogadro/molequeue/inputgenerator.h b/avogadro/molequeue/inputgenerator.h new file mode 100644 index 000000000..143370f21 --- /dev/null +++ b/avogadro/molequeue/inputgenerator.h @@ -0,0 +1,635 @@ +/****************************************************************************** + + This source file is part of the Avogadro project. + + Copyright 2013 Kitware, Inc. + + This source code is released under the New BSD License, (the "License"). + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +******************************************************************************/ + +#ifndef AVOGADRO_MOLEQUEUE_INPUTGENERATOR_H +#define AVOGADRO_MOLEQUEUE_INPUTGENERATOR_H + +#include +#include "avogadromolequeueexport.h" + +#include + +#include +#include +#include + +class QJsonDocument; +class QProcess; +class QTextCharFormat; + +namespace Avogadro { + +namespace Core { +class Molecule; +} + +namespace QtGui { +class GenericHighlighter; +class PythonScript; +} + +namespace MoleQueue { +/** + * @class InputGenerator inputgenerator.h + * @brief The InputGenerator class provides an interface to input generator + * scripts. + * @sa InputGeneratorWidget + * + * The QuantumInput extension provides a scriptable method for users to add + * custom input generators to Avogadro. By writing an executable that implements + * the interface defined below, new input generators can be created faster and + * easier than writing full Avogadro extensions. + * + * Script Entry Points + * =================== + * + * The script must handle the following command-line arguments: + * - `--debug` Enable extra debugging output. Used with other commands. + * It is not required that the script support extra debugging, but it should + * not crash when this option is passed. + * - `--print-options` Print the available options supported by the + * script, e.g. simulation parameters, etc. See below for more details. + * - `--generate-input` Read an option block from stdin and print + * input files to stdout. See below for more details. + * - `--display-name` Print a user-friendly name for the input generator. + * This is used in the GUI for menu entries, window titles, etc. + * + * Specifying parameters with `--print-options` + * ============================================ + * + * The format of the `--print-options` output must be a JSON object of + * the following form: +~~~{.js} +{ + "userOptions": { + ... + }, + "highlightStyles": [ + { + "style": "Descriptive name", + "rules": [ + { + "patterns": [ ... ], + "format": { ... } + }, + ... + ], + }, + ... + ], + "inputMoleculeFormat": "cjson" +} +~~~ + * The `userOptions` block contains a JSON object keyed with option names + * (e.g. "First option name"), which are used in the GUI to label simulation + * parameter settings. Various parameter types are supported: + * + * Fixed Mutually-Exclusive Parameter Lists + * ---------------------------------------- + * + * Parameters that have a fixed number of mutually-exclusive string values will + * be presented using a QComboBox. Such a parameter can be specified in the + * `userOptions` block as: +~~~{.js} +{ + "userOptions": { + "Parameter Name": { + "type": "stringList", + "values": ["Option 1", "Option 2", "Option 3"], + "default": 0 + } + } +} +~~~ + * Here, `Parameter Name` is the label that will be displayed in the GUI as a + * label next to the combo box. + * The array of strings in `values` will be used as the available entries in + * the combo box in the order they are written. + * `default` is a zero-based index into the `values` array and indicates + * which value should be initially selected by default. + * + * Short Free-Form Text Parameters + * ------------------------------- + * + * A short text string can be requested (e.g. for the "title" of an + * optimization) via: +~~~{.js} +{ + "userOptions": { + "Parameter Name": { + "type": "string", + "default": "blah blah blah" + } + } +} +~~~ + * This will add a QLineEdit to the GUI, initialized with the text specified by + * `default`. + * + * Existing files + * -------------- + * + * An input generator can ask for the absolute path to an existing file using + * the following option block: +~~~{.js} +{ + "userOptions": { + "Parameter Name": { + "type": "filePath", + "default": "/path/to/some/file" + } + } +} +~~~ + * This will add an Avogadro::QtGui::FileBrowseWidget to the GUI, initialized to + * the file pointed to by default. + * + * Clamped Integer Values + * ---------------------- + * + * Scripts may request integer values from a specified range by adding a + * user-option of the following form: +~~~{.js} +{ + "userOptions": { + "Parameter Name": { + "type": "integer", + "minimum": -5, + "maximum": 5, + "default": 0, + "prefix": "some text ", + "suffix": " units" + } + } +} +~~~ + * This block will result in a QSpinBox, configured as follows: + * - `minimum` and `maximum` indicate the valid range of integers for the + * parameter. + * - `default` is the integer value that will be shown initially. + * - (optional) `prefix` and `suffix` are used to insert text before or + * after the integer value in the spin box. + * This is handy for specifying units. + * Note that any prefix or suffix will be stripped out of the corresponding + * entry in the call to `--generate-input`, and just the raw integer value + * will be sent. + * + * Boolean Parameters + * ------------------ + * + * If a simple on/off value is needed, a boolean type option can be requested: +~~~{.js} +{ + "userOptions": { + "Parameter Name": { + "type": "boolean", + "default": true, + } + } +} +~~~ + * This will result in a QCheckBox in the dynamically generated GUI, with + * the inital check state shown in `default`. + * + * Special Parameters + * ------------------ + * + * Some parameters are common to most calculation codes. + * If the following parameter names are found, they will be handled specially + * while creating the GUI. + * It is recommended to use the names below for these options to provide a + * consistent interface and ensure that MoleQueue job staging uses correct + * values where appropriate. + * + * | Option name | type | description | + * | :----------------: | :--------: | :------------------------------------------------------------------ | + * | "Title" | string | Input file title comment, MoleQueue job description. | + * | "Filename Base" | string | Input file base name, e.g. "job" in "job.inp". | + * | "Processor Cores" | integer | Number of cores to use. Will be passed to MoleQueue. | + * | "Calculation Type" | stringList | Type of calculation, e.g. "Single Point" or "Equilibrium Geometry". | + * | "Theory" | stringList | Levels of QM theory, e.g. "RHF", "B3LYP", "MP2", "CCSD", etc. | + * | "Basis" | stringList | Available basis sets, e.g. "STO-3G", "6-31G**", etc. | + * | "Charge" | integer | Charge on the system. | + * | "Multiplicity" | integer | Spin multiplicity of the system. | + * + * Syntax Highlighting + * ------------------- + * + * Rules for syntax highlighting can be specified as a collection of regular + * expressions or wildcard patterns and text format specifications in the + * "highlightRules" array. The `highlightRules` format is: +~~~{.js} + "highlightStyles": [ + { + "style": "Style 1", + "rules": [ (list of highlight rules, see below) ], + }, + { + "style": "Style 2", + "rules": [ (list of highlight rules, see below) ], + }, + ... + ], +~~~ + * The `style` name is unique to the style object, and used to associate a + * set of highlighting rules with particular output files. See the + * `--generate-input` documentation for more details. + * + * The general form of a highlight rule is: +~~~{.js} +{ + "patterns": [ + { "regexp": "^Some regexp?$" }, + { "wildcard": "A * wildcard expression" }, + { "string": "An exact string to match.", + "caseSensitive": false + }, + ... + ], + "format": { + "preset": "" + } +} +~~~ + * + * or, + * +~~~{.js} +{ + "patterns": [ + ... + ], + "format": { + "foreground": [ 255, 128, 64 ], + "background": [ 0, 128, 128 ], + "attributes": ["bold", "italic", "underline"], + "family": "serif" + } +} +~~~ + * + * The `patterns` array contains a collection of fixed strings, wildcard + * expressions, and regular expressions (using the QRegExp syntax flavor, see + * the QRegExp documentation) that are used to identify strings that should be + * formatted. + * There must be one of the following members present in each pattern object: + * - `regexp` A QRegExp-style regular expression. If no capture groups ("(...)") + * are defined, the entire match is formatted. If one or more capture groups, + * only the captured texts will be marked. + * - `wildcard` A wildcard expression + * - `string` An exact string to match. + * + * Any pattern object may also set a boolean `caseSensitive` member to indicate + * whether the match should consider character case. If omitted, a + * case-sensitive match is assumed. + * + * The preferred form of the `format` member is simply a specification of a + * preset format. + * This allows for consistent color schemes across input generators. + * The recognized presets are: + * - `"title"`: A human readable title string. + * - `"keyword"`: directives defined by the target input format specification + * to have special meaning, such as tags indicating where coordinates are + * to be found. + * - `"property"`: A property of the simulation, such as level of theory, basis + * set, minimization method, etc. + * - `"literal"`: A numeric literal (i.e. a raw number, such as a coordinate). + * - `"comment"`: Sections of the input that are ignored by the simulation code. + * + * If advanced formatting is desired, the second form of the `format` member + * allows fine-tuning of the font properties: + * - `foreground` color as an RGB tuple, ranged 0-255 + * - `background` color as an RGB tuple, ranged 0-255 + * - `attributes` array of font attributes, valid strings are `"bold"`, + * `"italic"`, or `"underline"` + * - `family` of font. Valid values are `"serif"`, `"sans"`, or `"mono"` + * + * Any of the font property members may be omitted and default QTextCharFormat + * settings will be substituted. + * + * The input generator extension will apply the entries in the `highlightRules` + * object to the text in the order they appear. Thus, later rules will + * override the formatting of earlier rules should a conflict arise. + * + * Requesting Full Structure of Current Molecule + * --------------------------------------------- + * + * The `inputMoleculeFormat` is optional, and can be used to request a + * representation of the current molecule's geometry when + * `--generate-input` is called. The corresponding value + * indicates the format of the molecule that the script expects. If this value + * is omitted, no representation of the structure will be provided. + * + * @note Currently valid options for inputMoleculeFormat are "cjson" for + * Chemical JSON or "cml" for Chemical Markup Language. + * + * Handling User Selections: `--generate-input` + * ============================================ + * + * When `--generate-input` is passed, the information needed to generate + * the input file will be written to the script's standard input + * channel as JSON string of the following form: +~~~{.js} +{ + "cjson": {...}, + "options": { + "First option name": "Value 2", + "Second option name": "Value 1", + ... + } +} +~~~ + * The `cjson` entry will contain a Chemical JSON representation + * of the molecule if `inputMoleculeFormat` is set to "cjson" in the + * `--print-options` output. + * Similarly, a `cml` entry and CML string will exist if a Chemical Markup + * Language representation was requested. + * It will be omitted entirely if `inputMoleculeFormat` is not set. + * The `options` block contains key/value + * pairs for each of the options specified in the `userOptions` block of the + * `--print-options` output. + * + * If the script is called with `--generate-input`, it must write a JSON + * string to standard output with the following format: +~~~{.js} +{ + "files": [ + { + "filename": "file1.ext", + "contents": "...", + "highlightStyles": [ ... ] + }, + { + "filename": "file2.ext", + "filePath": "/path/to/file/on/local/filesystem" + }, + ... + ], + "warnings": ["First warning.", "Second warning.", ... ], + "mainFile": "file2.ext" +} +~~~ + * The `files` block is an array of objects, which define the actual input + * files. The `filename` member provides the name of the file, and + * either `contents` or `filePath` provide the text that goes into the file. + * The `contents` string will be used as the file contents, and `filePath` + * should contain an absolute path to a file on the filesystem to read and use + * as the input file contents. + * The optional `highlightStyles` member is an array of strings describing any + * highlight styles to apply to the file (see `--print-options` documentation). + * Each string in this array must match a `style` description in a highlighting + * rule in the `--print-options` output. + * Zero or more highlighting styles may be applied to any file. + * The order of the files in the + * GUI will match the order of the files in the array, and the first file will + * be displayed first. + * + * The `warnings` member provides an array of strings that describe non-fatal + * warnings to be shown to the users. This is useful for describing + * the resolution of conflicting options, e.g. "Ignoring basis set for + * semi-empirical calculation.". This member is optional and should be omitted + * if no warnings are present. + * + * The `mainFile` member points to the primary input file for a calculation. + * This is the file that will be used as a command line argument when executing + * the simulation code (if applicable), and used by MoleQueue to set the + * `$$inputFileName$$` and `$$inputFileBaseName$$` input template keywords. + * This is optional; if present, the filename must exist in the `files` array. + * If absent and only one file is specified in `files`, the single input file + * will be used. Otherwise, the main file will be left unspecified. + * + * Automatic Generation of Geometry + * ================================ + * + * The generation of molecular geometry descriptions may be skipped in the + * script and deferred to the InputGenerator class by use of a special keyword. + * The "contents" string may contain a keyword of the form +~~~ +$$coords:[coordSpec]$$ +~~~ + * where `[coordSpec]` is a sequence of characters. + * The characters in `[coordSpec]` indicate the information needed about each + * atom in the coordinate block. + * See the CoordinateBlockGenerator documentation for a list of recognized + * characters. + * + * Other keywords that can be used in the input files are: + * - `$$atomCount$$`: Number of atoms in the molecule. + * - `$$bondCount$$`: Number of bonds in the molecule. + * + * Error Handling + * ============== + * + * In general, these scripts should be written robustly so that they will not + * fail under normal circumstances. However, if for some reason an error + * occurs that must be reported to the user, simply write the error message to + * standard output as plain text (i.e. not JSON), and it will be shown to the + * user. + * + * Debugging + * ========= + * + * Debugging may be enabled by defining AVO_QM_INPUT_DEBUG in the process's + * environment. This will cause the --debug option to be passed in + * all calls to generator scripts, and will print extra information to the + * qDebug() stream from within avogadro. The script is free to handle the + * debug flag as the author wishes. + */ +class AVOGADROMOLEQUEUE_EXPORT InputGenerator : public QObject +{ + Q_OBJECT +public: + /** + * Constructor + * @param scriptFilePath_ Absolute path to generator script. + */ + explicit InputGenerator(const QString &scriptFilePath_, + QObject *parent_ = NULL); + explicit InputGenerator(QObject *parent_ = NULL); + ~InputGenerator(); + + /** + * @return True if debugging is enabled. + */ + bool debug() const; + + /** + * @return True if the generator is configured with a valid script. + */ + bool isValid() const; + + /** + * Query the script for the available options (--generate-options) + * and return the output as a JSON object. + * @note The results will be cached the first time this function is called + * and reused in subsequent calls. + * @note If an error occurs, the error string will be set. Call hasErrors() + * to check for success, and errorString() or errorList() to get a + * user-friendly description of the error. + */ + QJsonObject options() const; + + /** + * Query the script for a user-friendly name (--display-name). + * @note The results will be cached the first time this function is called + * and reused in subsequent calls. + * @note If an error occurs, the error string will be set. Call hasErrors() + * to check for success, and errorString() or errorList() to get a + * user-friendly description of the error. + */ + QString displayName() const; + + /** + * @return The path to the generator file. + */ + QString scriptFilePath() const; + + /** + * Set the path to the input generator script file. This will reset any + * cached data held by this class. + */ + void setScriptFilePath(const QString &scriptFile); + + /** + * Clear any cached data and return to an uninitialized state. + */ + void reset(); + + /** + * Request input files from the script using the supplied options object and + * molecule. See the class documentation for details on the @p options_ + * object format. + * + * If the files are generated successfully, use the functions + * numberOfInputFiles(), fileNames(), and fileContents() to retrieve them. + * + * @return true on success and false on failure. + * @note If an error occurs, the error string will be set. Call hasErrors() + * to check for success, and errorString() or errorList() to get a + * user-friendly description of the error. + */ + bool generateInput(const QJsonObject &options_, const Core::Molecule &mol); + + /** + * @return The number of input files stored by generateInput(). + * @note This function is only valid after a successful call to + * generateInput(). + */ + int numberOfInputFiles() const; + + /** + * @return A list of filenames created by generateInput(). + * @note This function is only valid after a successful call to + * generateInput(). + */ + QStringList fileNames() const; + + /** + * @return The "main" input file of the collection. This is the input file + * used by MoleQueue to determine the $$inputFileName$$ and + * $$inputFileBaseName$$ keywords. + * @note This function is only valid after a successful call to + * generateInput(). + */ + QString mainFileName() const; + + /** + * @return A file contents corresponding to @p fileName. Must call + * generateInput() first. + * @sa fileNames + */ + QString fileContents(const QString &fileName) const; + + /** + * @return A syntax highlighter for the file @a fileName. Must call + * generateInput() first. The caller takes ownership of the returned object. + * If no syntax highlighter is defined, this function returns NULL. + * @sa fileNames + */ + QtGui::GenericHighlighter *createFileHighlighter(const QString &fileName) const; + + /** + * @return True if an error is set. + */ + bool hasErrors() const { return !m_errors.isEmpty(); } + + /** + * Reset the error counter. + */ + void clearErrors() { m_errors.clear(); } + + /** + * @return A QStringList containing all errors that occurred in the last call + * to the input generator script. + */ + QStringList errorList() const { return m_errors; } + + /** + * @return A QStringList containing all warnings returned by the input + * generator script in the last call to generateInput. These are + * script-specific warnings. + */ + QStringList warningList() const { return m_warnings; } + +public slots: + /** + * Enable/disable debugging. + */ + void setDebug(bool d); + +private: + QtGui::PythonScript *m_interpreter; + + void setDefaultPythonInterpretor(); + QByteArray execute(const QStringList &args, + const QByteArray &scriptStdin = QByteArray()) const; + bool parseJson(const QByteArray &json, QJsonDocument &doc) const; + QString processErrorString(const QProcess &proc) const; + bool insertMolecule(QJsonObject &json, const Core::Molecule &mol) const; + QString generateCoordinateBlock(const QString &spec, + const Core::Molecule &mol) const; + void replaceKeywords(QString &str, const Core::Molecule &mol) const; + bool parseHighlightStyles(const QJsonArray &json) const; + bool parseRules(const QJsonArray &json, QtGui::GenericHighlighter &highligher) const; + bool parseFormat(const QJsonObject &json, QTextCharFormat &format) const; + bool parsePattern(const QJsonValue &json, QRegExp &pattern) const; + + // File extension of requested molecule format + mutable QString m_moleculeExtension; + mutable QString m_displayName; + mutable QJsonObject m_options; + mutable QStringList m_warnings; + mutable QStringList m_errors; + + QStringList m_filenames; + QString m_mainFileName; + QMap m_files; + QMap m_fileHighlighters; + + mutable QMap m_highlightStyles; + +}; + +inline bool InputGenerator::isValid() const +{ + displayName(); + return !hasErrors(); +} + +} // namespace MoleQueue +} // namespace Avogadro + +#endif // AVOGADRO_MOLEQUEUE_INPUTGENERATOR_H