diff --git a/cmake/templates/FixupBundlePostBuild.cmake.in b/cmake/templates/FixupBundlePostBuild.cmake.in index 57379bb48b..bb96fe49f3 100644 --- a/cmake/templates/FixupBundlePostBuild.cmake.in +++ b/cmake/templates/FixupBundlePostBuild.cmake.in @@ -11,6 +11,35 @@ include(BundleUtilities) +# replace copy_resolved_item_into_bundle +# +# The official version of copy_resolved_item_into_bundle will print out a "warning:" when +# the resolved item matches the resolved embedded item. This not not really an issue that +# should rise to the level of a "warning" so we replace this message with a "status:" +# +# Source: https://github.com/jherico/OculusMinimalExample/blob/master/cmake/templates/FixupBundlePostBuild.cmake.in +# +function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) + if (WIN32) + # ignore case on Windows + string(TOLOWER "${resolved_item}" resolved_item_compare) + string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) + else() + set(resolved_item_compare "${resolved_item}") + set(resolved_embedded_item_compare "${resolved_embedded_item}") + endif() + + if ("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") + # this is our only change from the original version + message(STATUS "status: resolved_item == resolved_embedded_item - not copying...") + else() + execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") + if (UNIX AND NOT APPLE) + file(RPATH_REMOVE FILE "${resolved_embedded_item}") + endif() + endif() +endfunction() + function(gp_resolved_file_type_override resolved_file type_var) if( file MATCHES ".*VCRUNTIME140.*" ) set(type "system" PARENT_SCOPE) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index 2369b01690..6bcabc0bf1 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -34,8 +34,9 @@ static const chrono::minutes MAX_REFRESH_TIME { 5 }; Q_DECLARE_LOGGING_CATEGORY(asset_backup) Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup"); -AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : - _assetsDirectory(backupDirectory + ASSETS_DIR) +AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory, bool assetServerEnabled) : + _assetsDirectory(backupDirectory + ASSETS_DIR), + _assetServerEnabled(assetServerEnabled) { // Make sure the asset directory exists. QDir(_assetsDirectory).mkpath("."); @@ -53,6 +54,7 @@ void AssetsBackupHandler::setupRefreshTimer() { auto nodeList = DependencyManager::get(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { + assert(_assetServerEnabled); // run immediately for the first time. _mappingsRefreshTimer.start(0); } @@ -233,12 +235,12 @@ void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { return; } - if (_lastMappingsRefresh.time_since_epoch().count() == 0) { + if (_assetServerEnabled && _lastMappingsRefresh.time_since_epoch().count() == 0) { qCWarning(asset_backup) << "Current mappings not yet loaded."; return; } - if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { + if (_assetServerEnabled && (p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { qCWarning(asset_backup) << "Backing up asset mappings that might be stale."; } diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 82d684c2c3..427dc6831a 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -30,7 +30,7 @@ class AssetsBackupHandler : public QObject, public BackupHandlerInterface { Q_OBJECT public: - AssetsBackupHandler(const QString& backupDirectory); + AssetsBackupHandler(const QString& backupDirectory, bool assetServerEnabled); std::pair isAvailable(const QString& backupName) override; std::pair getRecoveryStatus() override; @@ -65,6 +65,7 @@ private: void updateMappings(); QString _assetsDirectory; + bool _assetServerEnabled { false }; QTimer _mappingsRefreshTimer; p_high_resolution_clock::time_point _lastMappingsRefresh; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e69e241182..a34deebc95 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -307,7 +307,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){ _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir(), isAssetServerEnabled()))); _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); }); @@ -991,15 +991,11 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet(static_cast(defaultedType) + 1)) { if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::AgentType) { - if (defaultedType == Assignment::AssetServerType) { - // Make sure the asset-server is enabled before adding it here. - // Initially we do not assign it by default so we can test it in HF domains first - static const QString ASSET_SERVER_ENABLED_KEYPATH = "asset_server.enabled"; - - if (!_settingsManager.valueOrDefaultValueForKeyPath(ASSET_SERVER_ENABLED_KEYPATH).toBool()) { - // skip to the next iteration if asset-server isn't enabled - continue; - } + // Make sure the asset-server is enabled before adding it here. + // Initially we do not assign it by default so we can test it in HF domains first + if (defaultedType == Assignment::AssetServerType && !isAssetServerEnabled()) { + // skip to the next iteraion if asset-server isn't enabled + continue; } // type has not been set from a command line or config file config, use the default @@ -2946,6 +2942,12 @@ bool DomainServer::shouldReplicateNode(const Node& node) { } }; + +bool DomainServer::isAssetServerEnabled() { + static const QString ASSET_SERVER_ENABLED_KEYPATH = "asset_server.enabled"; + return _settingsManager.valueOrDefaultValueForKeyPath(ASSET_SERVER_ENABLED_KEYPATH).toBool(); +} + void DomainServer::nodeAdded(SharedNodePointer node) { // we don't use updateNodeWithData, so add the DomainServerNodeData to the node here node->setLinkedData(std::unique_ptr { new DomainServerNodeData() }); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 3462f14e3c..3703877fa1 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -72,6 +72,8 @@ public: static const QString REPLACEMENT_FILE_EXTENSION; + bool isAssetServerEnabled(); + public slots: /// Called by NodeList to inform us a node has been added void nodeAdded(SharedNodePointer node); diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index 6743d08275..917068ac01 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -165,11 +165,11 @@ TextField { anchors.left: parent.left Binding on anchors.right { - when: parent.right - value: parent.right + when: textField.right + value: textField.right } Binding on wrapMode { - when: parent.right + when: textField.right value: Text.WordWrap } diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 3f3a47a297..d9e93c2fa7 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -16,10 +16,11 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "toolbars" import "../styles-uit" +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Column { id: root; - visible: false; + visible: !!suggestions.count; property int cardWidth: 212; property int cardHeight: 152; @@ -32,21 +33,37 @@ Column { property int stackedCardShadowHeight: 4; property int labelSize: 20; - property string metaverseServerUrl: ''; property string protocol: ''; property string actions: 'snapshot'; // sendToScript doesn't get wired until after everything gets created. So we have to queue fillDestinations on nextTick. property string labelText: actions; property string filter: ''; - onFilterChanged: filterChoicesByText(); property var goFunction: null; - property var rpc: null; + property var http: null; HifiConstants { id: hifi } - ListModel { id: suggestions; } + Component.onCompleted: suggestions.getFirstPage(); + HifiModels.PSFListModel { + id: suggestions; + http: root.http; + property var options: [ + 'include_actions=' + actions, + 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), + 'require_online=true', + 'protocol=' + encodeURIComponent(Window.protocolSignature()) + ]; + endpoint: '/api/v1/user_stories?' + options.join('&'); + itemsPerPage: 3; + processPage: function (data) { + return data.user_stories.map(makeModelData); + }; + listModelName: actions; + listView: scroll; + searchFilter: filter; + } function resolveUrl(url) { - return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url; + return (url.indexOf('/') === 0) ? (Account.metaverseServerURL + url) : url; } function makeModelData(data) { // create a new obj from data // ListModel elements will only ever have those properties that are defined by the first obj that is added. @@ -55,16 +72,11 @@ Column { tags = data.tags || [data.action, data.username], description = data.description || "", thumbnail_url = data.thumbnail_url || ""; - if (actions === 'concurrency,snapshot') { - // A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements. - data.details.connections = 4; - data.action = 'announcement'; - } return { place_name: name, username: data.username || "", path: data.path || "", - created_at: data.created_at || "", + created_at: data.created_at || data.updated_at || "", // FIXME why aren't we getting created_at? action: data.action || "", thumbnail_url: resolveUrl(thumbnail_url), image_url: resolveUrl(data.details && data.details.image_url), @@ -74,125 +86,9 @@ Column { tags: tags, description: description, online_users: data.details.connections || data.details.concurrency || 0, - drillDownToPlace: false, - - searchText: [name].concat(tags, description || []).join(' ').toUpperCase() - } - } - property var allStories: []; - property var placeMap: ({}); // Used for making stacks. - property int requestId: 0; - function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey - if (!error && (data.status === 'success')) { - return; - } - if (!error) { // Create a message from the data - error = data.status + ': ' + data.error; - } - if (typeof(error) === 'string') { // Make a proper Error object - error = new Error(error); - } - error.message += ' in ' + url; // Include the url. - cb(error); - return true; - } - function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model - // If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness. - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + actions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + protocol, - 'page=' + pageNumber - ]; - var url = metaverseBase + 'user_stories?' + options.join('&'); - var thisRequestId = ++requestId; - rpc('request', url, function (error, data) { - if (thisRequestId !== requestId) { - error = 'stale'; - } - if (handleError(url, error, data, cb)) { - return; // abandon stale requests - } - allStories = allStories.concat(data.user_stories.map(makeModelData)); - if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now - if ((pageNumber === 1) && cb1) { - cb1(); - } - return getUserStoryPage(pageNumber + 1, cb); - } - cb(); - }); - } - function fillDestinations() { // Public - console.debug('Feed::fillDestinations()') - - function report(label, error) { - console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count); - } - var filter = makeFilteredStoryProcessor(), counter = 0; - allStories = []; - suggestions.clear(); - placeMap = {}; - getUserStoryPage(1, function (error) { - allStories.slice(counter).forEach(filter); - report('user stories update', error); - root.visible = !!suggestions.count; - }, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed. - allStories.forEach(function (story) { - counter++; - filter(story); - root.visible = !!suggestions.count; - }); - report('user stories'); - }); - } - function identity(x) { - return x; - } - function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches - var words = filter.toUpperCase().split(/\s+/).filter(identity); - function suggestable(story) { - // We could filter out places we don't want to suggest, such as those where (story.place_name === AddressManager.placename) or (story.username === Account.username). - return true; - } - function matches(story) { - if (!words.length) { - return suggestable(story); - } - return words.every(function (word) { - return story.searchText.indexOf(word) >= 0; - }); - } - function addToSuggestions(place) { - var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement'); - if (collapse) { - var existing = placeMap[place.place_name]; - if (existing) { - existing.drillDownToPlace = true; - return; - } - } - suggestions.append(place); - if (collapse) { - placeMap[place.place_name] = suggestions.get(suggestions.count - 1); - } else if (place.action === 'concurrency') { - suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories). - } - } - return function (story) { - if (matches(story)) { - addToSuggestions(story); - } + drillDownToPlace: false }; } - function filterChoicesByText() { - suggestions.clear(); - placeMap = {}; - allStories.forEach(makeFilteredStoryProcessor()); - root.visible = !!suggestions.count; - } RalewayBold { id: label; @@ -208,6 +104,7 @@ Column { highlightMoveDuration: -1; highlightMoveVelocity: -1; currentIndex: -1; + onAtXEndChanged: { if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } } spacing: 12; width: parent.width; @@ -239,21 +136,4 @@ Column { unhoverThunk: function () { hovered = false } } } - NumberAnimation { - id: anim; - target: scroll; - property: "contentX"; - duration: 250; - } - function scrollToIndex(index) { - anim.running = false; - var pos = scroll.contentX; - var destPos; - scroll.positionViewAtIndex(index, ListView.Contain); - destPos = scroll.contentX; - anim.from = pos; - anim.to = destPos; - scroll.currentIndex = index; - anim.running = true; - } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index d779b4ba42..8dcb76442b 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -18,6 +18,7 @@ import Qt.labs.settings 1.0 import "../styles-uit" import "../controls-uit" as HifiControlsUit import "../controls" as HifiControls +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. // references HMD, Users, UserActivityLogger from root context @@ -37,13 +38,42 @@ Rectangle { property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: "", isPresent: true}); // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities. - property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. property bool iAmAdmin: false; property var activeTab: "nearbyTab"; property bool currentlyEditingDisplayName: false property bool punctuationMode: false; HifiConstants { id: hifi; } + RootHttpRequest { id: http; } + HifiModels.PSFListModel { + id: connectionsUserModel; + http: http; + endpoint: "/api/v1/users?filter=connections"; + property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); + sortProperty: switch (sortColumn && sortColumn.role) { + case 'placeName': + 'location'; + break; + case 'connection': + 'is_friend'; + break; + default: + 'username'; + } + sortAscending: connectionsTable.sortIndicatorOrder === Qt.AscendingOrder; + itemsPerPage: 9; + listView: connectionsTable; + processPage: function (data) { + return data.users.map(function (user) { + return { + userName: user.username, + connection: user.connection, + profileUrl: user.images.thumbnail, + placeName: (user.location.root || user.location.domain || {}).name || '' + }; + }); + }; + } // The letterbox used for popup messages LetterboxMessage { @@ -106,16 +136,6 @@ Rectangle { }); return sessionIDs; } - function getSelectedConnectionsUserNames() { - var userNames = []; - connectionsTable.selection.forEach(function (userIndex) { - var datum = connectionsUserModelData[userIndex]; - if (datum) { - userNames.push(datum.userName); - } - }); - return userNames; - } function refreshNearbyWithFilter() { // We should just be able to set settings.filtered to inViewCheckbox.checked, but see #3249, so send to .js for saving. var userIds = getSelectedNearbySessionIDs(); @@ -232,9 +252,7 @@ Rectangle { anchors.fill: parent; onClicked: { if (activeTab != "connectionsTab") { - connectionsLoading.visible = false; - connectionsLoading.visible = true; - pal.sendToScript({method: 'refreshConnections'}); + connectionsUserModel.getFirstPage(); } activeTab = "connectionsTab"; connectionsHelpText.color = hifi.colors.blueAccent; @@ -258,11 +276,7 @@ Rectangle { id: reloadConnections; width: reloadConnections.height; glyph: hifi.glyphs.reload; - onClicked: { - connectionsLoading.visible = false; - connectionsLoading.visible = true; - pal.sendToScript({method: 'refreshConnections'}); - } + onClicked: connectionsUserModel.getFirstPage('delayRefresh'); } } // "CONNECTIONS" text @@ -472,7 +486,7 @@ Rectangle { visible: !isCheckBox && !isButton && !isAvgAudio; uuid: model ? model.sessionId : ""; selected: styleData.selected; - isReplicated: model.isReplicated; + isReplicated: model && model.isReplicated; isAdmin: model && model.admin; isPresent: model && model.isPresent; // Size @@ -702,7 +716,7 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: 185; anchors.horizontalCenter: parent.horizontalCenter; - visible: true; + visible: !connectionsUserModel.retrievedAtLeastOnePage; onVisibleChanged: { if (visible) { connectionsTimeoutTimer.start(); @@ -747,14 +761,6 @@ Rectangle { headerVisible: true; sortIndicatorColumn: settings.connectionsSortIndicatorColumn; sortIndicatorOrder: settings.connectionsSortIndicatorOrder; - onSortIndicatorColumnChanged: { - settings.connectionsSortIndicatorColumn = sortIndicatorColumn; - sortConnectionsModel(); - } - onSortIndicatorOrderChanged: { - settings.connectionsSortIndicatorOrder = sortIndicatorOrder; - sortConnectionsModel(); - } TableViewColumn { id: connectionsUserNameHeader; @@ -779,8 +785,14 @@ Rectangle { resizable: false; } - model: ListModel { - id: connectionsUserModel; + model: connectionsUserModel; + Connections { + target: connectionsTable.flickableItem; + onAtYEndChanged: { + if (connectionsTable.flickableItem.atYEnd && !connectionsTable.flickableItem.atYBeginning) { + connectionsUserModel.getNextPage(); + } + } } // This Rectangle refers to each Row in the connectionsTable. @@ -859,12 +871,9 @@ Rectangle { checked: model && (model.connection === "friend"); boxSize: 24; onClicked: { - var newValue = model.connection !== "friend"; - connectionsUserModel.setProperty(model.userIndex, styleData.role, (newValue ? "friend" : "connection")); - connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming - pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName}); + pal.sendToScript({method: checked ? 'addFriend' : 'removeFriend', params: model.userName}); - UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); + UserActivityLogger["palAction"](checked ? styleData.role : "un-" + styleData.role, model.sessionId); } } } @@ -1130,16 +1139,6 @@ Rectangle { sortModel(); reloadNearby.color = 0; break; - case 'connections': - var data = message.params; - if (pal.debug) { - console.log('Got connection data: ', JSON.stringify(data)); - } - connectionsUserModelData = data; - sortConnectionsModel(); - connectionsLoading.visible = false; - connectionsRefreshProblemText.visible = false; - break; case 'select': var sessionIds = message.params[0]; var selected = message.params[1]; @@ -1239,6 +1238,14 @@ Rectangle { reloadNearby.color = 2; } break; + case 'inspectionCertificate_resetCert': + // marketplaces.js sends out a signal to QML with that method when the tablet screen changes and it's not changed to a commerce-related screen. + // We want it to only be handled by the InspectionCertificate.qml, but there's not an easy way of doing that. + // As a part of a "cleanup inspectionCertificate_resetCert" ticket, we'll have to figure out less logspammy way of doing what has to be done. + break; + case 'http.response': + http.handleHttpResponse(message); + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } @@ -1287,45 +1294,6 @@ Rectangle { nearbyTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); } } - function sortConnectionsModel() { - var column = connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); - var sortProperty = column ? column.role : "userName"; - var before = (connectionsTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; - var after = -1 * before; - // get selection(s) before sorting - var selectedIDs = getSelectedConnectionsUserNames(); - connectionsUserModelData.sort(function (a, b) { - var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); - if (!aValue && !bValue) { - return 0; - } else if (!aValue) { - return after; - } else if (!bValue) { - return before; - } - switch (true) { - case (aValue < bValue): return before; - case (aValue > bValue): return after; - default: return 0; - } - }); - connectionsTable.selection.clear(); - - connectionsUserModel.clear(); - var userIndex = 0; - var newSelectedIndexes = []; - connectionsUserModelData.forEach(function (datum) { - datum.userIndex = userIndex++; - connectionsUserModel.append(datum); - if (selectedIDs.indexOf(datum.sessionId) != -1) { - newSelectedIndexes.push(datum.userIndex); - } - }); - if (newSelectedIndexes.length > 0) { - connectionsTable.selection.select(newSelectedIndexes); - connectionsTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); - } - } signal sendToScript(var message); function noticeSelection() { var userIds = []; diff --git a/interface/resources/qml/hifi/RootHttpRequest.qml b/interface/resources/qml/hifi/RootHttpRequest.qml new file mode 100644 index 0000000000..0355626996 --- /dev/null +++ b/interface/resources/qml/hifi/RootHttpRequest.qml @@ -0,0 +1,39 @@ +// +// RootHttpRequest.qml +// qml/hifi +// +// Create an item of this in the ROOT qml to be able to make http requests. +// Used by PSFListModel.qml +// +// Created by Howard Stearns on 5/29/2018 +// Copyright 2018 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 +// + +import QtQuick 2.5 + +Item { + property var httpCalls: ({}); + property var httpCounter: 0; + // Public function for initiating an http request. + // REQUIRES parent to be root to have sendToScript! + function request(options, callback) { + console.debug('HttpRequest', JSON.stringify(options)); + httpCalls[httpCounter] = callback; + var message = {method: 'http.request', params: options, id: httpCounter++, jsonrpc: "2.0"}; + parent.sendToScript(message); + } + // REQUIRES that parent/root handle http.response message.method in fromScript, by calling this function. + function handleHttpResponse(message) { + var callback = httpCalls[message.id]; // FIXME: as different top level tablet apps gets loaded, the id repeats. We should drop old app callbacks without warning. + if (!callback) { + console.warn('No callback for', JSON.stringify(message)); + return; + } + delete httpCalls[message.id]; + console.debug('HttpRequest response', JSON.stringify(message)); + callback(message.error, message.response); + } +} diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index f25282c738..16c1b55930 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -42,7 +42,7 @@ Rectangle { property bool alreadyOwned: false; property int itemPrice: -1; property bool isCertified; - property string itemType; + property string itemType: "unknown"; property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar", "unknown"]; property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar", "item"]; property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR", "REZ"]; @@ -98,9 +98,6 @@ Rectangle { } else { root.certificateId = result.data.certificate_id; root.itemHref = result.data.download_url; - if (result.data.categories.indexOf("Wearables") > -1) { - root.itemType = "wearable"; - } root.activeView = "checkoutSuccess"; UserActivityLogger.commercePurchaseSuccess(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned); } @@ -170,9 +167,6 @@ Rectangle { root.activeView = "checkoutFailure"; } else { root.itemHref = result.data.download_url; - if (result.data.categories.indexOf("Wearables") > -1) { - root.itemType = "wearable"; - } root.activeView = "checkoutSuccess"; } } @@ -186,20 +180,6 @@ Rectangle { itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; } - onItemHrefChanged: { - if (root.itemHref.indexOf(".fst") > -1) { - root.itemType = "avatar"; - } else if (root.itemHref.indexOf('.json.gz') > -1 || root.itemHref.indexOf('.content.zip') > -1) { - root.itemType = "contentSet"; - } else if (root.itemHref.indexOf('.app.json') > -1) { - root.itemType = "app"; - } else if (root.itemHref.indexOf('.json') > -1) { - root.itemType = "entity"; // "wearable" type handled later - } else { - root.itemType = "unknown"; - } - } - onItemTypeChanged: { if (root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "avatar" || root.itemType === "app") { @@ -1102,6 +1082,7 @@ Rectangle { root.referrer = message.params.referrer; root.itemAuthor = message.params.itemAuthor; root.itemEdition = message.params.itemEdition || -1; + root.itemType = message.params.itemType || "unknown"; refreshBuyUI(); break; default: diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml index 66a9f9a822..41eacd68d5 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml @@ -44,7 +44,7 @@ Item { Item { id: avatarImage; - visible: profileUrl !== "" && userName !== ""; + visible: profilePicUrl !== "" && userName !== ""; // Size anchors.verticalCenter: parent.verticalCenter; anchors.left: parent.left; diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index c7c72e5f7c..3e4bae4780 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -19,6 +19,7 @@ import "../../../../styles-uit" import "../../../../controls-uit" as HifiControlsUit import "../../../../controls" as HifiControls import "../" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Item { HifiConstants { id: hifi; } @@ -36,6 +37,8 @@ Item { property string assetName: ""; property string assetCertID: ""; property string sendingPubliclyEffectImage; + property var http; + property var listModelName; // This object is always used in a popup or full-screen Wallet section. // This MouseArea is used to prevent a user from being @@ -118,9 +121,7 @@ Item { if (root.currentActiveView === 'chooseRecipientConnection') { // Refresh connections model - connectionsLoading.visible = false; - connectionsLoading.visible = true; - sendSignalToParent({method: 'refreshConnections'}); + connectionsModel.getFirstPage(); } else if (root.currentActiveView === 'sendAssetHome') { Commerce.balance(); } else if (root.currentActiveView === 'chooseRecipientNearby') { @@ -392,11 +393,17 @@ Item { hoverEnabled: true; } - ListModel { + HifiModels.PSFListModel { id: connectionsModel; - } - ListModel { - id: filteredConnectionsModel; + http: root.http; + listModelName: root.listModelName; + endpoint: "/api/v1/users?filter=connections"; + itemsPerPage: 8; + listView: connectionsList; + processPage: function (data) { + return data.users; + }; + searchFilter: filterBar.text; } Rectangle { @@ -472,10 +479,6 @@ Item { anchors.fill: parent; centerPlaceholderGlyph: hifi.glyphs.search; - onTextChanged: { - buildFilteredConnectionsModel(); - } - onAccepted: { focus = false; } @@ -495,6 +498,7 @@ Item { AnimatedImage { id: connectionsLoading; + visible: !connectionsModel.retrievedAtLeastOnePage; source: "../../../../../icons/profilePicLoading.gif" width: 120; height: width; @@ -515,14 +519,15 @@ Item { } visible: !connectionsLoading.visible; clip: true; - model: filteredConnectionsModel; + model: connectionsModel; + onAtYEndChanged: if (connectionsList.atYEnd && !connectionsList.atYBeginning) { connectionsModel.getNextPage(); } snapMode: ListView.SnapToItem; // Anchors anchors.fill: parent; delegate: ConnectionItem { isSelected: connectionsList.currentIndex === index; - userName: model.userName; - profilePicUrl: model.profileUrl; + userName: model.username; + profilePicUrl: model.images.thumbnail; anchors.topMargin: 6; anchors.bottomMargin: 6; @@ -553,7 +558,7 @@ Item { // "Make a Connection" instructions Rectangle { id: connectionInstructions; - visible: connectionsModel.count === 0 && !connectionsLoading.visible; + visible: connectionsModel.count === 0 && !connectionsModel.searchFilter && !connectionsLoading.visible; anchors.fill: parent; color: "white"; @@ -1806,22 +1811,6 @@ Item { // FUNCTION DEFINITIONS START // - function updateConnections(connections) { - connectionsModel.clear(); - connectionsModel.append(connections); - buildFilteredConnectionsModel(); - connectionsLoading.visible = false; - } - - function buildFilteredConnectionsModel() { - filteredConnectionsModel.clear(); - for (var i = 0; i < connectionsModel.count; i++) { - if (connectionsModel.get(i).userName.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { - filteredConnectionsModel.append(connectionsModel.get(i)); - } - } - } - function resetSendAssetData() { amountTextField.focus = false; optionalMessage.focus = false; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 8fe1ebe6c9..0d2acf4ec3 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -16,10 +16,12 @@ import QtQuick 2.5 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. import "../wallet" as HifiWallet import "../common" as HifiCommerceCommon import "../inspectionCertificate" as HifiInspectionCertificate import "../common/sendAsset" as HifiSendAsset +import "../.." as HifiCommon // references XXX from root context @@ -34,12 +36,17 @@ Rectangle { property bool punctuationMode: false; property bool isShowingMyItems: false; property bool isDebuggingFirstUseTutorial: false; - property int pendingItemCount: 0; property string installedApps; property bool keyboardRaised: false; property int numUpdatesAvailable: 0; // Style color: hifi.colors.white; + function getPurchases() { + root.activeView = "purchasesMain"; + root.installedApps = Commerce.getInstalledApps(); + purchasesModel.getFirstPage(); + Commerce.getAvailableUpdates(); + } Connections { target: Commerce; @@ -62,10 +69,7 @@ Rectangle { if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { root.activeView = "firstUseTutorial"; } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { - root.activeView = "purchasesMain"; - root.installedApps = Commerce.getInstalledApps(); - Commerce.inventory(); - Commerce.getAvailableUpdates(); + getPurchases(); } } else { console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus); @@ -81,39 +85,7 @@ Rectangle { } onInventoryResult: { - purchasesReceived = true; - - if (result.status !== 'success') { - console.log("Failed to get purchases", result.message); - } else if (!purchasesContentsList.dragging) { // Don't modify the view if the user's scrolling - var inventoryResult = processInventoryResult(result.data.assets); - - var currentIndex = purchasesContentsList.currentIndex === -1 ? 0 : purchasesContentsList.currentIndex; - purchasesModel.clear(); - purchasesModel.append(inventoryResult); - - root.pendingItemCount = 0; - for (var i = 0; i < purchasesModel.count; i++) { - if (purchasesModel.get(i).status === "pending") { - root.pendingItemCount++; - } - } - - if (previousPurchasesModel.count !== 0) { - checkIfAnyItemStatusChanged(); - } else { - // Fill statusChanged default value - // Not doing this results in the default being true... - for (var i = 0; i < purchasesModel.count; i++) { - purchasesModel.setProperty(i, "statusChanged", false); - } - } - previousPurchasesModel.append(inventoryResult); - - buildFilteredPurchasesModel(); - - purchasesContentsList.positionViewAtIndex(currentIndex, ListView.Beginning); - } + purchasesModel.handlePage(result.status !== "success" && result.message, result); } onAvailableUpdatesResult: { @@ -134,6 +106,10 @@ Rectangle { } } + onIsShowingMyItemsChanged: { + getPurchases(); + } + Timer { id: notSetUpTimer; interval: 200; @@ -172,8 +148,14 @@ Rectangle { } } + HifiCommon.RootHttpRequest { + id: http; + } + HifiSendAsset.SendAsset { id: sendAsset; + http: http; + listModelName: "Gift Connections"; z: 998; visible: root.activeView === "giftAsset"; anchors.fill: parent; @@ -183,9 +165,7 @@ Rectangle { Connections { onSendSignalToParent: { if (msg.method === 'sendAssetHome_back' || msg.method === 'closeSendAsset') { - root.activeView = "purchasesMain"; - Commerce.inventory(); - Commerce.getAvailableUpdates(); + getPurchases(); } else { sendToScript(msg); } @@ -449,10 +429,7 @@ Rectangle { case 'tutorial_skipClicked': case 'tutorial_finished': Settings.setValue("isFirstUseOfPurchases", false); - root.activeView = "purchasesMain"; - root.installedApps = Commerce.getInstalledApps(); - Commerce.inventory(); - Commerce.getAvailableUpdates(); + getPurchases(); break; } } @@ -528,7 +505,7 @@ Rectangle { }, { "displayName": "Content Set", - "filterName": "contentSet" + "filterName": "content_set" }, { "displayName": "Entity", @@ -540,7 +517,7 @@ Rectangle { }, { "displayName": "Updatable", - "filterName": "updatable" + "filterName": "updated" } ] filterBar.primaryFilterChoices.clear(); @@ -548,14 +525,12 @@ Rectangle { } onPrimaryFilter_displayNameChanged: { - buildFilteredPurchasesModel(); - purchasesContentsList.positionViewAtIndex(0, ListView.Beginning) + purchasesModel.tagsFilter = filterBar.primaryFilter_filterName; filterBar.previousPrimaryFilter = filterBar.primaryFilter_displayName; } onTextChanged: { - buildFilteredPurchasesModel(); - purchasesContentsList.positionViewAtIndex(0, ListView.Beginning) + purchasesModel.searchFilter = filterBar.text; filterBar.previousText = filterBar.text; } } @@ -574,24 +549,41 @@ Rectangle { anchors.topMargin: 16; } - ListModel { + HifiModels.PSFListModel { id: purchasesModel; - } - ListModel { - id: previousPurchasesModel; - } - HifiCommerceCommon.SortableListModel { - id: tempPurchasesModel; - } - HifiCommerceCommon.SortableListModel { - id: filteredPurchasesModel; + itemsPerPage: 6; + listModelName: 'purchases'; + getPage: function () { + console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); + Commerce.inventory( + root.isShowingMyItems ? "proofs" : "purchased", + filterBar.primaryFilter_filterName, + filterBar.text, + purchasesModel.currentPageToRetrieve, + purchasesModel.itemsPerPage + ); + } + processPage: function(data) { + purchasesReceived = true; // HRS FIXME? + data.assets.forEach(function (item) { + if (item.status.length > 1) { console.warn("Unrecognized inventory status", item); } + item.status = item.status[0]; + item.categories = item.categories.join(';'); + item.cardBackVisible = false; + item.isInstalled = root.installedApps.indexOf(item.id) > -1; + item.wornEntityID = ''; + }); + sendToScript({ method: 'purchases_updateWearables' }); + + return data.assets; + } } ListView { id: purchasesContentsList; - visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0); + visible: purchasesModel.count !== 0; clip: true; - model: filteredPurchasesModel; + model: purchasesModel; snapMode: ListView.SnapToItem; // Anchors anchors.top: separator.bottom; @@ -608,13 +600,13 @@ Rectangle { itemEdition: model.edition_number; numberSold: model.number_sold; limitedRun: model.limited_run; - displayedItemCount: model.displayedItemCount; - cardBackVisible: model.cardBackVisible; - isInstalled: model.isInstalled; + displayedItemCount: 999; // For now (and maybe longer), we're going to display all the edition numbers. + cardBackVisible: model.cardBackVisible || false; + isInstalled: model.isInstalled || false; wornEntityID: model.wornEntityID; upgradeUrl: model.upgrade_url; upgradeTitle: model.upgrade_title; - itemType: model.itemType; + itemType: model.item_type; isShowingMyItems: root.isShowingMyItems; valid: model.valid; anchors.topMargin: 10; @@ -706,11 +698,11 @@ Rectangle { } else if (msg.method === "setFilterText") { filterBar.text = msg.filterText; } else if (msg.method === "flipCard") { - for (var i = 0; i < filteredPurchasesModel.count; i++) { + for (var i = 0; i < purchasesModel.count; i++) { if (i !== index || msg.closeAll) { - filteredPurchasesModel.setProperty(i, "cardBackVisible", false); + purchasesModel.setProperty(i, "cardBackVisible", false); } else { - filteredPurchasesModel.setProperty(i, "cardBackVisible", true); + purchasesModel.setProperty(i, "cardBackVisible", true); } } } else if (msg.method === "updateItemClicked") { @@ -761,7 +753,7 @@ Rectangle { lightboxPopup.button2text = "CONFIRM"; lightboxPopup.button2method = function() { Entities.deleteEntity(msg.wornEntityID); - filteredPurchasesModel.setProperty(index, 'wornEntityID', ''); + purchasesModel.setProperty(index, 'wornEntityID', ''); root.activeView = "giftAsset"; lightboxPopup.visible = false; }; @@ -773,6 +765,14 @@ Rectangle { } } } + + + onAtYEndChanged: { + if (purchasesContentsList.atYEnd && !purchasesContentsList.atYBeginning) { + console.log("User scrolled to the bottom of 'Purchases'."); + purchasesModel.getNextPage(); + } + } } Rectangle { @@ -953,146 +953,14 @@ Rectangle { // // FUNCTION DEFINITIONS START // - - function processInventoryResult(inventory) { - for (var i = 0; i < inventory.length; i++) { - if (inventory[i].status.length > 1) { - console.log("WARNING: Inventory result index " + i + " has a status of length >1!") - } - inventory[i].status = inventory[i].status[0]; - inventory[i].categories = inventory[i].categories.join(';'); - } - return inventory; - } - - function populateDisplayedItemCounts() { - var itemCountDictionary = {}; - var currentItemId; - for (var i = 0; i < filteredPurchasesModel.count; i++) { - currentItemId = filteredPurchasesModel.get(i).id; - if (itemCountDictionary[currentItemId] === undefined) { - itemCountDictionary[currentItemId] = 1; - } else { - itemCountDictionary[currentItemId]++; - } - } - - for (var i = 0; i < filteredPurchasesModel.count; i++) { - filteredPurchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[filteredPurchasesModel.get(i).id]); - } - } - - function sortByDate() { - filteredPurchasesModel.sortColumnName = "purchase_date"; - filteredPurchasesModel.isSortingDescending = true; - filteredPurchasesModel.valuesAreNumerical = true; - filteredPurchasesModel.quickSort(); - } - - function buildFilteredPurchasesModel() { - var sameItemCount = 0; - - tempPurchasesModel.clear(); - - for (var i = 0; i < purchasesModel.count; i++) { - if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { - if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) { - tempPurchasesModel.insert(0, purchasesModel.get(i)); - } else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === "0") || - (!root.isShowingMyItems && purchasesModel.get(i).edition_number !== "0")) { - tempPurchasesModel.append(purchasesModel.get(i)); - } - } - } - - // primaryFilter filtering and adding of itemType property to model - var currentItemType, currentRootFileUrl, currentCategories; - for (var i = 0; i < tempPurchasesModel.count; i++) { - currentRootFileUrl = tempPurchasesModel.get(i).root_file_url; - currentCategories = tempPurchasesModel.get(i).categories; - - if (currentRootFileUrl.indexOf(".fst") > -1) { - currentItemType = "avatar"; - } else if (currentCategories.indexOf("Wearables") > -1) { - currentItemType = "wearable"; - } else if (currentRootFileUrl.endsWith('.json.gz') || currentRootFileUrl.endsWith('.content.zip')) { - currentItemType = "contentSet"; - } else if (currentRootFileUrl.endsWith('.app.json')) { - currentItemType = "app"; - } else if (currentRootFileUrl.endsWith('.json')) { - currentItemType = "entity"; - } else { - currentItemType = "unknown"; - } - if (filterBar.primaryFilter_displayName !== "" && - ((filterBar.primaryFilter_displayName === "Updatable" && tempPurchasesModel.get(i).upgrade_url === "") || - (filterBar.primaryFilter_displayName !== "Updatable" && filterBar.primaryFilter_filterName.toLowerCase() !== currentItemType.toLowerCase()))) { - tempPurchasesModel.remove(i); - i--; - } else { - tempPurchasesModel.setProperty(i, 'itemType', currentItemType); - } - } - - for (var i = 0; i < tempPurchasesModel.count; i++) { - if (!filteredPurchasesModel.get(i)) { - sameItemCount = -1; - break; - } else if (tempPurchasesModel.get(i).itemId === filteredPurchasesModel.get(i).itemId && - tempPurchasesModel.get(i).edition_number === filteredPurchasesModel.get(i).edition_number && - tempPurchasesModel.get(i).status === filteredPurchasesModel.get(i).status) { - sameItemCount++; - } - } - - if (sameItemCount !== tempPurchasesModel.count || - filterBar.text !== filterBar.previousText || - filterBar.primaryFilter !== filterBar.previousPrimaryFilter) { - filteredPurchasesModel.clear(); - var currentId; - for (var i = 0; i < tempPurchasesModel.count; i++) { - currentId = tempPurchasesModel.get(i).id; - filteredPurchasesModel.append(tempPurchasesModel.get(i)); - filteredPurchasesModel.setProperty(i, 'cardBackVisible', false); - filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1)); - filteredPurchasesModel.setProperty(i, 'wornEntityID', ''); - } - - sendToScript({ method: 'purchases_updateWearables' }); - populateDisplayedItemCounts(); - sortByDate(); - } - } - - function checkIfAnyItemStatusChanged() { - var currentPurchasesModelId, currentPurchasesModelEdition, currentPurchasesModelStatus; - var previousPurchasesModelStatus; - for (var i = 0; i < purchasesModel.count; i++) { - currentPurchasesModelId = purchasesModel.get(i).id; - currentPurchasesModelEdition = purchasesModel.get(i).edition_number; - currentPurchasesModelStatus = purchasesModel.get(i).status; - - for (var j = 0; j < previousPurchasesModel.count; j++) { - previousPurchasesModelStatus = previousPurchasesModel.get(j).status; - if (currentPurchasesModelId === previousPurchasesModel.get(j).id && - currentPurchasesModelEdition === previousPurchasesModel.get(j).edition_number && - currentPurchasesModelStatus !== previousPurchasesModelStatus) { - - purchasesModel.setProperty(i, "statusChanged", true); - } else { - purchasesModel.setProperty(i, "statusChanged", false); - } - } - } - } function updateCurrentlyWornWearables(wearables) { - for (var i = 0; i < filteredPurchasesModel.count; i++) { + for (var i = 0; i < purchasesModel.count; i++) { for (var j = 0; j < wearables.length; j++) { - if (filteredPurchasesModel.get(i).itemType === "wearable" && - wearables[j].entityCertID === filteredPurchasesModel.get(i).certificate_id && - wearables[j].entityEdition.toString() === filteredPurchasesModel.get(i).edition_number) { - filteredPurchasesModel.setProperty(i, 'wornEntityID', wearables[j].entityID); + if (purchasesModel.get(i).itemType === "wearable" && + wearables[j].entityCertID === purchasesModel.get(i).certificate_id && + wearables[j].entityEdition.toString() === purchasesModel.get(i).edition_number) { + purchasesModel.setProperty(i, 'wornEntityID', wearables[j].entityID); break; } } @@ -1149,7 +1017,7 @@ Rectangle { switch (message.method) { case 'updatePurchases': referrerURL = message.referrerURL || ""; - titleBarContainer.referrerURL = message.referrerURL; + titleBarContainer.referrerURL = message.referrerURL || ""; filterBar.text = message.filterText ? message.filterText : ""; break; case 'inspectionCertificate_setCertificateId': @@ -1168,6 +1036,9 @@ Rectangle { case 'updateWearables': updateCurrentlyWornWearables(message.wornWearables); break; + case 'http.response': + http.handleHttpResponse(message); + break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 86700f702e..603d7fb676 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -19,6 +19,7 @@ import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../common" as HifiCommerceCommon import "../common/sendAsset" +import "../.." as HifiCommon Rectangle { HifiConstants { id: hifi; } @@ -343,8 +344,14 @@ Rectangle { } } + HifiCommon.RootHttpRequest { + id: http; + } + SendAsset { id: sendMoney; + http: http; + listModelName: "Send Money Connections"; z: 997; visible: root.activeView === "sendMoney"; anchors.fill: parent; @@ -768,6 +775,13 @@ Rectangle { case 'updateSelectedRecipientUsername': sendMoney.fromScript(message); break; + case 'http.response': + http.handleHttpResponse(message); + break; + case 'palIsStale': + case 'avatarDisconnected': + // Because we don't have "channels" for sending messages to a specific QML object, the messages are broadcast to all QML Items. If an Item of yours happens to be visible when some script sends a message with a method you don't expect, you'll get "Unrecognized message..." logs. + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 7a14ee060f..4cf6db7889 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -18,27 +18,17 @@ import QtQuick.Controls 2.2 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Item { HifiConstants { id: hifi; } id: root; - property bool initialHistoryReceived: false; - property bool historyRequestPending: true; - property bool noMoreHistoryData: false; - property int pendingCount: 0; - property int currentHistoryPage: 1; - property var pagesAlreadyAdded: new Array(); onVisibleChanged: { if (visible) { - transactionHistoryModel.clear(); Commerce.balance(); - initialHistoryReceived = false; - root.currentHistoryPage = 1; - root.noMoreHistoryData = false; - root.historyRequestPending = true; - Commerce.history(root.currentHistoryPage); + transactionHistoryModel.getFirstPage(); Commerce.getAvailableUpdates(); } else { refreshTimer.stop(); @@ -53,86 +43,7 @@ Item { } onHistoryResult : { - root.initialHistoryReceived = true; - root.historyRequestPending = false; - - if (result.status === 'success') { - var currentPage = parseInt(result.current_page); - - if (result.data.history.length === 0) { - root.noMoreHistoryData = true; - console.log("No more data to retrieve from Commerce.history() endpoint.") - } else if (root.currentHistoryPage === 1) { - var sameItemCount = 0; - tempTransactionHistoryModel.clear(); - - tempTransactionHistoryModel.append(result.data.history); - - for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - if (!transactionHistoryModel.get(i)) { - sameItemCount = -1; - break; - } else if (tempTransactionHistoryModel.get(i).transaction_type === transactionHistoryModel.get(i).transaction_type && - tempTransactionHistoryModel.get(i).text === transactionHistoryModel.get(i).text) { - sameItemCount++; - } - } - - if (sameItemCount !== tempTransactionHistoryModel.count) { - transactionHistoryModel.clear(); - for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - transactionHistoryModel.append(tempTransactionHistoryModel.get(i)); - } - calculatePendingAndInvalidated(); - } - } else { - if (root.pagesAlreadyAdded.indexOf(currentPage) !== -1) { - console.log("Page " + currentPage + " of history has already been added to the list."); - } else { - // First, add the history result to a temporary model - tempTransactionHistoryModel.clear(); - tempTransactionHistoryModel.append(result.data.history); - - // Make a note that we've already added this page to the model... - root.pagesAlreadyAdded.push(currentPage); - - var insertionIndex = 0; - // If there's nothing in the model right now, we don't need to modify insertionIndex. - if (transactionHistoryModel.count !== 0) { - var currentIteratorPage; - // Search through the whole transactionHistoryModel and look for the insertion point. - // The insertion point is found when the result page from the server is less than - // the page that the current item came from, OR when we've reached the end of the whole model. - for (var i = 0; i < transactionHistoryModel.count; i++) { - currentIteratorPage = transactionHistoryModel.get(i).resultIsFromPage; - - if (currentPage < currentIteratorPage) { - insertionIndex = i; - break; - } else if (i === transactionHistoryModel.count - 1) { - insertionIndex = i + 1; - break; - } - } - } - - // Go through the results we just got back from the server, setting the "resultIsFromPage" - // property of those results and adding them to the main model. - for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - tempTransactionHistoryModel.setProperty(i, "resultIsFromPage", currentPage); - transactionHistoryModel.insert(i + insertionIndex, tempTransactionHistoryModel.get(i)) - } - - calculatePendingAndInvalidated(); - } - } - } - - // Only auto-refresh if the user hasn't scrolled - // and there is more data to grab - if (transactionHistory.atYBeginning && !root.noMoreHistoryData) { - refreshTimer.start(); - } + transactionHistoryModel.handlePage(null, result); } onAvailableUpdatesResult: { @@ -147,7 +58,7 @@ Item { Connections { target: GlobalServices onMyUsernameChanged: { - transactionHistoryModel.clear(); + transactionHistoryModel.resetModel(); usernameText.text = Account.username; } } @@ -235,9 +146,8 @@ Item { onTriggered: { if (transactionHistory.atYBeginning) { console.log("Refreshing 1st Page of Recent Activity..."); - root.historyRequestPending = true; Commerce.balance(); - Commerce.history(1); + transactionHistoryModel.getFirstPage("delayedClear"); } } } @@ -299,11 +209,42 @@ Item { } } - ListModel { - id: tempTransactionHistoryModel; - } - ListModel { + HifiModels.PSFListModel { id: transactionHistoryModel; + listModelName: "transaction history"; // For debugging. Alternatively, we could specify endpoint for that purpose, even though it's not used directly. + itemsPerPage: 6; + getPage: function () { + console.debug('getPage', transactionHistoryModel.listModelName, transactionHistoryModel.currentPageToRetrieve); + Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); + } + processPage: function (data) { + console.debug('processPage', transactionHistoryModel.listModelName, JSON.stringify(data)); + var result, pending; // Set up or get the accumulator for pending. + if (transactionHistoryModel.currentPageToRetrieve == 1) { + pending = {transaction_type: "pendingCount", count: 0}; + result = [pending]; + } else { + pending = transactionHistoryModel.get(0); + result = []; + } + + // Either add to pending, or to result. + // Note that you only see a page of pending stuff until you scroll... + data.history.forEach(function (item) { + if (item.status === 'pending') { + pending.count++; + } else { + result = result.concat(item); + } + }); + + // Only auto-refresh if the user hasn't scrolled + // and there is more data to grab + if (transactionHistory.atYBeginning && data.history.length) { + refreshTimer.start(); + } + return result; + } } Item { anchors.top: recentActivityText.bottom; @@ -312,8 +253,8 @@ Item { anchors.left: parent.left; anchors.right: parent.right; - Item { - visible: transactionHistoryModel.count === 0 && root.initialHistoryReceived; + Item { // On empty history. We don't want to flash and then replace, so don't show until we know we're zero. + visible: transactionHistoryModel.count === 0 && transactionHistoryModel.currentPageToRetrieve < 0; anchors.centerIn: parent; width: parent.width - 12; height: parent.height; @@ -385,10 +326,10 @@ Item { model: transactionHistoryModel; delegate: Item { width: parent.width; - height: (model.transaction_type === "pendingCount" && root.pendingCount !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); + height: (model.transaction_type === "pendingCount" && model.count !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); Item { - visible: model.transaction_type === "pendingCount" && root.pendingCount !== 0; + visible: model.transaction_type === "pendingCount" && model.count !== 0; anchors.top: parent.top; anchors.left: parent.left; width: parent.width; @@ -397,7 +338,7 @@ Item { AnonymousProRegular { id: pendingCountText; anchors.fill: parent; - text: root.pendingCount + ' Transaction' + (root.pendingCount > 1 ? 's' : '') + ' Pending'; + text: model.count + ' Transaction' + (model.count > 1 ? 's' : '') + ' Pending'; size: 18; color: hifi.colors.blueAccent; verticalAlignment: Text.AlignVCenter; @@ -460,14 +401,9 @@ Item { } } onAtYEndChanged: { - if (transactionHistory.atYEnd) { + if (transactionHistory.atYEnd && !transactionHistory.atYBeginning) { console.log("User scrolled to the bottom of 'Recent Activity'."); - if (!root.historyRequestPending && !root.noMoreHistoryData) { - // Grab next page of results and append to model - root.historyRequestPending = true; - Commerce.history(++root.currentHistoryPage); - console.log("Fetching Page " + root.currentHistoryPage + " of Recent Activity..."); - } + transactionHistoryModel.getNextPage(); } } } @@ -506,40 +442,6 @@ Item { return year + '-' + month + '-' + day + '
' + drawnHour + ':' + min + amOrPm; } - - function calculatePendingAndInvalidated(startingPendingCount) { - var pendingCount = startingPendingCount ? startingPendingCount : 0; - for (var i = 0; i < transactionHistoryModel.count; i++) { - if (transactionHistoryModel.get(i).status === "pending") { - pendingCount++; - } - } - - root.pendingCount = pendingCount; - if (pendingCount > 0) { - transactionHistoryModel.insert(0, {"transaction_type": "pendingCount"}); - } - } - - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the JavaScript. - // Messages are in format "{method, params}", like json-rpc. - // - // Description: - // Called when a message is received from a script. - // - function fromScript(message) { - switch (message.method) { - default: - console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); - } - } signal sendSignalToWallet(var msg); // diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml new file mode 100644 index 0000000000..1bfa2f6ae0 --- /dev/null +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -0,0 +1,168 @@ +// +// PSFListModel.qml +// qml/hifi/commerce/common +// +// PSFListModel +// "PSF" stands for: +// - Paged +// - Sortable +// - Filterable +// +// Created by Zach Fox on 2018-05-15 +// Copyright 2018 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 +// + +import QtQuick 2.7 + +ListModel { + id: root; + // Used when printing debug statements + property string listModelName: endpoint; + + // Parameters. Even if you override getPage, below, please set these for clarity and consistency, when applicable. + // E.g., your getPage function could refer to this sortKey, etc. + property string endpoint; + property string sortProperty; // Currently only handles sorting on one column, which fits with current needs and tables. + property bool sortAscending; + property string sortKey: !sortProperty ? '' : (sortProperty + "," + (sortAscending ? "ASC" : "DESC")); + property string searchFilter: ""; + property string tagsFilter; + + // QML fires the following changed handlers even when first instantiating the Item. So we need a guard against firing them too early. + property bool initialized: false; + Component.onCompleted: initialized = true; + onEndpointChanged: if (initialized) { getFirstPage('delayClear'); } + onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); } + onSearchFilterChanged: if (initialized) { getFirstPage('delayClear'); } + onTagsFilterChanged: if (initialized) { getFirstPage('delayClear'); } + + property int itemsPerPage: 100; + + // State. + property int currentPageToRetrieve: 0; // 0 = before first page. -1 = we have them all. Otherwise 1-based page number. + property bool retrievedAtLeastOnePage: false; + // We normally clear on reset. But if we want to "refresh", we can delay clearing the model until we get a result. + // Not normally set directly, but rather by giving a truthy argument to getFirstPage(true); + property bool delayedClear: false; + function resetModel() { + if (!delayedClear) { root.clear(); } + currentPageToRetrieve = 1; + retrievedAtLeastOnePage = false; + } + + // Page processing. + + // Override to return one property of data, and/or to transform the elements. Must return an array of model elements. + property var processPage: function (data) { return data; } + + property var listView; // Optional. For debugging. + // Check consistency and call processPage. + function handlePage(error, response) { + var processed; + console.debug('handlePage', listModelName, additionalFirstPageRequested, error, JSON.stringify(response)); + function fail(message) { + console.warn("Warning page fail", listModelName, JSON.stringify(message)); + currentPageToRetrieve = -1; + requestPending = false; + delayedClear = false; + } + if (error || (response.status !== 'success')) { + return fail(error || response.status); + } + if (!requestPending) { + return fail("No request in flight."); + } + requestPending = false; + if (response.current_page && response.current_page !== currentPageToRetrieve) { // Not all endpoints specify this property. + return fail("Mismatched page, expected:" + currentPageToRetrieve); + } + processed = processPage(response.data || response); + if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { + currentPageToRetrieve = -1; + } + + if (delayedClear) { + root.clear(); + delayedClear = false; + } + root.append(processed); // FIXME keep index steady, and apply any post sort + retrievedAtLeastOnePage = true; + // Suppose two properties change at once, and both of their change handlers request a new first page. + // (An example is when the a filter box gets cleared with text in it, so that the search and tags are both reset.) + // Or suppose someone just types new search text quicker than the server response. + // In these cases, we would have multiple requests in flight, and signal based responses aren't generally very good + // at matching up the right handler with the right message. Rather than require all the APIs to carefully handle such, + // and also to cut down on useless requests, we take care of that case here. + if (additionalFirstPageRequested) { + console.debug('deferred getFirstPage', listModelName); + additionalFirstPageRequested = false; + getFirstPage('delayedClear'); + } + } + function debugView(label) { + if (!listView) { return; } + console.debug(label, listModelName, 'perPage:', itemsPerPage, 'count:', listView.count, + 'index:', listView.currentIndex, 'section:', listView.currentSection, + 'atYBeginning:', listView.atYBeginning, 'atYEnd:', listView.atYEnd, + 'y:', listView.y, 'contentY:', listView.contentY); + } + + // Override either http or getPage. + property var http; // An Item that has a request function. + property var getPage: function () { // Any override MUST call handlePage(), above, even if results empty. + if (!http) { return console.warn("Neither http nor getPage was set for", listModelName); } + // If it is a path starting with slash, add the metaverseServer domain. + var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint; + var parameters = [ + 'per_page=' + itemsPerPage, + 'page=' + currentPageToRetrieve + ]; + if (searchFilter) { + parameters.splice(parameters.length, 0, 'search=' + searchFilter); + } + if (sortKey) { + parameters.splice(parameters.length, 0, 'sort=' + sortKey); + } + + var parametersSeparator = /\?/.test(url) ? '&' : '?'; + url = url + parametersSeparator + parameters.join('&'); + console.debug('getPage', listModelName, currentPageToRetrieve); + http.request({uri: url}, handlePage); + } + + // Start the show by retrieving data according to `getPage()`. + // It can be custom-defined by this item's Parent. + property var getFirstPage: function (delayClear) { + if (requestPending) { + console.debug('deferring getFirstPage', listModelName); + additionalFirstPageRequested = true; + return; + } + delayedClear = !!delayClear; + resetModel(); + requestPending = true; + console.debug("getFirstPage", listModelName, currentPageToRetrieve); + getPage(); + } + property bool additionalFirstPageRequested: false; + property bool requestPending: false; // For de-bouncing getNextPage. + // This function, will get the _next_ page of data according to `getPage()`. + // It can be custom-defined by this item's Parent. Typical usage: + // ListView { + // id: theList + // model: thisPSFListModelId + // onAtYEndChanged: if (theList.atYEnd && !theList.atYBeginning) { thisPSFListModelId.getNextPage(); } + // ...} + property var getNextPage: function () { + if (requestPending || currentPageToRetrieve < 0) { + return; + } + currentPageToRetrieve++; + console.debug("getNextPage", listModelName, currentPageToRetrieve); + requestPending = true; + getPage(); + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index dc67494e27..08f86770e6 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -34,41 +34,16 @@ StackView { height: parent !== null ? parent.height : undefined property int cardWidth: 212; property int cardHeight: 152; - property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; property var tablet: null; - // This version only implements rpc(method, parameters, callback(error, result)) calls initiated from here, not initiated from .js, nor "notifications". - property var rpcCalls: ({}); - property var rpcCounter: 0; + RootHttpRequest { id: http; } signal sendToScript(var message); - function rpc(method, parameters, callback) { - console.debug('TabletAddressDialog: rpc: method = ', method, 'parameters = ', parameters, 'callback = ', callback) - - rpcCalls[rpcCounter] = callback; - var message = {method: method, params: parameters, id: rpcCounter++, jsonrpc: "2.0"}; - sendToScript(message); - } function fromScript(message) { - if (message.method === 'refreshFeeds') { - var feeds = [happeningNow, places, snapshots]; - console.debug('TabletAddressDialog::fromScript: refreshFeeds', 'feeds = ', feeds); - - feeds.forEach(function(feed) { - feed.protocol = encodeURIComponent(message.protocolSignature); - Qt.callLater(feed.fillDestinations); - }); - - return; + switch (message.method) { + case 'http.response': + http.handleHttpResponse(message); + break; } - - var callback = rpcCalls[message.id]; - if (!callback) { - // FIXME: We often recieve very long messages here, the logging of which is drastically slowing down the main thread - //console.log('No callback for message fromScript', JSON.stringify(message)); - return; - } - delete rpcCalls[message.id]; - callback(message.error, message.result); } Component { id: tabletWebView; TabletWebView {} } @@ -346,12 +321,11 @@ StackView { width: parent.width; cardWidth: 312 + (2 * 4); cardHeight: 163 + (2 * 4); - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'HAPPENING NOW'; actions: 'announcement'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } Feed { id: places; @@ -359,12 +333,11 @@ StackView { cardWidth: 210; cardHeight: 110 + messageHeight; messageHeight: 44; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'PLACES'; actions: 'concurrency'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } Feed { id: snapshots; @@ -373,12 +346,11 @@ StackView { cardHeight: 75 + messageHeight + 4; messageHeight: 32; textPadding: 6; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'RECENT SNAPS'; actions: 'snapshot'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } } } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 15db5d8f88..fa268ad6ee 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -65,6 +65,18 @@ Item { return false; } + function closeDialog() { + if (openMessage != null) { + openMessage.destroy(); + openMessage = null; + } + + if (openModal != null) { + openModal.destroy(); + openModal = null; + } + } + function isUrlLoaded(url) { if (currentApp >= 0) { var currentAppUrl = tabletApps.get(currentApp).appUrl; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 2e9c9fdecd..4d133706e6 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -468,13 +468,14 @@ void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) { _shouldRender = shouldRenderAvatars; const render::ScenePointer& scene = qApp->getMain3DScene(); render::Transaction transaction; + auto avatarHashCopy = getHashCopy(); if (_shouldRender) { - for (auto avatarData : _avatarHash) { + for (auto avatarData : avatarHashCopy) { auto avatar = std::static_pointer_cast(avatarData); avatar->addToScene(avatar, scene, transaction); } } else { - for (auto avatarData : _avatarHash) { + for (auto avatarData : avatarHashCopy) { auto avatar = std::static_pointer_cast(avatarData); avatar->removeFromScene(avatar, scene, transaction); } @@ -514,7 +515,8 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic glm::vec3 normDirection = glm::normalize(ray.direction); - for (auto avatarData : _avatarHash) { + auto avatarHashCopy = getHashCopy(); + for (auto avatarData : avatarHashCopy) { auto avatar = std::static_pointer_cast(avatarData); if ((avatarsToInclude.size() > 0 && !avatarsToInclude.contains(avatar->getID())) || (avatarsToDiscard.size() > 0 && avatarsToDiscard.contains(avatar->getID()))) { diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index f791ea25bc..69698e82a6 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -134,8 +134,14 @@ void Ledger::balance(const QStringList& keys) { keysQuery("balance", "balanceSuccess", "balanceFailure"); } -void Ledger::inventory(const QStringList& keys) { - keysQuery("inventory", "inventorySuccess", "inventoryFailure"); +void Ledger::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { + QJsonObject params; + params["edition_filter"] = editionFilter; + params["type_filter"] = typeFilter; + params["title_filter"] = titleFilter; + params["page"] = page; + params["per_page"] = perPage; + keysQuery("inventory", "inventorySuccess", "inventoryFailure", params); } QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) { @@ -260,9 +266,9 @@ void Ledger::historyFailure(QNetworkReply& reply) { failResponse("history", reply); } -void Ledger::history(const QStringList& keys, const int& pageNumber) { +void Ledger::history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage) { QJsonObject params; - params["per_page"] = 100; + params["per_page"] = itemsPerPage; params["page"] = pageNumber; keysQuery("history", "historySuccess", "historyFailure", params); } diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index abc97bfe72..8a8fd2630a 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -28,8 +28,8 @@ public: void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); bool receiveAt(const QString& hfc_key, const QString& signing_key); void balance(const QStringList& keys); - void inventory(const QStringList& keys); - void history(const QStringList& keys, const int& pageNumber); + void inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); + void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); void account(); void updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings = false, const bool controlledFailure = false); void certificateInfo(const QString& certificateId); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 722f29ba2f..b960c0b703 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -105,21 +105,21 @@ void QmlCommerce::balance() { } } -void QmlCommerce::inventory() { +void QmlCommerce::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->inventory(cachedPublicKeys); + ledger->inventory(editionFilter, typeFilter, titleFilter, page, perPage); } } -void QmlCommerce::history(const int& pageNumber) { +void QmlCommerce::history(const int& pageNumber, const int& itemsPerPage) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->history(cachedPublicKeys, pageNumber); + ledger->history(cachedPublicKeys, pageNumber, itemsPerPage); } } @@ -227,10 +227,13 @@ QString QmlCommerce::getInstalledApps() { QString scriptURL = appFileJsonObject["scriptURL"].toString(); // If the script .app.json is on the user's local disk but the associated script isn't running - // for some reason, start that script again. + // for some reason (i.e. the user stopped it from Running Scripts), + // delete the .app.json from the user's local disk. if (!runningScripts.contains(scriptURL)) { - if ((DependencyManager::get()->loadScript(scriptURL.trimmed())).isNull()) { - qCDebug(commerce) << "Couldn't start script while checking installed apps."; + if (!appFile.remove()) { + qCWarning(commerce) + << "Couldn't delete local .app.json file (app's script isn't running). App filename is:" + << appFileName; } } } else { diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 27e97fe7db..a0c6916799 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -73,8 +73,8 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); - Q_INVOKABLE void inventory(); - Q_INVOKABLE void history(const int& pageNumber); + Q_INVOKABLE void inventory(const QString& editionFilter = QString(), const QString& typeFilter = QString(), const QString& titleFilter = QString(), const int& page = 1, const int& perPage = 20); + Q_INVOKABLE void history(const int& pageNumber, const int& itemsPerPage = 100); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void account(); diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index d01e7d6671..83601a2797 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -93,10 +93,18 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { static const QUrl url("dialogs/TabletConnectionFailureDialog.qml"); auto hmd = DependencyManager::get(); if (visible) { + _dialogCreatedWhileShown = tablet->property("tabletShown").toBool(); tablet->initialScreen(url); if (!hmd->getShouldShowTablet()) { hmd->openTablet(); } + } else if (tablet->isPathLoaded(url)) { + tablet->closeDialog(); + tablet->gotoHomeScreen(); + if (!_dialogCreatedWhileShown) { + hmd->closeTablet(); + } + _dialogCreatedWhileShown = false; } } } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index f17ac39a7e..0633dec573 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -80,6 +80,7 @@ private: QPointer _octreeStatsDialog; QPointer _testingDialog; QPointer _domainConnectionDialog; + bool _dialogCreatedWhileShown { false }; bool _addressBarVisible { false }; }; diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index e7e3c91d13..d131bb3467 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -18,7 +18,6 @@ #include "InterfaceLogging.h" OverlayConductor::OverlayConductor() { - } OverlayConductor::~OverlayConductor() { @@ -33,8 +32,8 @@ bool OverlayConductor::headOutsideOverlay() const { glm::vec3 uiPos = uiTransform.getTranslation(); glm::vec3 uiForward = uiTransform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - const float MAX_COMPOSITOR_DISTANCE = 0.99f; // If you're 1m from center of ui sphere, you're at the surface. - const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled + const float MAX_COMPOSITOR_DISTANCE = 0.99f; // If you're 1m from center of ui sphere, you're at the surface. + const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled if (glm::distance(uiPos, hmdPos) > MAX_COMPOSITOR_DISTANCE || glm::dot(uiForward, hmdForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE))) { return true; @@ -43,10 +42,9 @@ bool OverlayConductor::headOutsideOverlay() const { } bool OverlayConductor::updateAvatarIsAtRest() { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s + const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s const quint64 REST_DISABLE_TIME_USECS = 200 * 1000; // 200 ms const float AT_REST_THRESHOLD = 0.01f; @@ -69,31 +67,6 @@ bool OverlayConductor::updateAvatarIsAtRest() { return _currentAtRest; } -bool OverlayConductor::updateAvatarHasDriveInput() { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000; // 200 ms - const quint64 DRIVE_DISABLE_TIME_USECS = 1000 * 1000; // 1 s - - bool desiredDriving = myAvatar->hasDriveInput(); - if (desiredDriving != _desiredDriving) { - // start timer - _desiredDrivingTimer = usecTimestampNow() + (desiredDriving ? DRIVE_ENABLE_TIME_USECS : DRIVE_DISABLE_TIME_USECS); - } - - _desiredDriving = desiredDriving; - - if (_desiredDrivingTimer != 0 && usecTimestampNow() > _desiredDrivingTimer) { - // timer expired - // change state! - _currentDriving = _desiredDriving; - // disable timer - _desiredDrivingTimer = 0; - } - - return _currentDriving; -} - void OverlayConductor::centerUI() { // place the overlay at the current hmd position in sensor space auto camMat = cancelOutRollAndPitch(qApp->getHMDSensorPose()); @@ -115,20 +88,19 @@ void OverlayConductor::update(float dt) { _hmdMode = false; } - bool prevDriving = _currentDriving; - bool isDriving = updateAvatarHasDriveInput(); - bool drivingChanged = prevDriving != isDriving; bool isAtRest = updateAvatarIsAtRest(); + bool isMoving = !isAtRest; + bool shouldRecenter = false; - if (_flags & SuppressedByDrive) { - if (!isDriving) { - _flags &= ~SuppressedByDrive; - shouldRecenter = true; + if (_flags & SuppressedByMove) { + if (!isMoving) { + _flags &= ~SuppressedByMove; + shouldRecenter = true; } } else { - if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { - _flags |= SuppressedByDrive; + if (myAvatar->getClearOverlayWhenMoving() && isMoving) { + _flags |= SuppressedByMove; } } @@ -143,7 +115,6 @@ void OverlayConductor::update(float dt) { } } - bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); if (targetVisible != currentVisible) { offscreenUi->setPinned(!targetVisible); diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index cdd596a7bc..cf69c32fc5 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -23,23 +23,17 @@ public: private: bool headOutsideOverlay() const; - bool updateAvatarHasDriveInput(); bool updateAvatarIsAtRest(); enum SupressionFlags { - SuppressedByDrive = 0x01, + SuppressedByMove = 0x01, SuppressedByHead = 0x02, SuppressMask = 0x03, }; - uint8_t _flags { SuppressedByDrive }; + uint8_t _flags { SuppressedByMove }; bool _hmdMode { false }; - // used by updateAvatarHasDriveInput - uint64_t _desiredDrivingTimer { 0 }; - bool _desiredDriving { false }; - bool _currentDriving { false }; - // used by updateAvatarIsAtRest uint64_t _desiredAtRestTimer { 0 }; bool _desiredAtRest { true }; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index de2ce2fdc4..944df98f9f 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -100,24 +100,32 @@ void ModelOverlay::update(float deltatime) { processMaterials(); emit DependencyManager::get()->modelAddedToScene(getID(), NestableType::Overlay, _model); } + bool metaDirty = false; if (_visibleDirty) { _visibleDirty = false; // don't show overlays in mirrors or spectator-cam unless _isVisibleInSecondaryCamera is true uint8_t modelRenderTagMask = (_isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW); _model->setTagMask(modelRenderTagMask, scene); _model->setVisibleInScene(getVisible(), scene); + metaDirty = true; } if (_drawInFrontDirty) { _drawInFrontDirty = false; _model->setLayeredInFront(getDrawInFront(), scene); + metaDirty = true; } if (_drawInHUDDirty) { _drawInHUDDirty = false; _model->setLayeredInHUD(getDrawHUDLayer(), scene); + metaDirty = true; } if (_groupCulledDirty) { _groupCulledDirty = false; - _model->setGroupCulled(_isGroupCulled); + _model->setGroupCulled(_isGroupCulled, scene); + metaDirty = true; + } + if (metaDirty) { + transaction.updateItem(getRenderItemID(), [](Overlay& data) {}); } scene->enqueueTransaction(transaction); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 829c98a418..974ae92432 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -82,6 +82,7 @@ AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWe avatar->setSessionUUID(sessionUUID); avatar->setOwningAvatarMixer(mixerWeakPointer); + // addAvatar is only called from newOrExistingAvatar, which already locks _hashLock _avatarHash.insert(sessionUUID, avatar); emit avatarAddedEvent(sessionUUID); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 6747025de0..ef6f7845eb 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -46,7 +46,7 @@ class AvatarHashMap : public QObject, public Dependency { public: AvatarHash getHashCopy() { QReadLocker lock(&_hashLock); return _avatarHash; } const AvatarHash getHashCopy() const { QReadLocker lock(&_hashLock); return _avatarHash; } - int size() { return _avatarHash.size(); } + int size() { QReadLocker lock(&_hashLock); return _avatarHash.size(); } // Currently, your own avatar will be included as the null avatar id. @@ -152,8 +152,6 @@ protected: virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason); AvatarHash _avatarHash; - // "Case-based safety": Most access to the _avatarHash is on the same thread. Write access is protected by a write-lock. - // If you read from a different thread, you must read-lock the _hashLock. (Scripted write access is not supported). mutable QReadWriteLock _hashLock; private: diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index b6957a2712..2b50f6be97 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -157,18 +157,19 @@ void TextureBaker::processTexture() { return; } - const char* name = khronos::gl::texture::toString(memKTX->_header.getGLInternaFormat()); - if (name == nullptr) { - handleError("Could not determine internal format for compressed KTX: " + _textureURL.toString()); - return; - } // attempt to write the baked texture to the destination file path - { + if (memKTX->_header.isCompressed()) { + const char* name = khronos::gl::texture::toString(memKTX->_header.getGLInternaFormat()); + if (name == nullptr) { + handleError("Could not determine internal format for compressed KTX: " + _textureURL.toString()); + return; + } + const char* data = reinterpret_cast(memKTX->_storage->data()); const size_t length = memKTX->_storage->size(); - auto fileName = _baseFilename + BAKED_TEXTURE_BCN_SUFFIX; + auto fileName = _baseFilename + "_" + name + ".ktx"; auto filePath = _outputDirectory.absoluteFilePath(fileName); QFile bakedTextureFile { filePath }; if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { @@ -177,6 +178,18 @@ void TextureBaker::processTexture() { } _outputFiles.push_back(filePath); meta.availableTextureTypes[memKTX->_header.getGLInternaFormat()] = _metaTexturePathPrefix + fileName; + } else { + const char* data = reinterpret_cast(memKTX->_storage->data()); + const size_t length = memKTX->_storage->size(); + + auto fileName = _baseFilename + ".ktx"; + auto filePath = _outputDirectory.absoluteFilePath(fileName); + QFile ktxTextureFile { filePath }; + if (!ktxTextureFile.open(QIODevice::WriteOnly) || ktxTextureFile.write(data, length) == -1) { + handleError("Could not write ktx texture for " + _textureURL.toString()); + return; + } + _outputFiles.push_back(filePath); } diff --git a/libraries/entities-renderer/src/textured_particle.slv b/libraries/entities-renderer/src/textured_particle.slv index cab76227c4..1d4261b1cc 100644 --- a/libraries/entities-renderer/src/textured_particle.slv +++ b/libraries/entities-renderer/src/textured_particle.slv @@ -37,8 +37,8 @@ layout(std140) uniform particleBuffer { ParticleUniforms particle; }; -in vec3 inPosition; -in vec2 inColor; // This is actual Lifetime + Seed +layout(location=0) in vec3 inPosition; +layout(location=2) in vec2 inColor; // This is actual Lifetime + Seed out vec4 varColor; out vec2 varTexcoord; diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 977cabb57a..317be194b8 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -157,7 +157,11 @@ void AddressManager::storeCurrentAddress() { // be loaded over http(s) // url.scheme() == URL_SCHEME_HTTP || // url.scheme() == URL_SCHEME_HTTPS || - currentAddressHandle.set(url); + if (isConnected()) { + currentAddressHandle.set(url); + } else { + qCWarning(networking) << "Ignoring attempt to save current address because not connected to domain:" << url; + } } else { qCWarning(networking) << "Ignoring attempt to save current address with an invalid url:" << url; } diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index d06b74b724..6df15129a2 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -38,6 +38,11 @@ ResourceManager::ResourceManager(bool atpSupportEnabled) : _atpSupportEnabled(at _thread.start(); } +ResourceManager::~ResourceManager() { + _thread.terminate(); + _thread.wait(); +} + void ResourceManager::setUrlPrefixOverride(const QString& prefix, const QString& replacement) { QMutexLocker locker(&_prefixMapLock); if (replacement.isEmpty()) { diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h index 9fc636f5fe..a79222d2d8 100644 --- a/libraries/networking/src/ResourceManager.h +++ b/libraries/networking/src/ResourceManager.h @@ -28,6 +28,7 @@ class ResourceManager: public QObject, public Dependency { public: ResourceManager(bool atpSupportEnabled = true); + ~ResourceManager(); void setUrlPrefixOverride(const QString& prefix, const QString& replacement); QString normalizeURL(const QString& urlString); diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 9070d87a3c..2c52e669a0 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -431,6 +431,19 @@ bool TabletProxy::isMessageDialogOpen() { return result.toBool(); } +void TabletProxy::closeDialog() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "closeDialog"); + return; + } + + if (!_qmlTabletRoot) { + return; + } + + QMetaObject::invokeMethod(_qmlTabletRoot, "closeDialog"); +} + void TabletProxy::emitWebEvent(const QVariant& msg) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "emitWebEvent", Q_ARG(QVariant, msg)); diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index 43d889f1d1..1ab29ca3fd 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -308,6 +308,12 @@ public: */ Q_INVOKABLE bool isMessageDialogOpen(); + /**jsdoc + * Close any open dialogs. + * @function TabletProxy#closeDialog + */ + Q_INVOKABLE void closeDialog(); + /**jsdoc * Creates a new button, adds it to this and returns it. * @function TabletProxy#addButton diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 06ce05e721..ddbeaaeea9 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -12,6 +12,7 @@ // var DEFAULT_SCRIPTS_COMBINED = [ + "system/request-service.js", "system/progress.js", "system/away.js", "system/audio.js", diff --git a/scripts/system/+android/radar.js b/scripts/system/+android/radar.js index 5d93ed4db1..1cbe721ad0 100644 --- a/scripts/system/+android/radar.js +++ b/scripts/system/+android/radar.js @@ -1119,7 +1119,7 @@ function startRadar() { function endRadar() { printd("-- endRadar"); - Camera.mode = "first person"; + Camera.mode = "third person"; radar = false; Controller.setVPadEnabled(true); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index aea752c565..ac25269e41 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -511,6 +511,9 @@ case 'wallet_availableUpdatesReceived': // NOP break; + case 'http.request': + // Handled elsewhere, don't log. + break; default: print('Unrecognized message from QML:', JSON.stringify(message)); } diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index 08b88fe74d..91c8d89daf 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -273,7 +273,6 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.shouldSendStart = false; this.equipedWithSecondary = false; this.handHasBeenRightsideUp = false; - this.mouseEquip = false; this.parameters = makeDispatcherModuleParameters( 300, @@ -283,11 +282,10 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var equipHotspotBuddy = new EquipHotspotBuddy(); - this.setMessageGrabData = function(entityProperties, mouseEquip) { + this.setMessageGrabData = function(entityProperties) { if (entityProperties) { this.messageGrabEntity = true; this.grabEntityProps = entityProperties; - this.mouseEquip = mouseEquip; } }; @@ -584,7 +582,6 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa this.targetEntityID = null; this.messageGrabEntity = false; this.grabEntityProps = null; - this.mouseEquip = false; }; this.updateInputs = function (controllerData) { @@ -630,14 +627,12 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa // if the potentialHotspot is cloneable, clone it and return it // if the potentialHotspot os not cloneable and locked return null - if (potentialEquipHotspot && (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || this.messageGrabEntity)) { this.grabbedHotspot = potentialEquipHotspot; this.targetEntityID = this.grabbedHotspot.entityID; this.startEquipEntity(controllerData); - this.messageGrabEntity = false; this.equipedWithSecondary = this.secondarySmoothedSqueezed(); return makeRunningValues(true, [potentialEquipHotspot.entityID], []); } else { @@ -661,7 +656,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var timestamp = Date.now(); this.updateInputs(controllerData); - if (!this.mouseEquip && !this.isTargetIDValid(controllerData)) { + if (!this.messageGrabEntity && !this.isTargetIDValid(controllerData)) { this.endEquipEntity(); return makeRunningValues(false, [], []); } @@ -762,9 +757,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var equipModule = (data.hand === "left") ? leftEquipEntity : rightEquipEntity; var entityProperties = Entities.getEntityProperties(data.entityID, DISPATCHER_PROPERTIES); entityProperties.id = data.entityID; - var mouseEquip = false; - equipModule.setMessageGrabData(entityProperties, mouseEquip); - + equipModule.setMessageGrabData(entityProperties); } catch (e) { print("WARNING: equipEntity.js -- error parsing Hifi-Hand-Grab message: " + message); } @@ -812,15 +805,14 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var distanceToLeftHand = Vec3.distance(entityProperties.position, leftHandPosition); var leftHandAvailable = leftEquipEntity.targetEntityID === null; var rightHandAvailable = rightEquipEntity.targetEntityID === null; - var mouseEquip = true; if (rightHandAvailable && (distanceToRightHand < distanceToLeftHand || !leftHandAvailable)) { // clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags) clearGrabActions(entityID); - rightEquipEntity.setMessageGrabData(entityProperties, mouseEquip); + rightEquipEntity.setMessageGrabData(entityProperties); } else if (leftHandAvailable && (distanceToLeftHand < distanceToRightHand || !rightHandAvailable)) { // clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags) clearGrabActions(entityID); - leftEquipEntity.setMessageGrabData(entityProperties, mouseEquip); + leftEquipEntity.setMessageGrabData(entityProperties); } } } diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 66cd197abd..e4563fda14 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -282,7 +282,7 @@ Script.include("/~/system/libraries/Xform.js"); this.previousRoomControllerPosition = roomControllerPosition; }; - this.endNearGrabAction = function () { + this.endFarGrabAction = function () { ensureDynamic(this.grabbedThingID); this.distanceHolding = false; this.distanceRotating = false; @@ -402,7 +402,7 @@ Script.include("/~/system/libraries/Xform.js"); this.run = function (controllerData) { if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.notPointingAtEntity(controllerData) || this.targetIsNull()) { - this.endNearGrabAction(); + this.endFarGrabAction(); Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); this.highlightedEntity = null; @@ -430,11 +430,12 @@ Script.include("/~/system/libraries/Xform.js"); } if (this.actionID) { - // if we are doing a distance grab and the object gets close enough to the controller, + // if we are doing a distance grab and the object or tablet gets close enough to the controller, // stop the far-grab so the near-grab or equip can take over. for (var k = 0; k < nearGrabReadiness.length; k++) { - if (nearGrabReadiness[k].active && nearGrabReadiness[k].targets[0] === this.grabbedThingID) { - this.endNearGrabAction(); + if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.grabbedThingID + || HMD.tabletID && nearGrabReadiness[k].targets[0] === HMD.tabletID)) { + this.endFarGrabAction(); return makeRunningValues(false, [], []); } } @@ -445,7 +446,7 @@ Script.include("/~/system/libraries/Xform.js"); // where it could near-grab something, stop searching. for (var j = 0; j < nearGrabReadiness.length; j++) { if (nearGrabReadiness[j].active) { - this.endNearGrabAction(); + this.endFarGrabAction(); return makeRunningValues(false, [], []); } } @@ -577,7 +578,7 @@ Script.include("/~/system/libraries/Xform.js"); var disableModule = getEnabledModuleByName(moduleName); if (disableModule) { if (disableModule.disableModules) { - this.endNearGrabAction(); + this.endFarGrabAction(); Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); this.highlightedEntity = null; diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 864c7d92b4..799a974fd6 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -250,7 +250,7 @@ }); } - function buyButtonClicked(id, name, author, price, href, referrer, edition) { + function buyButtonClicked(id, name, author, price, href, referrer, edition, type) { EventBridge.emitWebEvent(JSON.stringify({ type: "CHECKOUT", itemId: id, @@ -259,7 +259,8 @@ itemHref: href, referrer: referrer, itemAuthor: author, - itemEdition: edition + itemEdition: edition, + itemType: type.trim() })); } @@ -328,7 +329,8 @@ $(this).closest('.grid-item').find('.item-cost').text(), $(this).attr('data-href'), "mainPage", - -1); + -1, + $(this).closest('.grid-item').find('.item-type').text()); }); } @@ -419,6 +421,7 @@ } var cost = $('.item-cost').text(); + var type = $('.item-type').text(); var isUpdating = window.location.href.indexOf('edition=') > -1; var urlParams = new URLSearchParams(window.location.search); if (isUpdating) { @@ -438,7 +441,8 @@ cost, href, "itemPage", - urlParams.get('edition')); + urlParams.get('edition'), + type); } }); maybeAddPurchasesButton(); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index f6e337755b..fd7b9c703a 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -987,6 +987,11 @@ var selectionDisplay = null; // for gridTool.js to ignore sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect, SEND_ASSET_PARTICLE_TIMER_UPDATE); } break; + case 'http.request': + // Handled elsewhere, don't log. + break; + case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about? + break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index c70c2729f5..41774629e7 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -317,6 +317,8 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } ); break; + case 'http.request': + break; // Handled by request-service. default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } diff --git a/scripts/system/request-service.js b/scripts/system/request-service.js new file mode 100644 index 0000000000..b57f2d4cd7 --- /dev/null +++ b/scripts/system/request-service.js @@ -0,0 +1,48 @@ +"use strict"; +// +// request-service.js +// +// Created by Howard Stearns on May 22, 2018 +// Copyright 2018 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 +// + +(function() { // BEGIN LOCAL_SCOPE + + // QML has its own XMLHttpRequest, but: + // - npm request is easier to use. + // - It is not easy to hack QML's XMLHttpRequest to use our MetaverseServer, and to supply the user's auth when contacting it. + // a. Our custom XMLHttpRequestClass object only works with QScriptEngine, not QML's javascript. + // b. We have hacked profiles that intercept requests to our MetavserseServer (providing the correct auth), but those + // only work in QML WebEngineView. Setting up communication between ordinary QML and a hiddent WebEngineView is + // tantamount to the following anyway, and would still have to duplicate the code from request.js. + + // So, this script does two things: + // 1. Allows any root .qml to signal sendToScript({id: aString, method: 'http.request', params: byNameOptions}) + // We will then asynchonously call fromScript({id: theSameString, method: 'http.response', error: errorOrFalsey, response: body}) + // on that root object. + // RootHttpRequest.qml does this. + // 2. If the uri used (computed from byNameOptions, see request.js) is to our metaverse, we will use the appropriate auth. + + var request = Script.require('request').request; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + function fromQml(message) { // messages are {id, method, params}, like json-rpc. See also sendToQml. + switch (message.method) { + case 'http.request': + request(message.params, function (error, response) { + tablet.sendToQml({ + id: message.id, + method: 'http.response', + error: error, // Alas, this isn't always a JSON-RPC conforming error object. + response: response, + jsonrpc: '2.0' + }); + }); + break; + } + } + tablet.fromQml.connect(fromQml); + Script.scriptEnding.connect(function () { tablet.fromQml.disconnect(fromQml); }); +}()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 9cd8420a88..46ddeb2bab 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -41,24 +41,6 @@ sortOrder: 8 }); - function fromQml(message) { - console.debug('tablet-goto::fromQml: message = ', JSON.stringify(message)); - - var response = {id: message.id, jsonrpc: "2.0"}; - switch (message.method) { - case 'request': - request(message.params, function (error, data) { - debug('rpc', request, 'error:', error, 'data:', data); - response.error = error; - response.result = data; - tablet.sendToQml(response); - }); - return; - default: - response.error = {message: 'Unrecognized message', data: message}; - } - tablet.sendToQml(response); - } function messagesWaiting(isWaiting) { button.editProperties({ icon: isWaiting ? WAITING_ICON : NORMAL_ICON @@ -66,21 +48,6 @@ }); } - var hasEventBridge = false; - function wireEventBridge(on) { - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } - } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } - } - } - function onClicked() { if (onGotoScreen) { // for toolbar-mode: go back to home screen, this will close the window. @@ -98,15 +65,11 @@ onGotoScreen = true; shouldActivateButton = true; button.editProperties({isActive: shouldActivateButton}); - wireEventBridge(true); messagesWaiting(false); - tablet.sendToQml({ method: 'refreshFeeds', protocolSignature: Window.protocolSignature() }) - } else { shouldActivateButton = false; onGotoScreen = false; button.editProperties({isActive: shouldActivateButton}); - wireEventBridge(false); } } button.clicked.connect(onClicked); diff --git a/server-console/src/main.js b/server-console/src/main.js index 8a92fc8a5d..b9f128d776 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -75,10 +75,14 @@ function getBuildInfo() { } const buildInfo = getBuildInfo(); -function getRootHifiDataDirectory() { +function getRootHifiDataDirectory(local) { var organization = buildInfo.organization; if (osType == 'Windows_NT') { - return path.resolve(osHomeDir(), 'AppData/Roaming', organization); + if (local) { + return path.resolve(osHomeDir(), 'AppData/Local', organization); + } else { + return path.resolve(osHomeDir(), 'AppData/Roaming', organization); + } } else if (osType == 'Darwin') { return path.resolve(osHomeDir(), 'Library/Application Support', organization); } else { @@ -94,8 +98,8 @@ function getAssignmentClientResourcesDirectory() { return path.join(getRootHifiDataDirectory(), '/assignment-client'); } -function getApplicationDataDirectory() { - return path.join(getRootHifiDataDirectory(), '/Server Console'); +function getApplicationDataDirectory(local) { + return path.join(getRootHifiDataDirectory(local), '/Server Console'); } // Update lock filepath @@ -104,7 +108,7 @@ const UPDATER_LOCK_FULL_PATH = getRootHifiDataDirectory() + "/" + UPDATER_LOCK_F // Configure log global.log = require('electron-log'); -const logFile = getApplicationDataDirectory() + '/log.txt'; +const logFile = getApplicationDataDirectory(true) + '/log.txt'; fs.ensureFileSync(logFile); // Ensure file exists log.transports.file.maxSize = 5 * 1024 * 1024; log.transports.file.file = logFile; @@ -221,7 +225,19 @@ function deleteOldFiles(directoryPath, maxAgeInSeconds, filenameRegex) { } } -var logPath = path.join(getApplicationDataDirectory(), '/logs'); +var oldLogPath = path.join(getApplicationDataDirectory(), '/logs'); +var logPath = path.join(getApplicationDataDirectory(true), '/logs'); + +if (oldLogPath != logPath) { + console.log("Migrating old logs from " + oldLogPath + " to " + logPath); + fs.copy(oldLogPath, logPath, err => { + if (err) { + console.error(err); + } else { + console.log('success!'); + } + }) +} log.debug("Log directory:", logPath); log.debug("Data directory:", getRootHifiDataDirectory()); diff --git a/tools/010-templates/README.md b/tools/010-templates/README.md new file mode 100644 index 0000000000..df3ce6d0e5 --- /dev/null +++ b/tools/010-templates/README.md @@ -0,0 +1 @@ +This directory contains [010 editor](https://www.sweetscape.com/010editor/) templates for parsing and inspecting different file types. diff --git a/tools/010-templates/fbx.bt b/tools/010-templates/fbx.bt new file mode 100644 index 0000000000..dcb620066e --- /dev/null +++ b/tools/010-templates/fbx.bt @@ -0,0 +1,102 @@ +// +// fbx.bt +// tools/010-templates +// +// Created by Ryan Huffman +// Copyright 2018 High Fidelity, Inc. +// +// FBX file template +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +local char use64BitAddresses = 1; + +struct Header { + char prefix[23]; + int32 version; +}; + +struct Property { + char type; + if (type == 'Y') { + int16 value; + } else if (type == 'C') { + char value; + } else if (type == 'I') { + int32 value; + } else if (type == 'F') { + float value; + } else if (type == 'D') { + double value; + } else if (type == 'L') { + int64 value; + } else if (type == 'S' || type == 'R') { + uint32 size; + char value[size]; + } else { + uint32 length; + uint32 encoding; + uint32 compressedLength; + if (encoding == 1) { + char compressedData[compressedLength]; + } else if (type == 'f') { + float values[this.length]; + } else if (type == 'd') { + double values[this.length]; + } else if (type == 'l') { + int64 values[this.length]; + } else if (type == 'i') { + int32 values[this.length]; + } else if (type == 'b') { + char values[this.length]; + } else { + Printf("%c", type); + Assert(false, "Error, unknown property type"); + } + } +}; + +struct Node; + +string nodeName(Node& node) { + if (!exists(node.name)) { + return "Node ----- "; + } + local string s; + SPrintf(s, "Node (%s) ", node.name); + return s; +} + +struct Node { + if (use64BitAddresses) { + int64 endOffset; + uint64 propertyCount; + uint64 propertyListLength; + } else { + int32 endOffset; + uint32 propertyCount; + uint32 propertyListLength; + } + uchar nameLength; + char name[this.nameLength]; + Property properties[this.propertyCount]; + while (FTell() < endOffset) { + Node children; + } +}; + +struct File { + Header header; + use64BitAddresses = header.version >= 7500; + local int i = 0; + Node node; + local string name = node.name; + while (name != "") { + Node node; + i++; + name = exists(node[i].name) ? node[i].name : ""; + } + +} file; diff --git a/tools/010-templates/ktx.bt b/tools/010-templates/ktx.bt new file mode 100644 index 0000000000..9690dbb391 --- /dev/null +++ b/tools/010-templates/ktx.bt @@ -0,0 +1,52 @@ +// +// ktx.bt +// tools/010-templates +// +// Created by Ryan Huffman +// Copyright 2018 High Fidelity, Inc. +// +// KTX file template +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +struct Header { + char identifier[12]; + uint32 endianness; + uint32 glType; + uint32 glTypeSize; + uint32 glFormat; + uint32 glInternalFormat; + uint32 glBaseInternalFormat; + uint32 pixelWidth; + uint32 pixelHeight; + uint32 pixelDepth; + uint32 numberOfArrayElements; + uint32 numberOfFaces; + uint32 numberOfMipmapLevels; + uint32 bytesOfKeyValueData; +}; + +struct KV { + uint32 byteSize; + local uint32 keyLength = ReadStringLength(FTell()); + char key[keyLength]; + char value[byteSize - keyLength] ; + char padding[3 - ((byteSize + 3) % 4)]; +}; + +string kvName(KV& kv) { + local string s; + SPrintf(s, "KeyValue (%s) ", kv.key); + return s; +} + +struct File { + Header header; + local uint32 endOfKV = FTell() + header.bytesOfKeyValueData; + while (FTell() < endOfKV) { + KV keyValue ; + } + char imageData[FileSize() - FTell()]; +} file; diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 71bb997303..1b77a2585f 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -11,7 +11,7 @@ if (WIN32) elseif (UNIX AND NOT APPLE) find_package(Threads REQUIRED) if(THREADS_HAVE_PTHREAD_ARG) - target_compile_options(PUBLIC oven "-pthread") + target_compile_options(oven PUBLIC "-pthread") endif() elseif (APPLE) # Fix up the rpath so macdeployqt works diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index a7b8401269..0db70f6fe4 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -16,6 +16,8 @@ #include #include +#include + #include "OvenCLIApplication.h" #include "ModelBakingLoggingCategory.h" #include "FBXBaker.h" @@ -38,17 +40,15 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& static const QString MODEL_EXTENSION { "fbx" }; static const QString SCRIPT_EXTENSION { "js" }; - QString extension = type; - - if (extension.isNull()) { - auto url = inputUrl.toDisplayString(); - extension = url.mid(url.lastIndexOf('.')); - } - // check what kind of baker we should be creating - bool isFBX = extension == MODEL_EXTENSION; - bool isScript = extension == SCRIPT_EXTENSION; + bool isFBX = type == MODEL_EXTENSION; + bool isScript = type == SCRIPT_EXTENSION; + // If the type doesn't match the above, we assume we have a texture, and the type specified is the + // texture usage type (albedo, cubemap, normals, etc.) + auto url = inputUrl.toDisplayString(); + auto idx = url.lastIndexOf('.'); + auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; bool isSupportedImage = QImageReader::supportedImageFormats().contains(extension.toLatin1()); _outputPath = outputPath; @@ -65,7 +65,29 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else if (isSupportedImage) { - _baker = std::unique_ptr { new TextureBaker(inputUrl, image::TextureUsage::CUBE_TEXTURE, outputPath) }; + static const std::unordered_map STRING_TO_TEXTURE_USAGE_TYPE_MAP { + { "default", image::TextureUsage::DEFAULT_TEXTURE }, + { "strict", image::TextureUsage::STRICT_TEXTURE }, + { "albedo", image::TextureUsage::ALBEDO_TEXTURE }, + { "normal", image::TextureUsage::NORMAL_TEXTURE }, + { "bump", image::TextureUsage::BUMP_TEXTURE }, + { "specular", image::TextureUsage::SPECULAR_TEXTURE }, + { "metallic", image::TextureUsage::METALLIC_TEXTURE }, + { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, + { "gloss", image::TextureUsage::GLOSS_TEXTURE }, + { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, + { "cube", image::TextureUsage::CUBE_TEXTURE }, + { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, + { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, + { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, + }; + + auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type); + if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) { + qCDebug(model_baking) << "Unknown texture usage type:" << type; + QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); + } + _baker = std::unique_ptr { new TextureBaker(inputUrl, it->second, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl; diff --git a/tools/oven/src/OvenCLIApplication.cpp b/tools/oven/src/OvenCLIApplication.cpp index ab3178db01..6f87359134 100644 --- a/tools/oven/src/OvenCLIApplication.cpp +++ b/tools/oven/src/OvenCLIApplication.cpp @@ -14,11 +14,14 @@ #include #include +#include + #include "BakerCLI.h" static const QString CLI_INPUT_PARAMETER = "i"; static const QString CLI_OUTPUT_PARAMETER = "o"; static const QString CLI_TYPE_PARAMETER = "t"; +static const QString CLI_DISABLE_TEXTURE_COMPRESSION_PARAMETER = "disable-texture-compression"; OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) : QCoreApplication(argc, argv) @@ -29,7 +32,8 @@ OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) : parser.addOptions({ { CLI_INPUT_PARAMETER, "Path to file that you would like to bake.", "input" }, { CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" }, - { CLI_TYPE_PARAMETER, "Type of asset.", "type" } + { CLI_TYPE_PARAMETER, "Type of asset.", "type" }, + { CLI_DISABLE_TEXTURE_COMPRESSION_PARAMETER, "Disable texture compression." } }); parser.addHelpOption(); @@ -40,6 +44,15 @@ OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) : QUrl inputUrl(QDir::fromNativeSeparators(parser.value(CLI_INPUT_PARAMETER))); QUrl outputUrl(QDir::fromNativeSeparators(parser.value(CLI_OUTPUT_PARAMETER))); QString type = parser.isSet(CLI_TYPE_PARAMETER) ? parser.value(CLI_TYPE_PARAMETER) : QString::null; + + if (parser.isSet(CLI_DISABLE_TEXTURE_COMPRESSION_PARAMETER)) { + qDebug() << "Disabling texture compression"; + image::setColorTexturesCompressionEnabled(false); + image::setGrayscaleTexturesCompressionEnabled(false); + image::setNormalTexturesCompressionEnabled(false); + image::setCubeTexturesCompressionEnabled(false); + } + QMetaObject::invokeMethod(cli, "bakeFile", Qt::QueuedConnection, Q_ARG(QUrl, inputUrl), Q_ARG(QString, outputUrl.toString()), Q_ARG(QString, type)); } else {