diff --git a/interface/resources/icons/load-script.svg b/interface/resources/icons/load-script.svg new file mode 100644 index 0000000000..21be61c321 --- /dev/null +++ b/interface/resources/icons/load-script.svg @@ -0,0 +1,125 @@ + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/new-script.svg b/interface/resources/icons/new-script.svg new file mode 100644 index 0000000000..f68fcfa967 --- /dev/null +++ b/interface/resources/icons/new-script.svg @@ -0,0 +1,129 @@ + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/save-script.svg b/interface/resources/icons/save-script.svg new file mode 100644 index 0000000000..04d41b8302 --- /dev/null +++ b/interface/resources/icons/save-script.svg @@ -0,0 +1,674 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + T.Hofmeister + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/start-script.svg b/interface/resources/icons/start-script.svg new file mode 100644 index 0000000000..86354a555d --- /dev/null +++ b/interface/resources/icons/start-script.svg @@ -0,0 +1,557 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Maximillian Merlin + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/stop-script.svg b/interface/resources/icons/stop-script.svg new file mode 100644 index 0000000000..31cdcee749 --- /dev/null +++ b/interface/resources/icons/stop-script.svg @@ -0,0 +1,163 @@ + + + + + + + + + + image/svg+xml + + + + + Maximillian Merlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a885c80968..b95d3e4b38 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -799,6 +799,7 @@ void Application::keyPressEvent(QKeyEvent* event) { if (activeWindow() == _window) { bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isOption = event->modifiers().testFlag(Qt::AltModifier); switch (event->key()) { break; case Qt::Key_BracketLeft: @@ -843,9 +844,11 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_S: - if (isShifted && isMeta) { + if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); - } else if (!isShifted && isMeta) { + } else if (isOption && !isShifted && !isMeta) { + Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); + } else if (!isOption && !isShifted && isMeta) { takeSnapshot(); } else { _myAvatar->setDriveKeys(BACK, 1.f); @@ -3306,13 +3309,14 @@ void Application::stopAllScripts() { bumpSettings(); } -void Application::stopScript(const QString &scriptName) -{ - _scriptEnginesHash.value(scriptName)->stop(); - qDebug() << "stopping script..." << scriptName; - _scriptEnginesHash.remove(scriptName); - _runningScriptsWidget->setRunningScripts(getRunningScripts()); - bumpSettings(); +void Application::stopScript(const QString &scriptName) { + if (_scriptEnginesHash.contains(scriptName)) { + _scriptEnginesHash.value(scriptName)->stop(); + qDebug() << "stopping script..." << scriptName; + _scriptEnginesHash.remove(scriptName); + _runningScriptsWidget->setRunningScripts(getRunningScripts()); + bumpSettings(); + } } void Application::reloadAllScripts() { @@ -3373,7 +3377,10 @@ void Application::uploadSkeleton() { uploadFST(false); } -void Application::loadScript(const QString& scriptName) { +ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScriptFromEditor) { + if(loadScriptFromEditor && _scriptEnginesHash.contains(scriptName) && !_scriptEnginesHash[scriptName]->isFinished()){ + return _scriptEnginesHash[scriptName]; + } // start the script on a new thread... ScriptEngine* scriptEngine = new ScriptEngine(QUrl(scriptName), &_controllerScriptingInterface); @@ -3381,7 +3388,7 @@ void Application::loadScript(const QString& scriptName) { if (!scriptEngine->hasScript()) { qDebug() << "Application::loadScript(), script failed to load..."; - return; + return NULL; } _runningScriptsWidget->setRunningScripts(getRunningScripts()); @@ -3429,8 +3436,12 @@ void Application::loadScript(const QString& scriptName) { workerThread->start(); // restore the main window's active state - _window->activateWindow(); + if (!loadScriptFromEditor) { + _window->activateWindow(); + } bumpSettings(); + + return scriptEngine; } void Application::loadDialog() { diff --git a/interface/src/Application.h b/interface/src/Application.h index 9d609ad5f5..fb48acb721 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -121,7 +121,7 @@ public: ~Application(); void restoreSizeAndPosition(); - void loadScript(const QString& fileNameString); + ScriptEngine* loadScript(const QString& fileNameString, bool loadScriptFromEditor = false); void loadScripts(); void storeSizeAndPosition(); void clearScriptsBeforeRunning(); @@ -247,6 +247,7 @@ public: void skipVersion(QString latestVersion); QStringList getRunningScripts() { return _scriptEnginesHash.keys(); } + ScriptEngine* getScriptEngine(QString scriptHash) { return _scriptEnginesHash.contains(scriptHash) ? _scriptEnginesHash[scriptHash] : NULL; } signals: diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7cdd72afd5..b5b0f65d82 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -195,6 +195,7 @@ Menu::Menu() : QMenu* toolsMenu = addMenu("Tools"); addActionToQMenuAndActionHash(toolsMenu, MenuOption::MetavoxelEditor, 0, this, SLOT(showMetavoxelEditor())); + addActionToQMenuAndActionHash(toolsMenu, MenuOption::ScriptEditor, Qt::ALT | Qt::Key_S, this, SLOT(showScriptEditor())); #ifdef HAVE_QXMPP _chatAction = addActionToQMenuAndActionHash(toolsMenu, @@ -1124,6 +1125,13 @@ void Menu::showMetavoxelEditor() { _MetavoxelEditor->raise(); } +void Menu::showScriptEditor() { + if(!_ScriptEditor || !_ScriptEditor->isVisible()) { + _ScriptEditor = new ScriptEditorWindow(); + } + _ScriptEditor->raise(); +} + void Menu::showChat() { QMainWindow* mainWindow = Application::getInstance()->getWindow(); if (!_chatWindow) { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 597ae4a74b..a55570afaf 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -26,6 +26,7 @@ #include "location/LocationManager.h" #include "ui/PreferencesDialog.h" #include "ui/ChatWindow.h" +#include "ui/ScriptEditorWindow.h" const float ADJUST_LOD_DOWN_FPS = 40.0; const float ADJUST_LOD_UP_FPS = 55.0; @@ -177,6 +178,7 @@ private slots: void cycleFrustumRenderMode(); void runTests(); void showMetavoxelEditor(); + void showScriptEditor(); void showChat(); void toggleChat(); void audioMuteToggled(); @@ -228,6 +230,7 @@ private: FrustumDrawMode _frustumDrawMode; ViewFrustumOffset _viewFrustumOffset; QPointer _MetavoxelEditor; + QPointer _ScriptEditor; QPointer _chatWindow; OctreeStatsDialog* _octreeStatsDialog; LodToolsDialog* _lodToolsDialog; @@ -345,6 +348,7 @@ namespace MenuOption { const QString ResetAvatarSize = "Reset Avatar Size"; const QString RunningScripts = "Running Scripts"; const QString RunTimingTests = "Run Timing Tests"; + const QString ScriptEditor = "Script Editor..."; const QString SettingsExport = "Export Settings"; const QString SettingsImport = "Import Settings"; const QString Shadows = "Shadows"; diff --git a/interface/src/ScriptHighlighting.cpp b/interface/src/ScriptHighlighting.cpp new file mode 100644 index 0000000000..3ab1394097 --- /dev/null +++ b/interface/src/ScriptHighlighting.cpp @@ -0,0 +1,95 @@ +// +// ScriptHighlighting.cpp +// interface/src +// +// Created by Thijs Wenker on 4/15/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptHighlighting.h" +#include + +ScriptHighlighting::ScriptHighlighting(QTextDocument* parent) : + QSyntaxHighlighter(parent) +{ + _keywordRegex = QRegExp("\\b(break|case|catch|continue|debugger|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|this|throw|try|typeof|var|void|while|with)\\b"); + _qoutedTextRegex = QRegExp("\".*\""); + _multiLineCommentBegin = QRegExp("/\\*"); + _multiLineCommentEnd = QRegExp("\\*/"); + _numberRegex = QRegExp("[0-9]+(\\.[0-9]+){0,1}"); + _singleLineComment = QRegExp("//[^\n]*"); + _truefalseRegex = QRegExp("\\b(true|false)\\b"); +} + +void ScriptHighlighting::highlightBlock(const QString& text) { + this->highlightKeywords(text); + this->formatNumbers(text); + this->formatTrueFalse(text); + this->formatQoutedText(text); + this->formatComments(text); +} + +void ScriptHighlighting::highlightKeywords(const QString& text) { + int index = _keywordRegex.indexIn(text); + while (index >= 0) { + int length = _keywordRegex.matchedLength(); + setFormat(index, length, Qt::blue); + index = _keywordRegex.indexIn(text, index + length); + } +} + +void ScriptHighlighting::formatComments(const QString& text) { + + setCurrentBlockState(BlockStateClean); + + int start = (previousBlockState() != BlockStateInMultiComment) ? text.indexOf(_multiLineCommentBegin) : 0; + + while (start > -1) { + int end = text.indexOf(_multiLineCommentEnd, start); + int length = (end == -1 ? text.length() : (end + _multiLineCommentEnd.matchedLength())) - start; + setFormat(start, length, Qt::lightGray); + start = text.indexOf(_multiLineCommentBegin, start + length); + if (end == -1) { + setCurrentBlockState(BlockStateInMultiComment); + } + } + + int index = _singleLineComment.indexIn(text); + while (index >= 0) { + int length = _singleLineComment.matchedLength(); + setFormat(index, length, Qt::lightGray); + index = _singleLineComment.indexIn(text, index + length); + } +} + +void ScriptHighlighting::formatQoutedText(const QString& text){ + int index = _qoutedTextRegex.indexIn(text); + while (index >= 0) { + int length = _qoutedTextRegex.matchedLength(); + setFormat(index, length, Qt::red); + index = _qoutedTextRegex.indexIn(text, index + length); + } +} + +void ScriptHighlighting::formatNumbers(const QString& text){ + int index = _numberRegex.indexIn(text); + while (index >= 0) { + int length = _numberRegex.matchedLength(); + setFormat(index, length, Qt::green); + index = _numberRegex.indexIn(text, index + length); + } +} + +void ScriptHighlighting::formatTrueFalse(const QString& text){ + int index = _truefalseRegex.indexIn(text); + while (index >= 0) { + int length = _truefalseRegex.matchedLength(); + QFont* font = new QFont(this->document()->defaultFont()); + font->setBold(true); + setFormat(index, length, *font); + index = _truefalseRegex.indexIn(text, index + length); + } +} diff --git a/interface/src/ScriptHighlighting.h b/interface/src/ScriptHighlighting.h new file mode 100644 index 0000000000..d86d6f4d77 --- /dev/null +++ b/interface/src/ScriptHighlighting.h @@ -0,0 +1,46 @@ +// +// ScriptHighlighting.h +// interface/src +// +// Created by Thijs Wenker on 4/15/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptHighlighting_h +#define hifi_ScriptHighlighting_h + +#include + +class ScriptHighlighting : public QSyntaxHighlighter { + Q_OBJECT + +public: + ScriptHighlighting(QTextDocument* parent = NULL); + + enum BlockState { + BlockStateClean, + BlockStateInMultiComment + }; + +protected: + void highlightBlock(const QString& text); + void highlightKeywords(const QString& text); + void formatComments(const QString& text); + void formatQoutedText(const QString& text); + void formatNumbers(const QString& text); + void formatTrueFalse(const QString& text); + +private: + QRegExp _keywordRegex; + QRegExp _qoutedTextRegex; + QRegExp _multiLineCommentBegin; + QRegExp _multiLineCommentEnd; + QRegExp _numberRegex; + QRegExp _singleLineComment; + QRegExp _truefalseRegex; +}; + +#endif // hifi_ScriptHighlighting_h diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp new file mode 100644 index 0000000000..1765a5ea1a --- /dev/null +++ b/interface/src/ui/ScriptEditorWidget.cpp @@ -0,0 +1,158 @@ +// +// ScriptEditorWidget.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ui_scriptEditorWidget.h" +#include "ScriptEditorWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "ScriptHighlighting.h" + +ScriptEditorWidget::ScriptEditorWidget() : + _scriptEditorWidgetUI(new Ui::ScriptEditorWidget), + _scriptEngine(NULL) +{ + _scriptEditorWidgetUI->setupUi(this); + + connect(_scriptEditorWidgetUI->scriptEdit->document(), SIGNAL(modificationChanged(bool)), this, SIGNAL(scriptModified())); + connect(_scriptEditorWidgetUI->scriptEdit->document(), SIGNAL(contentsChanged()), this, SLOT(onScriptModified())); + + // remove the title bar (see the Qt docs on setTitleBarWidget) + setTitleBarWidget(new QWidget()); + QFontMetrics fm(_scriptEditorWidgetUI->scriptEdit->font()); + _scriptEditorWidgetUI->scriptEdit->setTabStopWidth(fm.width('0') * 4); + ScriptHighlighting* highlighting = new ScriptHighlighting(_scriptEditorWidgetUI->scriptEdit->document()); + QTimer::singleShot(0, _scriptEditorWidgetUI->scriptEdit, SLOT(setFocus())); +} + +ScriptEditorWidget::~ScriptEditorWidget() { + delete _scriptEditorWidgetUI; +} + +void ScriptEditorWidget::onScriptModified() { + if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { + setRunning(false); + setRunning(true); + } +} + +bool ScriptEditorWidget::isModified() { + return _scriptEditorWidgetUI->scriptEdit->document()->isModified(); +} + +bool ScriptEditorWidget::isRunning() { + return (_scriptEngine != NULL) ? _scriptEngine->isRunning() : false; +} + +bool ScriptEditorWidget::setRunning(bool run) { + if (run && !save()) { + return false; + } + // Clean-up old connections. + disconnect(this, SLOT(onScriptError(const QString&))); + disconnect(this, SLOT(onScriptPrint(const QString&))); + + if (run) { + _scriptEngine = Application::getInstance()->loadScript(_currentScript, true); + connect(_scriptEngine, SIGNAL(runningStateChanged()), this, SIGNAL(runningStateChanged())); + + // Make new connections. + connect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(onScriptError(const QString&))); + connect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(onScriptPrint(const QString&))); + } else { + Application::getInstance()->stopScript(_currentScript); + _scriptEngine = NULL; + } + return true; +} + +bool ScriptEditorWidget::saveFile(const QString &scriptPath) { + QFile file(scriptPath); + if (!file.open(QFile::WriteOnly | QFile::Text)) { + QMessageBox::warning(this, tr("Interface"), tr("Cannot write script %1:\n%2.").arg(scriptPath) + .arg(file.errorString())); + return false; + } + + QTextStream out(&file); + out << _scriptEditorWidgetUI->scriptEdit->toPlainText(); + + setScriptFile(scriptPath); + return true; +} + +void ScriptEditorWidget::loadFile(const QString& scriptPath) { + QFile file(scriptPath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + QMessageBox::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath).arg(file.errorString())); + return; + } + + QTextStream in(&file); + _scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll()); + + setScriptFile(scriptPath); + + disconnect(this, SLOT(onScriptError(const QString&))); + disconnect(this, SLOT(onScriptPrint(const QString&))); + + _scriptEngine = Application::getInstance()->getScriptEngine(scriptPath); + if (_scriptEngine != NULL) { + connect(_scriptEngine, SIGNAL(runningStateChanged()), this, SIGNAL(runningStateChanged())); + connect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(onScriptError(const QString&))); + connect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(onScriptPrint(const QString&))); + } +} + +bool ScriptEditorWidget::save() { + return _currentScript.isEmpty() ? saveAs() : saveFile(_currentScript); +} + +bool ScriptEditorWidget::saveAs() { + QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"), QString(), tr("Javascript (*.js)")); + return !fileName.isEmpty() ? saveFile(fileName) : false; +} + +void ScriptEditorWidget::setScriptFile(const QString& scriptPath) { + _currentScript = scriptPath; + _scriptEditorWidgetUI->scriptEdit->document()->setModified(false); + setWindowModified(false); + + emit scriptnameChanged(); +} + +bool ScriptEditorWidget::questionSave() { + if (_scriptEditorWidgetUI->scriptEdit->document()->isModified()) { + QMessageBox::StandardButton button = QMessageBox::warning(this, tr("Interface"), + tr("The script has been modified.\nDo you want to save your changes?"), QMessageBox::Save | QMessageBox::Discard | + QMessageBox::Cancel, QMessageBox::Save); + return button == QMessageBox::Save ? save() : (button == QMessageBox::Cancel ? false : true); + } + return true; +} + +void ScriptEditorWidget::onScriptError(const QString& message) { + _scriptEditorWidgetUI->debugText->appendPlainText("ERROR: " + message); +} + +void ScriptEditorWidget::onScriptPrint(const QString& message) { + _scriptEditorWidgetUI->debugText->appendPlainText("> " + message); +} diff --git a/interface/src/ui/ScriptEditorWidget.h b/interface/src/ui/ScriptEditorWidget.h new file mode 100644 index 0000000000..3e50280a62 --- /dev/null +++ b/interface/src/ui/ScriptEditorWidget.h @@ -0,0 +1,57 @@ +// +// ScriptEditorWidget.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditorWidget_h +#define hifi_ScriptEditorWidget_h + +#include +#include "ScriptEditorWidget.h" +#include "ScriptEngine.h" + +namespace Ui { + class ScriptEditorWidget; +} + +class ScriptEditorWidget : public QDockWidget { + Q_OBJECT + +public: + ScriptEditorWidget(); + ~ScriptEditorWidget(); + + bool isModified(); + bool isRunning(); + bool setRunning(bool run); + bool saveFile(const QString& scriptPath); + void loadFile(const QString& scriptPath); + void setScriptFile(const QString& scriptPath); + bool save(); + bool saveAs(); + bool questionSave(); + const QString getScriptName() const { return _currentScript; }; + +signals: + void runningStateChanged(); + void scriptnameChanged(); + void scriptModified(); + +private slots: + void onScriptError(const QString& message); + void onScriptPrint(const QString& message); + void onScriptModified(); + +private: + Ui::ScriptEditorWidget* _scriptEditorWidgetUI; + ScriptEngine* _scriptEngine; + QString _currentScript; +}; + +#endif // hifi_ScriptEditorWidget_h diff --git a/interface/src/ui/ScriptEditorWindow.cpp b/interface/src/ui/ScriptEditorWindow.cpp new file mode 100644 index 0000000000..0c34959353 --- /dev/null +++ b/interface/src/ui/ScriptEditorWindow.cpp @@ -0,0 +1,200 @@ +// +// ScriptEditorWindow.cpp +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ui_scriptEditorWindow.h" +#include "ScriptEditorWindow.h" +#include "ScriptEditorWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "FlowLayout.h" + +ScriptEditorWindow::ScriptEditorWindow() : + _ScriptEditorWindowUI(new Ui::ScriptEditorWindow), + _loadMenu(new QMenu), + _saveMenu(new QMenu) +{ + _ScriptEditorWindowUI->setupUi(this); + show(); + addScriptEditorWidget("New script"); + connect(_loadMenu, SIGNAL(aboutToShow()), this, SLOT(loadMenuAboutToShow())); + _ScriptEditorWindowUI->loadButton->setMenu(_loadMenu); + + _saveMenu->addAction("Save as..", this, SLOT(saveScriptAsClicked()), Qt::CTRL | Qt::SHIFT | Qt::Key_S); + + _ScriptEditorWindowUI->saveButton->setMenu(_saveMenu); + + connect(new QShortcut(QKeySequence("Ctrl+N"), this), SIGNAL(activated()), this, SLOT(newScriptClicked())); + connect(new QShortcut(QKeySequence("Ctrl+S"), this), SIGNAL(activated()), this, SLOT(saveScriptClicked())); + connect(new QShortcut(QKeySequence("Ctrl+O"), this), SIGNAL(activated()), this, SLOT(loadScriptClicked())); + connect(new QShortcut(QKeySequence("F5"), this), SIGNAL(activated()), this, SLOT(toggleRunScriptClicked())); +} + +ScriptEditorWindow::~ScriptEditorWindow() { + delete _ScriptEditorWindowUI; +} + +void ScriptEditorWindow::setRunningState(bool run) { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->setRunning(run); + } + this->updateButtons(); +} + +void ScriptEditorWindow::updateButtons() { + _ScriptEditorWindowUI->toggleRunButton->setEnabled(_ScriptEditorWindowUI->tabWidget->currentIndex() != -1); + _ScriptEditorWindowUI->toggleRunButton->setIcon(_ScriptEditorWindowUI->tabWidget->currentIndex() != -1 && + static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning() ? + QIcon("../resources/icons/stop-script.svg") : QIcon("../resources/icons/start-script.svg")); +} + +void ScriptEditorWindow::loadScriptMenu(const QString& scriptName) { + addScriptEditorWidget("loading...")->loadFile(scriptName); + updateButtons(); +} + +void ScriptEditorWindow::loadScriptClicked() { + QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"), QString(), tr("Javascript (*.js)")); + if (!scriptName.isEmpty()) { + addScriptEditorWidget("loading...")->loadFile(scriptName); + updateButtons(); + } +} + +void ScriptEditorWindow::loadMenuAboutToShow() { + _loadMenu->clear(); + QStringList runningScripts = Application::getInstance()->getRunningScripts(); + if (runningScripts.count() > 0) { + QSignalMapper* signalMapper = new QSignalMapper(this); + foreach (const QString& runningScript, runningScripts) { + QAction* runningScriptAction = new QAction(runningScript, _loadMenu); + connect(runningScriptAction, SIGNAL(triggered()), signalMapper, SLOT(map())); + signalMapper->setMapping(runningScriptAction, runningScript); + _loadMenu->addAction(runningScriptAction); + } + connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(loadScriptMenu(const QString&))); + } else { + QAction* naAction = new QAction("(no running scripts)", _loadMenu); + naAction->setDisabled(true); + _loadMenu->addAction(naAction); + } +} + +void ScriptEditorWindow::newScriptClicked() { + addScriptEditorWidget(QString("New script")); +} + +void ScriptEditorWindow::toggleRunScriptClicked() { + this->setRunningState(!(_ScriptEditorWindowUI->tabWidget->currentIndex() !=-1 + && static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning())); +} + +void ScriptEditorWindow::saveScriptClicked() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + currentScriptWidget->save(); + } +} + +void ScriptEditorWindow::saveScriptAsClicked() { + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + currentScriptWidget->saveAs(); + } +} + +ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) { + ScriptEditorWidget* newScriptEditorWidget = new ScriptEditorWidget(); + connect(newScriptEditorWidget, SIGNAL(scriptnameChanged()), this, SLOT(updateScriptNameOrStatus())); + connect(newScriptEditorWidget, SIGNAL(scriptModified()), this, SLOT(updateScriptNameOrStatus())); + connect(newScriptEditorWidget, SIGNAL(runningStateChanged()), this, SLOT(updateButtons())); + _ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title); + _ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget); + newScriptEditorWidget->setUpdatesEnabled(true); + newScriptEditorWidget->adjustSize(); + return newScriptEditorWidget; +} + +void ScriptEditorWindow::tabSwitched(int tabIndex) { + this->updateButtons(); + if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { + ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->currentWidget()); + QString modifiedStar = (currentScriptWidget->isModified() ? "*" : ""); + if (currentScriptWidget->getScriptName().length() > 0) { + this->setWindowTitle("Script Editor [" + currentScriptWidget->getScriptName() + modifiedStar + "]"); + } else { + this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); + } + } else { + this->setWindowTitle("Script Editor"); + } +} + +void ScriptEditorWindow::tabCloseRequested(int tabIndex) { + ScriptEditorWidget* closingScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget + ->widget(tabIndex)); + if(closingScriptWidget->questionSave()) { + _ScriptEditorWindowUI->tabWidget->removeTab(tabIndex); + } +} + +void ScriptEditorWindow::closeEvent(QCloseEvent *event) { + bool unsaved_docs_warning = false; + for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ + if(static_cast(_ScriptEditorWindowUI->tabWidget->widget(i))->isModified()){ + unsaved_docs_warning = true; + break; + } + } + + if (!unsaved_docs_warning || QMessageBox::warning(this, tr("Interface"), + tr("There are some unsaved scripts, are you sure you want to close the editor? Changes will be lost!"), + QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Discard) { + event->accept(); + } else { + event->ignore(); + } +} + +void ScriptEditorWindow::updateScriptNameOrStatus() { + ScriptEditorWidget* source = static_cast(QObject::sender()); + QString modifiedStar = (source->isModified()? "*" : ""); + if (source->getScriptName().length() > 0) { + for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ + if (_ScriptEditorWindowUI->tabWidget->widget(i) == source) { + _ScriptEditorWindowUI->tabWidget->setTabText(i, modifiedStar + QFileInfo(source->getScriptName()).fileName()); + _ScriptEditorWindowUI->tabWidget->setTabToolTip(i, source->getScriptName()); + } + } + } + + if (_ScriptEditorWindowUI->tabWidget->currentWidget() == source) { + if (source->getScriptName().length() > 0) { + this->setWindowTitle("Script Editor [" + source->getScriptName() + modifiedStar + "]"); + } else { + this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); + } + } +} diff --git a/interface/src/ui/ScriptEditorWindow.h b/interface/src/ui/ScriptEditorWindow.h new file mode 100644 index 0000000000..0bf5015ccf --- /dev/null +++ b/interface/src/ui/ScriptEditorWindow.h @@ -0,0 +1,54 @@ +// +// ScriptEditorWindow.h +// interface/src/ui +// +// Created by Thijs Wenker on 4/14/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ScriptEditorWindow_h +#define hifi_ScriptEditorWindow_h + +#include "ScriptEditorWidget.h" + +namespace Ui { + class ScriptEditorWindow; +} + +class ScriptEditorWindow : public QWidget { + Q_OBJECT + +public: + ScriptEditorWindow(); + ~ScriptEditorWindow(); + +protected: + void closeEvent(QCloseEvent* event); + +private: + Ui::ScriptEditorWindow* _ScriptEditorWindowUI; + QMenu* _loadMenu; + QMenu* _saveMenu; + + ScriptEditorWidget* addScriptEditorWidget(QString title); + void setRunningState(bool run); + void setScriptName(const QString& scriptName); + +private slots: + void loadScriptMenu(const QString& scriptName); + void loadScriptClicked(); + void newScriptClicked(); + void toggleRunScriptClicked(); + void saveScriptClicked(); + void saveScriptAsClicked(); + void loadMenuAboutToShow(); + void tabSwitched(int tabIndex); + void tabCloseRequested(int tabIndex); + void updateScriptNameOrStatus(); + void updateButtons(); +}; + +#endif // hifi_ScriptEditorWindow_h diff --git a/interface/ui/scriptEditorWidget.ui b/interface/ui/scriptEditorWidget.ui new file mode 100644 index 0000000000..88761c91c5 --- /dev/null +++ b/interface/ui/scriptEditorWidget.ui @@ -0,0 +1,148 @@ + + + ScriptEditorWidget + + + + 0 + 0 + 691 + 549 + + + + + 0 + 0 + + + + + 541 + 238 + + + + font-family: Helvetica, Arial, sans-serif; + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Qt::NoDockWidgetArea + + + Edit Script + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Courier + 9 + 50 + false + false + + + + font: 9pt "Courier"; + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Debug Log: + + + + + + + Run on the fly (Careful: Any valid change made to the code will run immediately) + + + + + + + Clear + + + + 16 + 16 + + + + + + + + + + font: 8pt "Courier"; + + + true + + + + + + + + + + clearButton + clicked() + debugText + clear() + + + 663 + 447 + + + 350 + 501 + + + + + diff --git a/interface/ui/scriptEditorWindow.ui b/interface/ui/scriptEditorWindow.ui new file mode 100644 index 0000000000..9e1b08de3e --- /dev/null +++ b/interface/ui/scriptEditorWindow.ui @@ -0,0 +1,331 @@ + + + ScriptEditorWindow + + + Qt::WindowModal + + + + 0 + 0 + 706 + 682 + + + + + 400 + 250 + + + + Script Editor + + + font-family: Helvetica, Arial, sans-serif; + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 3 + + + QLayout::SetNoConstraint + + + 0 + + + 0 + + + + + New Script (Ctrl+N) + + + New + + + + ../resources/icons/new-script.svg + ../resources/icons/new-script.svg../resources/icons/new-script.svg + + + + 32 + 32 + + + + + + + + + 30 + 0 + + + + + 25 + 0 + + + + Load Script (Ctrl+O) + + + Load + + + + ../resources/icons/load-script.svg../resources/icons/load-script.svg + + + + 32 + 32 + + + + false + + + QToolButton::MenuButtonPopup + + + Qt::ToolButtonIconOnly + + + + + + + + 30 + 0 + + + + + 32 + 0 + + + + Qt::NoFocus + + + Qt::NoContextMenu + + + Save Script (Ctrl+S) + + + Save + + + + ../resources/icons/save-script.svg../resources/icons/save-script.svg + + + + 32 + 32 + + + + 316 + + + QToolButton::MenuButtonPopup + + + + + + + Toggle Run Script (F5) + + + Run/Stop + + + + ../resources/icons/start-script.svg../resources/icons/start-script.svg + + + + 32 + 32 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + true + + + + 250 + 80 + + + + QTabWidget::West + + + QTabWidget::Triangular + + + -1 + + + Qt::ElideNone + + + true + + + true + + + + + + + + + saveButton + clicked() + ScriptEditorWindow + saveScriptClicked() + + + 236 + 10 + + + 199 + 264 + + + + + toggleRunButton + clicked() + ScriptEditorWindow + toggleRunScriptClicked() + + + 330 + 10 + + + 199 + 264 + + + + + newButton + clicked() + ScriptEditorWindow + newScriptClicked() + + + 58 + 10 + + + 199 + 264 + + + + + loadButton + clicked() + ScriptEditorWindow + loadScriptClicked() + + + 85 + 10 + + + 199 + 264 + + + + + tabWidget + currentChanged(int) + ScriptEditorWindow + tabSwitched(int) + + + 352 + 360 + + + 352 + 340 + + + + + tabWidget + tabCloseRequested(int) + ScriptEditorWindow + tabCloseRequested(int) + + + 352 + 360 + + + 352 + 340 + + + + + diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 684c55fbb0..2e92567fe7 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -43,6 +43,11 @@ static QScriptValue soundConstructor(QScriptContext* context, QScriptEngine* eng return soundScriptValue; } +static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ + qDebug() << "script:print()<<" << context->argument(0).toString(); + engine->evaluate("Script.print('" + context->argument(0).toString() + "')"); + return QScriptValue(); +} ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString, AbstractControllerScriptingInterface* controllerScriptingInterface) : @@ -115,6 +120,7 @@ ScriptEngine::ScriptEngine(const QUrl& scriptURL, _scriptContents = in.readAll(); } else { qDebug() << "ERROR Loading file:" << fileName; + emit errorMessage("ERROR Loading file:" + fileName); } } else { QNetworkAccessManager* networkManager = new QNetworkAccessManager(this); @@ -200,6 +206,9 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(&_engine); qScriptRegisterSequenceMetaType >(&_engine); + QScriptValue printConstructorValue = _engine.newFunction(debugPrint); + _engine.globalObject().setProperty("print", printConstructorValue); + QScriptValue soundConstructorValue = _engine.newFunction(soundConstructor); QScriptValue soundMetaObject = _engine.newQMetaObject(&Sound::staticMetaObject, soundConstructorValue); _engine.globalObject().setProperty("Sound", soundMetaObject); @@ -246,6 +255,7 @@ void ScriptEngine::evaluate() { if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); qDebug() << "Uncaught exception at line" << line << ":" << result.toString(); + emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + result.toString()); } } @@ -266,11 +276,14 @@ void ScriptEngine::run() { init(); } _isRunning = true; + emit runningStateChanged(); QScriptValue result = _engine.evaluate(_scriptContents); if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); + qDebug() << "Uncaught exception at line" << line << ":" << result.toString(); + emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + result.toString()); } timeval startTime; @@ -401,6 +414,7 @@ void ScriptEngine::run() { if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); qDebug() << "Uncaught exception at line" << line << ":" << _engine.uncaughtException().toString(); + emit errorMessage("Uncaught exception at line" + QString::number(line) + ":" + _engine.uncaughtException().toString()); } } emit scriptEnding(); @@ -436,10 +450,12 @@ void ScriptEngine::run() { emit finished(_fileNameString); _isRunning = false; + emit runningStateChanged(); } void ScriptEngine::stop() { _isFinished = true; + emit runningStateChanged(); } void ScriptEngine::timerFired() { @@ -510,6 +526,10 @@ QUrl ScriptEngine::resolveInclude(const QString& include) const { return url; } +void ScriptEngine::print(const QString& message) { + emit printedMessage(message); +} + void ScriptEngine::include(const QString& includeFile) { QUrl url = resolveInclude(includeFile); QString includeContents; @@ -523,6 +543,7 @@ void ScriptEngine::include(const QString& includeFile) { includeContents = in.readAll(); } else { qDebug() << "ERROR Loading file:" << fileName; + emit errorMessage("ERROR Loading file:" + fileName); } } else { QNetworkAccessManager* networkManager = new QNetworkAccessManager(this); @@ -538,5 +559,6 @@ void ScriptEngine::include(const QString& includeFile) { if (_engine.hasUncaughtException()) { int line = _engine.uncaughtExceptionLineNumber(); qDebug() << "Uncaught exception at (" << includeFile << ") line" << line << ":" << result.toString(); + emit errorMessage("Uncaught exception at (" + includeFile + ") line" + QString::number(line) + ":" + result.toString()); } -} +} \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 941c6bbe27..9ea99276d3 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -80,6 +80,9 @@ public: bool hasScript() const { return !_scriptContents.isEmpty(); } + bool isFinished() const { return _isFinished; } + bool isRunning() const { return _isRunning; } + public slots: void stop(); @@ -88,12 +91,16 @@ public slots: void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void include(const QString& includeFile); + void print(const QString& message); signals: void update(float deltaTime); void scriptEnding(); void finished(const QString& fileNameString); void cleanupMenuItem(const QString& menuItemString); + void printedMessage(const QString& message); + void errorMessage(const QString& message); + void runningStateChanged(); protected: QString _scriptContents;