diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 13257967ac..da1a7d3163 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -155,7 +155,7 @@ void AssetServer::performMappingMigration() { // add a new mapping with the old extension and a truncated version of the hash static const int TRUNCATED_HASH_NUM_CHAR = 16; - auto fakeFileName = hash.left(TRUNCATED_HASH_NUM_CHAR) + fullExtension; + auto fakeFileName = "/" + hash.left(TRUNCATED_HASH_NUM_CHAR) + fullExtension; qDebug() << "\tAdding a migration mapping from" << fakeFileName << "to" << hash; @@ -220,12 +220,10 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode auto it = _fileMappings.find(assetPath); if (it != _fileMappings.end()) { auto assetHash = it->toString(); - qDebug() << "Found mapping for: " << assetPath << "=>" << assetHash; replyPacket.writePrimitive(AssetServerError::NoError); replyPacket.write(QByteArray::fromHex(assetHash.toUtf8())); } else { - qDebug() << "Mapping not found for: " << assetPath; replyPacket.writePrimitive(AssetServerError::AssetNotFound); } } @@ -314,7 +312,7 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh replyPacket->write(assetHash); QString fileName = QString(hexHash); - QFileInfo fileInfo { _resourcesDirectory.filePath(fileName) }; + QFileInfo fileInfo { _filesDirectory.filePath(fileName) }; if (fileInfo.exists() && fileInfo.isReadable()) { qDebug() << "Opening file: " << fileInfo.filePath(); @@ -498,6 +496,7 @@ bool AssetServer::setMapping(const AssetPath& path, const AssetHash& hash) { // attempt to write to file if (writeMappingsToFile()) { // persistence succeeded, we are good to go + qDebug() << "Set mapping:" << path << "=>" << hash; return true; } else { // failed to persist this mapping to file - put back the old one in our in-memory representation @@ -507,21 +506,51 @@ bool AssetServer::setMapping(const AssetPath& path, const AssetHash& hash) { _fileMappings[path] = oldMapping; } + qWarning() << "Failed to persist mapping:" << path << "=>" << hash; + return false; } } +bool pathIsFolder(const AssetPath& path) { + return path.endsWith('/'); +} + bool AssetServer::deleteMappings(const AssetPathList& paths) { // take a copy of the current mappings in case persistence of these deletes fails auto oldMappings = _fileMappings; // enumerate the paths to delete and remove them all for (auto& path : paths) { - auto oldMapping = _fileMappings.take(path); - if (!oldMapping.isNull()) { - qDebug() << "Deleted a mapping:" << path << "=>" << oldMapping.toString(); + + // figure out if this path will delete a file or folder + if (pathIsFolder(path)) { + // enumerate the in memory file mappings and remove anything that matches + auto it = _fileMappings.begin(); + auto sizeBefore = _fileMappings.size(); + + while (it != _fileMappings.end()) { + if (it.key().startsWith(path)) { + it = _fileMappings.erase(it); + } else { + ++it; + } + } + + auto sizeNow = _fileMappings.size(); + if (sizeBefore != sizeNow) { + qDebug() << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path; + } else { + qDebug() << "Did not find any mappings in folder:" << path; + } + } else { - qDebug() << "Unable to delete a mapping that was not found:" << path; + auto oldMapping = _fileMappings.take(path); + if (!oldMapping.isNull()) { + qDebug() << "Deleted a mapping:" << path << "=>" << oldMapping.toString(); + } else { + qDebug() << "Unable to delete a mapping that was not found:" << path; + } } } @@ -530,6 +559,8 @@ bool AssetServer::deleteMappings(const AssetPathList& paths) { // persistence succeeded we are good to go return true; } else { + qWarning() << "Failed to persist deleted mappings, rolling back"; + // we didn't delete the previous mapping, put it back in our in-memory representation _fileMappings = oldMappings; @@ -538,23 +569,88 @@ bool AssetServer::deleteMappings(const AssetPathList& paths) { } bool AssetServer::renameMapping(const AssetPath& oldPath, const AssetPath& newPath) { - // take the old hash to remove the old mapping - auto oldMapping = _fileMappings[oldPath].toString(); + // figure out if this rename is for a file or folder + if (pathIsFolder(oldPath)) { + if (!pathIsFolder(newPath)) { + // we were asked to rename a path to a folder to a path that isn't a folder, this is a fail + qWarning() << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath; - if (!oldMapping.isEmpty()) { - _fileMappings[newPath] = oldMapping; + return false; + } + + // take a copy of the old mappings + auto oldMappings = _fileMappings; + + // iterate the current mappings and adjust any that matches the renamed folder + auto it = oldMappings.begin(); + + while (it != oldMappings.end()) { + if (it.key().startsWith(oldPath)) { + auto newKey = it.key(); + newKey.replace(0, oldPath.size(), newPath); + + // remove the old version from the in memory file mappings + _fileMappings.remove(it.key()); + _fileMappings.insert(newKey, it.value()); + } + + ++it; + } if (writeMappingsToFile()) { - // persisted the renamed mapping, return success + // persisted the changed mappings, return success + qDebug() << "Renamed folder mapping:" << oldPath << "=>" << newPath; + return true; } else { - // we couldn't persist the renamed mapping, rollback and return failure - _fileMappings[oldPath] = oldMapping; + // couldn't persist the renamed paths, rollback and return failure + _fileMappings = oldMappings; + + qWarning() << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath; return false; } } else { - // failed to find a mapping that was to be renamed, return failure - return false; + if (pathIsFolder(newPath)) { + // we were asked to rename a path to a file to a path that is a folder, this is a fail + qWarning() << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath; + + return false; + } + + // take the old hash to remove the old mapping + auto oldSourceMapping = _fileMappings.take(oldPath).toString(); + + // in case we're overwriting, keep the current destination mapping for potential rollback + auto oldDestinationMapping = _fileMappings.value(newPath); + + if (!oldSourceMapping.isEmpty()) { + _fileMappings[newPath] = oldSourceMapping; + + if (writeMappingsToFile()) { + // persisted the renamed mapping, return success + qDebug() << "Renamed mapping:" << oldPath << "=>" << newPath; + + return true; + } else { + // we couldn't persist the renamed mapping, rollback and return failure + _fileMappings[oldPath] = oldSourceMapping; + + if (!oldDestinationMapping.isNull()) { + // put back the overwritten mapping for the destination path + _fileMappings[newPath] = oldDestinationMapping.toString(); + } else { + // clear the new mapping + _fileMappings.remove(newPath); + } + + qDebug() << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath; + + return false; + } + } else { + // failed to find a mapping that was to be renamed, return failure + return false; + } } } diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index d76374c10c..8134d78b26 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -35,6 +35,7 @@ Window { property var scripts: ScriptDiscoveryService; property var scriptsModel: Assets.mappingModel; property var currentDirectory; + property alias currentFileUrl: fileUrlTextField.text; Settings { category: "Overlay.AssetServer" @@ -43,11 +44,69 @@ Window { property alias directory: root.currentDirectory } + function doDeleteFile(path) { + console.log("Deleting " + path); + + Assets.deleteMappings([path], function(err) { + print("Finished deleting path: ", path, err); + reload(); + }); + + } + function doUploadFile(path, mapping, addToWorld) { + console.log("Uploading " + path + " to " + mapping + " (addToWorld: " + addToWorld + ")"); + + + } + function doRenameFile(oldPath, newPath) { + console.log("Renaming " + oldPath + " to " + newPath); + + console.log("Renaming " + path + " to " + destinationPath); + Assets.renameMapping(path, destinationPath, function(err) { + print("Finished rename: ", err); + reload(); + }); + } + + function fileExists(destinationPath) { + return true; // TODO get correct value + } + + function askForOverride(path, callback) { + var object = desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.No, + text: "Override?", + informativeText: "The following file already exists:\n" + path + + "\nDo you want to override it?" + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Yes) { + callback(); + } + }); + } + + function canAddToWorld() { + var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i]; + var path = scriptsModel.data(treeView.currentIndex, 0x100); + + return supportedExtensions.reduce(function(total, current) { + return total | new RegExp(current).test(path); + }, false); + } + function reload() { print("reload"); scriptsModel.refresh(); } function addToWorld() { + var path = scriptsModel.data(treeView.currentIndex, 0x100); + if (!path) { + return; + } + print("addToWorld"); } function renameFile() { @@ -63,11 +122,13 @@ Window { placeholderText: "Enter path here" }); object.selected.connect(function(destinationPath) { - console.log("Renaming " + path + " to " + destinationPath); - Assets.renameMapping(path, destinationPath, function(err) { - print("Finished rename: ", err); - reload(); - }); + if (fileExists(destinationPath)) { + askForOverride(path, function() { + doRenameFile(path, destinationPath); + }); + } else { + doRenameFile(path, destinationPath); + } }); } function deleteFile() { @@ -88,12 +149,7 @@ Window { }); object.selected.connect(function(button) { if (button === OriginalDialogs.StandardButton.Yes) { - console.log("Deleting " + path); - - Assets.deleteMappings([path], function(err) { - print("Finished deleting path: ", path, err); - reload(); - }); + doDeleteFile(path); } }); } @@ -123,12 +179,13 @@ Window { placeholderText: "Enter path here" }); object.selected.connect(function(destinationPath) { - console.log("Uploading " + fileUrl + " to " + destinationPath + " (addToWorld: " + addToWorld + ")"); - - - - - + if (fileExists(destinationPath)) { + askForOverride(fileUrl, function() { + doUploadFile(fileUrl, destinationPath, addToWorld); + }); + } else { + doUploadFile(fileUrl, destinationPath, addToWorld); + } }); } @@ -164,6 +221,8 @@ Window { height: 26 width: 120 + enabled: canAddToWorld() + onClicked: root.addToWorld() } diff --git a/interface/resources/qml/hifi/MenuOption.qml b/interface/resources/qml/hifi/MenuOption.qml index 477197f57e..da28d1daf3 100644 --- a/interface/resources/qml/hifi/MenuOption.qml +++ b/interface/resources/qml/hifi/MenuOption.qml @@ -155,7 +155,6 @@ QtObject { readonly property string toolWindow: "Tool Window"; readonly property string transmitterDrive: "Transmitter Drive"; readonly property string turnWithHead: "Turn using Head"; - readonly property string uploadAsset: "Upload File to Asset Server"; readonly property string useAudioForMouth: "Use Audio for Mouth"; readonly property string useCamera: "Use Camera"; readonly property string velocityFilter: "Velocity Filter"; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index abeff8ade6..17aebb6e49 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -159,7 +159,6 @@ #include "Stars.h" #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" -#include "ui/AssetUploadDialogFactory.h" #include "ui/DialogsManager.h" #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" @@ -3981,9 +3980,6 @@ void Application::nodeAdded(SharedNodePointer node) { if (node->getType() == NodeType::AvatarMixer) { // new avatar mixer, send off our identity packet right away getMyAvatar()->sendIdentityPacket(); - } else if (node->getType() == NodeType::AssetServer) { - // the addition of an asset-server always re-enables the upload to asset server menu option - Menu::getInstance()->getActionForOption(MenuOption::UploadAsset)->setEnabled(true); } } @@ -4033,10 +4029,6 @@ void Application::nodeKilled(SharedNodePointer node) { } else if (node->getType() == NodeType::AvatarMixer) { // our avatar mixer has gone away - clear the hash of avatars DependencyManager::get()->clearOtherAvatars(); - } else if (node->getType() == NodeType::AssetServer - && !DependencyManager::get()->soloNodeOfType(NodeType::AssetServer)) { - // this was our last asset server - disable the menu option to upload an asset - Menu::getInstance()->getActionForOption(MenuOption::UploadAsset)->setEnabled(false); } } void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { @@ -4262,7 +4254,10 @@ bool Application::acceptURL(const QString& urlString, bool defaultUpload) { } } - return defaultUpload && askToUploadAsset(urlString); + if (defaultUpload) { + toggleAssetServerWidget(urlString); + } + return defaultUpload; } void Application::setSessionUUID(const QUuid& sessionUUID) { @@ -4324,80 +4319,6 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { return true; } -bool Application::askToUploadAsset(const QString& filename) { - if (!DependencyManager::get()->getThisNodeCanRez()) { - OffscreenUi::warning(_window, "Failed Upload", - QString("You don't have upload rights on that domain.\n\n")); - return false; - } - - QUrl url { filename }; - if (auto upload = DependencyManager::get()->createUpload(url.toLocalFile())) { - - QMessageBox messageBox; - messageBox.setWindowTitle("Asset upload"); - messageBox.setText("You are about to upload the following file to the asset server:\n" + - url.toDisplayString()); - messageBox.setInformativeText("Do you want to continue?"); - messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - messageBox.setDefaultButton(QMessageBox::Ok); - - // Option to drop model in world for models - if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive) || filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { - auto checkBox = new QCheckBox(&messageBox); - checkBox->setText("Add to scene"); - messageBox.setCheckBox(checkBox); - } - - if (messageBox.exec() != QMessageBox::Ok) { - upload->deleteLater(); - return false; - } - - // connect to the finished signal so we know when the AssetUpload is done - if (messageBox.checkBox() && (messageBox.checkBox()->checkState() == Qt::Checked)) { - // Custom behavior for models - QObject::connect(upload, &AssetUpload::finished, this, &Application::modelUploadFinished); - } else { - QObject::connect(upload, &AssetUpload::finished, - &AssetUploadDialogFactory::getInstance(), - &AssetUploadDialogFactory::handleUploadFinished); - } - - // start the upload now - upload->start(); - return true; - } - - // display a message box with the error - OffscreenUi::warning(_window, "Failed Upload", QString("Failed to upload %1.\n\n").arg(filename)); - return false; -} - -void Application::modelUploadFinished(AssetUpload* upload, const QString& hash) { - auto fileInfo = QFileInfo(upload->getFilename()); - auto filename = fileInfo.fileName(); - - if ((upload->getError() == AssetUpload::NoError) && - (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive) || - filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive))) { - - auto entities = DependencyManager::get(); - - EntityItemProperties properties; - properties.setType(EntityTypes::Model); - properties.setModelURL(QString("%1:%2.%3").arg(URL_SCHEME_ATP).arg(hash).arg(fileInfo.completeSuffix())); - properties.setPosition(_myCamera.getPosition() + _myCamera.getOrientation() * Vectors::FRONT * 2.0f); - properties.setName(QUrl(upload->getFilename()).fileName()); - - entities->addEntity(properties); - - upload->deleteLater(); - } else { - AssetUploadDialogFactory::getInstance().handleUploadFinished(upload, hash); - } -} - bool Application::askToWearAvatarAttachmentUrl(const QString& url) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); @@ -4497,9 +4418,12 @@ void Application::toggleRunningScriptsWidget() { //} } -void Application::toggleAssetServerWidget() { +void Application::toggleAssetServerWidget(QString filePath) { static const QUrl url("AssetServer.qml"); - DependencyManager::get()->show(url, "AssetServer"); + auto urlSetter = [=](QQmlContext* context, QObject* newObject){ + newObject->setProperty("currentFileUrl", filePath); + }; + DependencyManager::get()->show(url, "AssetServer", urlSetter); } void Application::packageModel() { diff --git a/interface/src/Application.h b/interface/src/Application.h index c704e61bfd..61da53e437 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -242,7 +242,7 @@ public slots: Q_INVOKABLE void loadScriptURLDialog(); void toggleLogDialog(); void toggleRunningScriptsWidget(); - void toggleAssetServerWidget(); + void toggleAssetServerWidget(QString filePath = ""); void handleLocalServerConnection(); void readArgumentsFromLocalSocket(); @@ -304,8 +304,6 @@ private slots: bool acceptSnapshot(const QString& urlString); bool askToSetAvatarUrl(const QString& url); bool askToLoadScript(const QString& scriptFilenameOrURL); - bool askToUploadAsset(const QString& asset); - void modelUploadFinished(AssetUpload* upload, const QString& hash); bool askToWearAvatarAttachmentUrl(const QString& url); void displayAvatarAttachmentWarning(const QString& message) const; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index d91829b26a..ad512865ec 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -35,7 +35,6 @@ #include "MainWindow.h" #include "render/DrawStatus.h" #include "scripting/MenuScriptingInterface.h" -#include "ui/AssetUploadDialogFactory.h" #include "ui/DialogsManager.h" #include "ui/StandAloneJSConsole.h" #include "InterfaceLogging.h" @@ -365,17 +364,6 @@ Menu::Menu() { // Developer > Assets >>> MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); - auto& assetDialogFactory = AssetUploadDialogFactory::getInstance(); - assetDialogFactory.setDialogParent(this); - QAction* assetUpload = addActionToQMenuAndActionHash(assetDeveloperMenu, - MenuOption::UploadAsset, - 0, - &assetDialogFactory, - SLOT(showDialog())); - - // disable the asset upload action by default - it gets enabled only if asset server becomes present - assetUpload->setEnabled(false); - auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 880b48a51b..3e18da10b9 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -169,7 +169,6 @@ namespace MenuOption { const QString ToolWindow = "Tool Window"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; - const QString UploadAsset = "Upload File to Asset Server"; const QString UseAudioForMouth = "Use Audio for Mouth"; const QString UseCamera = "Use Camera"; const QString UseAnimPreAndPostRotations = "Use Anim Pre and Post Rotations"; diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 7c967e681a..19b0be3917 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -91,8 +91,6 @@ void AssetClient::clearCache() { return; } - _mappingCache.clear(); - if (auto cache = NetworkAccessManager::getInstance().cache()) { qDebug() << "AssetClient::clearCache(): Clearing disk cache."; cache->clear(); @@ -578,6 +576,4 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { messageMapIt->second.clear(); } } - - _mappingCache.clear(); } diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 9e34774816..e46c8c6524 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -97,8 +97,6 @@ private: std::unordered_map> _pendingInfoRequests; std::unordered_map> _pendingUploads; - QHash _mappingCache; - friend class AssetRequest; friend class AssetUpload; friend class GetMappingRequest; diff --git a/libraries/networking/src/MappingRequest.cpp b/libraries/networking/src/MappingRequest.cpp index 9afa3e7432..afa20e551c 100644 --- a/libraries/networking/src/MappingRequest.cpp +++ b/libraries/networking/src/MappingRequest.cpp @@ -32,14 +32,6 @@ void GetMappingRequest::doStart() { auto assetClient = DependencyManager::get(); - // Check cache - auto it = assetClient->_mappingCache.constFind(_path); - if (it != assetClient->_mappingCache.constEnd()) { - _hash = it.value(); - emit finished(this); - return; - } - assetClient->getAssetMapping(_path, [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { if (!responseReceived) { _error = NetworkError; @@ -59,7 +51,6 @@ void GetMappingRequest::doStart() { if (!_error) { _hash = message->read(SHA256_HASH_LENGTH).toHex(); - assetClient->_mappingCache[_path] = _hash; } emit finished(this); }); @@ -88,12 +79,10 @@ void GetAllMappingsRequest::doStart() { if (!_error) { int numberOfMappings; message->readPrimitive(&numberOfMappings); - assetClient->_mappingCache.clear(); for (auto i = 0; i < numberOfMappings; ++i) { auto path = message->readString(); auto hash = message->read(SHA256_HASH_LENGTH).toHex(); _mappings[path] = hash; - assetClient->_mappingCache[path] = hash; } } emit finished(this); @@ -122,9 +111,6 @@ void SetMappingRequest::doStart() { } } - if (!_error) { - assetClient->_mappingCache[_path] = _hash; - } emit finished(this); }); }; @@ -151,12 +137,6 @@ void DeleteMappingsRequest::doStart() { } } - if (!_error) { - // enumerate the paths and remove them from the cache - for (auto& path : _paths) { - assetClient->_mappingCache.remove(path); - } - } emit finished(this); }); }; @@ -189,14 +169,6 @@ void RenameMappingRequest::doStart() { } } - if (!_error) { - // take the hash mapped for the old path from the cache - auto hash = assetClient->_mappingCache.take(_oldPath); - if (!hash.isEmpty()) { - // use the hash mapped for the old path for the new path - assetClient->_mappingCache[_newPath] = hash; - } - } emit finished(this); }); } diff --git a/libraries/script-engine/src/AssetMappingsScriptingInterface.h b/libraries/script-engine/src/AssetMappingsScriptingInterface.h index 3afdab35cf..0bf98f47cc 100644 --- a/libraries/script-engine/src/AssetMappingsScriptingInterface.h +++ b/libraries/script-engine/src/AssetMappingsScriptingInterface.h @@ -56,6 +56,7 @@ public: Q_INVOKABLE void setMapping(QString path, QString hash, QJSValue callback); Q_INVOKABLE void getMapping(QString path, QJSValue callback); Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback); + Q_INVOKABLE void deleteMapping(QString path, QJSValue callback) { deleteMappings(QStringList(path), callback); } Q_INVOKABLE void getAllMappings(QJSValue callback); Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback); protected: diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 63b16fd51d..2c2c596e09 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -26,6 +26,7 @@ public: Q_INVOKABLE void uploadData(QString data, QScriptValue callback); Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); + protected: QSet _pendingRequests; QScriptEngine* _engine;