// // 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 "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"; QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) { if (!result.value.isValid()) { return QScriptValue::UndefinedValue; } Q_ASSERT(result.value.userType() == qMetaTypeId()); return engine->toScriptValue(result.value.toMap()); } void CustomPromptResultFromScriptValue(const QScriptValue& object, CustomPromptResult& result) { result.value = object.toVariant(); } 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::geometryChanged); } 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([] { auto window = qApp->getWindow(); window->activateWindow(); window->setFocus(); }); } void WindowScriptingInterface::raiseMainWindow() { // It's forbidden to call raise() from another thread. qApp->postLambdaEvent([] { qApp->getWindow()->raise(); }); } /// Display an alert box /// \param const QString& message message to display /// \return QScriptValue::UndefinedValue void WindowScriptingInterface::alert(const QString& message) { OffscreenUi::asyncWarning("", message); } /// 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))); } /// 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) { bool ok = false; QString result = OffscreenUi::getText(nullptr, "", message, QLineEdit::Normal, defaultText, &ok); return ok ? QScriptValue(result) : QScriptValue::NullValue; } /// 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) { ModalDialogListener* dlg = OffscreenUi::getTextAsync(nullptr, "", message, QLineEdit::Normal, defaultText); connect(dlg, &ModalDialogListener::response, this, [=] (QVariant result) { disconnect(dlg, &ModalDialogListener::response, this, nullptr); emit promptTextChanged(result.toString()); }); } void WindowScriptingInterface::disconnectedFromDomain() { emit domainChanged(""); } CustomPromptResult WindowScriptingInterface::customPrompt(const QVariant& config) { CustomPromptResult result; bool ok = false; auto configMap = config.toMap(); result.value = OffscreenUi::getCustomInfo(OffscreenUi::ICON_NONE, "", configMap, &ok); return ok ? result : CustomPromptResult(); } 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::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); emit openFileChanged(result); // Deprecated signal; to be removed in due course. }); } /// 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::getX() { return qApp->getWindow()->x(); } int WindowScriptingInterface::getY() { return qApp->getWindow()->y(); } 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) { qApp->takeSnapshot(notify, includeAnimated, aspectRatio); } void WindowScriptingInterface::takeSecondaryCameraSnapshot() { qApp->takeSecondaryCameraSnapshot(); } 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 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); } }