// // WindowScriptingInterface.cpp // interface/src/scripting // // Created by Ryan Huffman on 4/29/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 "WindowScriptingInterface.h" #include #include #include #include #include #include #include #include #include #include "AndroidHelper.h" #include "Application.h" #include "DomainHandler.h" #include "MainWindow.h" #include "Menu.h" #include "OffscreenUi.h" static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation"; static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation"; WindowScriptingInterface::WindowScriptingInterface() { const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &WindowScriptingInterface::disconnectedFromDomain); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused); connect(qApp, &Application::svoImportRequested, [this](const QString& urlString) { static const QMetaMethod svoImportRequestedSignal = QMetaMethod::fromSignal(&WindowScriptingInterface::svoImportRequested); if (isSignalConnected(svoImportRequestedSignal)) { QUrl url(urlString); emit svoImportRequested(url.url()); } else { OffscreenUi::asyncWarning("Import SVO Error", "You need to be running edit.js to import entities."); } }); connect(qApp->getWindow(), &MainWindow::windowGeometryChanged, this, &WindowScriptingInterface::onWindowGeometryChanged); } WindowScriptingInterface::~WindowScriptingInterface() { QHashIterator i(_messageBoxes); while (i.hasNext()) { i.next(); auto messageBox = i.value(); disconnect(messageBox); messageBox->setVisible(false); messageBox->deleteLater(); } _messageBoxes.clear(); } QScriptValue WindowScriptingInterface::hasFocus() { return qApp->hasFocus(); } void WindowScriptingInterface::setFocus() { // It's forbidden to call focus() from another thread. qApp->postLambdaEvent([] { qApp->setFocus(); }); } void WindowScriptingInterface::raise() { // It's forbidden to call raise() from another thread. qApp->postLambdaEvent([] { qApp->raise(); }); } /// Display an alert box /// \param const QString& message message to display /// \return QScriptValue::UndefinedValue void WindowScriptingInterface::alert(const QString& message) { OffscreenUi::asyncWarning("", message, QMessageBox::Ok, QMessageBox::Ok); } /// Display a confirmation box with the options 'Yes' and 'No' /// \param const QString& message message to display /// \return QScriptValue `true` if 'Yes' was clicked, `false` otherwise QScriptValue WindowScriptingInterface::confirm(const QString& message) { return QScriptValue((QMessageBox::Yes == OffscreenUi::question("", message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes))); } /// Display a prompt with a text box /// \param const QString& message message to display /// \param const QString& defaultText default text in the text box /// \return QScriptValue string text value in text box if the dialog was accepted, `null` otherwise. QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) { QString result = OffscreenUi::getText(nullptr, "", message, QLineEdit::Normal, defaultText); if (QScriptValue(result).equals("")) { return QScriptValue::NullValue; } return QScriptValue(result); } /// Display a prompt with a text box /// \param const QString& message message to display /// \param const QString& defaultText default text in the text box void WindowScriptingInterface::promptAsync(const QString& message, const QString& defaultText) { bool ok = false; ModalDialogListener* dlg = OffscreenUi::getTextAsync(nullptr, "", message, QLineEdit::Normal, defaultText, &ok); connect(dlg, &ModalDialogListener::response, this, [=] (QVariant result) { disconnect(dlg, &ModalDialogListener::response, this, nullptr); emit promptTextChanged(result.toString()); }); } void WindowScriptingInterface::disconnectedFromDomain() { emit domainChanged(QUrl()); } void WindowScriptingInterface::openUrl(const QUrl& url) { if (!url.isEmpty()) { if (url.scheme() == URL_SCHEME_HIFI) { DependencyManager::get()->handleLookupString(url.toString()); } else { #if defined(Q_OS_ANDROID) QList args = { url.toString() }; AndroidHelper::instance().requestActivity("WebView", true, args); #else // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); #endif } } } void WindowScriptingInterface::openAndroidActivity(const QString& activityName, const bool backToScene) { #if defined(Q_OS_ANDROID) AndroidHelper::instance().requestActivity(activityName, backToScene); #endif } QString fixupPathForMac(const QString& directory) { // On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus // filename if the directory is valid. QString path = ""; QFileInfo fileInfo = QFileInfo(directory); if (fileInfo.isDir()) { fileInfo.setFile(directory, "__HIFI_INVALID_FILE__"); path = fileInfo.filePath(); } return path; } QString WindowScriptingInterface::getPreviousBrowseLocation() const { return Setting::Handle(LAST_BROWSE_LOCATION_SETTING, DESKTOP_LOCATION).get(); } void WindowScriptingInterface::setPreviousBrowseLocation(const QString& location) { Setting::Handle(LAST_BROWSE_LOCATION_SETTING).set(location); } QString WindowScriptingInterface::getPreviousBrowseAssetLocation() const { QString ASSETS_ROOT_PATH = "/"; return Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING, ASSETS_ROOT_PATH).get(); } void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& location) { Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location); } bool WindowScriptingInterface::getInterstitialModeEnabled() const { return DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); } void WindowScriptingInterface::setInterstitialModeEnabled(bool enableInterstitialMode) { DependencyManager::get()->getDomainHandler().setInterstitialModeEnabled(enableInterstitialMode); } bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) { auto offscreenUi = DependencyManager::get(); return offscreenUi->isPointOnDesktopWindow(point); } /// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and /// might be in same thread as a script that sets the reticle to invisible void WindowScriptingInterface::ensureReticleVisible() const { auto compositorHelper = DependencyManager::get(); if (!compositorHelper->getReticleVisible()) { compositorHelper->setReticleVisible(true); } } /// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the directory browser at /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif QString result = OffscreenUi::getExistingDirectory(nullptr, title, path); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } /// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the directory browser at void WindowScriptingInterface::browseDirAsync(const QString& title, const QString& directory) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif ModalDialogListener* dlg = OffscreenUi::getExistingDirectoryAsync(nullptr, title, path); connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { const QString& result = response.toString(); disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } emit browseDirChanged(result); }); } /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif QString result = OffscreenUi::getOpenFileName(nullptr, title, path, nameFilter); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } /// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` void WindowScriptingInterface::browseAsync(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(nullptr, title, path, nameFilter); connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { const QString& result = response.toString(); disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } emit browseChanged(result); }); } /// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` /// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::save(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif QString result = OffscreenUi::getSaveFileName(nullptr, title, path, nameFilter); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } /// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the file browser at /// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` void WindowScriptingInterface::saveAsync(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseLocation(); } #ifndef Q_OS_WIN path = fixupPathForMac(directory); #endif ModalDialogListener* dlg = OffscreenUi::getSaveFileNameAsync(nullptr, title, path, nameFilter); connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { const QString& result = response.toString(); disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } emit saveFileChanged(result); }); } /// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid /// directory the browser will start at the root directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the asset browser at /// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog` /// \return QScriptValue asset path as a string if one was selected, otherwise `QScriptValue::NullValue` QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseAssetLocation(); } if (path.left(1) != "/") { path = "/" + path; } if (path.right(1) != "/") { path = path + "/"; } QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter); if (!result.isEmpty()) { setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); } return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } /// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid /// directory the browser will start at the root directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the asset browser at /// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog` void WindowScriptingInterface::browseAssetsAsync(const QString& title, const QString& directory, const QString& nameFilter) { ensureReticleVisible(); QString path = directory; if (path.isEmpty()) { path = getPreviousBrowseAssetLocation(); } if (path.left(1) != "/") { path = "/" + path; } if (path.right(1) != "/") { path = path + "/"; } ModalDialogListener* dlg = OffscreenUi::getOpenAssetNameAsync(nullptr, title, path, nameFilter); connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { const QString& result = response.toString(); disconnect(dlg, &ModalDialogListener::response, this, nullptr); if (!result.isEmpty()) { setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); } emit assetsDirChanged(result); }); } void WindowScriptingInterface::showAssetServer(const QString& upload) { QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload)); } QString WindowScriptingInterface::checkVersion() { return QCoreApplication::applicationVersion(); } QString WindowScriptingInterface::protocolSignature() { return protocolVersionsSignatureBase64(); } int WindowScriptingInterface::getInnerWidth() { return qApp->getWindow()->geometry().width(); } int WindowScriptingInterface::getInnerHeight() { return qApp->getWindow()->geometry().height() - qApp->getPrimaryMenu()->geometry().height(); } glm::vec2 WindowScriptingInterface::getDeviceSize() const { return qApp->getDeviceSize(); } int WindowScriptingInterface::getLastDomainConnectionError() const { return DependencyManager::get()->getDomainHandler().getLastDomainConnectionError(); } int WindowScriptingInterface::getX() { return qApp->getWindow()->geometry().x(); } int WindowScriptingInterface::getY() { auto menu = qApp->getPrimaryMenu(); int menuHeight = menu ? menu->geometry().height() : 0; return qApp->getWindow()->geometry().y() + menuHeight; } void WindowScriptingInterface::onWindowGeometryChanged(const QRect& windowGeometry) { auto geometry = windowGeometry; auto menu = qApp->getPrimaryMenu(); if (menu) { geometry.setY(geometry.y() + menu->geometry().height()); } emit geometryChanged(geometry); } void WindowScriptingInterface::copyToClipboard(const QString& text) { if (QThread::currentThread() != qApp->thread()) { QMetaObject::invokeMethod(this, "copyToClipboard", Q_ARG(QString, text)); return; } qDebug() << "Copying"; QApplication::clipboard()->setText(text); } bool WindowScriptingInterface::setDisplayTexture(const QString& name) { return qApp->getActiveDisplayPlugin()->setDisplayTexture(name); // Plugins that don't know how, answer false. } void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { qApp->takeSnapshot(notify, includeAnimated, aspectRatio, filename); } void WindowScriptingInterface::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { qApp->takeSecondaryCameraSnapshot(notify, filename); } void WindowScriptingInterface::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { qApp->takeSecondaryCamera360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); } void WindowScriptingInterface::shareSnapshot(const QString& path, const QUrl& href) { qApp->shareSnapshot(path, href); } void WindowScriptingInterface::makeConnection(bool success, const QString& userNameOrError) { if (success) { emit connectionAdded(userNameOrError); } else { emit connectionError(userNameOrError); } } void WindowScriptingInterface::displayAnnouncement(const QString& message) { emit announcement(message); } bool WindowScriptingInterface::isPhysicsEnabled() { return qApp->isPhysicsEnabled(); } int WindowScriptingInterface::openMessageBox(QString title, QString text, int buttons, int defaultButton) { if (QThread::currentThread() != thread()) { int result; BLOCKING_INVOKE_METHOD(this, "openMessageBox", Q_RETURN_ARG(int, result), Q_ARG(QString, title), Q_ARG(QString, text), Q_ARG(int, buttons), Q_ARG(int, defaultButton)); return result; } return createMessageBox(title, text, buttons, defaultButton); } /**jsdoc *

The buttons that may be included in a message box created by {@link Window.openMessageBox|openMessageBox} are defined by * numeric values: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
ButtonValueDescription
NoButton 0x0 An invalid button.
Ok 0x400 "OK"
Save 0x800 "Save"
SaveAll 0x1000 "Save All"
Open 0x2000 "Open"
Yes 0x4000 "Yes"
YesToAll 0x8000 "Yes to All"
No 0x10000 "No"
NoToAll 0x20000 "No to All"
Abort 0x40000 "Abort"
Retry 0x80000 "Retry"
Ignore 0x100000 "Ignore"
Close 0x200000 "Close"
Cancel 0x400000 "Cancel"
Discard 0x800000 "Discard" or "Don't Save"
Help 0x1000000 "Help"
Apply 0x2000000 "Apply"
Reset 0x4000000 "Reset"
RestoreDefaults 0x8000000 "Restore Defaults"
* @typedef {number} Window.MessageBoxButton */ int WindowScriptingInterface::createMessageBox(QString title, QString text, int buttons, int defaultButton) { auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, static_cast>(buttons), static_cast(defaultButton)); connect(messageBox, SIGNAL(selected(int)), this, SLOT(onMessageBoxSelected(int))); _lastMessageBoxID += 1; _messageBoxes.insert(_lastMessageBoxID, messageBox); return _lastMessageBoxID; } void WindowScriptingInterface::updateMessageBox(int id, QString title, QString text, int buttons, int defaultButton) { auto messageBox = _messageBoxes.value(id); if (messageBox) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "updateMessageBox", Q_ARG(int, id), Q_ARG(QString, title), Q_ARG(QString, text), Q_ARG(int, buttons), Q_ARG(int, defaultButton)); return; } messageBox->setProperty("title", title); messageBox->setProperty("text", text); messageBox->setProperty("buttons", buttons); messageBox->setProperty("defaultButton", defaultButton); } } void WindowScriptingInterface::closeMessageBox(int id) { auto messageBox = _messageBoxes.value(id); if (messageBox) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "closeMessageBox", Q_ARG(int, id)); return; } disconnect(messageBox); messageBox->setVisible(false); messageBox->deleteLater(); _messageBoxes.remove(id); } } void WindowScriptingInterface::onMessageBoxSelected(int button) { auto messageBox = qobject_cast(sender()); auto keys = _messageBoxes.keys(messageBox); if (keys.length() > 0) { auto id = keys[0]; // Should be just one message box. emit messageBoxClosed(id, button); disconnect(messageBox); messageBox->setVisible(false); messageBox->deleteLater(); _messageBoxes.remove(id); } } float WindowScriptingInterface::domainLoadingProgress() { return qApp->getOctreePacketProcessor().domainLoadingProgress(); }