From ed811d0431405bf960d6a1bb91813f731e72ff27 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Jul 2016 15:01:21 -0700 Subject: [PATCH 1/7] replace Row with ListView, but otherwise same user-functionality --- interface/resources/qml/AddressBarDialog.qml | 58 ++++++++++---------- interface/resources/qml/hifi/Card.qml | 11 ++-- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 792410c59d..df34295a0f 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -46,7 +46,7 @@ Window { } function goCard(card) { - addressLine.text = card.userStory.name; + addressLine.text = card.path; toggleOrGo(true); } property var allDomains: []; @@ -60,32 +60,27 @@ Window { implicitWidth: backgroundImage.width implicitHeight: backgroundImage.height - Row { - width: backgroundImage.width; + ListModel { id: suggestions } + + ListView { + width: (3 * cardWidth) + (2 * hifi.layout.spacing); + height: cardHeight; + spacing: hifi.layout.spacing; anchors { bottom: backgroundImage.top; bottomMargin: 2 * hifi.layout.spacing; right: backgroundImage.right; - rightMargin: -104; // FIXME } - spacing: hifi.layout.spacing; - Card { - id: s0; + model: suggestions; + orientation: ListView.Horizontal; + delegate: Card { width: cardWidth; height: cardHeight; - goFunction: goCard - } - Card { - id: s1; - width: cardWidth; - height: cardHeight; - goFunction: goCard - } - Card { - id: s2; - width: cardWidth; - height: cardHeight; - goFunction: goCard + goFunction: goCard; + path: model.name; + thumbnail: model.thumbnail; + placeText: model.name; + usersText: model.online_users + ((model.online_users === 1) ? ' user' : ' users') } } @@ -225,6 +220,7 @@ Window { function addPictureToDomain(domainInfo, cb) { // asynchronously add thumbnail and lobby to domainInfo, if available, and cb(error) // This requests data for all the names at once, and just uses the first one to come back. // We might change this to check one at a time, which would be less requests and more latency. + domainInfo.thumbnail = ''; // Regardless of whether we fill it in later, qt models must start with the all values they will have. asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(identity), function (name, icb) { var url = "https://metaverse.highfidelity.com/api/v1/places/" + name; getRequest(url, function (error, json) { @@ -280,17 +276,19 @@ Window { } function filterChoicesByText() { - function fill1(target, data) { + function fill1(targetIndex, data) { if (!data) { - target.visible = false; + if (targetIndex < suggestions.count) { + suggestions.remove(targetIndex); + } return; } console.log('suggestion:', JSON.stringify(data)); - target.userStory = data; - target.image.source = data.lobby || target.defaultPicture; - target.placeText = data.name; - target.usersText = data.online_users + ((data.online_users === 1) ? ' user' : ' users'); - target.visible = true; + if (suggestions.count <= targetIndex) { + suggestions.append(data); + } else { + suggestions.set(targetIndex, data); + } } var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity); var filtered = !words.length ? suggestionChoices : allDomains.filter(function (domain) { @@ -303,9 +301,9 @@ Window { return text.indexOf(word) >= 0; }); }); - fill1(s0, filtered[0]); - fill1(s1, filtered[1]); - fill1(s2, filtered[2]); + fill1(0, filtered[0]); + fill1(1, filtered[1]); + fill1(2, filtered[2]); } function fillDestinations() { diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 7758c5800a..4996bc9789 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -18,27 +18,28 @@ import "../styles-uit" Rectangle { property var goFunction: null; - property var userStory: null; property alias image: lobby; property alias placeText: place.text; property alias usersText: users.text; property int textPadding: 20; property int textSize: 24; - property string defaultPicture: "../../images/default-domain.gif"; + property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); + property string thumbnail: defaultThumbnail; + property string path: ""; HifiConstants { id: hifi } Image { id: lobby; width: parent.width; height: parent.height; - source: defaultPicture; + source: thumbnail || defaultThumbnail; fillMode: Image.PreserveAspectCrop; // source gets filled in later anchors.verticalCenter: parent.verticalCenter; anchors.left: parent.left; onStatusChanged: { if (status == Image.Error) { - console.log("source: " + source + ": failed to load " + JSON.stringify(userStory)); - source = defaultPicture; + console.log("source: " + source + ": failed to load " + path); + source = defaultThumbnail; } } } From ff17761769cd688a7fc01a935e620f39d4d2be2e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Jul 2016 15:46:32 -0700 Subject: [PATCH 2/7] basic minority-report scroll (fixed initial data, but infinite cards) --- interface/resources/qml/AddressBarDialog.qml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index df34295a0f..6d60ba7749 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -66,6 +66,7 @@ Window { width: (3 * cardWidth) + (2 * hifi.layout.spacing); height: cardHeight; spacing: hifi.layout.spacing; + clip: true; anchors { bottom: backgroundImage.top; bottomMargin: 2 * hifi.layout.spacing; @@ -245,7 +246,7 @@ Window { if (!domainsBaseUrl) { var domainsOptions = [ 'open', // published hours handle now - 'active', // has at least one person connected. FIXME: really want any place that is verified accessible. + // fixme hrs restore 'active', // has at least one person connected. FIXME: really want any place that is verified accessible. // FIXME: really want places I'm allowed in, not just open ones. 'restriction=open', // Not by whitelist, etc. FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. // FIXME add maturity @@ -276,7 +277,8 @@ Window { } function filterChoicesByText() { - function fill1(targetIndex, data) { + function fill1(targetIndex) { + var data = filtered[targetIndex]; if (!data) { if (targetIndex < suggestions.count) { suggestions.remove(targetIndex); @@ -301,9 +303,7 @@ Window { return text.indexOf(word) >= 0; }); }); - fill1(0, filtered[0]); - fill1(1, filtered[1]); - fill1(2, filtered[2]); + for (var index in filtered) { fill1(index); } } function fillDestinations() { @@ -316,13 +316,13 @@ Window { var here = AddressManager.hostname; // don't show where we are now. allDomains = domains.filter(function (domain) { return domain.name !== here; }); // Whittle down suggestions to those that have at least one user, and try to get pictures. - suggestionChoices = allDomains.filter(function (domain) { return domain.online_users; }); + suggestionChoices = allDomains.filter(function (domain) { return true/*fixme hrs restore domain.online_users*/; }); asyncEach(domains, addPictureToDomain, function (error) { if (error) { console.log('place picture query failed:', error); } // Whittle down more by requiring a picture. - suggestionChoices = suggestionChoices.filter(function (domain) { return domain.lobby; }); + // fixme hrs restore suggestionChoices = suggestionChoices.filter(function (domain) { return domain.lobby; }); filterChoicesByText(); }); }); From ae9421f1f69652ee590fbdfd7d92fd587515f8bf Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 29 Jul 2016 15:22:08 -0700 Subject: [PATCH 3/7] smaller user label, and use the word "people" --- interface/resources/qml/AddressBarDialog.qml | 2 +- interface/resources/qml/hifi/Card.qml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 6d60ba7749..ef8f297a75 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -81,7 +81,7 @@ Window { path: model.name; thumbnail: model.thumbnail; placeText: model.name; - usersText: model.online_users + ((model.online_users === 1) ? ' user' : ' users') + usersText: model.online_users + ((model.online_users === 1) ? ' person' : ' people'); } } diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 4996bc9789..ba26c51bf8 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -23,6 +23,7 @@ Rectangle { property alias usersText: users.text; property int textPadding: 20; property int textSize: 24; + property int textSizeSmall: 18; property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); property string thumbnail: defaultThumbnail; property string path: ""; @@ -80,7 +81,7 @@ Rectangle { } RalewayRegular { id: users; - size: textSize; + size: textSizeSmall; color: hifi.colors.white; anchors { bottom: parent.bottom; From 611223f0134c14f8d75ed8047866db35130e98be Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 29 Jul 2016 15:49:20 -0700 Subject: [PATCH 4/7] highlight to show that the cards can be clicked --- interface/resources/qml/AddressBarDialog.qml | 3 +++ interface/resources/qml/hifi/Card.qml | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index ef8f297a75..6da55c59d4 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -82,7 +82,10 @@ Window { thumbnail: model.thumbnail; placeText: model.name; usersText: model.online_users + ((model.online_users === 1) ? ' person' : ' people'); + hoverThunk: function () { ListView.view.currentIndex = index; } + unhoverThunk: function () { ListView.view.currentIndex = -1; } } + highlight: Rectangle { color: "transparent"; border.width: 2; border.color: "#1FA5E8"; z: 1; } } Image { diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index ba26c51bf8..5fae628283 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -89,10 +89,15 @@ Rectangle { margins: textPadding; } } + property var hoverThunk: function () { }; + property var unhoverThunk: function () { }; MouseArea { + id: zmouseArea; anchors.fill: parent; acceptedButtons: Qt.LeftButton; onClicked: goFunction(parent); hoverEnabled: true; + onEntered: hoverThunk(); + onExited: unhoverThunk(); } } From 7cf41d441ecbfa76210cde844fa584c223c5d713 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 1 Aug 2016 12:28:57 -0700 Subject: [PATCH 5/7] filtering by place --- interface/resources/qml/AddressBarDialog.qml | 237 ++++++++++--------- 1 file changed, 128 insertions(+), 109 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 6da55c59d4..b80ab579c8 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -49,9 +49,7 @@ Window { addressLine.text = card.path; toggleOrGo(true); } - property var allDomains: []; - property var suggestionChoices: []; - property var domainsBaseUrl: null; + property var allPlaces: []; property int cardWidth: 200; property int cardHeight: 152; @@ -78,7 +76,7 @@ Window { width: cardWidth; height: cardHeight; goFunction: goCard; - path: model.name; + path: model.name + model.path; thumbnail: model.thumbnail; placeText: model.name; usersText: model.online_users + ((model.online_users === 1) ? ' person' : ' people'); @@ -199,135 +197,156 @@ Window { request.open("GET", url, true); request.send(); } - // call iterator(element, icb) once for each element of array, and then cb(error) when icb(error) has been called by each iterator. - // short-circuits if error. Note that iterator MUST be an asynchronous function. (Use setTimeout if necessary.) - function asyncEach(array, iterator, cb) { - var count = array.length; - function icb(error) { - if (!--count || error) { - count = -1; // don't cb multiple times (e.g., if error) - cb(error); - } - } + function asyncMap(array, iterator, cb) { + // call iterator(element, icb) once for each element of array, and then cb(error, mappedResult) + // when icb(error, mappedElement) has been called by each iterator. + // Calls to iterator are overlapped and map call icb in any order, but the mappedResults are collected in the same + // order as the elements of the array. + // short-circuits if error. Note that iterator MUST be an asynchronous function. (Use setTimeout if necessary.) + var count = array.length, results = []; if (!count) { - return cb(); + return cb(null, results); } - array.forEach(function (element) { - iterator(element, icb); + array.forEach(function (element, index) { + if (count < 0) { // don't keep iterating after we short-circuit + return; + } + iterator(element, function (error, mapped) { + results[index] = mapped; + if (error || !--count) { + count = 1; // don't cb multiple times if error + cb(error, results); + } + }); }); } + // Example: + /*asyncMap([0, 1, 2, 3, 4, 5, 6], function (elt, icb) { + console.log('called', elt); + setTimeout(function () { + console.log('answering', elt); + icb(null, elt); + }, Math.random() * 1000); + }, console.log); */ function identity(x) { return x; } - function addPictureToDomain(domainInfo, cb) { // asynchronously add thumbnail and lobby to domainInfo, if available, and cb(error) - // This requests data for all the names at once, and just uses the first one to come back. - // We might change this to check one at a time, which would be less requests and more latency. - domainInfo.thumbnail = ''; // Regardless of whether we fill it in later, qt models must start with the all values they will have. - asyncEach([domainInfo.name].concat(domainInfo.names || null).filter(identity), function (name, icb) { - var url = "https://metaverse.highfidelity.com/api/v1/places/" + name; - getRequest(url, function (error, json) { - var previews = !error && json.data.place.previews; - if (previews) { - if (!domainInfo.thumbnail) { // just grab the first one - domainInfo.thumbnail = previews.thumbnail; - } - if (!domainInfo.lobby) { - domainInfo.lobby = previews.lobby; - } - } - icb(error); - }); - }, cb); + function handleError(error, data, cb) { // cb(error) and answer truthy if needed, else falsey + if (!error && (data.status === 'success')) { + return; + } + cb(error || new Error(data.status + ': ' + data.error)); + return true; } - function getDomains(options, cb) { // cb(error, arrayOfData) - if (!options.page) { - options.page = 1; - } - if (!domainsBaseUrl) { - var domainsOptions = [ - 'open', // published hours handle now - // fixme hrs restore 'active', // has at least one person connected. FIXME: really want any place that is verified accessible. - // FIXME: really want places I'm allowed in, not just open ones. - 'restriction=open', // Not by whitelist, etc. FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. - // FIXME add maturity - 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), - 'sort_by=users', - 'sort_order=desc', - ]; - domainsBaseUrl = "https://metaverse.highfidelity.com/api/v1/domains/all?" + domainsOptions.join('&'); - } - var url = domainsBaseUrl + "&page=" + options.page + "&users=" + options.minUsers + "-" + options.maxUsers; - getRequest(url, function (error, json) { - if (!error && (json.status !== 'success')) { - error = new Error("Bad response: " + JSON.stringify(json)); - } - if (error) { - error.message += ' for ' + url; - return cb(error); - } - var domains = json.data.domains; - if (json.current_page < json.total_pages) { - options.page++; - return getDomains(options, function (error, others) { - cb(error, domains.concat(others)); - }); - } - cb(null, domains); - }); - } - - function filterChoicesByText() { - function fill1(targetIndex) { - var data = filtered[targetIndex]; - if (!data) { - if (targetIndex < suggestions.count) { - suggestions.remove(targetIndex); - } + function getPlace(placeData, cb) { // cb(error, side-effected-placeData), after adding path, thumbnails, and description + getRequest('https://metaverse.highfidelity.com/api/v1/places/' + placeData.name, function (error, data) { + if (handleError(error, data, cb)) { return; } - console.log('suggestion:', JSON.stringify(data)); - if (suggestions.count <= targetIndex) { - suggestions.append(data); - } else { - suggestions.set(targetIndex, data); + var place = data.data.place, previews = place.previews; + placeData.path = place.path; + if (previews && previews.thumbnail) { + placeData.thumbnail = previews.thumbnail; } + if (place.description) { + placeData.description = place.description; + placeData.searchText += ' ' + place.description.toUpperCase(); + } + cb(error, placeData); + }); + } + function mapDomainPlaces(domain, cb) { // cb(error, arrayOfDomainPlaceData) + function addPlace(name, icb) { + getPlace({ + name: name, + tags: domain.tags, + thumbnail: "", + description: "", + path: "", + searchText: [name].concat(domain.tags).join(' ').toUpperCase(), + online_users: domain.online_users + }, icb); } - var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity); - var filtered = !words.length ? suggestionChoices : allDomains.filter(function (domain) { - var text = domain.names.concat(domain.tags).join(' '); - if (domain.description) { - text += domain.description; + // IWBNI we could get these results in order with most-recent-entered first. + // In any case, we don't really need to preserve the domain.names order in the results. + asyncMap(domain.names, addPlace, cb); + } + + function suggestable(place) { + return (place.name !== AddressManager.hostname) // Not our entry, but do show other entry points to current domain. + && place.thumbnail + && place.online_users; // at least one present means it's actually online + } + function getDomainPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model + // Each page of results is processed completely before we start on the next page. + // For each page of domains, we process each domain in parallel, and for each domain, process each place name in parallel. + // This gives us minimum latency within the page, but we do preserve the order within the page by using asyncMap and + // only appending the collected results. + var params = [ + 'open', // published hours handle now + // FIXME: should determine if place is actually running + 'restriction=open', // Not by whitelist, etc. FIXME: If logged in, add hifi to the restriction options, in order to include places that require login. + // FIXME add maturity + 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), + 'sort_by=users', + 'sort_order=desc', + 'page=' + pageNumber + ]; + getRequest('https://metaverse.highfidelity.com/api/v1/domains/all?' + params.join('&'), function (error, data) { + if (handleError(error, data, cb)) { + return; } - text = text.toUpperCase(); - return words.every(function (word) { - return text.indexOf(word) >= 0; + asyncMap(data.data.domains, mapDomainPlaces, function (error, pageResults) { + if (error) { + return cb(error); + } + // pageResults is now [ [ placeDataOneForDomainOne, placeDataTwoForDomainOne, ...], [ placeDataTwoForDomainTwo...] ] + pageResults.forEach(function (domainResults) { + allPlaces = allPlaces.concat(domainResults); + if (!addressLine.text) { // Don't add if the user is already filtering + domainResults.forEach(function (place) { + if (suggestable(place)) { + suggestions.append(place); + } + }); + } + }); + if (data.current_page < data.total_pages) { + return getDomainPage(pageNumber + 1, cb); + } + cb(); }); }); - for (var index in filtered) { fill1(index); } + } + function filterChoicesByText() { + suggestions.clear(); + var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity); + function matches(place) { + if (!words.length) { + return suggestable(place); + } + return words.every(function (word) { + return place.searchText.indexOf(word) >= 0; + }); + } + allPlaces.forEach(function (place) { + if (matches(place)) { + suggestions.append(place); + } + }); } function fillDestinations() { - allDomains = suggestionChoices = []; - getDomains({minUsers: 0, maxUsers: 20}, function (error, domains) { + allPlaces = []; + suggestions.clear(); + getDomainPage(1, function (error) { if (error) { console.log('domain query failed:', error); - return filterChoicesByText(); } - var here = AddressManager.hostname; // don't show where we are now. - allDomains = domains.filter(function (domain) { return domain.name !== here; }); - // Whittle down suggestions to those that have at least one user, and try to get pictures. - suggestionChoices = allDomains.filter(function (domain) { return true/*fixme hrs restore domain.online_users*/; }); - asyncEach(domains, addPictureToDomain, function (error) { - if (error) { - console.log('place picture query failed:', error); - } - // Whittle down more by requiring a picture. - // fixme hrs restore suggestionChoices = suggestionChoices.filter(function (domain) { return domain.lobby; }); - filterChoicesByText(); - }); + console.log('domain query finished', allPlaces.length); }); } From c3bf52267d4c57af4aa2b291cfc8ae6d8f33a894 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 1 Aug 2016 15:27:29 -0700 Subject: [PATCH 6/7] Don't suggest loaded places. --- interface/resources/qml/AddressBarDialog.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index b80ab579c8..9a8cfddc02 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -278,7 +278,8 @@ Window { function suggestable(place) { return (place.name !== AddressManager.hostname) // Not our entry, but do show other entry points to current domain. && place.thumbnail - && place.online_users; // at least one present means it's actually online + && place.online_users // at least one present means it's actually online + && place.online_users <= 20; } function getDomainPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model // Each page of results is processed completely before we start on the next page. From 70e07f329918ebf3883c217639dbd032eeec3234 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 2 Aug 2016 13:56:01 -0700 Subject: [PATCH 7/7] remove highlight move animation --- interface/resources/qml/AddressBarDialog.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 9a8cfddc02..b9e7941b76 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -83,6 +83,8 @@ Window { hoverThunk: function () { ListView.view.currentIndex = index; } unhoverThunk: function () { ListView.view.currentIndex = -1; } } + highlightMoveDuration: -1; + highlightMoveVelocity: -1; highlight: Rectangle { color: "transparent"; border.width: 2; border.color: "#1FA5E8"; z: 1; } } @@ -272,7 +274,7 @@ Window { } // IWBNI we could get these results in order with most-recent-entered first. // In any case, we don't really need to preserve the domain.names order in the results. - asyncMap(domain.names, addPlace, cb); + asyncMap(domain.names || [], addPlace, cb); } function suggestable(place) {