diff --git a/android/app/build.gradle b/android/app/build.gradle index 46de9642d9..699008092c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,7 +22,7 @@ android { '-DHIFI_ANDROID_PRECOMPILED=' + HIFI_ANDROID_PRECOMPILED, '-DRELEASE_NUMBER=' + RELEASE_NUMBER, '-DRELEASE_TYPE=' + RELEASE_TYPE, - '-DBUILD_BRANCH=' + BUILD_BRANCH, + '-DSTABLE_BUILD=' + STABLE_BUILD, '-DDISABLE_QML=OFF', '-DDISABLE_KTX_CACHE=OFF' } @@ -128,4 +128,3 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') } - diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java index e05b25f3c3..b98849d051 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java @@ -64,7 +64,11 @@ public class HomeFragment extends Fragment { mDomainsView.setLayoutManager(gridLayoutMgr); mDomainAdapter = new DomainAdapter(getContext(), HifiUtils.getInstance().protocolVersionSignature(), nativeGetLastLocation()); mDomainAdapter.setClickListener((view, position, domain) -> { - new Handler(getActivity().getMainLooper()).postDelayed(() -> mListener.onSelectedDomain(domain.url), 400); // a delay so the ripple effect can be seen + new Handler(getActivity().getMainLooper()).postDelayed(() -> { + if (mListener != null) { + mListener.onSelectedDomain(domain.url); + } + }, 400); // a delay so the ripple effect can be seen }); mDomainAdapter.setListener(new DomainAdapter.AdapterListener() { @Override @@ -116,7 +120,9 @@ public class HomeFragment extends Fragment { if (!urlString.trim().isEmpty()) { urlString = HifiUtils.getInstance().sanitizeHifiUrl(urlString); } - mListener.onSelectedDomain(urlString); + if (mListener != null) { + mListener.onSelectedDomain(urlString); + } return true; } return false; diff --git a/android/build.gradle b/android/build.gradle index f09dc97850..f1fe4ffc7f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -37,7 +37,7 @@ task clean(type: Delete) { ext { RELEASE_NUMBER = project.hasProperty('RELEASE_NUMBER') ? project.getProperty('RELEASE_NUMBER') : '0' RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV' - BUILD_BRANCH = project.hasProperty('BUILD_BRANCH') ? project.getProperty('BUILD_BRANCH') : '' + STABLE_BUILD = project.hasProperty('STABLE_BUILD') ? project.getProperty('STABLE_BUILD') : '0' EXEC_SUFFIX = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : '' QT5_DEPS = [ 'Qt5Concurrent', @@ -542,7 +542,7 @@ task cleanDependencies(type: Delete) { -// FIXME this code is prototyping the desired functionality for doing build time binary dependency resolution. +// FIXME this code is prototyping the desired functionality for doing build time binary dependency resolution. // See the comment on the qtBundle task above /* // FIXME derive the path from the gradle environment diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 36e5a065df..029c829022 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -17,14 +17,12 @@ macro(SET_PACKAGING_PARAMETERS) set(DEV_BUILD 0) set(BUILD_GLOBAL_SERVICES "DEVELOPMENT") set(USE_STABLE_GLOBAL_SERVICES 0) + set(BUILD_NUMBER 0) set_from_env(RELEASE_TYPE RELEASE_TYPE "DEV") set_from_env(RELEASE_NUMBER RELEASE_NUMBER "") - set_from_env(BUILD_BRANCH BRANCH "") - string(TOLOWER "${BUILD_BRANCH}" BUILD_BRANCH) + set_from_env(STABLE_BUILD STABLE_BUILD 0) - message(STATUS "The BUILD_BRANCH variable is: ${BUILD_BRANCH}") - message(STATUS "The BRANCH environment variable is: $ENV{BRANCH}") message(STATUS "The RELEASE_TYPE variable is: ${RELEASE_TYPE}") # setup component categories for installer @@ -46,17 +44,17 @@ macro(SET_PACKAGING_PARAMETERS) # if the build is a PRODUCTION_BUILD from the "stable" branch # then use the STABLE gobal services - if (BUILD_BRANCH STREQUAL "stable") - message(STATUS "The RELEASE_TYPE is PRODUCTION and the BUILD_BRANCH is stable...") + if (STABLE_BUILD) + message(STATUS "The RELEASE_TYPE is PRODUCTION and STABLE_BUILD is 1") set(BUILD_GLOBAL_SERVICES "STABLE") set(USE_STABLE_GLOBAL_SERVICES 1) - endif() + endif () elseif (RELEASE_TYPE STREQUAL "PR") set(DEPLOY_PACKAGE TRUE) set(PR_BUILD 1) set(BUILD_VERSION "PR${RELEASE_NUMBER}") - set(BUILD_ORGANIZATION "High Fidelity - ${BUILD_VERSION}") + set(BUILD_ORGANIZATION "High Fidelity - PR${RELEASE_NUMBER}") set(INTERFACE_BUNDLE_NAME "Interface") set(INTERFACE_ICON_PREFIX "interface-beta") @@ -75,6 +73,54 @@ macro(SET_PACKAGING_PARAMETERS) string(TIMESTAMP BUILD_TIME "%d/%m/%Y") + # if STABLE_BUILD is 1, PRODUCTION_BUILD must be 1 and + # DEV_BUILD and PR_BUILD must be 0 + if (STABLE_BUILD) + if ((NOT PRODUCTION_BUILD) OR PR_BUILD OR DEV_BUILD) + message(FATAL_ERROR "Cannot produce STABLE_BUILD without PRODUCTION_BUILD") + endif () + endif () + + if ((PRODUCTION_BUILD OR PR_BUILD) AND NOT STABLE_BUILD) + # append the abbreviated commit SHA to the build version + # since this is a PR build or master/nightly builds + + # for PR_BUILDS, we need to grab the abbreviated SHA + # for the second parent of HEAD (not HEAD) since that is the + # SHA of the commit merged to master for the build + if (PR_BUILD) + set(_GIT_LOG_FORMAT "%p") + else () + set(_GIT_LOG_FORMAT "%h") + endif () + + execute_process( + COMMAND git log -1 --format=${_GIT_LOG_FORMAT} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE _GIT_LOG_OUTPUT + ERROR_VARIABLE _GIT_LOG_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if (PR_BUILD) + separate_arguments(_COMMIT_PARENTS UNIX_COMMAND ${_GIT_LOG_OUTPUT}) + list(GET _COMMIT_PARENTS 1 GIT_COMMIT_HASH) + else () + set(GIT_COMMIT_HASH ${_GIT_LOG_OUTPUT}) + endif () + + if (_GIT_LOG_ERROR OR NOT GIT_COMMIT_HASH) + message(FATAL_ERROR "Could not retreive abbreviated SHA for PR or production master build") + endif () + + set(BUILD_VERSION_NO_SHA ${BUILD_VERSION}) + set(BUILD_VERSION "${BUILD_VERSION}-${GIT_COMMIT_HASH}") + + # pass along a release number without the SHA in case somebody + # wants to compare master or PR builds as integers + set(BUILD_NUMBER ${RELEASE_NUMBER}) + endif () + if (DEPLOY_PACKAGE) # for deployed packages always grab the serverless content set(DOWNLOAD_SERVERLESS_CONTENT ON) @@ -127,8 +173,8 @@ macro(SET_PACKAGING_PARAMETERS) set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface") set(CONSOLE_SHORTCUT_NAME "Sandbox") else () - set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface - ${BUILD_VERSION}") - set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION}") + set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface - ${BUILD_VERSION_NO_SHA}") + set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION_NO_SHA}") endif () set(INTERFACE_HF_SHORTCUT_NAME "${INTERFACE_SHORTCUT_NAME}") diff --git a/cmake/templates/BuildInfo.h.in b/cmake/templates/BuildInfo.h.in index 904d17293b..9fc9d9be81 100644 --- a/cmake/templates/BuildInfo.h.in +++ b/cmake/templates/BuildInfo.h.in @@ -24,8 +24,26 @@ namespace BuildInfo { const QString MODIFIED_ORGANIZATION = "@BUILD_ORGANIZATION@"; const QString ORGANIZATION_DOMAIN = "highfidelity.io"; const QString VERSION = "@BUILD_VERSION@"; - const QString BUILD_BRANCH = "@BUILD_BRANCH@"; + const QString BUILD_NUMBER = "@BUILD_NUMBER@"; const QString BUILD_GLOBAL_SERVICES = "@BUILD_GLOBAL_SERVICES@"; const QString BUILD_TIME = "@BUILD_TIME@"; -} + enum BuildType { + Dev, + PR, + Master, + Stable + }; + +#if defined(PR_BUILD) + const BuildType BUILD_TYPE = PR; + const QString BUILD_TYPE_STRING = "pr"; +#elif defined(PRODUCTION_BUILD) + const BuildType BUILD_TYPE = @STABLE_BUILD@ ? Stable : Master; + const QString BUILD_TYPE_STRING = @STABLE_BUILD@ ? "stable" : "master"; +#else + const BuildType BUILD_TYPE = Dev; + const QString BUILD_TYPE_STRING = "dev"; +#endif + +} diff --git a/cmake/templates/console-build-info.json.in b/cmake/templates/console-build-info.json.in index c1ef010e08..6b4ee99292 100644 --- a/cmake/templates/console-build-info.json.in +++ b/cmake/templates/console-build-info.json.in @@ -1,4 +1,5 @@ { "releaseType": "@RELEASE_TYPE@", - "buildIdentifier": "@BUILD_VERSION@" + "buildIdentifier": "@BUILD_VERSION@", + "organization": "@BUILD_ORGANIZATION@" } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 42a74aa2fc..e69e241182 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -176,7 +176,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : qDebug() << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); qDebug() << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; qDebug() << "[VERSION] VERSION:" << BuildInfo::VERSION; - qDebug() << "[VERSION] BUILD_BRANCH:" << BuildInfo::BUILD_BRANCH; + qDebug() << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; qDebug() << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; qDebug() << "[VERSION] We will be using this name to find ICE servers:" << _iceServerAddr; @@ -1122,7 +1122,7 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) { } void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) { - const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + + const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + 4; // setup the extended header for the domain list packets @@ -2684,7 +2684,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : ""; QString hexHeaderPassword = headerPassword.isEmpty() ? "" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex(); - + if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) { return true; } 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/src/Application.cpp b/interface/src/Application.cpp index 62b2486555..c5857dac53 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -697,8 +697,8 @@ private: }; /**jsdoc - *

The Controller.Hardware.Application object has properties representing Interface's state. The property - * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or + *

The Controller.Hardware.Application object has properties representing Interface's state. The property + * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). * Each data value is either 1.0 for "true" or 0.0 for "false".

* @@ -780,7 +780,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); - // Ignore any previous crashes if running from command line with a test script. + // Ignore any previous crashes if running from command line with a test script. bool inTestMode { false }; for (int i = 0; i < argc; ++i) { QString parameter(argv[i]); @@ -1108,7 +1108,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; - qCDebug(interfaceapp) << "[VERSION] BUILD_BRANCH:" << BuildInfo::BUILD_BRANCH; + qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; #if USE_STABLE_GLOBAL_SERVICES qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; @@ -1365,11 +1365,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo initializeGL(); qCDebug(interfaceapp, "Initialized GL"); - // Initialize the display plugin architecture + // Initialize the display plugin architecture initializeDisplayPlugins(); qCDebug(interfaceapp, "Initialized Display"); - // Create the rendering engine. This can be slow on some machines due to lots of + // Create the rendering engine. This can be slow on some machines due to lots of // GPU pipeline creation. initializeRenderEngine(); qCDebug(interfaceapp, "Initialized Render Engine."); @@ -1413,7 +1413,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // In practice we shouldn't run across installs that don't have a known installer type. // Client or Client+Server installs should always have the installer.ini next to their // respective interface.exe, and Steam installs will be detected as such. If a user were - // to delete the installer.ini, though, and as an example, we won't know the context of the + // to delete the installer.ini, though, and as an example, we won't know the context of the // original install. constexpr auto INSTALLER_KEY_TYPE = "type"; constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; @@ -1461,6 +1461,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) }, { "installer_campaign", installerCampaign }, { "installer_type", installerType }, + { "build_type", BuildInfo::BUILD_TYPE_STRING }, { "previousSessionCrashed", _previousSessionCrashed }, { "previousSessionRuntime", sessionRunTime.get() }, { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, @@ -2178,7 +2179,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo if (testProperty.isValid()) { auto scriptEngines = DependencyManager::get(); const auto testScript = property(hifi::properties::TEST).toUrl(); - + // Set last parameter to exit interface when the test script finishes, if so requested scriptEngines->loadScript(testScript, false, false, false, false, quitWhenFinished); @@ -2395,7 +2396,7 @@ void Application::onAboutToQuit() { } } - // The active display plugin needs to be loaded before the menu system is active, + // The active display plugin needs to be loaded before the menu system is active, // so its persisted explicitly here Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); @@ -2630,7 +2631,7 @@ void Application::initializeGL() { // Create the GPU backend // Requires the window context, because that's what's used in the actual rendering - // and the GPU backend will make things like the VAO which cannot be shared across + // and the GPU backend will make things like the VAO which cannot be shared across // contexts _glWidget->makeCurrent(); gpu::Context::init(); @@ -2653,7 +2654,7 @@ void Application::initializeDisplayPlugins() { auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); auto defaultDisplayPlugin = displayPlugins.at(0); - // Once time initialization code + // Once time initialization code DisplayPluginPointer targetDisplayPlugin; foreach(auto displayPlugin, displayPlugins) { displayPlugin->setContext(_gpuContext); @@ -2666,7 +2667,7 @@ void Application::initializeDisplayPlugins() { } // The default display plugin needs to be activated first, otherwise the display plugin thread - // may be launched by an external plugin, which is bad + // may be launched by an external plugin, which is bad setDisplayPlugin(defaultDisplayPlugin); // Now set the desired plugin if it's not the same as the default plugin @@ -5787,7 +5788,7 @@ void Application::update(float deltaTime) { viewIsDifferentEnough = true; } - + // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; auto now = SteadyClock::now(); @@ -6155,7 +6156,9 @@ void Application::updateWindowTitle() const { auto nodeList = DependencyManager::get(); auto accountManager = DependencyManager::get(); - QString buildVersion = " (build " + applicationVersion() + ")"; + QString buildVersion = " - " + + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + + " " + applicationVersion(); QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; @@ -7716,7 +7719,7 @@ void Application::sendLambdaEvent(const std::function& f) { } else { LambdaEvent event(f); QCoreApplication::sendEvent(this, &event); - } + } } void Application::initPlugins(const QStringList& arguments) { @@ -7939,7 +7942,7 @@ void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { } // FIXME don't have the application directly set the state of the UI, - // instead emit a signal that the display plugin is changing and let + // instead emit a signal that the display plugin is changing and let // the desktop lock itself. Reduces coupling between the UI and display // plugins auto offscreenUi = DependencyManager::get(); diff --git a/interface/src/Crashpad.cpp b/interface/src/Crashpad.cpp index 45f1d0778f..88651925d5 100644 --- a/interface/src/Crashpad.cpp +++ b/interface/src/Crashpad.cpp @@ -18,6 +18,7 @@ #if HAS_CRASHPAD #include +#include #include #include @@ -69,6 +70,8 @@ bool startCrashHandler() { annotations["token"] = BACKTRACE_TOKEN; annotations["format"] = "minidump"; annotations["version"] = BuildInfo::VERSION.toStdString(); + annotations["build_number"] = BuildInfo::BUILD_NUMBER.toStdString(); + annotations["build_type"] = BuildInfo::BUILD_TYPE_STRING.toStdString(); arguments.push_back("--no-rate-limit"); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 0e5ba0e4e7..6f4300862d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -588,6 +588,10 @@ Menu::Menu() { }); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ToggleHipsFollowing, 0, false, + avatar.get(), SLOT(setToggleHips(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawBaseOfSupport, 0, false, + avatar.get(), SLOT(setEnableDebugDrawBaseOfSupport(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawDefaultPose, 0, false, avatar.get(), SLOT(setEnableDebugDrawDefaultPose(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawAnimPose, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8569911cbd..6fb089acd8 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -30,6 +30,7 @@ namespace MenuOption { const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; const QString AnimDebugDrawAnimPose = "Debug Draw Animation"; + const QString AnimDebugDrawBaseOfSupport = "Debug Draw Base of Support"; const QString AnimDebugDrawDefaultPose = "Debug Draw Default Pose"; const QString AnimDebugDrawPosition= "Debug Draw Position"; const QString AskToResetSettings = "Ask To Reset Settings on Start"; @@ -202,6 +203,7 @@ namespace MenuOption { const QString ThirdPerson = "Third Person"; const QString ThreePointCalibration = "3 Point Calibration"; const QString ThrottleFPSIfNotFocus = "Throttle FPS If Not Focus"; // FIXME - this value duplicated in Basic2DWindowOpenGLDisplayPlugin.cpp + const QString ToggleHipsFollowing = "Toggle Hips Following"; const QString ToolWindow = "Tool Window"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; 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/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f956bc90c3..c816d1d3a1 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -52,6 +52,7 @@ #include "MyHead.h" #include "MySkeletonModel.h" +#include "AnimUtil.h" #include "Application.h" #include "AvatarManager.h" #include "AvatarActionHold.h" @@ -422,12 +423,12 @@ void MyAvatar::update(float deltaTime) { } #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE - glm::vec3 p = transformPoint(getSensorToWorldMatrix(), getControllerPoseInAvatarFrame(controller::Pose::HEAD) * - glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); - DebugDraw::getInstance().addMarker("facing-avg", getOrientation(), p, glm::vec4(1.0f)); - p = transformPoint(getSensorToWorldMatrix(), getHMDSensorPosition() + - glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); - DebugDraw::getInstance().addMarker("facing", getOrientation(), p, glm::vec4(1.0f)); + auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); + glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); + glm::vec3 worldFacingAverage = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); + glm::vec3 worldFacing = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); + DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacing, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); + DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacingAverage, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)); #endif if (_goToPending) { @@ -712,7 +713,8 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { _hmdSensorOrientation = glmExtractRotation(hmdSensorMatrix); auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD); if (headPose.isValid()) { - _headControllerFacing = getFacingDir2D(headPose.rotation); + glm::quat bodyOrientation = computeBodyFacingFromHead(headPose.rotation, Vectors::UNIT_Y); + _headControllerFacing = getFacingDir2D(bodyOrientation); } else { _headControllerFacing = glm::vec2(1.0f, 0.0f); } @@ -1079,6 +1081,22 @@ float loadSetting(Settings& settings, const QString& name, float defaultValue) { return value; } +void MyAvatar::setToggleHips(bool followHead) { + _follow.setToggleHipsFollowing(followHead); +} + +void MyAvatar::FollowHelper::setToggleHipsFollowing(bool followHead) { + _toggleHipsFollowing = followHead; +} + +bool MyAvatar::FollowHelper::getToggleHipsFollowing() const { + return _toggleHipsFollowing; +} + +void MyAvatar::setEnableDebugDrawBaseOfSupport(bool isEnabled) { + _enableDebugDrawBaseOfSupport = isEnabled; +} + void MyAvatar::setEnableDebugDrawDefaultPose(bool isEnabled) { _enableDebugDrawDefaultPose = isEnabled; @@ -1210,6 +1228,8 @@ void MyAvatar::loadData() { settings.endGroup(); setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); + _follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); + setEnableDebugDrawBaseOfSupport(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBaseOfSupport)); setEnableDebugDrawDefaultPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawDefaultPose)); setEnableDebugDrawAnimPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawAnimPose)); setEnableDebugDrawPosition(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawPosition)); @@ -2819,6 +2839,7 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD); if (headPose.isValid()) { headPosition = headPose.translation; + // AJT: TODO: can remove this Y_180 headOrientation = headPose.rotation * Quaternions::Y_180; } const glm::quat headOrientationYawOnly = cancelOutRollAndPitch(headOrientation); @@ -2841,6 +2862,8 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { // eyeToNeck offset is relative full HMD orientation. // while neckToRoot offset is only relative to HMDs yaw. // Y_180 is necessary because rig is z forward and hmdOrientation is -z forward + + // AJT: TODO: can remove this Y_180, if we remove the higher level one. glm::vec3 headToNeck = headOrientation * Quaternions::Y_180 * (localNeck - localHead); glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck; @@ -2850,6 +2873,202 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { return createMatFromQuatAndPos(headOrientationYawOnly, bodyPos); } +// ease in function for dampening cg movement +static float slope(float num) { + const float CURVE_CONSTANT = 1.0f; + float ret = 1.0f; + if (num > 0.0f) { + ret = 1.0f - (1.0f / (1.0f + CURVE_CONSTANT * num)); + } + return ret; +} + +// This function gives a soft clamp at the edge of the base of support +// dampenCgMovement returns the damped cg value in Avatar space. +// cgUnderHeadHandsAvatarSpace is also in Avatar space +// baseOfSupportScale is based on the height of the user +static glm::vec3 dampenCgMovement(glm::vec3 cgUnderHeadHandsAvatarSpace, float baseOfSupportScale) { + float distanceFromCenterZ = cgUnderHeadHandsAvatarSpace.z; + float distanceFromCenterX = cgUnderHeadHandsAvatarSpace.x; + + // In the forward direction we need a different scale because forward is in + // the direction of the hip extensor joint, which means bending usually happens + // well before reaching the edge of the base of support. + const float clampFront = DEFAULT_AVATAR_SUPPORT_BASE_FRONT * DEFAULT_AVATAR_FORWARD_DAMPENING_FACTOR * baseOfSupportScale; + float clampBack = DEFAULT_AVATAR_SUPPORT_BASE_BACK * DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR * baseOfSupportScale; + float clampLeft = DEFAULT_AVATAR_SUPPORT_BASE_LEFT * DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR * baseOfSupportScale; + float clampRight = DEFAULT_AVATAR_SUPPORT_BASE_RIGHT * DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR * baseOfSupportScale; + glm::vec3 dampedCg(0.0f, 0.0f, 0.0f); + + // find the damped z coord of the cg + if (cgUnderHeadHandsAvatarSpace.z < 0.0f) { + // forward displacement + dampedCg.z = slope(fabs(distanceFromCenterZ / clampFront)) * clampFront; + } else { + // backwards displacement + dampedCg.z = slope(fabs(distanceFromCenterZ / clampBack)) * clampBack; + } + + // find the damped x coord of the cg + if (cgUnderHeadHandsAvatarSpace.x > 0.0f) { + // right of center + dampedCg.x = slope(fabs(distanceFromCenterX / clampRight)) * clampRight; + } else { + // left of center + dampedCg.x = slope(fabs(distanceFromCenterX / clampLeft)) * clampLeft; + } + return dampedCg; +} + +// computeCounterBalance returns the center of gravity in Avatar space +glm::vec3 MyAvatar::computeCounterBalance() const { + struct JointMass { + QString name; + float weight; + glm::vec3 position; + JointMass() {}; + JointMass(QString n, float w, glm::vec3 p) { + name = n; + weight = w; + position = p; + } + }; + + // init the body part weights + JointMass cgHeadMass(QString("Head"), DEFAULT_AVATAR_HEAD_MASS, glm::vec3(0.0f, 0.0f, 0.0f)); + JointMass cgLeftHandMass(QString("LeftHand"), DEFAULT_AVATAR_LEFTHAND_MASS, glm::vec3(0.0f, 0.0f, 0.0f)); + JointMass cgRightHandMass(QString("RightHand"), DEFAULT_AVATAR_RIGHTHAND_MASS, glm::vec3(0.0f, 0.0f, 0.0f)); + glm::vec3 tposeHead = DEFAULT_AVATAR_HEAD_POS; + glm::vec3 tposeHips = glm::vec3(0.0f, 0.0f, 0.0f); + + if (_skeletonModel->getRig().indexOfJoint(cgHeadMass.name) != -1) { + cgHeadMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgHeadMass.name)); + tposeHead = getAbsoluteDefaultJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgHeadMass.name)); + } + if (_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name) != -1) { + cgLeftHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name)); + } else { + cgLeftHandMass.position = DEFAULT_AVATAR_LEFTHAND_POS; + } + if (_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name) != -1) { + cgRightHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name)); + } else { + cgRightHandMass.position = DEFAULT_AVATAR_RIGHTHAND_POS; + } + if (_skeletonModel->getRig().indexOfJoint("Hips") != -1) { + tposeHips = getAbsoluteDefaultJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("Hips")); + } + + // find the current center of gravity position based on head and hand moments + glm::vec3 sumOfMoments = (cgHeadMass.weight * cgHeadMass.position) + (cgLeftHandMass.weight * cgLeftHandMass.position) + (cgRightHandMass.weight * cgRightHandMass.position); + float totalMass = cgHeadMass.weight + cgLeftHandMass.weight + cgRightHandMass.weight; + + glm::vec3 currentCg = (1.0f / totalMass) * sumOfMoments; + currentCg.y = 0.0f; + // dampening the center of gravity, in effect, limits the value to the perimeter of the base of support + float baseScale = 1.0f; + if (getUserEyeHeight() > 0.0f) { + baseScale = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT; + } + glm::vec3 desiredCg = dampenCgMovement(currentCg, baseScale); + + // compute hips position to maintain desiredCg + glm::vec3 counterBalancedForHead = (totalMass + DEFAULT_AVATAR_HIPS_MASS) * desiredCg; + counterBalancedForHead -= sumOfMoments; + glm::vec3 counterBalancedCg = (1.0f / DEFAULT_AVATAR_HIPS_MASS) * counterBalancedForHead; + + // find the height of the hips + glm::vec3 xzDiff((cgHeadMass.position.x - counterBalancedCg.x), 0.0f, (cgHeadMass.position.z - counterBalancedCg.z)); + float headMinusHipXz = glm::length(xzDiff); + float headHipDefault = glm::length(tposeHead - tposeHips); + float hipHeight = 0.0f; + if (headHipDefault > headMinusHipXz) { + hipHeight = sqrtf((headHipDefault * headHipDefault) - (headMinusHipXz * headMinusHipXz)); + } + counterBalancedCg.y = (cgHeadMass.position.y - hipHeight); + + // this is to be sure that the feet don't lift off the floor. + // add 5 centimeters to allow for going up on the toes. + if (counterBalancedCg.y > (tposeHips.y + 0.05f)) { + // if the height is higher than default hips, clamp to default hips + counterBalancedCg.y = tposeHips.y + 0.05f; + } + return counterBalancedCg; +} + +// this function matches the hips rotation to the new cghips-head axis +// headOrientation, headPosition and hipsPosition are in avatar space +// returns the matrix of the hips in Avatar space +static glm::mat4 computeNewHipsMatrix(glm::quat headOrientation, glm::vec3 headPosition, glm::vec3 hipsPosition) { + + glm::quat bodyOrientation = computeBodyFacingFromHead(headOrientation, Vectors::UNIT_Y); + + const float MIX_RATIO = 0.3f; + glm::quat hipsRot = safeLerp(Quaternions::IDENTITY, bodyOrientation, MIX_RATIO); + glm::vec3 hipsFacing = hipsRot * Vectors::UNIT_Z; + + glm::vec3 spineVec = headPosition - hipsPosition; + glm::vec3 u, v, w; + generateBasisVectors(glm::normalize(spineVec), hipsFacing, u, v, w); + return glm::mat4(glm::vec4(w, 0.0f), + glm::vec4(u, 0.0f), + glm::vec4(v, 0.0f), + glm::vec4(hipsPosition, 1.0f)); +} + +static void drawBaseOfSupport(float baseOfSupportScale, float footLocal, glm::mat4 avatarToWorld) { + // scale the base of support based on user height + float clampFront = DEFAULT_AVATAR_SUPPORT_BASE_FRONT * baseOfSupportScale; + float clampBack = DEFAULT_AVATAR_SUPPORT_BASE_BACK * baseOfSupportScale; + float clampLeft = DEFAULT_AVATAR_SUPPORT_BASE_LEFT * baseOfSupportScale; + float clampRight = DEFAULT_AVATAR_SUPPORT_BASE_RIGHT * baseOfSupportScale; + float floor = footLocal + 0.05f; + + // transform the base of support corners to world space + glm::vec3 frontRight = transformPoint(avatarToWorld, { clampRight, floor, clampFront }); + glm::vec3 frontLeft = transformPoint(avatarToWorld, { clampLeft, floor, clampFront }); + glm::vec3 backRight = transformPoint(avatarToWorld, { clampRight, floor, clampBack }); + glm::vec3 backLeft = transformPoint(avatarToWorld, { clampLeft, floor, clampBack }); + + // draw the borders + const glm::vec4 rayColor = { 1.0f, 0.0f, 0.0f, 1.0f }; + DebugDraw::getInstance().drawRay(backLeft, frontLeft, rayColor); + DebugDraw::getInstance().drawRay(backLeft, backRight, rayColor); + DebugDraw::getInstance().drawRay(backRight, frontRight, rayColor); + DebugDraw::getInstance().drawRay(frontLeft, frontRight, rayColor); +} + +// this function finds the hips position using a center of gravity model that +// balances the head and hands with the hips over the base of support +// returns the rotation (-z forward) and position of the Avatar in Sensor space +glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { + glm::mat4 sensorToWorldMat = getSensorToWorldMatrix(); + glm::mat4 worldToSensorMat = glm::inverse(sensorToWorldMat); + auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD); + + glm::mat4 sensorHeadMat = createMatFromQuatAndPos(headPose.rotation * Quaternions::Y_180, headPose.translation); + + // convert into avatar space + glm::mat4 avatarToWorldMat = getTransform().getMatrix(); + glm::mat4 avatarHeadMat = glm::inverse(avatarToWorldMat) * sensorToWorldMat * sensorHeadMat; + + if (_enableDebugDrawBaseOfSupport) { + float scaleBaseOfSupport = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT; + glm::vec3 rightFootPositionLocal = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("RightFoot")); + drawBaseOfSupport(scaleBaseOfSupport, rightFootPositionLocal.y, avatarToWorldMat); + } + + // get the new center of gravity + const glm::vec3 cgHipsPosition = computeCounterBalance(); + + // find the new hips rotation using the new head-hips axis as the up axis + glm::mat4 avatarHipsMat = computeNewHipsMatrix(glmExtractRotation(avatarHeadMat), extractTranslation(avatarHeadMat), cgHipsPosition); + + // convert hips from avatar to sensor space + // The Y_180 is to convert from z forward to -z forward. + return worldToSensorMat * avatarToWorldMat * avatarHipsMat; +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3014,9 +3233,7 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); - return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; - } bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { @@ -3087,11 +3304,19 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat AnimPose followWorldPose(currentWorldMatrix); + glm::quat currentHipsLocal = myAvatar.getAbsoluteJointRotationInObjectFrame(myAvatar.getJointIndex("Hips")); + const glm::quat hipsinWorldSpace = followWorldPose.rot() * (Quaternions::Y_180 * (currentHipsLocal)); + const glm::vec3 avatarUpWorld = glm::normalize(followWorldPose.rot()*(Vectors::UP)); + glm::quat resultingSwingInWorld; + glm::quat resultingTwistInWorld; + swingTwistDecomposition(hipsinWorldSpace, avatarUpWorld, resultingSwingInWorld, resultingTwistInWorld); + // remove scale present from sensorToWorldMatrix followWorldPose.scale() = glm::vec3(1.0f); if (isActive(Rotation)) { - followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); + //use the hmd reading for the hips follow + followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); } if (isActive(Horizontal)) { glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix); @@ -3487,6 +3712,10 @@ void MyAvatar::updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& } } +bool MyAvatar::isRecenteringHorizontally() const { + return _follow.isActive(FollowHelper::Horizontal); +} + const MyHead* MyAvatar::getMyHead() const { return static_cast(getHead()); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7afb234da4..b3ed04e8de 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -105,6 +105,9 @@ class MyAvatar : public Avatar { * by 30cm. Read-only. * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, with the position * by 30cm. Read-only. + * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to the center of + * gravity model that balance the center of gravity over the base of support of the feet. Setting the value false + * will result in the default behaviour where the hips are placed under the head. * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. @@ -199,6 +202,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) Q_PROPERTY(bool isAway READ getIsAway WRITE setAway) + Q_PROPERTY(bool centerOfGravityModelEnabled READ getCenterOfGravityModelEnabled WRITE setCenterOfGravityModelEnabled) Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) @@ -480,7 +484,16 @@ public: */ Q_INVOKABLE QString getDominantHand() const { return _dominantHand; } - + /**jsdoc + * @function MyAvatar.setCenterOfGravityModelEnabled + * @param {boolean} enabled + */ + Q_INVOKABLE void setCenterOfGravityModelEnabled(bool value) { _centerOfGravityModelEnabled = value; } + /**jsdoc + * @function MyAvatar.getCenterOfGravityModelEnabled + * @returns {boolean} + */ + Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } /**jsdoc * @function MyAvatar.setHMDLeanRecenterEnabled * @param {boolean} enabled @@ -564,6 +577,13 @@ public: */ Q_INVOKABLE void triggerRotationRecenter(); + /**jsdoc + *The isRecenteringHorizontally function returns true if MyAvatar + *is translating the root of the Avatar to keep the center of gravity under the head. + *isActive(Horizontal) is returned. + *@function MyAvatar.isRecenteringHorizontally + */ + Q_INVOKABLE bool isRecenteringHorizontally() const; eyeContactTarget getEyeContactTarget(); @@ -956,10 +976,18 @@ public: void removeHoldAction(AvatarActionHold* holdAction); // thread-safe void updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose); + // derive avatar body position and orientation from the current HMD Sensor location. - // results are in HMD frame + // results are in sensor frame (-z forward) glm::mat4 deriveBodyFromHMDSensor() const; + glm::vec3 computeCounterBalance() const; + + // derive avatar body position and orientation from using the current HMD Sensor location in relation to the previous + // location of the base of support of the avatar. + // results are in sensor frame (-z foward) + glm::mat4 deriveBodyUsingCgModel() const; + /**jsdoc * @function MyAvatar.isUp * @param {Vec3} direction @@ -1107,7 +1135,16 @@ public slots: */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); - + /**jsdoc + * @function MyAvatar.setToggleHips + * @param {boolean} enabled + */ + void setToggleHips(bool followHead); + /**jsdoc + * @function MyAvatar.setEnableDebugDrawBaseOfSupport + * @param {boolean} enabled + */ + void setEnableDebugDrawBaseOfSupport(bool isEnabled); /**jsdoc * @function MyAvatar.setEnableDebugDrawDefaultPose * @param {boolean} enabled @@ -1458,8 +1495,8 @@ private: glm::quat _hmdSensorOrientation; glm::vec3 _hmdSensorPosition; // cache head controller pose in sensor space - glm::vec2 _headControllerFacing; // facing vector in xz plane - glm::vec2 _headControllerFacingMovingAverage { 0, 0 }; // facing vector in xz plane + glm::vec2 _headControllerFacing; // facing vector in xz plane (sensor space) + glm::vec2 _headControllerFacingMovingAverage { 0.0f, 0.0f }; // facing vector in xz plane (sensor space) // cache of the current body position and orientation of the avatar's body, // in sensor space. @@ -1495,9 +1532,12 @@ private: void setForceActivateVertical(bool val); bool getForceActivateHorizontal() const; void setForceActivateHorizontal(bool val); - std::atomic _forceActivateRotation{ false }; - std::atomic _forceActivateVertical{ false }; - std::atomic _forceActivateHorizontal{ false }; + bool getToggleHipsFollowing() const; + void setToggleHipsFollowing(bool followHead); + std::atomic _forceActivateRotation { false }; + std::atomic _forceActivateVertical { false }; + std::atomic _forceActivateHorizontal { false }; + std::atomic _toggleHipsFollowing { true }; }; FollowHelper _follow; @@ -1510,6 +1550,7 @@ private: bool _prevShouldDrawHead; bool _rigEnabled { true }; + bool _enableDebugDrawBaseOfSupport { false }; bool _enableDebugDrawDefaultPose { false }; bool _enableDebugDrawAnimPose { false }; bool _enableDebugDrawHandControllers { false }; @@ -1532,6 +1573,7 @@ private: std::map _controllerPoseMap; mutable std::mutex _controllerPoseMapMutex; + bool _centerOfGravityModelEnabled { true }; bool _hmdLeanRecenterEnabled { true }; bool _sprint { false }; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index f317f6b2c1..c15b00ca19 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -45,7 +45,14 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { return result; } - glm::mat4 hipsMat = myAvatar->deriveBodyFromHMDSensor(); + glm::mat4 hipsMat; + if (myAvatar->getCenterOfGravityModelEnabled()) { + // then we use center of gravity model + hipsMat = myAvatar->deriveBodyUsingCgModel(); + } else { + // otherwise use the default of putting the hips under the head + hipsMat = myAvatar->deriveBodyFromHMDSensor(); + } glm::vec3 hipsPos = extractTranslation(hipsMat); glm::quat hipsRot = glmExtractRotation(hipsMat); @@ -53,8 +60,11 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { glm::mat4 avatarToSensorMat = worldToSensorMat * avatarToWorldMat; // dampen hips rotation, by mixing it with the avatar orientation in sensor space - const float MIX_RATIO = 0.5f; - hipsRot = safeLerp(glmExtractRotation(avatarToSensorMat), hipsRot, MIX_RATIO); + // turning this off for center of gravity model because it is already mixed in there + if (!(myAvatar->getCenterOfGravityModelEnabled())) { + const float MIX_RATIO = 0.5f; + hipsRot = safeLerp(glmExtractRotation(avatarToSensorMat), hipsRot, MIX_RATIO); + } if (isFlying) { // rotate the hips back to match the flying animation. @@ -73,6 +83,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { hipsPos = headPos + tiltRot * (hipsPos - headPos); } + // AJT: TODO can we remove this? return AnimPose(hipsRot * Quaternions::Y_180, hipsPos); } @@ -170,6 +181,15 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } } + bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); + if (isFlying != _prevIsFlying) { + const float FLY_TO_IDLE_HIPS_TRANSITION_TIME = 0.5f; + _flyIdleTimer = FLY_TO_IDLE_HIPS_TRANSITION_TIME; + } else { + _flyIdleTimer -= deltaTime; + } + _prevIsFlying = isFlying; + // if hips are not under direct control, estimate the hips position. if (avatarHeadPose.isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] & (uint8_t)Rig::ControllerFlags::Enabled)) { bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); @@ -181,14 +201,28 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying); + // timescale in seconds + const float TRANS_HORIZ_TIMESCALE = 0.15f; + const float TRANS_VERT_TIMESCALE = 0.01f; // We want the vertical component of the hips to follow quickly to prevent spine squash/stretch. + const float ROT_TIMESCALE = 0.15f; + const float FLY_IDLE_TRANSITION_TIMESCALE = 0.25f; + + float transHorizAlpha, transVertAlpha, rotAlpha; + if (_flyIdleTimer < 0.0f) { + transHorizAlpha = glm::min(deltaTime / TRANS_HORIZ_TIMESCALE, 1.0f); + transVertAlpha = glm::min(deltaTime / TRANS_VERT_TIMESCALE, 1.0f); + rotAlpha = glm::min(deltaTime / ROT_TIMESCALE, 1.0f); + } else { + transHorizAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); + transVertAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); + rotAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); + } + // smootly lerp hips, in sensorframe, with different coeff for horiz and vertical translation. - const float ROT_ALPHA = 0.9f; - const float TRANS_HORIZ_ALPHA = 0.9f; - const float TRANS_VERT_ALPHA = 0.1f; float hipsY = hips.trans().y; - hips.trans() = lerp(hips.trans(), _prevHips.trans(), TRANS_HORIZ_ALPHA); - hips.trans().y = lerp(hipsY, _prevHips.trans().y, TRANS_VERT_ALPHA); - hips.rot() = safeLerp(hips.rot(), _prevHips.rot(), ROT_ALPHA); + hips.trans() = lerp(_prevHips.trans(), hips.trans(), transHorizAlpha); + hips.trans().y = lerp(_prevHips.trans().y, hipsY, transVertAlpha); + hips.rot() = safeLerp(_prevHips.rot(), hips.rot(), rotAlpha); _prevHips = hips; _prevHipsValid = true; diff --git a/interface/src/avatar/MySkeletonModel.h b/interface/src/avatar/MySkeletonModel.h index 252b6c293b..ebef9796a4 100644 --- a/interface/src/avatar/MySkeletonModel.h +++ b/interface/src/avatar/MySkeletonModel.h @@ -28,6 +28,8 @@ private: AnimPose _prevHips; // sensor frame bool _prevHipsValid { false }; + bool _prevIsFlying { false }; + float _flyIdleTimer { 0.0f }; std::map _jointRotationFrameOffsetMap; }; 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..834de2150d 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); } } 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/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index f7e749317d..e003ae88a0 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -536,7 +536,6 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() { // be sure to add the public key so we don't do this over and over _publicKeys.push_back(publicKey.toBase64()); - DependencyManager::get()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY); return true; } } @@ -615,7 +614,11 @@ void Wallet::updateImageProvider() { SecurityImageProvider* securityImageProvider; // inform offscreenUI security image provider - QQmlEngine* engine = DependencyManager::get()->getSurfaceContext()->engine(); + auto offscreenUI = DependencyManager::get(); + if (!offscreenUI) { + return; + } + QQmlEngine* engine = offscreenUI->getSurfaceContext()->engine(); securityImageProvider = reinterpret_cast(engine->imageProvider(SecurityImageProvider::PROVIDER_NAME)); securityImageProvider->setSecurityImage(_securityImage); diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index fbb5aae84c..c8056c11c8 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/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index 65c605b5ba..acb90126fc 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -9,7 +9,9 @@ // #include "AnimUtil.h" -#include "GLMHelpers.h" +#include +#include +#include // TODO: use restrict keyword // TODO: excellent candidate for simd vectorization. @@ -107,3 +109,44 @@ AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone) { glm::vec4(bone.trans(), 1.0f)); return AnimPose(lookAt); } + +// This will attempt to determine the proper body facing of a characters body +// assumes headRot is z-forward and y-up. +// and returns a bodyRot that is also z-forward and y-up +glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& up) { + + glm::vec3 bodyUp = glm::normalize(up); + + // initially take the body facing from the head. + glm::vec3 headUp = headRot * Vectors::UNIT_Y; + glm::vec3 headForward = headRot * Vectors::UNIT_Z; + glm::vec3 headLeft = headRot * Vectors::UNIT_X; + const float NOD_THRESHOLD = cosf(glm::radians(45.0f)); + const float TILT_THRESHOLD = cosf(glm::radians(30.0f)); + + glm::vec3 bodyForward = headForward; + + float nodDot = glm::dot(headForward, bodyUp); + float tiltDot = glm::dot(headLeft, bodyUp); + + if (fabsf(tiltDot) < TILT_THRESHOLD) { // if we are not tilting too much + if (nodDot < -NOD_THRESHOLD) { // head is looking downward + // the body should face in the same direction as the top the head. + bodyForward = headUp; + } else if (nodDot > NOD_THRESHOLD) { // head is looking upward + // the body should face away from the top of the head. + bodyForward = -headUp; + } + } + + // cancel out upward component + bodyForward = glm::normalize(bodyForward - nodDot * bodyUp); + + glm::vec3 u, v, w; + generateBasisVectors(bodyForward, bodyUp, u, v, w); + + // create matrix from orthogonal basis vectors + glm::mat4 bodyMat(glm::vec4(w, 0.0f), glm::vec4(v, 0.0f), glm::vec4(u, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + + return glmExtractRotation(bodyMat); +} diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index f2cceb361b..3cd7f4b6fb 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -33,4 +33,9 @@ inline glm::quat safeLerp(const glm::quat& a, const glm::quat& b, float alpha) { AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone); +// This will attempt to determine the proper body facing of a characters body +// assumes headRot is z-forward and y-up. +// and returns a bodyRot that is also z-forward and y-up +glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& up); + #endif diff --git a/libraries/auto-updater/src/AutoUpdater.cpp b/libraries/auto-updater/src/AutoUpdater.cpp index e58ac067a6..6749cd9e10 100644 --- a/libraries/auto-updater/src/AutoUpdater.cpp +++ b/libraries/auto-updater/src/AutoUpdater.cpp @@ -11,6 +11,8 @@ #include "AutoUpdater.h" +#include + #include #include #include @@ -157,10 +159,8 @@ void AutoUpdater::parseLatestVersionData() { } void AutoUpdater::checkVersionAndNotify() { - if (QCoreApplication::applicationVersion() == "dev" || - QCoreApplication::applicationVersion().contains("PR") || - _builds.empty()) { - // No version checking is required in dev builds or when no build + if (BuildInfo::BUILD_TYPE != BuildInfo::BuildType::Stable || _builds.empty()) { + // No version checking is required in nightly/PR/dev builds or when no build // data was found for the platform return; } @@ -196,4 +196,4 @@ void AutoUpdater::appendBuildData(int versionNumber, thisBuildDetails.insert("pullRequestNumber", pullRequestNumber); _builds.insert(versionNumber, thisBuildDetails); -} \ No newline at end of file +} 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/render-utils/src/RenderViewTask.cpp b/libraries/render-utils/src/RenderViewTask.cpp index 122fc16e61..82426a3a1f 100644 --- a/libraries/render-utils/src/RenderViewTask.cpp +++ b/libraries/render-utils/src/RenderViewTask.cpp @@ -19,7 +19,9 @@ void RenderViewTask::build(JobModel& task, const render::Varying& input, render: // Warning : the cull functor passed to the shadow pass should only be testing for LOD culling. If frustum culling // is performed, then casters not in the view frustum will be removed, which is not what we wish. - task.addJob("RenderShadowTask", cullFunctor, tagBits, tagMask); + if (isDeferred) { + task.addJob("RenderShadowTask", cullFunctor, tagBits, tagMask); + } const auto items = task.addJob("FetchCullSort", cullFunctor, tagBits, tagMask); assert(items.canCast()); diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h index 930da6a494..e90e25d5b0 100644 --- a/libraries/shared/src/AvatarConstants.h +++ b/libraries/shared/src/AvatarConstants.h @@ -20,6 +20,16 @@ const float DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD = 0.11f; // meters const float DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD = 0.185f; // meters const float DEFAULT_AVATAR_NECK_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD; const float DEFAULT_AVATAR_EYE_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD; +const float DEFAULT_AVATAR_SUPPORT_BASE_LEFT = -0.25f; +const float DEFAULT_AVATAR_SUPPORT_BASE_RIGHT = 0.25f; +const float DEFAULT_AVATAR_SUPPORT_BASE_FRONT = -0.20f; +const float DEFAULT_AVATAR_SUPPORT_BASE_BACK = 0.10f; +const float DEFAULT_AVATAR_FORWARD_DAMPENING_FACTOR = 0.5f; +const float DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR = 2.0f; +const float DEFAULT_AVATAR_HIPS_MASS = 40.0f; +const float DEFAULT_AVATAR_HEAD_MASS = 20.0f; +const float DEFAULT_AVATAR_LEFTHAND_MASS = 2.0f; +const float DEFAULT_AVATAR_RIGHTHAND_MASS = 2.0f; // Used when avatar is missing joints... (avatar space) const glm::quat DEFAULT_AVATAR_MIDDLE_EYE_ROT { Quaternions::Y_180 }; diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 75446754d5..4be8ad0e41 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -574,8 +574,9 @@ void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& seconda vAxisOut = glm::cross(wAxisOut, uAxisOut); } +// assumes z-forward and y-up glm::vec2 getFacingDir2D(const glm::quat& rot) { - glm::vec3 facing3D = rot * Vectors::UNIT_NEG_Z; + glm::vec3 facing3D = rot * Vectors::UNIT_Z; glm::vec2 facing2D(facing3D.x, facing3D.z); const float ALMOST_ZERO = 0.0001f; if (glm::length(facing2D) < ALMOST_ZERO) { @@ -585,8 +586,9 @@ glm::vec2 getFacingDir2D(const glm::quat& rot) { } } +// assumes z-forward and y-up glm::vec2 getFacingDir2D(const glm::mat4& m) { - glm::vec3 facing3D = transformVectorFast(m, Vectors::UNIT_NEG_Z); + glm::vec3 facing3D = transformVectorFast(m, Vectors::UNIT_Z); glm::vec2 facing2D(facing3D.x, facing3D.z); const float ALMOST_ZERO = 0.0001f; if (glm::length(facing2D) < ALMOST_ZERO) { diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 0e1af27cd2..7e6ef4cb28 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -250,7 +250,10 @@ glm::vec3 transformVectorFull(const glm::mat4& m, const glm::vec3& v); void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& secondaryAxis, glm::vec3& uAxisOut, glm::vec3& vAxisOut, glm::vec3& wAxisOut); +// assumes z-forward and y-up glm::vec2 getFacingDir2D(const glm::quat& rot); + +// assumes z-forward and y-up glm::vec2 getFacingDir2D(const glm::mat4& m); inline bool isNaN(const glm::vec3& value) { return isNaN(value.x) || isNaN(value.y) || isNaN(value.z); } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 045dff1295..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", @@ -32,7 +33,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/emote.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ - "system/controllers/controllerScripts.js" + "system/controllers/controllerScripts.js", //"system/chat.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/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 dc4d5aa844..208e64fd5e 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -988,6 +988,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 b08db6222f..8a92fc8a5d 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -76,10 +76,7 @@ function getBuildInfo() { const buildInfo = getBuildInfo(); function getRootHifiDataDirectory() { - var organization = "High Fidelity"; - if (buildInfo.releaseType != "PRODUCTION") { - organization += ' - ' + buildInfo.buildIdentifier; - } + var organization = buildInfo.organization; if (osType == 'Windows_NT') { return path.resolve(osHomeDir(), 'AppData/Roaming', organization); } else if (osType == 'Darwin') {