From a613da8032939171af048475ea143b08caf049d3 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Mon, 24 Mar 2014 21:25:59 -0700 Subject: [PATCH 1/6] add Script.include() --- examples/includeExample.js | 16 ++++++ libraries/script-engine/src/ScriptEngine.cpp | 55 +++++++++++++++++++- libraries/script-engine/src/ScriptEngine.h | 8 +-- 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 examples/includeExample.js diff --git a/examples/includeExample.js b/examples/includeExample.js new file mode 100644 index 0000000000..489928d759 --- /dev/null +++ b/examples/includeExample.js @@ -0,0 +1,16 @@ +// +// includeExample.js +// hifi +// +// Created by Brad Hefta-Gaub on 3/24/14 +// Copyright (c) 2013 HighFidelity, Inc. All rights reserved. +// +// This is an example script that demonstrates use of the Script.include() feature +// + +// You can include scripts from URLs +Script.include("http://public.highfidelity.io/scripts/lookWithTouch.js"); + +// You can also include scripts that are relative to the current script +Script.include("editVoxels.js"); +Script.include("../examples/selectAudioDevice.js"); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 38948071ff..834cb33158 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -113,11 +113,12 @@ void ScriptEngine::cleanupMenuItems() { } } -bool ScriptEngine::setScriptContents(const QString& scriptContents) { +bool ScriptEngine::setScriptContents(const QString& scriptContents, const QString& fileNameString) { if (_isRunning) { return false; } _scriptContents = scriptContents; + _fileNameString = fileNameString; return true; } @@ -436,3 +437,55 @@ void ScriptEngine::stopTimer(QTimer *timer) { delete timer; } } + +QUrl ScriptEngine::resolveInclude(const QString& include) const { + // first lets check to see if it's already a full URL + QUrl url(include); + if (!url.scheme().isEmpty()) { + return url; + } + + // we apparently weren't a fully qualified url, so, let's assume we're relative + // to the original URL of our script + QUrl parentURL(_fileNameString); + + // if the parent URL's scheme is empty, then this is probably a local file... + if (parentURL.scheme().isEmpty()) { + parentURL = QUrl::fromLocalFile(_fileNameString); + } + + // at this point we should have a legitimate fully qualified URL for our parent + url = parentURL.resolved(url); + return url; +} + +void ScriptEngine::include(const QString& includeFile) { + QUrl url = resolveInclude(includeFile); + QString includeContents; + + if (url.scheme() == "file") { + QString fileName = url.toLocalFile(); + QFile scriptFile(fileName); + if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { + qDebug() << "Loading file:" << fileName; + QTextStream in(&scriptFile); + includeContents = in.readAll(); + } else { + qDebug() << "ERROR Loading file:" << fileName; + } + } else { + QNetworkAccessManager* networkManager = new QNetworkAccessManager(this); + QNetworkReply* reply = networkManager->get(QNetworkRequest(url)); + qDebug() << "Downloading included script at" << includeFile; + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + includeContents = reply->readAll(); + } + + QScriptValue result = _engine.evaluate(includeContents); + if (_engine.hasUncaughtException()) { + int line = _engine.uncaughtExceptionLineNumber(); + qDebug() << "Uncaught exception at (" << includeFile << ") line" << line << ":" << result.toString(); + } +} diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 755418b0c1..2afe7b475b 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -33,8 +33,8 @@ const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0 / 60.0f) * 1000 * 10 class ScriptEngine : public QObject { Q_OBJECT public: - ScriptEngine(const QString& scriptContents = NO_SCRIPT, bool wantMenuItems = false, - const QString& scriptMenuName = QString(""), + ScriptEngine(const QString& scriptContents = NO_SCRIPT, bool wantMenuItems = false, + const QString& fileNameString = QString(""), AbstractControllerScriptingInterface* controllerScriptingInterface = NULL); /// Access the VoxelsScriptingInterface in order to initialize it with a custom packet sender and jurisdiction listener @@ -44,7 +44,7 @@ public: static ParticlesScriptingInterface* getParticlesScriptingInterface() { return &_particlesScriptingInterface; } /// sets the script contents, will return false if failed, will fail if script is already running - bool setScriptContents(const QString& scriptContents); + bool setScriptContents(const QString& scriptContents, const QString& fileNameString = QString("")); const QString& getScriptMenuName() const { return _scriptMenuName; } void cleanupMenuItems(); @@ -75,6 +75,7 @@ public slots: QObject* setTimeout(const QScriptValue& function, int timeoutMS); void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } + void include(const QString& includeFile); signals: void update(float deltaTime); @@ -97,6 +98,7 @@ protected: int _numAvatarSoundSentBytes; private: + QUrl resolveInclude(const QString& include) const; void sendAvatarIdentityPacket(); void sendAvatarBillboardPacket(); From c34b64e296bdd2d92c9feae9e4b8b99e01e4eb57 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Tue, 25 Mar 2014 00:17:19 -0700 Subject: [PATCH 2/6] add load script from URL support --- interface/src/Application.cpp | 58 +++++++++------- interface/src/Application.h | 1 + interface/src/Menu.cpp | 2 + interface/src/Menu.h | 5 +- libraries/script-engine/src/ScriptEngine.cpp | 71 ++++++++++++++++++-- libraries/script-engine/src/ScriptEngine.h | 5 ++ 6 files changed, 112 insertions(+), 30 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2ca4ef74cd..8bcfb5151b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -3508,35 +3509,17 @@ void Application::cleanupScriptMenuItem(const QString& scriptMenuName) { Menu::getInstance()->removeAction(Menu::getInstance()->getActiveScriptsMenu(), scriptMenuName); } -void Application::loadScript(const QString& fileNameString) { - QByteArray fileNameAscii = fileNameString.toLocal8Bit(); - const char* fileName = fileNameAscii.data(); - - std::ifstream file(fileName, std::ios::in|std::ios::binary|std::ios::ate); - if(!file.is_open()) { - qDebug("Error loading file %s", fileName); - return; - } - qDebug("Loading file %s...", fileName); - _activeScripts.append(fileNameString); - - // get file length.... - unsigned long fileLength = file.tellg(); - file.seekg( 0, std::ios::beg ); - - // read the entire file into a buffer, WHAT!? Why not. - char* entireFile = new char[fileLength+1]; - file.read((char*)entireFile, fileLength); - file.close(); - - entireFile[fileLength] = 0;// null terminate - QString script(entireFile); - delete[] entireFile; +void Application::loadScript(const QString& scriptName) { // start the script on a new thread... bool wantMenuItems = true; // tells the ScriptEngine object to add menu items for itself + ScriptEngine* scriptEngine = new ScriptEngine(QUrl(scriptName), wantMenuItems, &_controllerScriptingInterface); - ScriptEngine* scriptEngine = new ScriptEngine(script, wantMenuItems, fileName, &_controllerScriptingInterface); + if (!scriptEngine->hasScript()) { + qDebug() << "Application::loadScript(), script failed to load..."; + return; + } + _activeScripts.append(scriptName); // add a stop menu item Menu::getInstance()->addActionToQMenuAndActionHash(Menu::getInstance()->getActiveScriptsMenu(), @@ -3599,6 +3582,31 @@ void Application::loadDialog() { loadScript(fileNameString); } +void Application::loadScriptURLDialog() { + + QInputDialog scriptURLDialog(Application::getInstance()->getWindow()); + scriptURLDialog.setWindowTitle("Open and Run Script URL"); + scriptURLDialog.setLabelText("Script:"); + scriptURLDialog.setWindowFlags(Qt::Sheet); + const float DIALOG_RATIO_OF_WINDOW = 0.30f; + scriptURLDialog.resize(scriptURLDialog.parentWidget()->size().width() * DIALOG_RATIO_OF_WINDOW, + scriptURLDialog.size().height()); + + int dialogReturn = scriptURLDialog.exec(); + QString newScript; + if (dialogReturn == QDialog::Accepted) { + if (scriptURLDialog.textValue().size() > 0) { + // the user input a new hostname, use that + newScript = scriptURLDialog.textValue(); + } + loadScript(newScript); + } + + sendFakeEnterEvent(); +} + + + void Application::toggleLogDialog() { if (! _logDialog) { _logDialog = new LogDialog(_glWidget, getLogger()); diff --git a/interface/src/Application.h b/interface/src/Application.h index caeea529af..ccd07d41b6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -254,6 +254,7 @@ public slots: void setRenderVoxels(bool renderVoxels); void doKillLocalVoxels(); void loadDialog(); + void loadScriptURLDialog(); void toggleLogDialog(); void initAvatarAndViewFrustum(); void stopAllScripts(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7adef1be1c..49b839873f 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -107,6 +107,8 @@ Menu::Menu() : addDisabledActionAndSeparator(fileMenu, "Scripts"); addActionToQMenuAndActionHash(fileMenu, MenuOption::LoadScript, Qt::CTRL | Qt::Key_O, appInstance, SLOT(loadDialog())); + addActionToQMenuAndActionHash(fileMenu, MenuOption::LoadScriptURL, + Qt::CTRL | Qt::SHIFT | Qt::Key_O, appInstance, SLOT(loadScriptURLDialog())); addActionToQMenuAndActionHash(fileMenu, MenuOption::StopAllScripts, 0, appInstance, SLOT(stopAllScripts())); addActionToQMenuAndActionHash(fileMenu, MenuOption::ReloadAllScripts, 0, appInstance, SLOT(reloadAllScripts())); _activeScriptsMenu = fileMenu->addMenu("Running Scripts"); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index cb0ca4c5c4..08887d7b18 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -272,7 +272,8 @@ namespace MenuOption { const QString OffAxisProjection = "Off-Axis Projection"; const QString OldVoxelCullingMode = "Old Voxel Culling Mode"; const QString TurnWithHead = "Turn using Head"; - const QString LoadScript = "Open and Run Script..."; + const QString LoadScript = "Open and Run Script File..."; + const QString LoadScriptURL = "Open and Run Script from URL..."; const QString Oscilloscope = "Audio Oscilloscope"; const QString Pair = "Pair"; const QString Particles = "Particles"; @@ -304,4 +305,6 @@ namespace MenuOption { const QString VoxelTextures = "Voxel Textures"; } +void sendFakeEnterEvent(); + #endif /* defined(__hifi__Menu__) */ diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 834cb33158..9bd00a0019 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -64,13 +64,10 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, bool wantMenuItems, co _quatLibrary(), _vec3Library() { - QByteArray fileNameAscii = fileNameString.toLocal8Bit(); - const char* scriptMenuName = fileNameAscii.data(); - // some clients will use these menu features if (!fileNameString.isEmpty()) { _scriptMenuName = "Stop "; - _scriptMenuName.append(scriptMenuName); + _scriptMenuName.append(qPrintable(fileNameString)); _scriptMenuName.append(QString(" [%1]").arg(_scriptNumber)); } else { _scriptMenuName = "Stop Script "; @@ -79,6 +76,72 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, bool wantMenuItems, co _scriptNumber++; } +ScriptEngine::ScriptEngine(const QUrl& scriptURL, bool wantMenuItems, + AbstractControllerScriptingInterface* controllerScriptingInterface) : + _scriptContents(), + _isFinished(false), + _isRunning(false), + _isInitialized(false), + _engine(), + _isAvatar(false), + _avatarIdentityTimer(NULL), + _avatarBillboardTimer(NULL), + _timerFunctionMap(), + _isListeningToAudioStream(false), + _avatarSound(NULL), + _numAvatarSoundSentBytes(0), + _controllerScriptingInterface(controllerScriptingInterface), + _avatarData(NULL), + _wantMenuItems(wantMenuItems), + _scriptMenuName(), + _fileNameString(), + _quatLibrary(), + _vec3Library() +{ + QString scriptURLString = scriptURL.toString(); + _fileNameString = scriptURLString; + // some clients will use these menu features + if (!scriptURLString.isEmpty()) { + _scriptMenuName = "Stop "; + _scriptMenuName.append(qPrintable(scriptURLString)); + _scriptMenuName.append(QString(" [%1]").arg(_scriptNumber)); + } else { + _scriptMenuName = "Stop Script "; + _scriptMenuName.append(_scriptNumber); + } + _scriptNumber++; + + QUrl url(scriptURL); + + // if the scheme is empty, maybe they typed in a file, let's try + if (url.scheme().isEmpty()) { + url = QUrl::fromLocalFile(scriptURLString); + } + + // ok, let's see if it's valid... and if so, load it + if (url.isValid()) { + if (url.scheme() == "file") { + QString fileName = url.toLocalFile(); + QFile scriptFile(fileName); + if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { + qDebug() << "Loading file:" << fileName; + QTextStream in(&scriptFile); + _scriptContents = in.readAll(); + } else { + qDebug() << "ERROR Loading file:" << fileName; + } + } else { + QNetworkAccessManager* networkManager = new QNetworkAccessManager(this); + QNetworkReply* reply = networkManager->get(QNetworkRequest(url)); + qDebug() << "Downloading included script at" << url; + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + _scriptContents = reply->readAll(); + } + } +} + void ScriptEngine::setIsAvatar(bool isAvatar) { _isAvatar = isAvatar; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 2afe7b475b..4fc90d2959 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -33,6 +33,9 @@ const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0 / 60.0f) * 1000 * 10 class ScriptEngine : public QObject { Q_OBJECT public: + ScriptEngine(const QUrl& scriptURL, bool wantMenuItems = false, + AbstractControllerScriptingInterface* controllerScriptingInterface = NULL); + ScriptEngine(const QString& scriptContents = NO_SCRIPT, bool wantMenuItems = false, const QString& fileNameString = QString(""), AbstractControllerScriptingInterface* controllerScriptingInterface = NULL); @@ -68,6 +71,8 @@ public: void timerFired(); + bool hasScript() const { return !_scriptContents.isEmpty(); } + public slots: void stop(); From 571ecf096433ac0e19e1c48feea20d7f34dce1ef Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Tue, 25 Mar 2014 00:45:31 -0700 Subject: [PATCH 3/6] add first cut at firstrun support --- interface/src/Application.cpp | 24 ++++++++++++++++++++---- interface/src/Application.h | 1 + 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8bcfb5151b..437f050c88 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -251,7 +251,7 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : QMetaObject::invokeMethod(&accountManager, "checkAndSignalForAccessToken", Qt::QueuedConnection); _settings = new QSettings(this); - + // Check to see if the user passed in a command line option for loading a local // Voxel File. _voxelsFilename = getCmdOption(argc, constArgv, "-i"); @@ -330,9 +330,18 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : LocalVoxelsList::getInstance()->addPersistantTree(DOMAIN_TREE_NAME, _voxels.getTree()); LocalVoxelsList::getInstance()->addPersistantTree(CLIPBOARD_TREE_NAME, &_clipboard); - - // do this as late as possible so that all required subsystems are inialized - loadScripts(); + + // check first run... + QVariant firstRunValue = _settings->value("firstRun",QVariant(true)); + if (firstRunValue.isValid() && firstRunValue.toBool()) { + qDebug() << "This is a first run..."; + // clear the scripts, and set out script to our default scripts + clearScriptsBeforeRunning(); + loadScript("http://public.highfidelity.io/scripts/defaultScripts.js"); + } else { + // do this as late as possible so that all required subsystems are inialized + loadScripts(); + } } Application::~Application() { @@ -3454,6 +3463,13 @@ void Application::loadScripts() { settings->endArray(); } +void Application::clearScriptsBeforeRunning() { + // clears all scripts from the settings + QSettings* settings = new QSettings(this); + settings->beginWriteArray("Settings"); + settings->endArray(); +} + void Application::saveScripts() { // saves all current running scripts QSettings* settings = new QSettings(this); diff --git a/interface/src/Application.h b/interface/src/Application.h index ccd07d41b6..15778f2a17 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -115,6 +115,7 @@ public: void loadScript(const QString& fileNameString); void loadScripts(); void storeSizeAndPosition(); + void clearScriptsBeforeRunning(); void saveScripts(); void initializeGL(); void paintGL(); From f10d5e23f8732ff5594eefb6723ddd7e3d3317db Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Tue, 25 Mar 2014 00:57:29 -0700 Subject: [PATCH 4/6] set firstRun so it only happens once --- interface/src/Application.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 437f050c88..13c05fa702 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -338,6 +338,8 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : // clear the scripts, and set out script to our default scripts clearScriptsBeforeRunning(); loadScript("http://public.highfidelity.io/scripts/defaultScripts.js"); + + _settings->setValue("firstRun",QVariant(false)); } else { // do this as late as possible so that all required subsystems are inialized loadScripts(); From 7b50e6d4ef9362a2acb489d35a32094b8a7c2a67 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Tue, 25 Mar 2014 01:02:50 -0700 Subject: [PATCH 5/6] added hydraMove.js to default scripts --- examples/defaultScripts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index 4b228492c7..eae3c2eb8c 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -3,3 +3,4 @@ Script.include("lookWithTouch.js"); Script.include("editVoxels.js"); Script.include("selectAudioDevice.js"); +Script.include("hydraMove.js"); \ No newline at end of file From d8bf5be68b99d768edbe8468b36ea14b77928929 Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Tue, 25 Mar 2014 08:43:56 -0700 Subject: [PATCH 6/6] fix audio position in editVoxles.js --- examples/editVoxels.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/editVoxels.js b/examples/editVoxels.js index cb2553a8e3..5e4b77a10f 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -400,9 +400,9 @@ function calcScaleFromThumb(newThumbX) { } function setAudioPosition() { - var camera = Camera.getPosition(); + var position = MyAvatar.position; var forwardVector = Quat.getFront(MyAvatar.orientation); - audioOptions.position = Vec3.sum(camera, forwardVector); + audioOptions.position = Vec3.sum(position, forwardVector); } function getNewPasteVoxel(pickRay) { @@ -735,6 +735,7 @@ function trackKeyReleaseEvent(event) { if (event.text == "TAB") { editToolsOn = !editToolsOn; moveTools(); + setAudioPosition(); // make sure we set the audio position before playing sounds showPreviewGuides(); Audio.playSound(clickSound, audioOptions); }