From ed811d0431405bf960d6a1bb91813f731e72ff27 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 28 Jul 2016 15:01:21 -0700 Subject: [PATCH 01/81] 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 02/81] 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 03/81] 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 04/81] 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 05/81] 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 06/81] 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 1d7cdff2bcaf18e3d4835e063002dfd224936205 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 1 Aug 2016 13:24:22 -0700 Subject: [PATCH 07/81] Snapshot button plus script Minor change to allow snapshots to not notify (though the old way with CTRL+S still does). Added button to do it, saves them to disk. The plan is for us to add UI to share (or not) the snapshot. That's why we are not going through the notifications. Also, the script makes sure to hide/unhide hud and overlays. Next we will upload the pick to AWS via data-web (if you are logged in), and _then_ make the share UI. --- interface/src/Application.cpp | 6 +- interface/src/Application.h | 4 +- .../scripting/WindowScriptingInterface.cpp | 7 ++ .../src/scripting/WindowScriptingInterface.h | 3 +- scripts/system/assets/images/tools/snap.svg | 109 ++++++++++++++++++ scripts/system/notifications.js | 12 +- scripts/system/snapshot.js | 66 +++++++++++ 7 files changed, 196 insertions(+), 11 deletions(-) create mode 100755 scripts/system/assets/images/tools/snap.svg create mode 100644 scripts/system/snapshot.js diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e294ae38ad..abc0b91b2d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2281,7 +2281,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } else if (isOption && !isShifted && !isMeta) { Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { - takeSnapshot(); + takeSnapshot(true); } break; @@ -5070,7 +5070,7 @@ void Application::toggleLogDialog() { } } -void Application::takeSnapshot() { +void Application::takeSnapshot(bool notify) { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); @@ -5078,7 +5078,7 @@ void Application::takeSnapshot() { QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); - emit DependencyManager::get()->snapshotTaken(path); + emit DependencyManager::get()->snapshotTaken(path, notify); } float Application::getRenderResolutionScale() const { diff --git a/interface/src/Application.h b/interface/src/Application.h index 0af65f665f..ea6a117cfa 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -247,6 +247,8 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } + + void takeSnapshot(bool notify); signals: void svoImportRequested(const QString& url); @@ -373,8 +375,6 @@ private: int sendNackPackets(); - void takeSnapshot(); - MyAvatar* getMyAvatar() const; void checkSkeleton() const; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index b165cda135..fb7be86096 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,3 +199,10 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { qDebug() << "Copying"; QApplication::clipboard()->setText(text); } + +void WindowScriptingInterface::takeSnapshot(bool notify) { + // only evil-doers call takeSnapshot from a random thread + qApp->postLambdaEvent([&] { + qApp->takeSnapshot(notify); + }); +} diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 9d73111333..1abcb9db35 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -54,12 +54,13 @@ public slots: QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void copyToClipboard(const QString& text); + void takeSnapshot(bool notify); signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode); - void snapshotTaken(const QString& path); + void snapshotTaken(const QString& path, bool notify); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/scripts/system/assets/images/tools/snap.svg b/scripts/system/assets/images/tools/snap.svg new file mode 100755 index 0000000000..c540f307ae --- /dev/null +++ b/scripts/system/assets/images/tools/snap.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 98a31d708c..34f9814d8a 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -527,12 +527,14 @@ function onDomainConnectionRefused(reason) { createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); } -function onSnapshotTaken(path) { - var imageProperties = { - path: Script.resolvePath("file:///" + path), - aspectRatio: Window.innerWidth / Window.innerHeight +function onSnapshotTaken(path, notify) { + if (notify) { + var imageProperties = { + path: Script.resolvePath("file:///" + path), + aspectRatio: Window.innerWidth / Window.innerHeight + } + createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); } - createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); } // handles mouse clicks on buttons diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js new file mode 100644 index 0000000000..87ea3f29ec --- /dev/null +++ b/scripts/system/snapshot.js @@ -0,0 +1,66 @@ +// +// snapshot.js +// +// Created by David Kelly on 1 August 2016 +// Copyright 2016 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 +// +var SNAPSHOT_DELAY = 500; // 500ms +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var resetOverlays; +var button = toolBar.addButton({ + objectName: "snapshot", + imageURL: Script.resolvePath("assets/images/tools/snap.svg"), + visible: true, + buttonState: 1, + defaultState: 1, + hoverState: 1, + alpha: 0.9, +}); + +function onClicked() { + // update button states + resetOverlays = Menu.isOptionChecked("Overlays"); + Window.snapshotTaken.connect(resetButtons); + + button.writeProperty("buttonState", 0); + button.writeProperty("defaultState", 0); + button.writeProperty("hoverState", 2); + + // hide overlays if they are on + if (resetOverlays) { + Menu.setIsOptionChecked("Overlays", false); + } + + // hide hud + toolBar.writeProperty("visible", false); + + // take snapshot (with no notification) + Script.setTimeout(function () { + Window.takeSnapshot(false); + }, SNAPSHOT_DELAY); +} + +function resetButtons(path, notify) { + // show overlays if they were on + if (resetOverlays) { + Menu.setIsOptionChecked("Overlays", true); + } + // show hud + toolBar.writeProperty("visible", true); + + // update button states + button.writeProperty("buttonState", 1); + button.writeProperty("defaultState", 1); + button.writeProperty("hoverState", 3); + Window.snapshotTaken.disconnect(resetButtons); +} + +button.clicked.connect(onClicked); + +Script.scriptEnding.connect(function () { + toolBar.removeButton("snapshot"); + button.clicked.disconnect(onClicked); +}); From fcd2947b19fbd50998fccfce979594e43d37b287 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 1 Aug 2016 18:37:22 -0700 Subject: [PATCH 08/81] Doh -- forgot to push this same logic as before to get a mono image in the HMD. --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index e0c87fbbed..a4db2811e0 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -662,10 +662,15 @@ void OpenGLDisplayPlugin::withMainThreadContext(std::function f) const { QImage OpenGLDisplayPlugin::getScreenshot() const { using namespace oglplus; - QImage screenshot(_compositeFramebuffer->size.x, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); + auto width = _compositeFramebuffer->size.x; + if (isHmd()) { + width /= 2; + } + + QImage screenshot(width, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); withMainThreadContext([&] { Framebuffer::Bind(Framebuffer::Target::Read, _compositeFramebuffer->fbo); - Context::ReadPixels(0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); + Context::ReadPixels(0, 0, width, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); }); return screenshot.mirrored(false, true); } From 6e7b8282033a794285b5769119424eafee190cb9 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 2 Aug 2016 10:13:35 -0700 Subject: [PATCH 09/81] keep qml text fields up to date with vive virtual keyboard --- plugins/openvr/src/OpenVrHelpers.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index c93a2178b5..6f5c1b9511 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -170,8 +170,7 @@ void showOpenVrKeyboard(bool show = true) { } } -void finishOpenVrKeyboardInput() { - auto offscreenUi = DependencyManager::get(); +void updateFromOpenVrKeyboardInput() { auto chars = _overlay->GetKeyboardText(textArray, 8192); auto newText = QString(QByteArray(textArray, chars)); _keyboardFocusObject->setProperty("text", newText); @@ -181,6 +180,10 @@ void finishOpenVrKeyboardInput() { //QInputMethodEvent event(_existingText, QList()); //event.setCommitString(newText, 0, _existingText.size()); //qApp->sendEvent(_keyboardFocusObject, &event); +} +void finishOpenVrKeyboardInput() { + auto offscreenUi = DependencyManager::get(); + updateFromOpenVrKeyboardInput(); // Simulate an enter press on the top level window to trigger the action if (0 == (_currentHints & Qt::ImhMultiLine)) { qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n"))); @@ -261,6 +264,11 @@ void handleOpenVrEvents() { QMetaObject::invokeMethod(qApp, "quit"); break; + case vr::VREvent_KeyboardCharInput: + // Make the focused field match the keyboard results, inclusive of combining characters and such. + updateFromOpenVrKeyboardInput(); + break; + case vr::VREvent_KeyboardDone: finishOpenVrKeyboardInput(); From 70e07f329918ebf3883c217639dbd032eeec3234 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 2 Aug 2016 13:56:01 -0700 Subject: [PATCH 10/81] 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) { From d34e72512bd4c6debee3957a715b3da7480b75e4 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 2 Aug 2016 15:13:48 -0700 Subject: [PATCH 11/81] Add snapshot.js to defaultScripts --- scripts/defaultScripts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 817d63582d..2e707d95ce 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -27,3 +27,4 @@ Script.load("system/controllers/grab.js"); Script.load("system/controllers/teleport.js"); Script.load("system/dialTone.js"); Script.load("system/firstPersonHMD.js"); +Script.load("system/snapshot.js"); From e593077b4baa5ce1879d68bacaf702dc7532ff6c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 3 Aug 2016 09:39:27 -0700 Subject: [PATCH 12/81] basic display of user story feed (hardcoded switch place names vs feed) --- interface/resources/qml/AddressBarDialog.qml | 130 ++++++++++++++----- 1 file changed, 101 insertions(+), 29 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index b9e7941b76..df6a5837ac 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -49,9 +49,31 @@ Window { addressLine.text = card.path; toggleOrGo(true); } + property bool useFeed: false; property var allPlaces: []; + property var allStories: []; property int cardWidth: 200; property int cardHeight: 152; + function pastTime(timestamp) { // Answer a descriptive string + timestamp = new Date(timestamp); + var then = timestamp.getTime(), + now = Date.now(), + since = now - then, + ONE_MINUTE = 1000 * 60, + ONE_HOUR = ONE_MINUTE * 60, + hours = since / ONE_HOUR, + minutes = (hours % 1) * 60; + if (hours > 24) { + return timestamp.toDateString(); + } + if (hours > 1) { + return Math.floor(hours).toString() + ' hr ' + Math.floor(minutes) + ' min ago'; + } + if (minutes >= 2) { + return Math.floor(minutes).toString() + ' min ago'; + } + return 'about a minute ago'; + } AddressBarDialog { id: addressBarDialog @@ -76,10 +98,10 @@ Window { width: cardWidth; height: cardHeight; goFunction: goCard; - path: model.name + model.path; - thumbnail: model.thumbnail; - placeText: model.name; - usersText: model.online_users + ((model.online_users === 1) ? ' person' : ' people'); + path: model.place_name + model.path; + thumbnail: model.thumbnail_url; + placeText: model.place_name; + usersText: (model.created_at ? pastTime(model.created_at) : (model.online_users + ((model.online_users === 1) ? ' person' : ' people'))); hoverThunk: function () { ListView.view.currentIndex = index; } unhoverThunk: function () { ListView.view.currentIndex = -1; } } @@ -235,23 +257,31 @@ Window { return x; } - function handleError(error, data, cb) { // cb(error) and answer truthy if needed, else falsey + function handleError(url, 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)); + 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 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)) { + var url = 'https://metaverse.highfidelity.com/api/v1/places/' + placeData.place_name; + getRequest(url, function (error, data) { + if (handleError(url, error, data, cb)) { return; } var place = data.data.place, previews = place.previews; placeData.path = place.path; if (previews && previews.thumbnail) { - placeData.thumbnail = previews.thumbnail; + placeData.thumbnail_url = previews.thumbnail; } if (place.description) { placeData.description = place.description; @@ -260,17 +290,28 @@ Window { cb(error, placeData); }); } + function makeModelData(data, optionalPlaceName) { // create a new obj from data + // ListModel elements will only ever have those properties that are defined by the first obj that is added. + // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. + var name = optionalPlaceName || data.place_name, + tags = data.tags || [data.action], + description = data.description || ""; + return { + place_name: name, + path: data.path || "", + created_at: data.create_at || "8/3/2016", + thumbnail_url: data.thumbnail_url || "", + + tags: tags, + description: description, + online_users: data.online_users, + + searchText: [name].concat(tags, description).join(' ').toUpperCase() + } + } 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); + getPlace(makeModelData(domain, name), icb); } // 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. @@ -278,8 +319,11 @@ Window { } function suggestable(place) { - return (place.name !== AddressManager.hostname) // Not our entry, but do show other entry points to current domain. - && place.thumbnail + if (useFeed) { + return true; + } + return (place.place_name !== AddressManager.hostname) // Not our entry, but do show other entry points to current domain. + && place.thumbnail_url && place.online_users // at least one present means it's actually online && place.online_users <= 20; } @@ -298,8 +342,9 @@ Window { 'sort_order=desc', 'page=' + pageNumber ]; - getRequest('https://metaverse.highfidelity.com/api/v1/domains/all?' + params.join('&'), function (error, data) { - if (handleError(error, data, cb)) { + var url = 'https://metaverse.highfidelity.com/api/v1/domains/all?' + params.join('&'); + getRequest(url, function (error, data) { + if (handleError(url, error, data, cb)) { return; } asyncMap(data.data.domains, mapDomainPlaces, function (error, pageResults) { @@ -309,7 +354,7 @@ Window { // 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 + if (!addressLine.text && !useFeed) { // Don't add if the user is already filtering domainResults.forEach(function (place) { if (suggestable(place)) { suggestions.append(place); @@ -324,9 +369,35 @@ Window { }); }); } + function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model + var url = 'https://metaverse.highfidelity.com/api/v1/user_stories?page=' + pageNumber; + getRequest(url, function (error, data) { + if (handleError(url, error, data, cb)) { + return; + } + // FIXME: For debugging until we have real data + data = {user_stories: [ + {created_at: "8/3/2016", action: "snapshot", path: "/4257.1,126.084,1335.45/0,-0.857368,0,0.514705", place_name: "SpiritMoving", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/c28/a26/f0-/thumbnail/hifi-place-c28a26f0-6991-4654-9c2b-e64228c06954.jpg?1456878797"}, + {created_at: "8/3/2016", action: "snapshot", path: "/10077.4,4003.6,9972.56/0,-0.410351,0,0.911928", place_name: "Ventura", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/1f5/e6b/00-/thumbnail/hifi-place-1f5e6b00-2bf0-4319-b9ae-a2344a72354c.png?1454321596"} + ]}; + + var stories = data.user_stories.map(makeModelData); + allStories = allStories.concat(stories); + if (!addressLine.text && useFeed) { // Don't add if the user is already filtering + stories.forEach(function (story) { + suggestions.append(story); + }); + } + if (data.current_page < data.total_pages) { + return getUserStoryPage(pageNumber + 1, cb); + } + cb(); + }); + } function filterChoicesByText() { suggestions.clear(); - var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity); + var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity), + data = useFeed ? allStories : allPlaces; function matches(place) { if (!words.length) { return suggestable(place); @@ -335,7 +406,7 @@ Window { return place.searchText.indexOf(word) >= 0; }); } - allPlaces.forEach(function (place) { + data.forEach(function (place) { if (matches(place)) { suggestions.append(place); } @@ -344,12 +415,13 @@ Window { function fillDestinations() { allPlaces = []; + allStories = []; suggestions.clear(); getDomainPage(1, function (error) { - if (error) { - console.log('domain query failed:', error); - } - console.log('domain query finished', allPlaces.length); + console.log('domain query', error, allPlaces.length); + }); + getUserStoryPage(1, function (error) { + console.log('user stories query', error, allStories.length); }); } From 188e525efcdea070d2b227be4ae09abc2b918538 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 3 Aug 2016 11:48:58 -0700 Subject: [PATCH 13/81] basic ugly feed/places toggle button --- interface/resources/qml/AddressBarDialog.qml | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index df6a5837ac..de2e16ddfc 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -193,7 +193,24 @@ Window { helperText: "Go to: place, @user, /path, network address" onTextChanged: filterChoicesByText() } - + Rectangle { + color: "red"; + height: addressLine.height; + width: addressLine.height; + anchors { + left: addressLine.right; + bottom: addressLine.bottom; + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + console.log('fixme hrs toggle'); + useFeed = !useFeed; + filterChoicesByText(); + } + } + } } } @@ -299,7 +316,7 @@ Window { return { place_name: name, path: data.path || "", - created_at: data.create_at || "8/3/2016", + created_at: data.create_at || "", thumbnail_url: data.thumbnail_url || "", tags: tags, From 30327d81357316a5880cae84b2af3f241f08924f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 3 Aug 2016 16:02:22 -0700 Subject: [PATCH 14/81] New buttons, with hover behavior. Also don't impose test data if the api gives data. --- interface/resources/images/backward.svg | 28 ++++ interface/resources/images/forward.svg | 28 ++++ interface/resources/images/home.svg | 46 +++++++ interface/resources/images/places.svg | 33 +++++ interface/resources/images/snap-feed.svg | 41 ++++++ interface/resources/qml/AddressBarDialog.qml | 131 ++++++++++--------- 6 files changed, 242 insertions(+), 65 deletions(-) create mode 100644 interface/resources/images/backward.svg create mode 100644 interface/resources/images/forward.svg create mode 100644 interface/resources/images/home.svg create mode 100644 interface/resources/images/places.svg create mode 100644 interface/resources/images/snap-feed.svg diff --git a/interface/resources/images/backward.svg b/interface/resources/images/backward.svg new file mode 100644 index 0000000000..9d36778aa3 --- /dev/null +++ b/interface/resources/images/backward.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/interface/resources/images/forward.svg b/interface/resources/images/forward.svg new file mode 100644 index 0000000000..65ec62f947 --- /dev/null +++ b/interface/resources/images/forward.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/interface/resources/images/home.svg b/interface/resources/images/home.svg new file mode 100644 index 0000000000..1e201a1a9d --- /dev/null +++ b/interface/resources/images/home.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/images/places.svg b/interface/resources/images/places.svg new file mode 100644 index 0000000000..6336ab111f --- /dev/null +++ b/interface/resources/images/places.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/interface/resources/images/snap-feed.svg b/interface/resources/images/snap-feed.svg new file mode 100644 index 0000000000..3eae156cd6 --- /dev/null +++ b/interface/resources/images/snap-feed.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index de2e16ddfc..5a92360488 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -14,6 +14,7 @@ import "controls" import "styles" import "windows" import "hifi" +import "hifi/toolbars" Window { id: root @@ -118,64 +119,43 @@ Window { property int inputAreaHeight: 56.0 * root.scale // Height of the background's input area property int inputAreaStep: (height - inputAreaHeight) / 2 - Image { + ToolbarButton { id: homeButton - source: "../images/home-button.svg" - width: 29 - height: 26 + imageURL: "../images/home.svg" + buttonState: 1 + defaultState: 1 + hoverState: 2 + onClicked: addressBarDialog.loadHome(); anchors { left: parent.left - leftMargin: parent.height + 2 * hifi.layout.spacing + leftMargin: parent.height verticalCenter: parent.verticalCenter } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - addressBarDialog.loadHome() - } - } } - Image { - id: backArrow - source: addressBarDialog.backEnabled ? "../images/left-arrow.svg" : "../images/left-arrow-disabled.svg" - width: 22 - height: 26 + ToolbarButton { + id: backArrow; + imageURL: "../images/backward.svg"; + hoverState: addressBarDialog.backEnabled ? 2 : 0; + defaultState: addressBarDialog.backEnabled ? 1 : 0; + buttonState: addressBarDialog.backEnabled ? 1 : 0; // fixme: needs work + onClicked: addressBarDialog.loadBack(); anchors { left: homeButton.right - leftMargin: 2 * hifi.layout.spacing verticalCenter: parent.verticalCenter } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - addressBarDialog.loadBack() - } - } } - - Image { - id: forwardArrow - source: addressBarDialog.forwardEnabled ? "../images/right-arrow.svg" : "../images/right-arrow-disabled.svg" - width: 22 - height: 26 + ToolbarButton { + id: forwardArrow; + imageURL: "../images/forward.svg"; + hoverState: addressBarDialog.forwardEnabled ? 2 : 0; + defaultState: addressBarDialog.forwardEnabled ? 1 : 0; + buttonState: addressBarDialog.forwardEnabled ? 1 : 0; // fixme: needs work + onClicked: addressBarDialog.loadForward(); anchors { left: backArrow.right - leftMargin: 2 * hifi.layout.spacing verticalCenter: parent.verticalCenter } - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - addressBarDialog.loadForward() - } - } } // FIXME replace with TextField @@ -183,37 +163,54 @@ Window { id: addressLine focus: true anchors { - fill: parent + top: parent.top + left: parent.left + bottom: parent.bottom + right: placesButton.left leftMargin: parent.height + parent.height + hifi.layout.spacing * 7 - rightMargin: hifi.layout.spacing * 2 + rightMargin: hifi.layout.spacing topMargin: parent.inputAreaStep + hifi.layout.spacing bottomMargin: parent.inputAreaStep + hifi.layout.spacing } font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75 - helperText: "Go to: place, @user, /path, network address" + helperText: "Go to: place, @user, /path" //, network address" onTextChanged: filterChoicesByText() } - Rectangle { - color: "red"; - height: addressLine.height; - width: addressLine.height; + // These two are radio buttons. + ToolbarButton { + id: placesButton + imageURL: "../images/places.svg" + buttonState: 1 + defaultState: useFeed ? 0 : 1; + hoverState: useFeed ? 2 : -1; + onClicked: useFeed ? toggleFeed() : identity() anchors { - left: addressLine.right; + right: feedButton.left; bottom: addressLine.bottom; } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - console.log('fixme hrs toggle'); - useFeed = !useFeed; - filterChoicesByText(); - } + } + ToolbarButton { + id: feedButton; + imageURL: "../images/snap-feed.svg"; + buttonState: 0 + defaultState: useFeed ? 1 : 0; + hoverState: useFeed ? -1 : 2; + onClicked: useFeed ? identity() : toggleFeed(); + anchors { + right: parent.right; + bottom: addressLine.bottom; + rightMargin: hifi.layout.spacing; } } } } + function toggleFeed () { + useFeed = !useFeed; + placesButton.buttonState = useFeed ? 0 : 1; + feedButton.buttonState = useFeed ? 1 : 0; + filterChoicesByText(); + } function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. // TODO: make available to other .qml. var request = new XMLHttpRequest(); @@ -393,12 +390,16 @@ Window { return; } // FIXME: For debugging until we have real data - data = {user_stories: [ - {created_at: "8/3/2016", action: "snapshot", path: "/4257.1,126.084,1335.45/0,-0.857368,0,0.514705", place_name: "SpiritMoving", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/c28/a26/f0-/thumbnail/hifi-place-c28a26f0-6991-4654-9c2b-e64228c06954.jpg?1456878797"}, - {created_at: "8/3/2016", action: "snapshot", path: "/10077.4,4003.6,9972.56/0,-0.410351,0,0.911928", place_name: "Ventura", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/1f5/e6b/00-/thumbnail/hifi-place-1f5e6b00-2bf0-4319-b9ae-a2344a72354c.png?1454321596"} - ]}; + if (!data.user_stories.length) { + data.user_stories = [ + {created_at: "8/3/2016", action: "snapshot", path: "/4257.1,126.084,1335.45/0,-0.857368,0,0.514705", place_name: "SpiritMoving", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/c28/a26/f0-/thumbnail/hifi-place-c28a26f0-6991-4654-9c2b-e64228c06954.jpg?1456878797"}, + {created_at: "8/3/2016", action: "snapshot", path: "/10077.4,4003.6,9972.56/0,-0.410351,0,0.911928", place_name: "Ventura", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/1f5/e6b/00-/thumbnail/hifi-place-1f5e6b00-2bf0-4319-b9ae-a2344a72354c.png?1454321596"} + ]; + } - var stories = data.user_stories.map(makeModelData); + var stories = data.user_stories.map(function (story) { // explicit single-argument function + return makeModelData(story); + }); allStories = allStories.concat(stories); if (!addressLine.text && useFeed) { // Don't add if the user is already filtering stories.forEach(function (story) { @@ -435,10 +436,10 @@ Window { allStories = []; suggestions.clear(); getDomainPage(1, function (error) { - console.log('domain query', error, allPlaces.length); + console.log('domain query', error || 'ok', allPlaces.length); }); getUserStoryPage(1, function (error) { - console.log('user stories query', error, allStories.length); + console.log('user stories query', error || 'ok', allStories.length); }); } From 8c6744e5e8308d4a212bf304b8332b9c347b450a Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 3 Aug 2016 16:18:41 -0700 Subject: [PATCH 15/81] new address bar graphic --- interface/resources/images/address-bar.svg | 97 ++++---------------- interface/resources/qml/AddressBarDialog.qml | 9 +- 2 files changed, 22 insertions(+), 84 deletions(-) diff --git a/interface/resources/images/address-bar.svg b/interface/resources/images/address-bar.svg index 56dc4f028c..76ad7c0547 100644 --- a/interface/resources/images/address-bar.svg +++ b/interface/resources/images/address-bar.svg @@ -1,81 +1,18 @@ - - - - - - image/svg+xml - - - - - - - - - - - + + + + + + + diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 5a92360488..0ae4d6d9ff 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -128,7 +128,7 @@ Window { onClicked: addressBarDialog.loadHome(); anchors { left: parent.left - leftMargin: parent.height + leftMargin: hifi.layout.spacing * 2 verticalCenter: parent.verticalCenter } } @@ -164,10 +164,10 @@ Window { focus: true anchors { top: parent.top - left: parent.left bottom: parent.bottom + left: forwardArrow.right right: placesButton.left - leftMargin: parent.height + parent.height + hifi.layout.spacing * 7 + leftMargin: hifi.layout.spacing * 4 rightMargin: hifi.layout.spacing topMargin: parent.inputAreaStep + hifi.layout.spacing bottomMargin: parent.inputAreaStep + hifi.layout.spacing @@ -186,6 +186,7 @@ Window { onClicked: useFeed ? toggleFeed() : identity() anchors { right: feedButton.left; + rightMargin: hifi.layout.spacing * 3; bottom: addressLine.bottom; } } @@ -199,7 +200,7 @@ Window { anchors { right: parent.right; bottom: addressLine.bottom; - rightMargin: hifi.layout.spacing; + rightMargin: hifi.layout.spacing * 3; } } } From 4d0c5c5d19cc9febe1e456e0d306976056c47a1b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 3 Aug 2016 16:22:46 -0700 Subject: [PATCH 16/81] fix snapshot button hover --- scripts/system/snapshot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 87ea3f29ec..280bd3b5d0 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -16,7 +16,7 @@ var button = toolBar.addButton({ visible: true, buttonState: 1, defaultState: 1, - hoverState: 1, + hoverState: 2, alpha: 0.9, }); From c44ce0f5b59ad3f9249110f7def4fc8322a46723 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 3 Aug 2016 17:03:27 -0700 Subject: [PATCH 17/81] Fix forward/back button states. --- interface/resources/qml/AddressBarDialog.qml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 0ae4d6d9ff..fe44607b53 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -80,6 +80,9 @@ Window { id: addressBarDialog implicitWidth: backgroundImage.width implicitHeight: backgroundImage.height + // The buttons have their button state changed on hover, so we have to manually fix them up here + onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; + onForwardEnabledChanged: forwardArrow.buttonState = addressBarDialog.forwardEnabled ? 1 : 0; ListModel { id: suggestions } @@ -138,7 +141,7 @@ Window { imageURL: "../images/backward.svg"; hoverState: addressBarDialog.backEnabled ? 2 : 0; defaultState: addressBarDialog.backEnabled ? 1 : 0; - buttonState: addressBarDialog.backEnabled ? 1 : 0; // fixme: needs work + buttonState: addressBarDialog.backEnabled ? 1 : 0; onClicked: addressBarDialog.loadBack(); anchors { left: homeButton.right @@ -150,7 +153,7 @@ Window { imageURL: "../images/forward.svg"; hoverState: addressBarDialog.forwardEnabled ? 2 : 0; defaultState: addressBarDialog.forwardEnabled ? 1 : 0; - buttonState: addressBarDialog.forwardEnabled ? 1 : 0; // fixme: needs work + buttonState: addressBarDialog.forwardEnabled ? 1 : 0; onClicked: addressBarDialog.loadForward(); anchors { left: backArrow.right From 421707c5d84aaac954309b2945822aa51ea7042e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 4 Aug 2016 10:07:33 -0700 Subject: [PATCH 18/81] fix typo that was making user stories not show date --- interface/resources/qml/AddressBarDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index fe44607b53..fd2abd01f5 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -317,7 +317,7 @@ Window { return { place_name: name, path: data.path || "", - created_at: data.create_at || "", + created_at: data.created_at || "", thumbnail_url: data.thumbnail_url || "", tags: tags, From d39446f340157e3bb4ed1b666219d3d39217c367 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 4 Aug 2016 10:36:17 -0700 Subject: [PATCH 19/81] no place name over user-story scroll items --- interface/resources/qml/AddressBarDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index fd2abd01f5..0c6288c828 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -104,8 +104,8 @@ Window { goFunction: goCard; path: model.place_name + model.path; thumbnail: model.thumbnail_url; - placeText: model.place_name; - usersText: (model.created_at ? pastTime(model.created_at) : (model.online_users + ((model.online_users === 1) ? ' person' : ' people'))); + placeText: model.created_at ? "" : model.place_name; + usersText: model.created_at ? pastTime(model.created_at) : (model.online_users + ((model.online_users === 1) ? ' person' : ' people')); hoverThunk: function () { ListView.view.currentIndex = index; } unhoverThunk: function () { ListView.view.currentIndex = -1; } } From f5d07f418679df39140ed9b9dffd11eef53928f3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 4 Aug 2016 11:55:31 -0700 Subject: [PATCH 20/81] helper text size and italic --- interface/resources/qml/AddressBarDialog.qml | 4 +++- interface/resources/qml/controls/TextInput.qml | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 0c6288c828..29cba76f1d 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -176,7 +176,9 @@ Window { bottomMargin: parent.inputAreaStep + hifi.layout.spacing } font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75 - helperText: "Go to: place, @user, /path" //, network address" + helperText: "Go to: place, @user, /path, network address" + helperPixelSize: font.pixelSize * 0.75 + helperItalic: true onTextChanged: filterChoicesByText() } // These two are radio buttons. diff --git a/interface/resources/qml/controls/TextInput.qml b/interface/resources/qml/controls/TextInput.qml index b7ca6d2c1b..77e11177e1 100644 --- a/interface/resources/qml/controls/TextInput.qml +++ b/interface/resources/qml/controls/TextInput.qml @@ -12,6 +12,8 @@ Original.TextInput { verticalAlignment: Original.TextInput.AlignVCenter font.family: hifi.fonts.fontFamily font.pixelSize: hifi.fonts.pixelSize + property int helperPixelSize: font.pixelSize + property bool helperItalic: false /* Original.Rectangle { @@ -23,7 +25,8 @@ Original.TextInput { */ Text { anchors.fill: parent - font.pixelSize: parent.font.pixelSize + font.pixelSize: helperPixelSize + font.italic: helperItalic font.family: parent.font.family verticalAlignment: parent.verticalAlignment horizontalAlignment: parent.horizontalAlignment From 1ecb01feacea805d41f8ce39db9d47f00b785576 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 4 Aug 2016 12:06:07 -0700 Subject: [PATCH 21/81] center the scroll --- interface/resources/qml/AddressBarDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 29cba76f1d..3e676b7340 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -94,7 +94,7 @@ Window { anchors { bottom: backgroundImage.top; bottomMargin: 2 * hifi.layout.spacing; - right: backgroundImage.right; + horizontalCenter: backgroundImage.horizontalCenter } model: suggestions; orientation: ListView.Horizontal; From 04bd98b15ffd9a7f73966bd543a5896179eafffc Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 4 Aug 2016 12:33:50 -0700 Subject: [PATCH 22/81] new assets. remove margins between buttons (letting the graphics do the work) --- interface/resources/images/address-bar.svg | 12 ++-- interface/resources/images/backward.svg | 26 ++++---- interface/resources/images/forward.svg | 26 ++++---- interface/resources/images/home.svg | 62 +++++++++++--------- interface/resources/images/places.svg | 36 ++++++------ interface/resources/images/snap-feed.svg | 48 +++++++-------- interface/resources/qml/AddressBarDialog.qml | 9 ++- 7 files changed, 111 insertions(+), 108 deletions(-) diff --git a/interface/resources/images/address-bar.svg b/interface/resources/images/address-bar.svg index 76ad7c0547..a8cb158492 100644 --- a/interface/resources/images/address-bar.svg +++ b/interface/resources/images/address-bar.svg @@ -9,10 +9,10 @@ .st1{fill:#E6E7E8;} .st2{fill:#FFFFFF;} - - - + + + diff --git a/interface/resources/images/backward.svg b/interface/resources/images/backward.svg index 9d36778aa3..e4502fa80e 100644 --- a/interface/resources/images/backward.svg +++ b/interface/resources/images/backward.svg @@ -9,19 +9,19 @@ .st3{fill:#31D8FF;} - - - - + + + + diff --git a/interface/resources/images/forward.svg b/interface/resources/images/forward.svg index 65ec62f947..0c5cbe3d0c 100644 --- a/interface/resources/images/forward.svg +++ b/interface/resources/images/forward.svg @@ -9,19 +9,19 @@ .st3{fill:#31D8FF;} - - - - + + + + diff --git a/interface/resources/images/home.svg b/interface/resources/images/home.svg index 1e201a1a9d..7740dc9568 100644 --- a/interface/resources/images/home.svg +++ b/interface/resources/images/home.svg @@ -9,37 +9,41 @@ .st3{fill:#31D8FF;} - - - - - - + + - - + + + + + + + + + + diff --git a/interface/resources/images/places.svg b/interface/resources/images/places.svg index 6336ab111f..f70695d606 100644 --- a/interface/resources/images/places.svg +++ b/interface/resources/images/places.svg @@ -9,24 +9,24 @@ .st3{fill:#31D8FF;} - - - - - - - + + + + + + + diff --git a/interface/resources/images/snap-feed.svg b/interface/resources/images/snap-feed.svg index 3eae156cd6..c2dede6e0f 100644 --- a/interface/resources/images/snap-feed.svg +++ b/interface/resources/images/snap-feed.svg @@ -9,31 +9,31 @@ .st3{fill:#31D8FF;} - - - - - - - - + + + + + + + + - - + + diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 3e676b7340..7eb4de6bba 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -131,7 +131,7 @@ Window { onClicked: addressBarDialog.loadHome(); anchors { left: parent.left - leftMargin: hifi.layout.spacing * 2 + leftMargin: homeButton.width / 2 verticalCenter: parent.verticalCenter } } @@ -170,8 +170,8 @@ Window { bottom: parent.bottom left: forwardArrow.right right: placesButton.left - leftMargin: hifi.layout.spacing * 4 - rightMargin: hifi.layout.spacing + leftMargin: forwardArrow.width + rightMargin: placesButton.width topMargin: parent.inputAreaStep + hifi.layout.spacing bottomMargin: parent.inputAreaStep + hifi.layout.spacing } @@ -191,7 +191,6 @@ Window { onClicked: useFeed ? toggleFeed() : identity() anchors { right: feedButton.left; - rightMargin: hifi.layout.spacing * 3; bottom: addressLine.bottom; } } @@ -205,7 +204,7 @@ Window { anchors { right: parent.right; bottom: addressLine.bottom; - rightMargin: hifi.layout.spacing * 3; + rightMargin: feedButton.width / 2 } } } From 8a7bbbaaed84921548f4cfb8a8fbc45ca8b416ac Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 4 Aug 2016 12:37:09 -0700 Subject: [PATCH 23/81] limit user stories to first 100 --- interface/resources/qml/AddressBarDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7eb4de6bba..5883872c95 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -411,7 +411,7 @@ Window { suggestions.append(story); }); } - if (data.current_page < data.total_pages) { + if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now return getUserStoryPage(pageNumber + 1, cb); } cb(); From fed775ff83b5211350931a2be98f8623aa622db0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 4 Aug 2016 15:52:39 -0700 Subject: [PATCH 24/81] simple snapshot confirmation --- interface/src/scripting/WindowScriptingInterface.cpp | 2 +- scripts/system/snapshot.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index fb7be86096..026818ec82 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -96,7 +96,7 @@ void WindowScriptingInterface::alert(const QString& message) { /// \param const QString& message message to display /// \return QScriptValue `true` if 'Yes' was clicked, `false` otherwise QScriptValue WindowScriptingInterface::confirm(const QString& message) { - return QScriptValue((QMessageBox::Yes == OffscreenUi::question("", message))); + return QScriptValue((QMessageBox::Yes == OffscreenUi::question("", message, QMessageBox::Yes | QMessageBox::No))); } /// Display a prompt with a text box diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 87ea3f29ec..92ce36e935 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -20,6 +20,13 @@ var button = toolBar.addButton({ alpha: 0.9, }); +function confirmShare(data) { + if (!Window.confirm("Share snapshot " + data.localPath + "?")) { // This dialog will be more elaborate... + return; + } + Window.alert("Pretending to upload. That code will go here."); +} + function onClicked() { // update button states resetOverlays = Menu.isOptionChecked("Overlays"); @@ -56,7 +63,8 @@ function resetButtons(path, notify) { button.writeProperty("defaultState", 1); button.writeProperty("hoverState", 3); Window.snapshotTaken.disconnect(resetButtons); -} + confirmShare({localPath: path}); + } button.clicked.connect(onClicked); From c8f3a898d25824291982c52261de3a4cddfaecfd Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 5 Aug 2016 10:51:51 -0700 Subject: [PATCH 25/81] First cut of a snapshot uploader Will need to test end-to-end shortly, etc... --- interface/src/Application.cpp | 5 ++ interface/src/Application.h | 1 + .../scripting/WindowScriptingInterface.cpp | 8 +- .../src/scripting/WindowScriptingInterface.h | 2 + interface/src/ui/Snapshot.cpp | 88 +++++++++++++++++++ interface/src/ui/Snapshot.h | 11 +++ scripts/system/snapshot.js | 13 ++- 7 files changed, 126 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index baddc26301..5be03a5e4f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5082,6 +5082,11 @@ void Application::takeSnapshot(bool notify) { emit DependencyManager::get()->snapshotTaken(path, notify); } +void Application::shareSnapshot(const QString& path) { + // not much to do here, everything is done in snapshot code... + Snapshot::uploadSnapshot(path); +} + float Application::getRenderResolutionScale() const { if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionOne)) { return 1.0f; diff --git a/interface/src/Application.h b/interface/src/Application.h index ea6a117cfa..aa3244c53a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -249,6 +249,7 @@ public: float getAverageSimsPerSecond() const { return _simCounter.rate(); } void takeSnapshot(bool notify); + void shareSnapshot(const QString& filename); signals: void svoImportRequested(const QString& url); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 026818ec82..9ce0b5bca5 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -202,7 +202,13 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { void WindowScriptingInterface::takeSnapshot(bool notify) { // only evil-doers call takeSnapshot from a random thread - qApp->postLambdaEvent([&] { + qApp->postLambdaEvent([notify] { qApp->takeSnapshot(notify); }); } + +void WindowScriptingInterface::shareSnapshot(const QString& path) { + qApp->postLambdaEvent([path] { + qApp->shareSnapshot(path); + }); +} diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 1abcb9db35..24f2a619ce 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -55,12 +55,14 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void copyToClipboard(const QString& text); void takeSnapshot(bool notify); + void shareSnapshot(const QString& path); signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode); void snapshotTaken(const QString& path, bool notify); + void snapshotShared(bool success); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index aaf11d14a4..42ee4fa2f5 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -32,6 +32,7 @@ #include "Application.h" #include "Snapshot.h" +#include "scripting/WindowScriptingInterface.h" // filename format: hifi-snap-by-%username%-on-%date%_%time%_@-%location%.jpg // %1 <= username, %2 <= date and time, %3 <= current location @@ -141,3 +142,90 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { return imageTempFile; } } + +void Snapshot::uploadSnapshot(const QString& filename) { + + const QString SNAPSHOT_UPLOAD_URL = "/api/v1/snapshots"; + static SnapshotUploader uploader; + + qDebug() << "uploading snapshot " << filename; + + QFile* file = new QFile(filename); + Q_ASSERT(file->exists()); + file->open(QIODevice::ReadOnly); + + QHttpPart imagePart; + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\"")); + imagePart.setBodyDevice(file); + + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + file->setParent(multiPart); // we cannot delete the file now, so delete it with the multiPart + multiPart->append(imagePart); + + auto accountManager = DependencyManager::get(); + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = &uploader; + callbackParams.jsonCallbackMethod = "uploadSuccess"; + callbackParams.errorCallbackReceiver = &uploader; + callbackParams.errorCallbackMethod = "uploadFailure"; + + accountManager->sendRequest(SNAPSHOT_UPLOAD_URL, + AccountManagerAuth::Required, + QNetworkAccessManager::PostOperation, + callbackParams, + nullptr, + multiPart); +} + +void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { + const QString STORY_UPLOAD_URL = "/api/v1/user_stories"; + static SnapshotUploader uploader; + + // parse the reply for the thumbnail_url + QByteArray contents = reply.readAll(); + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(contents, &jsonError); + if (jsonError.error == QJsonParseError::NoError) { + QString thumbnail_url = doc.object().value("thumbnail_url").toString(); + + // create json post data + QJsonObject rootObject; + QJsonObject userStoryObject; + userStoryObject.insert("thumbnail_url", thumbnail_url); + userStoryObject.insert("action", "snapshot"); + rootObject.insert("user_story", userStoryObject); + + auto accountManager = DependencyManager::get(); + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = &uploader; + callbackParams.jsonCallbackMethod = "createStorySuccess"; + callbackParams.errorCallbackReceiver = &uploader; + callbackParams.errorCallbackMethod = "createStoryFailure"; + + accountManager->sendRequest(STORY_UPLOAD_URL, + AccountManagerAuth::Required, + QNetworkAccessManager::PostOperation, + callbackParams, + QJsonDocument(rootObject).toJson()); + + } else { + qDebug() << "Error parsing upload response: " << jsonError.errorString(); + emit DependencyManager::get()->snapshotShared(false); + } +} + +void SnapshotUploader::uploadFailure(QNetworkReply& reply) { + // TODO: parse response, potentially helpful for logging (?) + emit DependencyManager::get()->snapshotShared(false); +} + +void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { + emit DependencyManager::get()->snapshotShared(true); +} + +void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { + // TODO: parse response, potentially helpful for logging (?) + emit DependencyManager::get()->snapshotShared(false); +} diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 2e7986a5c0..e780537b76 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -32,6 +32,16 @@ private: QUrl _URL; }; + +class SnapshotUploader: public QObject { + Q_OBJECT +public slots: + void uploadSuccess(QNetworkReply& reply); + void uploadFailure(QNetworkReply& reply); + void createStorySuccess(QNetworkReply& reply); + void createStoryFailure(QNetworkReply& reply); +}; + class Snapshot { public: static QString saveSnapshot(QImage image); @@ -40,6 +50,7 @@ public: static Setting::Handle snapshotsLocation; static Setting::Handle hasSetSnapshotsLocation; + static void uploadSnapshot(const QString& filename); private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 92ce36e935..8a6b811085 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -24,7 +24,18 @@ function confirmShare(data) { if (!Window.confirm("Share snapshot " + data.localPath + "?")) { // This dialog will be more elaborate... return; } - Window.alert("Pretending to upload. That code will go here."); + Window.snapshotShared.connect(snapshotShared); + Window.shareSnapshot(data.localPath); +} + +function snapshotShared(success) { + if(success) { + // for now just print status + print('snapshot uploaded and shared'); + } else { + // for now just print an error. + print('snapshot upload/share failed'); + } } function onClicked() { From f34030d276421e3e48be7afa6f837c04a09df994 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 9 Aug 2016 12:48:01 -0700 Subject: [PATCH 26/81] factor out metaverseBase to make it easier to repoint to a test server --- interface/resources/qml/AddressBarDialog.qml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 5883872c95..8590121fa0 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -55,6 +55,7 @@ Window { property var allStories: []; property int cardWidth: 200; property int cardHeight: 152; + property string metaverseBase: "https://metaverse.highfidelity.com/api/v1/"; function pastTime(timestamp) { // Answer a descriptive string timestamp = new Date(timestamp); var then = timestamp.getTime(), @@ -292,7 +293,7 @@ Window { } function getPlace(placeData, cb) { // cb(error, side-effected-placeData), after adding path, thumbnails, and description - var url = 'https://metaverse.highfidelity.com/api/v1/places/' + placeData.place_name; + var url = metaverseBase + 'places/' + placeData.place_name; getRequest(url, function (error, data) { if (handleError(url, error, data, cb)) { return; @@ -361,7 +362,7 @@ Window { 'sort_order=desc', 'page=' + pageNumber ]; - var url = 'https://metaverse.highfidelity.com/api/v1/domains/all?' + params.join('&'); + var url = metaverseBase + 'domains/all?' + params.join('&'); getRequest(url, function (error, data) { if (handleError(url, error, data, cb)) { return; @@ -389,7 +390,7 @@ Window { }); } function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model - var url = 'https://metaverse.highfidelity.com/api/v1/user_stories?page=' + pageNumber; + var url = metaverseBase + 'user_stories?page=' + pageNumber; getRequest(url, function (error, data) { if (handleError(url, error, data, cb)) { return; From 8457acd234c0ffd52ec7fbc781a9c33304de510c Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 9 Aug 2016 13:42:48 -0700 Subject: [PATCH 27/81] First guess on how to deal with place Fall back to host when there is no place. Probably better ideas, like specify the name and the type (a place, a domain if no place, etc...) will come soon. --- interface/src/ui/Snapshot.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 42ee4fa2f5..71e7d1a703 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -188,12 +188,17 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(contents, &jsonError); if (jsonError.error == QJsonParseError::NoError) { - QString thumbnail_url = doc.object().value("thumbnail_url").toString(); + QString thumbnailUrl = doc.object().value("thumbnail_url").toString(); + QString placeName = DependencyManager::get()->getPlaceName(); + if(placeName.isEmpty()) { + placeName = DependencyManager::get()->getHost(); + } // create json post data QJsonObject rootObject; QJsonObject userStoryObject; - userStoryObject.insert("thumbnail_url", thumbnail_url); + userStoryObject.insert("thumbnail_url", thumbnailUrl); + userStoryObject.insert("place_name", placeName); userStoryObject.insert("action", "snapshot"); rootObject.insert("user_story", userStoryObject); From 18990821df2f209bcc62441a1cf7424ad69ccac3 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 9 Aug 2016 14:15:49 -0700 Subject: [PATCH 28/81] Added 'path' to user stories, works now. yay --- interface/src/ui/Snapshot.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 71e7d1a703..e15182934b 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -189,16 +189,19 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { auto doc = QJsonDocument::fromJson(contents, &jsonError); if (jsonError.error == QJsonParseError::NoError) { QString thumbnailUrl = doc.object().value("thumbnail_url").toString(); - QString placeName = DependencyManager::get()->getPlaceName(); + auto addressManager = DependencyManager::get(); + QString placeName = addressManager->getPlaceName(); if(placeName.isEmpty()) { - placeName = DependencyManager::get()->getHost(); + placeName = addressManager->getHost(); } + QString currentPath = addressManager->currentPath(true); // create json post data QJsonObject rootObject; QJsonObject userStoryObject; userStoryObject.insert("thumbnail_url", thumbnailUrl); userStoryObject.insert("place_name", placeName); + userStoryObject.insert("path", currentPath); userStoryObject.insert("action", "snapshot"); rootObject.insert("user_story", userStoryObject); From d4f376879d38011e1cca5d4029404b19848e09d4 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 9 Aug 2016 15:05:23 -0700 Subject: [PATCH 29/81] PR feedback --- interface/src/ui/Snapshot.cpp | 17 +++-------------- scripts/system/snapshot.js | 2 +- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index e15182934b..f6bd8e2db9 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -148,8 +148,6 @@ void Snapshot::uploadSnapshot(const QString& filename) { const QString SNAPSHOT_UPLOAD_URL = "/api/v1/snapshots"; static SnapshotUploader uploader; - qDebug() << "uploading snapshot " << filename; - QFile* file = new QFile(filename); Q_ASSERT(file->exists()); file->open(QIODevice::ReadOnly); @@ -165,11 +163,7 @@ void Snapshot::uploadSnapshot(const QString& filename) { multiPart->append(imagePart); auto accountManager = DependencyManager::get(); - JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = &uploader; - callbackParams.jsonCallbackMethod = "uploadSuccess"; - callbackParams.errorCallbackReceiver = &uploader; - callbackParams.errorCallbackMethod = "uploadFailure"; + JSONCallbackParameters callbackParams(&uploader, "uploadSuccess", &uploader, "uploadFailure"); accountManager->sendRequest(SNAPSHOT_UPLOAD_URL, AccountManagerAuth::Required, @@ -191,7 +185,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QString thumbnailUrl = doc.object().value("thumbnail_url").toString(); auto addressManager = DependencyManager::get(); QString placeName = addressManager->getPlaceName(); - if(placeName.isEmpty()) { + if (placeName.isEmpty()) { placeName = addressManager->getHost(); } QString currentPath = addressManager->currentPath(true); @@ -206,11 +200,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { rootObject.insert("user_story", userStoryObject); auto accountManager = DependencyManager::get(); - JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = &uploader; - callbackParams.jsonCallbackMethod = "createStorySuccess"; - callbackParams.errorCallbackReceiver = &uploader; - callbackParams.errorCallbackMethod = "createStoryFailure"; + JSONCallbackParameters callbackParams(&uploader, "createStorySuccess", &uploader, "createStoryFailure"); accountManager->sendRequest(STORY_UPLOAD_URL, AccountManagerAuth::Required, @@ -219,7 +209,6 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonDocument(rootObject).toJson()); } else { - qDebug() << "Error parsing upload response: " << jsonError.errorString(); emit DependencyManager::get()->snapshotShared(false); } } diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 8a6b811085..ac92a06749 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -29,7 +29,7 @@ function confirmShare(data) { } function snapshotShared(success) { - if(success) { + if (success) { // for now just print status print('snapshot uploaded and shared'); } else { From 5715fd00f312877395d0feeb2ddc898b8f4b8403 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 9 Aug 2016 16:42:41 -0700 Subject: [PATCH 30/81] proper share dialog --- scripts/system/html/ShareSnapshot.html | 78 ++++++++++++++++++++++++++ scripts/system/snapshot.js | 28 +++++++-- 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 scripts/system/html/ShareSnapshot.html diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html new file mode 100644 index 0000000000..4691b7488c --- /dev/null +++ b/scripts/system/html/ShareSnapshot.html @@ -0,0 +1,78 @@ + + + Share + + + + + + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 61345254be..84019e68cc 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -21,11 +21,27 @@ var button = toolBar.addButton({ }); function confirmShare(data) { - if (!Window.confirm("Share snapshot " + data.localPath + "?")) { // This dialog will be more elaborate... - return; + var dialog = new OverlayWebWindow('Snapshot', Script.resolvePath("html/ShareSnapshot.html"), 800, 300); + function onMessage(message) { + if (message == 'ready') { // The window can now receive data from us. + dialog.emitScriptEvent(data); // Send it. + return; + } // Rest is confirmation processing + dialog.webEventReceived.disconnect(onMessage); // I'm not certain that this is necessary. If it is, what do we do on normal close? + dialog.close(); + dialog.deleteLater(); + message = message.filter(function (picture) { return picture.share; }); + if (message.length) { + print('sharing', message.map(function (picture) { return picture.localPath; }).join(', ')); + message.forEach(function (data) { + Window.shareSnapshot(data.localPath); + }); + } else { + print('declined to share', JSON.stringify(data)); + } } - Window.snapshotShared.connect(snapshotShared); - Window.shareSnapshot(data.localPath); + dialog.webEventReceived.connect(onMessage); + dialog.raise(); } function snapshotShared(success) { @@ -74,12 +90,14 @@ function resetButtons(path, notify) { button.writeProperty("defaultState", 1); button.writeProperty("hoverState", 3); Window.snapshotTaken.disconnect(resetButtons); - confirmShare({localPath: path}); + confirmShare([{localPath: path}]); } button.clicked.connect(onClicked); +Window.snapshotShared.connect(snapshotShared); Script.scriptEnding.connect(function () { toolBar.removeButton("snapshot"); button.clicked.disconnect(onClicked); + Window.snapshotShared.disconnect(snapshotShared); }); From f8f99721c3fdd606885db7285c592fbc6ca4e3b0 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 10 Aug 2016 14:25:00 -0700 Subject: [PATCH 31/81] Minor update to add details And, now reflects data-web's slightly updated json structure for returning info on an uploaded snapshot. --- interface/src/ui/Snapshot.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index f6bd8e2db9..beb86af1c4 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -182,7 +182,9 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(contents, &jsonError); if (jsonError.error == QJsonParseError::NoError) { - QString thumbnailUrl = doc.object().value("thumbnail_url").toString(); + auto dataObject = doc.object().value("data").toObject(); + QString thumbnailUrl = dataObject.value("thumbnail_url").toString(); + QString imageUrl = dataObject.value("image_url").toString(); auto addressManager = DependencyManager::get(); QString placeName = addressManager->getPlaceName(); if (placeName.isEmpty()) { @@ -193,6 +195,9 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { // create json post data QJsonObject rootObject; QJsonObject userStoryObject; + QJsonObject detailsObject; + detailsObject.insert("image_url", imageUrl); + userStoryObject.insert("details", detailsObject); userStoryObject.insert("thumbnail_url", thumbnailUrl); userStoryObject.insert("place_name", placeName); userStoryObject.insert("path", currentPath); From 5c15b520404ed1afb3e66d02bbd28803c5426719 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 10 Aug 2016 14:54:29 -0700 Subject: [PATCH 32/81] expose getPlaceName() as placename Nice for scripts to get the placename. Nice for us, anyways. --- libraries/networking/src/AddressManager.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 2e9f177137..0315d749c4 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -38,6 +38,7 @@ class AddressManager : public QObject, public Dependency { Q_PROPERTY(QString protocol READ getProtocol) Q_PROPERTY(QString hostname READ getHost) Q_PROPERTY(QString pathname READ currentPath) + Q_PROPERTY(QString placename READ getPlaceName) public: Q_INVOKABLE QString protocolVersion(); using PositionGetter = std::function; From 47dea0ea2b60ae1f06b940e5cbf30892e99da94f Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 10 Aug 2016 17:09:43 -0700 Subject: [PATCH 33/81] Hide sharing if not logged in, or in an accessible place The grand future will have option to login, and so on... --- scripts/system/html/ShareSnapshot.html | 25 ++++++++++++++++++++----- scripts/system/snapshot.js | 8 +++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html index 4691b7488c..d838a01e99 100644 --- a/scripts/system/html/ShareSnapshot.html +++ b/scripts/system/html/ShareSnapshot.html @@ -28,16 +28,29 @@ paths.push(data); } + function handleShareButtons(canShare) { + if (!canShare) { + // hide the share/do not share buttons + document.getElementById("sharing").innerHTML = "

You can share if you are logged in and in a public place"; + } + } window.onload = function () { // Something like the following will allow testing in a browser. //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { + // last element of list contains a bool for whether or not we can share stuff + var canShare = message.pop().canShare; + print("canShare:"+canShare+", message:" + message.toString()) + handleShareButtons(canShare); + + // rest are image paths which we add message.forEach(addImage); }); EventBridge.emitWebEvent('ready'); }); + }; // beware of bug: Cannot send objects at top level. (Nested in arrays is fine.) shareSelected = function() { @@ -64,11 +77,13 @@

-
- -
-
- +
+
+ +
+
+ +
diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 84019e68cc..0acb5d5acf 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -77,6 +77,10 @@ function onClicked() { }, SNAPSHOT_DELAY); } +function canShare() { + return Account.isLoggedIn() && Boolean(Window.location.placename); +} + function resetButtons(path, notify) { // show overlays if they were on if (resetOverlays) { @@ -90,7 +94,9 @@ function resetButtons(path, notify) { button.writeProperty("defaultState", 1); button.writeProperty("hoverState", 3); Window.snapshotTaken.disconnect(resetButtons); - confirmShare([{localPath: path}]); + + // last element in data array tells dialog whether we can share or not + confirmShare([ { localPath: path }, { canShare: canShare() } ]); } button.clicked.connect(onClicked); From c1edd008a4f77eeb26fff9f7f95b7dc727d032cc Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 11 Aug 2016 11:47:55 -0700 Subject: [PATCH 34/81] PR feedback --- scripts/system/html/ShareSnapshot.html | 25 +++++++++++++++---------- scripts/system/snapshot.js | 9 ++++----- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html index d838a01e99..d41da0fb7a 100644 --- a/scripts/system/html/ShareSnapshot.html +++ b/scripts/system/html/ShareSnapshot.html @@ -28,10 +28,16 @@ paths.push(data); } - function handleShareButtons(canShare) { - if (!canShare) { - // hide the share/do not share buttons - document.getElementById("sharing").innerHTML = "

You can share if you are logged in and in a public place"; + function handleShareButtons(shareMsg) { + if (!shareMsg.canShare) { + // this means you may or may not be logged in, but can't share + // because you are not in a public place. + document.getElementById("sharing").innerHTML = "

You can share if you are in a public place"; + } else if (!shareMsg.isLoggedIn) { + // this means you are in a public place, but can't share because + // you need to login. Soon we will just bring up the share dialog + // in this case (and maybe set a boolean here to inform the html) + document.getElementById("sharing").innerHTML = "

You can share if you are logged in"; } } window.onload = function () { @@ -41,9 +47,8 @@ // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { // last element of list contains a bool for whether or not we can share stuff - var canShare = message.pop().canShare; - print("canShare:"+canShare+", message:" + message.toString()) - handleShareButtons(canShare); + var shareMsg = message.pop(); + handleShareButtons(shareMsg); // rest are image paths which we add message.forEach(addImage); @@ -74,10 +79,10 @@

-
- -
+
+ +
diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 0acb5d5acf..4c0ba37ab3 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -77,10 +77,6 @@ function onClicked() { }, SNAPSHOT_DELAY); } -function canShare() { - return Account.isLoggedIn() && Boolean(Window.location.placename); -} - function resetButtons(path, notify) { // show overlays if they were on if (resetOverlays) { @@ -96,7 +92,10 @@ function resetButtons(path, notify) { Window.snapshotTaken.disconnect(resetButtons); // last element in data array tells dialog whether we can share or not - confirmShare([ { localPath: path }, { canShare: canShare() } ]); + confirmShare([ + { localPath: path }, + { canShare: Boolean(Window.location.placename), isLoggedIn: Account.isLoggedIn() } + ]); } button.clicked.connect(onClicked); From e3aae0e93f0e2a2d55bbbd6055efaf6c313e9a1b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 11 Aug 2016 16:23:01 -0700 Subject: [PATCH 35/81] Fix up layout, and adapt to recent changes in file structure --- scripts/system/html/ShareSnapshot.html | 71 +++++++++++++++++++------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html index 4691b7488c..fd865cfb7a 100644 --- a/scripts/system/html/ShareSnapshot.html +++ b/scripts/system/html/ShareSnapshot.html @@ -1,9 +1,48 @@ Share - + + - + - + -
-
-
+
+
+
-
+
-
+
-
+
-
+
+
-
From 747c43d0656d0ec33050d3079ab2c55c490e18a0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 11 Aug 2016 16:43:29 -0700 Subject: [PATCH 36/81] basic user story card --- interface/resources/qml/AddressBarDialog.qml | 63 ++++++++------ .../resources/qml/controls-uit/Button.qml | 2 + interface/resources/qml/hifi/Card.qml | 51 ++++++++++-- .../resources/qml/hifi/UserStoryCard.qml | 83 +++++++++++++++++++ 4 files changed, 167 insertions(+), 32 deletions(-) create mode 100644 interface/resources/qml/hifi/UserStoryCard.qml diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 8590121fa0..a65a228cf8 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -47,7 +47,16 @@ Window { } function goCard(card) { - addressLine.text = card.path; + if (useFeed) { + storyCard.imageUrl = card.imageUrl; //"http://howard-stearns.github.io/models/images/dancing-avatars.jpg"; + storyCard.userName = card.userName; + storyCard.placeName = card.placeName; + storyCard.actionPhrase = card.actionPhrase; + storyCard.timePhrase = card.timePhrase; + storyCard.visible = true; + return; + } + addressLine.text = card.hifiUrl; toggleOrGo(true); } property bool useFeed: false; @@ -56,26 +65,6 @@ Window { property int cardWidth: 200; property int cardHeight: 152; property string metaverseBase: "https://metaverse.highfidelity.com/api/v1/"; - function pastTime(timestamp) { // Answer a descriptive string - timestamp = new Date(timestamp); - var then = timestamp.getTime(), - now = Date.now(), - since = now - then, - ONE_MINUTE = 1000 * 60, - ONE_HOUR = ONE_MINUTE * 60, - hours = since / ONE_HOUR, - minutes = (hours % 1) * 60; - if (hours > 24) { - return timestamp.toDateString(); - } - if (hours > 1) { - return Math.floor(hours).toString() + ' hr ' + Math.floor(minutes) + ' min ago'; - } - if (minutes >= 2) { - return Math.floor(minutes).toString() + ' min ago'; - } - return 'about a minute ago'; - } AddressBarDialog { id: addressBarDialog @@ -88,6 +77,7 @@ Window { ListModel { id: suggestions } ListView { + id: scroll width: (3 * cardWidth) + (2 * hifi.layout.spacing); height: cardHeight; spacing: hifi.layout.spacing; @@ -103,10 +93,14 @@ Window { width: cardWidth; height: cardHeight; goFunction: goCard; - path: model.place_name + model.path; + userName: model.username; + placeName: model.place_name; + hifiUrl: model.place_name + model.path; + imageUrl: model.thumbnail_url; // This is wrong, but it will have to wait. thumbnail: model.thumbnail_url; - placeText: model.created_at ? "" : model.place_name; - usersText: model.created_at ? pastTime(model.created_at) : (model.online_users + ((model.online_users === 1) ? ' person' : ' people')); + action: model.action; + timestamp: model.created_at; + onlineUsers: model.online_users; hoverThunk: function () { ListView.view.currentIndex = index; } unhoverThunk: function () { ListView.view.currentIndex = -1; } } @@ -209,8 +203,24 @@ Window { } } } + + UserStoryCard { + id: storyCard; + visible: false; + visitPlace: function (hifiUrl) { + storyCard.visible = false; + addressLine.text = hifiUrl; + toggleOrGo(true); + }; + anchors { + verticalCenter: scroll.verticalCenter; + horizontalCenter: scroll.horizontalCenter; + verticalCenterOffset: 50; + } + } } + function toggleFeed () { useFeed = !useFeed; placesButton.buttonState = useFeed ? 0 : 1; @@ -318,13 +328,15 @@ Window { description = data.description || ""; return { place_name: name, + username: data.username || "", path: data.path || "", created_at: data.created_at || "", + action: data.action || "", thumbnail_url: data.thumbnail_url || "", tags: tags, description: description, - online_users: data.online_users, + online_users: data.online_users || 0, searchText: [name].concat(tags, description).join(' ').toUpperCase() } @@ -402,7 +414,6 @@ Window { {created_at: "8/3/2016", action: "snapshot", path: "/10077.4,4003.6,9972.56/0,-0.410351,0,0.911928", place_name: "Ventura", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/1f5/e6b/00-/thumbnail/hifi-place-1f5e6b00-2bf0-4319-b9ae-a2344a72354c.png?1454321596"} ]; } - var stories = data.user_stories.map(function (story) { // explicit single-argument function return makeModelData(story); }); diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml index 298943b551..59f8a63238 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controls-uit/Button.qml @@ -21,6 +21,8 @@ Original.Button { width: 120 height: hifi.dimensions.controlLineHeight + HifiConstants { id: hifi } + style: ButtonStyle { background: Rectangle { diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 5fae628283..278f9330d3 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -17,17 +17,54 @@ import QtGraphicalEffects 1.0 import "../styles-uit" Rectangle { + property string userName: ""; + property string placeName: ""; + property string action: ""; + property string timestamp: ""; + property string hifiUrl: ""; + property string thumbnail: defaultThumbnail; + property string imageUrl: ""; property var goFunction: null; - property alias image: lobby; - property alias placeText: place.text; - property alias usersText: users.text; + + property string timePhrase: pastTime(timestamp); + property string actionPhrase: makeActionPhrase(action); + property int onlineUsers: 0; + property bool isUserStory: userName && !onlineUsers; + 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: ""; HifiConstants { id: hifi } + function pastTime(timestamp) { // Answer a descriptive string + timestamp = new Date(timestamp); + var then = timestamp.getTime(), + now = Date.now(), + since = now - then, + ONE_MINUTE = 1000 * 60, + ONE_HOUR = ONE_MINUTE * 60, + hours = since / ONE_HOUR, + minutes = (hours % 1) * 60; + if (hours > 24) { + return timestamp.toDateString(); + } + if (hours > 1) { + return Math.floor(hours).toString() + ' hr ' + Math.floor(minutes) + ' min ago'; + } + if (minutes >= 2) { + return Math.floor(minutes).toString() + ' min ago'; + } + return 'about a minute ago'; + } + function makeActionPhrase(actionLabel) { + switch (actionLabel) { + case "snapshot": + return "took a snapshot"; + default: + return "unknown" + } + } + Image { id: lobby; width: parent.width; @@ -39,7 +76,7 @@ Rectangle { anchors.left: parent.left; onStatusChanged: { if (status == Image.Error) { - console.log("source: " + source + ": failed to load " + path); + console.log("source: " + source + ": failed to load " + hfiUrl); source = defaultThumbnail; } } @@ -71,6 +108,7 @@ Rectangle { } RalewaySemiBold { id: place; + text: isUserStory ? "" : placeName; color: hifi.colors.white; size: textSize; anchors { @@ -81,6 +119,7 @@ Rectangle { } RalewayRegular { id: users; + text: isUserStory ? timePhrase : (onlineUsers + ((onlineUsers === 1) ? ' person' : ' people')); size: textSizeSmall; color: hifi.colors.white; anchors { diff --git a/interface/resources/qml/hifi/UserStoryCard.qml b/interface/resources/qml/hifi/UserStoryCard.qml new file mode 100644 index 0000000000..4b6c37dae8 --- /dev/null +++ b/interface/resources/qml/hifi/UserStoryCard.qml @@ -0,0 +1,83 @@ +// +// UserStoryCard.qml +// qml/hifi +// +// Displays a clickable card representing a user story or destination. +// +// Created by Howard Stearns on 8/11/2016 +// Copyright 2016 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 Hifi 1.0 +import QtQuick 2.5 +import "../styles-uit" as HifiStyles +import "../controls-uit" as HifiControls + +Rectangle { + id: storyCard; + width: 500; + height: 330; + property string userName: "User"; + property string placeName: "Home"; + property string actionPhrase: "did something"; + property string timePhrase: ""; + property string hifiUrl: storyCard.placeName; + property string imageUrl: Qt.resolvedUrl("../images/default-domain.gif"); + property var visitPlace: function (ignore) { }; + color: "white"; + HifiStyles.HifiConstants { id: otherHifi } + MouseArea { + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + onClicked: storyCard.visible = false; + hoverEnabled: true; + // The content of the storyCard has buttons. For these to work without being + // blanketed by the MouseArea, they need to be children of the MouseArea. + Image { + id: storyImage; + source: storyCard.imageUrl; + width: storyCard.width - 100; + height: storyImage.width / 1.91; + fillMode: Image.PreserveAspectCrop; + anchors { + horizontalCenter: parent.horizontalCenter; + top: parent.top; + topMargin: 20; + } + } + HifiStyles.RalewayRegular { + id: storyLabel; + text: storyCard.userName + " " + storyCard.actionPhrase + " in " + storyCard.placeName + size: 20; + color: "black" + anchors { + horizontalCenter: storyImage.horizontalCenter; + top: storyImage.bottom; + topMargin: hifi.layout.spacing + } + } + HifiStyles.RalewayRegular { + text: storyCard.timePhrase; + size: 15; + color: "slategrey" + anchors { + verticalCenter: visitButton.verticalCenter; + left: storyImage.left; + } + } + HifiControls.Button { + id: visitButton; + text: "visit " + storyCard.placeName; + color: otherHifi.buttons.blue; + onClicked: visitPlace(storyCard.hifiUrl); + anchors { + top: storyLabel.bottom; + topMargin: hifi.layout.spacing; + right: storyImage.right; + } + } + } +} From cd9bd8dddfed767b57d7042085fb2cad45d4b658 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 11 Aug 2016 16:58:06 -0700 Subject: [PATCH 37/81] we don't need the dummy data any more --- interface/resources/qml/AddressBarDialog.qml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index a65a228cf8..9e1eb37dea 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -407,13 +407,6 @@ Window { if (handleError(url, error, data, cb)) { return; } - // FIXME: For debugging until we have real data - if (!data.user_stories.length) { - data.user_stories = [ - {created_at: "8/3/2016", action: "snapshot", path: "/4257.1,126.084,1335.45/0,-0.857368,0,0.514705", place_name: "SpiritMoving", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/c28/a26/f0-/thumbnail/hifi-place-c28a26f0-6991-4654-9c2b-e64228c06954.jpg?1456878797"}, - {created_at: "8/3/2016", action: "snapshot", path: "/10077.4,4003.6,9972.56/0,-0.410351,0,0.911928", place_name: "Ventura", thumbnail_url:"https://hifi-metaverse.s3-us-west-1.amazonaws.com/images/places/previews/1f5/e6b/00-/thumbnail/hifi-place-1f5e6b00-2bf0-4319-b9ae-a2344a72354c.png?1454321596"} - ]; - } var stories = data.user_stories.map(function (story) { // explicit single-argument function return makeModelData(story); }); From b4a43db025f25ea58e634aeb6b7a52e458f66972 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 11 Aug 2016 17:04:48 -0700 Subject: [PATCH 38/81] include user name in search text for user stories --- interface/resources/qml/AddressBarDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 9e1eb37dea..12fe44dcd8 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -324,7 +324,7 @@ Window { // ListModel elements will only ever have those properties that are defined by the first obj that is added. // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. var name = optionalPlaceName || data.place_name, - tags = data.tags || [data.action], + tags = data.tags || [data.action, data.username], description = data.description || ""; return { place_name: name, @@ -338,7 +338,7 @@ Window { description: description, online_users: data.online_users || 0, - searchText: [name].concat(tags, description).join(' ').toUpperCase() + searchText: [name].concat(tags, description || []).join(' ').toUpperCase() } } function mapDomainPlaces(domain, cb) { // cb(error, arrayOfDomainPlaceData) From 8708e2ac2bd45e28708c9668e68946402083c5be Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 12 Aug 2016 07:00:07 -0700 Subject: [PATCH 39/81] image url, in anticipation of having it in details --- interface/resources/qml/AddressBarDialog.qml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 12fe44dcd8..988f13a0a7 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -48,7 +48,7 @@ Window { function goCard(card) { if (useFeed) { - storyCard.imageUrl = card.imageUrl; //"http://howard-stearns.github.io/models/images/dancing-avatars.jpg"; + storyCard.imageUrl = card.imageUrl; storyCard.userName = card.userName; storyCard.placeName = card.placeName; storyCard.actionPhrase = card.actionPhrase; @@ -96,7 +96,7 @@ Window { userName: model.username; placeName: model.place_name; hifiUrl: model.place_name + model.path; - imageUrl: model.thumbnail_url; // This is wrong, but it will have to wait. + imageUrl: model.image_url; thumbnail: model.thumbnail_url; action: model.action; timestamp: model.created_at; @@ -325,14 +325,24 @@ Window { // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. var name = optionalPlaceName || data.place_name, tags = data.tags || [data.action, data.username], - description = data.description || ""; + description = data.description || "", + thumbnail_url = data.thumbnail_url || "", + image_url = thumbnail_url; + if (data.details) { + try { + image_url = JSON.parse(data.details).image_url || thumbnail_url; + } catch (e) { + console.log(name, "has bad details", data.details); + } + } return { place_name: name, username: data.username || "", path: data.path || "", created_at: data.created_at || "", action: data.action || "", - thumbnail_url: data.thumbnail_url || "", + thumbnail_url: thumbnail_url, + image_url: image_url, tags: tags, description: description, From 3408ad6f545e24ec35a289d8303d1bccc93121dc Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 12 Aug 2016 10:23:33 -0700 Subject: [PATCH 40/81] Wording change --- scripts/system/html/ShareSnapshot.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html index c054a3389a..00f805728c 100644 --- a/scripts/system/html/ShareSnapshot.html +++ b/scripts/system/html/ShareSnapshot.html @@ -71,12 +71,12 @@ if (!shareMsg.canShare) { // this means you may or may not be logged in, but can't share // because you are not in a public place. - document.getElementById("sharing").innerHTML = "

You can share if you are in a public place"; + document.getElementById("sharing").innerHTML = "

Snapshots can be shared when they're taken in shareable places."; } else if (!shareMsg.isLoggedIn) { // this means you are in a public place, but can't share because // you need to login. Soon we will just bring up the share dialog // in this case (and maybe set a boolean here to inform the html) - document.getElementById("sharing").innerHTML = "

You can share if you are logged in"; + document.getElementById("sharing").innerHTML = "

Snapshots can be shared when you are logged in."; } } window.onload = function () { From df0ceb17e83e5b5eec980fc30e830d0416f19b1f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 12 Aug 2016 11:09:22 -0700 Subject: [PATCH 41/81] send (hi res image) details as a json string --- interface/src/ui/Snapshot.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index beb86af1c4..b7cdb1a126 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -197,7 +197,8 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonObject userStoryObject; QJsonObject detailsObject; detailsObject.insert("image_url", imageUrl); - userStoryObject.insert("details", detailsObject); + QString pickledDetails = QJsonDocument(detailsObject).toJson(); + userStoryObject.insert("details", pickledDetails); userStoryObject.insert("thumbnail_url", thumbnailUrl); userStoryObject.insert("place_name", placeName); userStoryObject.insert("path", currentPath); From ae9af88db287df65ad6d61cb24b2d29d84b996f8 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 12 Aug 2016 11:28:35 -0700 Subject: [PATCH 42/81] Spacing and better size for HMD pictures. --- scripts/system/html/ShareSnapshot.html | 8 +++++--- scripts/system/snapshot.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html index 00f805728c..a30532bbc9 100644 --- a/scripts/system/html/ShareSnapshot.html +++ b/scripts/system/html/ShareSnapshot.html @@ -35,11 +35,13 @@ } .snapsection{ - width:95% !important; - padding-right:0px; + width:95% !important; + padding-right:0px; + } + div#sharing { + margin-top:50px; } - div.clear { clear: both; } diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 4c0ba37ab3..39bcc7fe90 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -21,7 +21,7 @@ var button = toolBar.addButton({ }); function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot', Script.resolvePath("html/ShareSnapshot.html"), 800, 300); + var dialog = new OverlayWebWindow('Snapshot', Script.resolvePath("html/ShareSnapshot.html"), 800, 470); function onMessage(message) { if (message == 'ready') { // The window can now receive data from us. dialog.emitScriptEvent(data); // Send it. From 1b62332058079cd0779657de3a12316b8b92688c Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 12 Aug 2016 11:50:11 -0700 Subject: [PATCH 43/81] don't show checkboxes on confirmation dialog when there's only one picture --- scripts/system/html/ShareSnapshot.html | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html index a30532bbc9..f9cc959ba9 100644 --- a/scripts/system/html/ShareSnapshot.html +++ b/scripts/system/html/ShareSnapshot.html @@ -46,7 +46,7 @@ @@ -120,16 +134,23 @@

-
-
- -
-
- -
-
- +
+
+
Would you like to share your pic in the Snapshots feed?
+
+
+
+
+ +
+
+
+ + + + +
diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 795e469c83..6c9132194e 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -20,24 +20,39 @@ var button = toolBar.addButton({ alpha: 0.9, }); +function showFeedWindow() { + DialogsManager.showFeed(); +} + +var openFeed, outstanding; function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot', Script.resolvePath("html/ShareSnapshot.html"), 800, 470); + var dialog = new OverlayWebWindow('Snapshot Review', Script.resolvePath("html/ShareSnapshot.html"), 800, 470); function onMessage(message) { - if (message == 'ready') { // The window can now receive data from us. + switch (message) { + case 'ready': dialog.emitScriptEvent(data); // Send it. - return; - } // Rest is confirmation processing - dialog.webEventReceived.disconnect(onMessage); // I'm not certain that this is necessary. If it is, what do we do on normal close? - dialog.close(); - dialog.deleteLater(); - message = message.filter(function (picture) { return picture.share; }); - if (message.length) { - print('sharing', message.map(function (picture) { return picture.localPath; }).join(', ')); - message.forEach(function (data) { - Window.shareSnapshot(data.localPath); + openFeed = false; + outstanding = 0; + break; + case 'openSettings': + Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); + break; + default: + dialog.webEventReceived.disconnect(onMessage); // I'm not certain that this is necessary. If it is, what do we do on normal close? + dialog.close(); + dialog.deleteLater(); + message.forEach(function (submessage) { + if (submessage.share) { + print('sharing', submessage.localPath); + outstanding++; + Window.shareSnapshot(submessage.localPath); + } else if (submessage.openFeed) { + openFeed = true; + } }); - } else { - print('declined to share', JSON.stringify(data)); + if (openFeed && !outstanding) { + showFeedWindow(); + } } } dialog.webEventReceived.connect(onMessage); @@ -51,7 +66,10 @@ function snapshotShared(success) { } else { // for now just print an error. print('snapshot upload/share failed'); - } + } + if ((--outstanding <= 0) && openFeed) { + showFeedWindow(); + } } function onClicked() { From e75f950ac70587a93471ae1fb0402a7b7d54acca Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 17 Aug 2016 14:12:40 -0700 Subject: [PATCH 54/81] Signal receivedHifiSchemeURL --- interface/src/Application.cpp | 1 + interface/src/Application.h | 1 + interface/src/ui/AddressBarDialog.cpp | 1 + interface/src/ui/AddressBarDialog.h | 1 + 4 files changed, 4 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4077fc0d57..340597f18a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4853,6 +4853,7 @@ bool Application::canAcceptURL(const QString& urlString) const { bool Application::acceptURL(const QString& urlString, bool defaultUpload) { if (urlString.startsWith(HIFI_URL_SCHEME)) { // this is a hifi URL - have the AddressManager handle it + emit receivedHifiSchemeURL(urlString); QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", Qt::AutoConnection, Q_ARG(const QString&, urlString)); return true; diff --git a/interface/src/Application.h b/interface/src/Application.h index 713febeb83..9fcce66f55 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -266,6 +266,7 @@ signals: void activeDisplayPluginChanged(); void uploadRequest(QString path); + void receivedHifiSchemeURL(QString path); public slots: QVector pasteEntities(float x, float y, float z); diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index e5b4262770..3e84c4c3c5 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -39,6 +39,7 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare _backEnabled = !(DependencyManager::get()->getBackStack().isEmpty()); _forwardEnabled = !(DependencyManager::get()->getForwardStack().isEmpty()); connect(DependencyManager::get().data(), &DialogsManager::setUseFeed, this, &AddressBarDialog::setUseFeed); + connect(qApp, &Application::receivedHifiSchemeURL, this, &AddressBarDialog::receivedHifiSchemeURL); } void AddressBarDialog::loadAddress(const QString& address, bool fromSuggestions) { diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index 10f0e0822a..68a12d4eb0 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -33,6 +33,7 @@ signals: void backEnabledChanged(); void forwardEnabledChanged(); void useFeedChanged(); + void receivedHifiSchemeURL(QString url); protected: void displayAddressOfflineMessage(); From d58cc8ddd99814557ed608c809c97c1c613c1335 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 17 Aug 2016 14:15:27 -0700 Subject: [PATCH 55/81] use html card from user_story id, and close things when receiveHifiSchemeUrl --- interface/resources/qml/AddressBarDialog.qml | 34 +- interface/resources/qml/AddressBarDialog.qml~ | 510 ++++++++++++++++++ interface/resources/qml/hifi/Card.qml | 1 + 3 files changed, 541 insertions(+), 4 deletions(-) create mode 100644 interface/resources/qml/AddressBarDialog.qml~ diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 832ba14013..7c2ef17590 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -15,6 +15,7 @@ import "styles" import "windows" import "hifi" import "hifi/toolbars" +import "controls-uit" as HifiControls Window { id: root @@ -46,15 +47,21 @@ Window { anchors.centerIn = parent; } + function resetAfterTeleport() { + console.log('hrs fixme got reset') + storyCardFrame.shown = root.shown = false; + } function goCard(card) { if (addressBarDialog.useFeed) { - storyCard.imageUrl = card.imageUrl; + /*storyCard.imageUrl = card.imageUrl; // hrs fixme storyCard.userName = card.userName; storyCard.placeName = card.placeName; storyCard.actionPhrase = card.actionPhrase; storyCard.timePhrase = card.timePhrase; storyCard.hifiUrl = card.hifiUrl; - storyCard.visible = true; + storyCard.visible = true;*/ + storyCard.url = metaverseBase + "user_stories/" + card.storyId + ".html"; + storyCardFrame.shown = true; return; } addressLine.text = card.hifiUrl; @@ -74,6 +81,7 @@ Window { onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; onForwardEnabledChanged: forwardArrow.buttonState = addressBarDialog.forwardEnabled ? 1 : 0; onUseFeedChanged: { updateFeedState(); } + onReceivedHifiSchemeURL: { console.log('hrs status change'); console.log('hrs status got', url); resetAfterTeleport(); } ListModel { id: suggestions } @@ -102,6 +110,7 @@ Window { action: model.action; timestamp: model.created_at; onlineUsers: model.online_users; + storyId: model.metaverseId; hoverThunk: function () { ListView.view.currentIndex = index; } unhoverThunk: function () { ListView.view.currentIndex = -1; } } @@ -114,6 +123,7 @@ Window { Image { // Just a visual indicator that the user can swipe the cards over to see more. source: "../images/Swipe-Icon-single.svg" width: 50; + visible: suggestions.count > 3; anchors { right: scroll.right; verticalCenter: scroll.verticalCenter; @@ -214,14 +224,27 @@ Window { } } - UserStoryCard { + /*UserStoryCard { // hrs fixme remove id: storyCard; visible: false; visitPlace: function (hifiUrl) { storyCard.visible = false; addressLine.text = hifiUrl; toggleOrGo(true); - }; + };*/ + Window { + width: 750; + height: 360; + HifiControls.WebView { + anchors.fill: parent; + id: storyCard; + } + id: storyCardFrame; + + shown: false; + destroyOnCloseButton: false; + pinnable: false; + anchors { verticalCenter: scroll.verticalCenter; horizontalCenter: scroll.horizontalCenter; @@ -348,6 +371,7 @@ Window { console.log(name, "has bad details", data.details); } } + console.log('hrs fixme id', data.id, typeof data.id, JSON.stringify(data)); return { place_name: name, username: data.username || "", @@ -357,6 +381,8 @@ Window { thumbnail_url: thumbnail_url, image_url: image_url, + metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity. + tags: tags, description: description, online_users: data.online_users || 0, diff --git a/interface/resources/qml/AddressBarDialog.qml~ b/interface/resources/qml/AddressBarDialog.qml~ new file mode 100644 index 0000000000..5da49a1ee1 --- /dev/null +++ b/interface/resources/qml/AddressBarDialog.qml~ @@ -0,0 +1,510 @@ +// +// AddressBarDialog.qml +// +// Created by Austin Davis on 2015/04/14 +// Copyright 2015 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 Hifi 1.0 +import QtQuick 2.4 +import "controls" +import "styles" +import "windows" +import "hifi" +import "hifi/toolbars" + +Window { + id: root + HifiConstants { id: hifi } + + objectName: "AddressBarDialog" + frame: HiddenFrame {} + hideBackground: true + + shown: false + destroyOnHidden: false + resizable: false + scale: 1.25 // Make this dialog a little larger than normal + + width: addressBarDialog.implicitWidth + height: addressBarDialog.implicitHeight + + onShownChanged: addressBarDialog.observeShownChanged(shown); + Component.onCompleted: { + root.parentChanged.connect(center); + center(); + } + Component.onDestruction: { + root.parentChanged.disconnect(center); + } + + function center() { + // Explicitly center in order to avoid warnings at shutdown + anchors.centerIn = parent; + } + + function goCard(card) { + if (addressBarDialog.useFeed) { + storyCard.imageUrl = card.imageUrl; + storyCard.userName = card.userName; + storyCard.placeName = card.placeName; + storyCard.actionPhrase = card.actionPhrase; + storyCard.timePhrase = card.timePhrase; + storyCard.hifiUrl = card.hifiUrl; + storyCard.visible = true; + return; + } + addressLine.text = card.hifiUrl; + toggleOrGo(true); + } + property var allPlaces: []; + property var allStories: []; + property int cardWidth: 200; + property int cardHeight: 152; + //property string metaverseBase: "https://metaverse.highfidelity.com/api/v1/"; + property string metaverseBase: "http://10.0.0.242:3000/api/v1/"; + + AddressBarDialog { + id: addressBarDialog + implicitWidth: backgroundImage.width + implicitHeight: backgroundImage.height + // The buttons have their button state changed on hover, so we have to manually fix them up here + onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; + onForwardEnabledChanged: forwardArrow.buttonState = addressBarDialog.forwardEnabled ? 1 : 0; + onUseFeedChanged: { updateFeedState(); } + + ListModel { id: suggestions } + + ListView { + id: scroll + width: backgroundImage.width; + height: cardHeight; + spacing: hifi.layout.spacing; + clip: true; + anchors { + bottom: backgroundImage.top; + bottomMargin: 2 * hifi.layout.spacing; + horizontalCenter: backgroundImage.horizontalCenter + } + model: suggestions; + orientation: ListView.Horizontal; + delegate: Card { + width: cardWidth; + height: cardHeight; + goFunction: goCard; + userName: model.username; + placeName: model.place_name; + hifiUrl: model.place_name + model.path; + imageUrl: model.image_url; + thumbnail: model.thumbnail_url; + action: model.action; + timestamp: model.created_at; + onlineUsers: model.online_users; + hoverThunk: function () { ListView.view.currentIndex = index; } + unhoverThunk: function () { ListView.view.currentIndex = -1; } + } + highlightMoveDuration: -1; + highlightMoveVelocity: -1; + highlight: Rectangle { color: "transparent"; border.width: 4; border.color: "#1DB5ED"; z: 1; } + leftMargin: 50; // Start the first item over be about the same amount as the last item peeks through on the other side. + rightMargin: 50; + } + Image { // Just a visual indicator that the user can swipe the cards over to see more. + source: "../images/Swipe-Icon-single.svg" + width: 50; + anchors { + right: scroll.right; + verticalCenter: scroll.verticalCenter; + } + } + Image { + id: backgroundImage + source: "../images/address-bar.svg" + width: 576 * root.scale + height: 80 * root.scale + property int inputAreaHeight: 56.0 * root.scale // Height of the background's input area + property int inputAreaStep: (height - inputAreaHeight) / 2 + + ToolbarButton { + id: homeButton + imageURL: "../images/home.svg" + buttonState: 1 + defaultState: 1 + hoverState: 2 + onClicked: addressBarDialog.loadHome(); + anchors { + left: parent.left + leftMargin: homeButton.width / 2 + verticalCenter: parent.verticalCenter + } + } + + ToolbarButton { + id: backArrow; + imageURL: "../images/backward.svg"; + hoverState: addressBarDialog.backEnabled ? 2 : 0; + defaultState: addressBarDialog.backEnabled ? 1 : 0; + buttonState: addressBarDialog.backEnabled ? 1 : 0; + onClicked: addressBarDialog.loadBack(); + anchors { + left: homeButton.right + verticalCenter: parent.verticalCenter + } + } + ToolbarButton { + id: forwardArrow; + imageURL: "../images/forward.svg"; + hoverState: addressBarDialog.forwardEnabled ? 2 : 0; + defaultState: addressBarDialog.forwardEnabled ? 1 : 0; + buttonState: addressBarDialog.forwardEnabled ? 1 : 0; + onClicked: addressBarDialog.loadForward(); + anchors { + left: backArrow.right + verticalCenter: parent.verticalCenter + } + } + + // FIXME replace with TextField + TextInput { + id: addressLine + focus: true + anchors { + top: parent.top + bottom: parent.bottom + left: forwardArrow.right + right: placesButton.left + leftMargin: forwardArrow.width + rightMargin: placesButton.width + topMargin: parent.inputAreaStep + hifi.layout.spacing + bottomMargin: parent.inputAreaStep + hifi.layout.spacing + } + font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75 + helperText: "Go to: place, @user, /path, network address" + helperPixelSize: font.pixelSize * 0.75 + helperItalic: true + onTextChanged: filterChoicesByText() + } + // These two are radio buttons. + ToolbarButton { + id: placesButton + imageURL: "../images/places.svg" + buttonState: 1 + defaultState: addressBarDialog.useFeed ? 0 : 1; + hoverState: addressBarDialog.useFeed ? 2 : -1; + onClicked: addressBarDialog.useFeed ? toggleFeed() : identity() + anchors { + right: feedButton.left; + bottom: addressLine.bottom; + } + } + ToolbarButton { + id: feedButton; + imageURL: "../images/snap-feed.svg"; + buttonState: 0 + defaultState: addressBarDialog.useFeed ? 1 : 0; + hoverState: addressBarDialog.useFeed ? -1 : 2; + onClicked: addressBarDialog.useFeed ? identity() : toggleFeed(); + anchors { + right: parent.right; + bottom: addressLine.bottom; + rightMargin: feedButton.width / 2 + } + } + } + + UserStoryCard { + id: storyCard; + visible: false; + visitPlace: function (hifiUrl) { + storyCard.visible = false; + addressLine.text = hifiUrl; + toggleOrGo(true); + }; + anchors { + verticalCenter: scroll.verticalCenter; + horizontalCenter: scroll.horizontalCenter; + verticalCenterOffset: 50; + } + } + } + + + function toggleFeed() { + addressBarDialog.useFeed = !addressBarDialog.useFeed; + updateFeedState(); + } + function updateFeedState() { + placesButton.buttonState = addressBarDialog.useFeed ? 0 : 1; + feedButton.buttonState = addressBarDialog.useFeed ? 1 : 0; + filterChoicesByText(); + } + function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. + // TODO: make available to other .qml. + var request = new XMLHttpRequest(); + // QT bug: apparently doesn't handle onload. Workaround using readyState. + request.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (request.readyState >= READY_STATE_DONE) { + var error = (request.status !== HTTP_OK) && request.status.toString() + ':' + request.statusText, + response = !error && request.responseText, + contentType = !error && request.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + cb(error, response); + } + }; + request.open("GET", url, true); + request.send(); + } + 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(null, results); + } + 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 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 getPlace(placeData, cb) { // cb(error, side-effected-placeData), after adding path, thumbnails, and description + var url = metaverseBase + 'places/' + placeData.place_name; + getRequest(url, function (error, data) { + if (handleError(url, error, data, cb)) { + return; + } + var place = data.data.place, previews = place.previews; + placeData.path = place.path; + if (previews && previews.thumbnail) { + placeData.thumbnail_url = previews.thumbnail; + } + if (place.description) { + placeData.description = place.description; + placeData.searchText += ' ' + place.description.toUpperCase(); + } + cb(error, placeData); + }); + } + function makeModelData(data, optionalPlaceName) { // create a new obj from data + // ListModel elements will only ever have those properties that are defined by the first obj that is added. + // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. + var name = optionalPlaceName || data.place_name, + tags = data.tags || [data.action, data.username], + description = data.description || "", + thumbnail_url = data.thumbnail_url || "", + image_url = thumbnail_url; + if (data.details) { + try { + image_url = JSON.parse(data.details).image_url || thumbnail_url; + } catch (e) { + console.log(name, "has bad details", data.details); + } + } + return { + place_name: name, + username: data.username || "", + path: data.path || "", + created_at: data.created_at || "", + action: data.action || "", + thumbnail_url: thumbnail_url, + image_url: image_url, + + tags: tags, + description: description, + online_users: data.online_users || 0, + + searchText: [name].concat(tags, description || []).join(' ').toUpperCase() + } + } + function mapDomainPlaces(domain, cb) { // cb(error, arrayOfDomainPlaceData) + function addPlace(name, icb) { + getPlace(makeModelData(domain, name), icb); + } + // 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) { + if (addressBarDialog.useFeed) { + return true; + } + return (place.place_name !== AddressManager.hostname) // Not our entry, but do show other entry points to current domain. + && place.thumbnail_url + && 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. + // 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 + ]; + var url = metaverseBase + 'domains/all?' + params.join('&'); + getRequest(url, function (error, data) { + if (handleError(url, error, data, cb)) { + return; + } + 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 && !addressBarDialog.useFeed) { // 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(); + }); + }); + } + function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model + var url = metaverseBase + 'user_stories?page=' + pageNumber; + getRequest(url, function (error, data) { + if (handleError(url, error, data, cb)) { + return; + } + var stories = data.user_stories.map(function (story) { // explicit single-argument function + return makeModelData(story); + }); + allStories = allStories.concat(stories); + if (!addressLine.text && addressBarDialog.useFeed) { // Don't add if the user is already filtering + stories.forEach(function (story) { + suggestions.append(story); + }); + } + if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now + return getUserStoryPage(pageNumber + 1, cb); + } + cb(); + }); + } + function filterChoicesByText() { + suggestions.clear(); + var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity), + data = addressBarDialog.useFeed ? allStories : allPlaces; + function matches(place) { + if (!words.length) { + return suggestable(place); + } + return words.every(function (word) { + return place.searchText.indexOf(word) >= 0; + }); + } + data.forEach(function (place) { + if (matches(place)) { + suggestions.append(place); + } + }); + } + + function fillDestinations() { + allPlaces = []; + allStories = []; + suggestions.clear(); + getDomainPage(1, function (error) { + console.log('domain query', error || 'ok', allPlaces.length); + }); + getUserStoryPage(1, function (error) { + console.log('user stories query', error || 'ok', allStories.length); + }); + } + + onVisibleChanged: { + if (visible) { + addressLine.forceActiveFocus() + fillDestinations(); + } else { + addressLine.text = "" + } + } + + function toggleOrGo(fromSuggestions) { + if (addressLine.text !== "") { + addressBarDialog.loadAddress(addressLine.text, fromSuggestions) + } + root.shown = false; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + root.shown = false + event.accepted = true + break + case Qt.Key_Enter: + case Qt.Key_Return: + toggleOrGo() + event.accepted = true + break + } + } +} diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 278f9330d3..fe162349c1 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -25,6 +25,7 @@ Rectangle { property string thumbnail: defaultThumbnail; property string imageUrl: ""; property var goFunction: null; + property string storyId: ""; property string timePhrase: pastTime(timestamp); property string actionPhrase: makeActionPhrase(action); From 0f415abd6b36fa279ce26137e3f901fec611787f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 17 Aug 2016 16:56:52 -0700 Subject: [PATCH 56/81] dual purpose address bar - switchable between html and qml --- interface/resources/qml/AddressBarDialog.qml | 43 +- interface/resources/qml/AddressBarDialog.qml~ | 510 ------------------ 2 files changed, 26 insertions(+), 527 deletions(-) delete mode 100644 interface/resources/qml/AddressBarDialog.qml~ diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7c2ef17590..089bec07f4 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -48,20 +48,22 @@ Window { } function resetAfterTeleport() { - console.log('hrs fixme got reset') storyCardFrame.shown = root.shown = false; } function goCard(card) { if (addressBarDialog.useFeed) { - /*storyCard.imageUrl = card.imageUrl; // hrs fixme - storyCard.userName = card.userName; - storyCard.placeName = card.placeName; - storyCard.actionPhrase = card.actionPhrase; - storyCard.timePhrase = card.timePhrase; - storyCard.hifiUrl = card.hifiUrl; - storyCard.visible = true;*/ - storyCard.url = metaverseBase + "user_stories/" + card.storyId + ".html"; - storyCardFrame.shown = true; + if (useHTML) { + storyCardHTML.url = metaverseBase + "user_stories/" + card.storyId + ".html"; + storyCardFrame.shown = true; + } else { + storyCardQML.imageUrl = card.imageUrl; + storyCardQML.userName = card.userName; + storyCardQML.placeName = card.placeName; + storyCardQML.actionPhrase = card.actionPhrase; + storyCardQML.timePhrase = card.timePhrase; + storyCardQML.hifiUrl = card.hifiUrl; + storyCardQML.visible = true; + } return; } addressLine.text = card.hifiUrl; @@ -72,6 +74,8 @@ Window { property int cardWidth: 200; property int cardHeight: 152; property string metaverseBase: "https://metaverse.highfidelity.com/api/v1/"; + //property string metaverseBase: "http://10.0.0.241:3000/api/v1/"; + property bool useHTML: false; // fixme: remove this and all false branches after the server is updated AddressBarDialog { id: addressBarDialog @@ -81,7 +85,7 @@ Window { onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; onForwardEnabledChanged: forwardArrow.buttonState = addressBarDialog.forwardEnabled ? 1 : 0; onUseFeedChanged: { updateFeedState(); } - onReceivedHifiSchemeURL: { console.log('hrs status change'); console.log('hrs status got', url); resetAfterTeleport(); } + onReceivedHifiSchemeURL: resetAfterTeleport(); ListModel { id: suggestions } @@ -224,20 +228,26 @@ Window { } } - /*UserStoryCard { // hrs fixme remove - id: storyCard; + UserStoryCard { + id: storyCardQML; visible: false; visitPlace: function (hifiUrl) { - storyCard.visible = false; + storyCardQML.visible = false; addressLine.text = hifiUrl; toggleOrGo(true); - };*/ + }; + anchors { + verticalCenter: scroll.verticalCenter; + horizontalCenter: scroll.horizontalCenter; + verticalCenterOffset: 50; + } + } Window { width: 750; height: 360; HifiControls.WebView { anchors.fill: parent; - id: storyCard; + id: storyCardHTML; } id: storyCardFrame; @@ -371,7 +381,6 @@ Window { console.log(name, "has bad details", data.details); } } - console.log('hrs fixme id', data.id, typeof data.id, JSON.stringify(data)); return { place_name: name, username: data.username || "", diff --git a/interface/resources/qml/AddressBarDialog.qml~ b/interface/resources/qml/AddressBarDialog.qml~ deleted file mode 100644 index 5da49a1ee1..0000000000 --- a/interface/resources/qml/AddressBarDialog.qml~ +++ /dev/null @@ -1,510 +0,0 @@ -// -// AddressBarDialog.qml -// -// Created by Austin Davis on 2015/04/14 -// Copyright 2015 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 Hifi 1.0 -import QtQuick 2.4 -import "controls" -import "styles" -import "windows" -import "hifi" -import "hifi/toolbars" - -Window { - id: root - HifiConstants { id: hifi } - - objectName: "AddressBarDialog" - frame: HiddenFrame {} - hideBackground: true - - shown: false - destroyOnHidden: false - resizable: false - scale: 1.25 // Make this dialog a little larger than normal - - width: addressBarDialog.implicitWidth - height: addressBarDialog.implicitHeight - - onShownChanged: addressBarDialog.observeShownChanged(shown); - Component.onCompleted: { - root.parentChanged.connect(center); - center(); - } - Component.onDestruction: { - root.parentChanged.disconnect(center); - } - - function center() { - // Explicitly center in order to avoid warnings at shutdown - anchors.centerIn = parent; - } - - function goCard(card) { - if (addressBarDialog.useFeed) { - storyCard.imageUrl = card.imageUrl; - storyCard.userName = card.userName; - storyCard.placeName = card.placeName; - storyCard.actionPhrase = card.actionPhrase; - storyCard.timePhrase = card.timePhrase; - storyCard.hifiUrl = card.hifiUrl; - storyCard.visible = true; - return; - } - addressLine.text = card.hifiUrl; - toggleOrGo(true); - } - property var allPlaces: []; - property var allStories: []; - property int cardWidth: 200; - property int cardHeight: 152; - //property string metaverseBase: "https://metaverse.highfidelity.com/api/v1/"; - property string metaverseBase: "http://10.0.0.242:3000/api/v1/"; - - AddressBarDialog { - id: addressBarDialog - implicitWidth: backgroundImage.width - implicitHeight: backgroundImage.height - // The buttons have their button state changed on hover, so we have to manually fix them up here - onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; - onForwardEnabledChanged: forwardArrow.buttonState = addressBarDialog.forwardEnabled ? 1 : 0; - onUseFeedChanged: { updateFeedState(); } - - ListModel { id: suggestions } - - ListView { - id: scroll - width: backgroundImage.width; - height: cardHeight; - spacing: hifi.layout.spacing; - clip: true; - anchors { - bottom: backgroundImage.top; - bottomMargin: 2 * hifi.layout.spacing; - horizontalCenter: backgroundImage.horizontalCenter - } - model: suggestions; - orientation: ListView.Horizontal; - delegate: Card { - width: cardWidth; - height: cardHeight; - goFunction: goCard; - userName: model.username; - placeName: model.place_name; - hifiUrl: model.place_name + model.path; - imageUrl: model.image_url; - thumbnail: model.thumbnail_url; - action: model.action; - timestamp: model.created_at; - onlineUsers: model.online_users; - hoverThunk: function () { ListView.view.currentIndex = index; } - unhoverThunk: function () { ListView.view.currentIndex = -1; } - } - highlightMoveDuration: -1; - highlightMoveVelocity: -1; - highlight: Rectangle { color: "transparent"; border.width: 4; border.color: "#1DB5ED"; z: 1; } - leftMargin: 50; // Start the first item over be about the same amount as the last item peeks through on the other side. - rightMargin: 50; - } - Image { // Just a visual indicator that the user can swipe the cards over to see more. - source: "../images/Swipe-Icon-single.svg" - width: 50; - anchors { - right: scroll.right; - verticalCenter: scroll.verticalCenter; - } - } - Image { - id: backgroundImage - source: "../images/address-bar.svg" - width: 576 * root.scale - height: 80 * root.scale - property int inputAreaHeight: 56.0 * root.scale // Height of the background's input area - property int inputAreaStep: (height - inputAreaHeight) / 2 - - ToolbarButton { - id: homeButton - imageURL: "../images/home.svg" - buttonState: 1 - defaultState: 1 - hoverState: 2 - onClicked: addressBarDialog.loadHome(); - anchors { - left: parent.left - leftMargin: homeButton.width / 2 - verticalCenter: parent.verticalCenter - } - } - - ToolbarButton { - id: backArrow; - imageURL: "../images/backward.svg"; - hoverState: addressBarDialog.backEnabled ? 2 : 0; - defaultState: addressBarDialog.backEnabled ? 1 : 0; - buttonState: addressBarDialog.backEnabled ? 1 : 0; - onClicked: addressBarDialog.loadBack(); - anchors { - left: homeButton.right - verticalCenter: parent.verticalCenter - } - } - ToolbarButton { - id: forwardArrow; - imageURL: "../images/forward.svg"; - hoverState: addressBarDialog.forwardEnabled ? 2 : 0; - defaultState: addressBarDialog.forwardEnabled ? 1 : 0; - buttonState: addressBarDialog.forwardEnabled ? 1 : 0; - onClicked: addressBarDialog.loadForward(); - anchors { - left: backArrow.right - verticalCenter: parent.verticalCenter - } - } - - // FIXME replace with TextField - TextInput { - id: addressLine - focus: true - anchors { - top: parent.top - bottom: parent.bottom - left: forwardArrow.right - right: placesButton.left - leftMargin: forwardArrow.width - rightMargin: placesButton.width - topMargin: parent.inputAreaStep + hifi.layout.spacing - bottomMargin: parent.inputAreaStep + hifi.layout.spacing - } - font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75 - helperText: "Go to: place, @user, /path, network address" - helperPixelSize: font.pixelSize * 0.75 - helperItalic: true - onTextChanged: filterChoicesByText() - } - // These two are radio buttons. - ToolbarButton { - id: placesButton - imageURL: "../images/places.svg" - buttonState: 1 - defaultState: addressBarDialog.useFeed ? 0 : 1; - hoverState: addressBarDialog.useFeed ? 2 : -1; - onClicked: addressBarDialog.useFeed ? toggleFeed() : identity() - anchors { - right: feedButton.left; - bottom: addressLine.bottom; - } - } - ToolbarButton { - id: feedButton; - imageURL: "../images/snap-feed.svg"; - buttonState: 0 - defaultState: addressBarDialog.useFeed ? 1 : 0; - hoverState: addressBarDialog.useFeed ? -1 : 2; - onClicked: addressBarDialog.useFeed ? identity() : toggleFeed(); - anchors { - right: parent.right; - bottom: addressLine.bottom; - rightMargin: feedButton.width / 2 - } - } - } - - UserStoryCard { - id: storyCard; - visible: false; - visitPlace: function (hifiUrl) { - storyCard.visible = false; - addressLine.text = hifiUrl; - toggleOrGo(true); - }; - anchors { - verticalCenter: scroll.verticalCenter; - horizontalCenter: scroll.horizontalCenter; - verticalCenterOffset: 50; - } - } - } - - - function toggleFeed() { - addressBarDialog.useFeed = !addressBarDialog.useFeed; - updateFeedState(); - } - function updateFeedState() { - placesButton.buttonState = addressBarDialog.useFeed ? 0 : 1; - feedButton.buttonState = addressBarDialog.useFeed ? 1 : 0; - filterChoicesByText(); - } - function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. - // TODO: make available to other .qml. - var request = new XMLHttpRequest(); - // QT bug: apparently doesn't handle onload. Workaround using readyState. - request.onreadystatechange = function () { - var READY_STATE_DONE = 4; - var HTTP_OK = 200; - if (request.readyState >= READY_STATE_DONE) { - var error = (request.status !== HTTP_OK) && request.status.toString() + ':' + request.statusText, - response = !error && request.responseText, - contentType = !error && request.getResponseHeader('content-type'); - if (!error && contentType.indexOf('application/json') === 0) { - try { - response = JSON.parse(response); - } catch (e) { - error = e; - } - } - cb(error, response); - } - }; - request.open("GET", url, true); - request.send(); - } - 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(null, results); - } - 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 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 getPlace(placeData, cb) { // cb(error, side-effected-placeData), after adding path, thumbnails, and description - var url = metaverseBase + 'places/' + placeData.place_name; - getRequest(url, function (error, data) { - if (handleError(url, error, data, cb)) { - return; - } - var place = data.data.place, previews = place.previews; - placeData.path = place.path; - if (previews && previews.thumbnail) { - placeData.thumbnail_url = previews.thumbnail; - } - if (place.description) { - placeData.description = place.description; - placeData.searchText += ' ' + place.description.toUpperCase(); - } - cb(error, placeData); - }); - } - function makeModelData(data, optionalPlaceName) { // create a new obj from data - // ListModel elements will only ever have those properties that are defined by the first obj that is added. - // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. - var name = optionalPlaceName || data.place_name, - tags = data.tags || [data.action, data.username], - description = data.description || "", - thumbnail_url = data.thumbnail_url || "", - image_url = thumbnail_url; - if (data.details) { - try { - image_url = JSON.parse(data.details).image_url || thumbnail_url; - } catch (e) { - console.log(name, "has bad details", data.details); - } - } - return { - place_name: name, - username: data.username || "", - path: data.path || "", - created_at: data.created_at || "", - action: data.action || "", - thumbnail_url: thumbnail_url, - image_url: image_url, - - tags: tags, - description: description, - online_users: data.online_users || 0, - - searchText: [name].concat(tags, description || []).join(' ').toUpperCase() - } - } - function mapDomainPlaces(domain, cb) { // cb(error, arrayOfDomainPlaceData) - function addPlace(name, icb) { - getPlace(makeModelData(domain, name), icb); - } - // 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) { - if (addressBarDialog.useFeed) { - return true; - } - return (place.place_name !== AddressManager.hostname) // Not our entry, but do show other entry points to current domain. - && place.thumbnail_url - && 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. - // 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 - ]; - var url = metaverseBase + 'domains/all?' + params.join('&'); - getRequest(url, function (error, data) { - if (handleError(url, error, data, cb)) { - return; - } - 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 && !addressBarDialog.useFeed) { // 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(); - }); - }); - } - function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model - var url = metaverseBase + 'user_stories?page=' + pageNumber; - getRequest(url, function (error, data) { - if (handleError(url, error, data, cb)) { - return; - } - var stories = data.user_stories.map(function (story) { // explicit single-argument function - return makeModelData(story); - }); - allStories = allStories.concat(stories); - if (!addressLine.text && addressBarDialog.useFeed) { // Don't add if the user is already filtering - stories.forEach(function (story) { - suggestions.append(story); - }); - } - if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now - return getUserStoryPage(pageNumber + 1, cb); - } - cb(); - }); - } - function filterChoicesByText() { - suggestions.clear(); - var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity), - data = addressBarDialog.useFeed ? allStories : allPlaces; - function matches(place) { - if (!words.length) { - return suggestable(place); - } - return words.every(function (word) { - return place.searchText.indexOf(word) >= 0; - }); - } - data.forEach(function (place) { - if (matches(place)) { - suggestions.append(place); - } - }); - } - - function fillDestinations() { - allPlaces = []; - allStories = []; - suggestions.clear(); - getDomainPage(1, function (error) { - console.log('domain query', error || 'ok', allPlaces.length); - }); - getUserStoryPage(1, function (error) { - console.log('user stories query', error || 'ok', allStories.length); - }); - } - - onVisibleChanged: { - if (visible) { - addressLine.forceActiveFocus() - fillDestinations(); - } else { - addressLine.text = "" - } - } - - function toggleOrGo(fromSuggestions) { - if (addressLine.text !== "") { - addressBarDialog.loadAddress(addressLine.text, fromSuggestions) - } - root.shown = false; - } - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Escape: - case Qt.Key_Back: - root.shown = false - event.accepted = true - break - case Qt.Key_Enter: - case Qt.Key_Return: - toggleOrGo() - event.accepted = true - break - } - } -} From 5e8722ccf549a1f2b48c93d3dfc8d2c06787e773 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 18 Aug 2016 12:58:00 -0700 Subject: [PATCH 57/81] persist "open feed after" checkbox setting --- scripts/system/html/ShareSnapshot.html | 7 +++++-- scripts/system/snapshot.js | 27 ++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/ShareSnapshot.html index 9098b30121..9b9403cf78 100644 --- a/scripts/system/html/ShareSnapshot.html +++ b/scripts/system/html/ShareSnapshot.html @@ -84,6 +84,9 @@ } function handleShareButtons(shareMsg) { + var openFeed = document.getElementById('openFeed'); + openFeed.checked = shareMsg.openFeedAfterShare; + openFeed.onchange = function () { EventBridge.emitWebEvent(openFeed.checked ? 'setOpenFeedTrue' : 'setOpenFeedFalse'); }; if (!shareMsg.canShare) { // this means you may or may not be logged in, but can't share // because you are not in a public place. @@ -116,10 +119,10 @@ }; // beware of bug: Cannot send objects at top level. (Nested in arrays is fine.) shareSelected = function () { - EventBridge.emitWebEvent(paths.concat({openFeed: document.getElementById("openFeed").checked})); + EventBridge.emitWebEvent(paths); }; doNotShare = function () { - EventBridge.emitWebEvent([{openFeed: document.getElementById("openFeed").checked}]); + EventBridge.emitWebEvent([]); }; snapshotSettings = function () { EventBridge.emitWebEvent("openSettings"); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 6c9132194e..8e99783ff6 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -20,23 +20,32 @@ var button = toolBar.addButton({ alpha: 0.9, }); +function shouldOpenFeedAfterShare() { + var persisted = Settings.getValue('openFeedAfterShare', true); // might answer true, false, "true", or "false" + return persisted && (persisted !== 'false'); +} function showFeedWindow() { DialogsManager.showFeed(); } -var openFeed, outstanding; +var outstanding; function confirmShare(data) { var dialog = new OverlayWebWindow('Snapshot Review', Script.resolvePath("html/ShareSnapshot.html"), 800, 470); function onMessage(message) { switch (message) { case 'ready': dialog.emitScriptEvent(data); // Send it. - openFeed = false; outstanding = 0; break; case 'openSettings': Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); break; + case 'setOpenFeedFalse': + Settings.setValue('openFeedAfterShare', false) + break; + case 'setOpenFeedTrue': + Settings.setValue('openFeedAfterShare', true) + break; default: dialog.webEventReceived.disconnect(onMessage); // I'm not certain that this is necessary. If it is, what do we do on normal close? dialog.close(); @@ -46,11 +55,9 @@ function confirmShare(data) { print('sharing', submessage.localPath); outstanding++; Window.shareSnapshot(submessage.localPath); - } else if (submessage.openFeed) { - openFeed = true; } }); - if (openFeed && !outstanding) { + if (!outstanding && shouldOpenFeedAfterShare()) { showFeedWindow(); } } @@ -67,7 +74,7 @@ function snapshotShared(success) { // for now just print an error. print('snapshot upload/share failed'); } - if ((--outstanding <= 0) && openFeed) { + if ((--outstanding <= 0) && shouldOpenFeedAfterShare()) { showFeedWindow(); } } @@ -114,8 +121,12 @@ function resetButtons(path, notify) { // last element in data array tells dialog whether we can share or not confirmShare([ - { localPath: path }, - { canShare: Boolean(Window.location.placename), isLoggedIn: Account.isLoggedIn() } + { localPath: path }, + { + canShare: Boolean(Window.location.placename), + isLoggedIn: Account.isLoggedIn(), + openFeedAfterShare: shouldOpenFeedAfterShare() + } ]); } From 4492c4e64ecc83134c2cdbcac6a4e6bd8ed57eb3 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 18 Aug 2016 14:19:36 -0700 Subject: [PATCH 58/81] Allow javascript to safely open the correct login window (or answer that it is unneded). --- interface/src/scripting/AccountScriptingInterface.cpp | 5 +++++ interface/src/scripting/AccountScriptingInterface.h | 1 + 2 files changed, 6 insertions(+) diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp index 4090c99ac8..d8533bb769 100644 --- a/interface/src/scripting/AccountScriptingInterface.cpp +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -26,6 +26,11 @@ bool AccountScriptingInterface::isLoggedIn() { return accountManager->isLoggedIn(); } +bool AccountScriptingInterface::checkAndSignalForAccessToken() { + auto accountManager = DependencyManager::get(); + return accountManager->checkAndSignalForAccessToken(); +} + QString AccountScriptingInterface::getUsername() { auto accountManager = DependencyManager::get(); if (accountManager->isLoggedIn()) { diff --git a/interface/src/scripting/AccountScriptingInterface.h b/interface/src/scripting/AccountScriptingInterface.h index 49648781ce..0f958f2286 100644 --- a/interface/src/scripting/AccountScriptingInterface.h +++ b/interface/src/scripting/AccountScriptingInterface.h @@ -26,6 +26,7 @@ public slots: static AccountScriptingInterface* getInstance(); QString getUsername(); bool isLoggedIn(); + bool checkAndSignalForAccessToken(); }; #endif // hifi_AccountScriptingInterface_h From aab3b83ad9a138e3356a4851fa9758ec12d74562 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 18 Aug 2016 14:20:53 -0700 Subject: [PATCH 59/81] open login if user tries to share and is not logged in, rather than messaging user that they cannot share --- scripts/system/snapshot.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 8e99783ff6..92b16d31bc 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -32,6 +32,12 @@ var outstanding; function confirmShare(data) { var dialog = new OverlayWebWindow('Snapshot Review', Script.resolvePath("html/ShareSnapshot.html"), 800, 470); function onMessage(message) { + // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: + // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) + // 2. Although we currently use a single image, we would like to take snapshot, a selfie, a 360 etc. all at the + // same time, show the user all of them, and have the user deselect any that they do not want to share. + // So we'll ultimately be receiving a set of objects, perhaps with different post processing for each. + var isLoggedIn, needsLogin = false; switch (message) { case 'ready': dialog.emitScriptEvent(data); // Send it. @@ -50,16 +56,26 @@ function confirmShare(data) { dialog.webEventReceived.disconnect(onMessage); // I'm not certain that this is necessary. If it is, what do we do on normal close? dialog.close(); dialog.deleteLater(); + isLoggedIn = Account.isLoggedIn(); message.forEach(function (submessage) { + if (submessage.share && !isLoggedIn) { + needsLogin = true; + submessage.share = false; + } if (submessage.share) { print('sharing', submessage.localPath); outstanding++; Window.shareSnapshot(submessage.localPath); + } else { + print('not sharing', submessage.localPath); } }); if (!outstanding && shouldOpenFeedAfterShare()) { showFeedWindow(); } + if (needsLogin) { // after the possible feed, so that the login is on top + Account.checkAndSignalForAccessToken(); + } } } dialog.webEventReceived.connect(onMessage); @@ -124,7 +140,7 @@ function resetButtons(path, notify) { { localPath: path }, { canShare: Boolean(Window.location.placename), - isLoggedIn: Account.isLoggedIn(), + isLoggedIn: true, // Just have the dialog act as though we are. To be removed at both ends later. openFeedAfterShare: shouldOpenFeedAfterShare() } ]); From f6670a63746fcec41e9227b819a7519b4a2db16f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Thu, 18 Aug 2016 16:29:42 -0700 Subject: [PATCH 60/81] new optional aspect-ratio argument to snapshot chain (javascript through c++ display plugin). When non-zero, it pulls out the largest piece from the center that maintains that ratio. Snapshot button uses 1.91. --- interface/src/Application.cpp | 4 ++-- interface/src/Application.h | 2 +- .../src/scripting/WindowScriptingInterface.cpp | 6 +++--- .../src/scripting/WindowScriptingInterface.h | 2 +- .../src/display-plugins/NullDisplayPlugin.cpp | 2 +- .../src/display-plugins/NullDisplayPlugin.h | 2 +- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 17 ++++++++++++++--- .../src/display-plugins/OpenGLDisplayPlugin.h | 2 +- libraries/plugins/src/plugins/DisplayPlugin.h | 2 +- scripts/system/snapshot.js | 2 +- 10 files changed, 26 insertions(+), 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 340597f18a..59ec1c0641 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5130,13 +5130,13 @@ void Application::toggleLogDialog() { } } -void Application::takeSnapshot(bool notify) { +void Application::takeSnapshot(bool notify, float aspectRatio) { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); + QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); emit DependencyManager::get()->snapshotTaken(path, notify); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 9fcce66f55..667969abf1 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -250,7 +250,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify); + void takeSnapshot(bool notify, float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index f843673c07..f68f6d212a 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -204,10 +204,10 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshot(bool notify) { +void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { // only evil-doers call takeSnapshot from a random thread - qApp->postLambdaEvent([notify] { - qApp->takeSnapshot(notify); + qApp->postLambdaEvent([notify, aspectRatio] { + qApp->takeSnapshot(notify, aspectRatio); }); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 15dc1a8004..ca82753598 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -55,7 +55,7 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshot(bool notify); + void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); signals: diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 5ee05fa2e3..a4777b087b 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -31,6 +31,6 @@ void NullDisplayPlugin::submitFrame(const gpu::FramePointer& frame) { } } -QImage NullDisplayPlugin::getScreenshot() const { +QImage NullDisplayPlugin::getScreenshot(float aspectRatio) const { return QImage(); } diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index 198c89ae78..1852ed53ee 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -18,7 +18,7 @@ public: glm::uvec2 getRecommendedRenderSize() const override; bool hasFocus() const override; void submitFrame(const gpu::FramePointer& newFrame) override; - QImage getScreenshot() const override; + QImage getScreenshot(float aspectRatio = 0.0f) const override; private: static const QString NAME; }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 905042cb79..7334191b75 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -659,15 +659,26 @@ void OpenGLDisplayPlugin::withMainThreadContext(std::function f) const { _container->makeRenderingContextCurrent(); } -QImage OpenGLDisplayPlugin::getScreenshot() const { +QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { auto size = _compositeFramebuffer->getSize(); if (isHmd()) { size.x /= 2; } + auto bestSize = size; + uvec2 corner(0); + if (aspectRatio != 0.0f) { // Pick out the largest piece of the center that produces the requested width/height aspectRatio + if (((size.y * aspectRatio) + 0.5f) < size.x) { + bestSize.x = round(size.y * aspectRatio); + } else { + bestSize.y = round(size.x / aspectRatio); + } + corner.x = round((size.x - bestSize.x) / 2.0f); + corner.y = round((size.y - bestSize.y) / 2.0f); + } auto glBackend = const_cast(*this).getGLBackend(); - QImage screenshot(size.x, size.y, QImage::Format_ARGB32); + QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32); withMainThreadContext([&] { - glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(uvec2(0), size), screenshot); + glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); }); return screenshot.mirrored(false, true); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 48f9a78eda..51b33c9bcd 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -54,7 +54,7 @@ public: return getSurfaceSize(); } - QImage getScreenshot() const override; + QImage getScreenshot(float aspectRatio = 0.0f) const override; float presentRate() const override; diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 49c341cdcb..288cee3223 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -172,7 +172,7 @@ public: } // Fetch the most recently displayed image as a QImage - virtual QImage getScreenshot() const = 0; + virtual QImage getScreenshot(float aspectRatio = 0.0f) const = 0; // will query the underlying hmd api to compute the most recent head pose virtual bool beginFrameRender(uint32_t frameIndex) { return true; } diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 92b16d31bc..5c893fad4a 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -116,7 +116,7 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshot(false); + Window.takeSnapshot(false, 1.91); }, SNAPSHOT_DELAY); } From 4d7d483c0eac43fca4232c6b4d44ff1601d9c372 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 19 Aug 2016 09:51:58 -0700 Subject: [PATCH 61/81] use ceil --- .../display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 7334191b75..b304b3802e 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -667,7 +667,7 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { auto bestSize = size; uvec2 corner(0); if (aspectRatio != 0.0f) { // Pick out the largest piece of the center that produces the requested width/height aspectRatio - if (((size.y * aspectRatio) + 0.5f) < size.x) { + if (ceil(size.y * aspectRatio) < size.x) { bestSize.x = round(size.y * aspectRatio); } else { bestSize.y = round(size.x / aspectRatio); From a3d64dbd9c360d15fe53e706fde540db1ca83097 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 19 Aug 2016 14:14:31 -0700 Subject: [PATCH 62/81] code cleanup --- interface/resources/qml/AddressBarDialog.qml | 3 +-- interface/src/Application.h | 2 +- interface/src/scripting/WindowScriptingInterface.h | 2 +- interface/src/ui/AddressBarDialog.h | 5 ++++- interface/src/ui/Snapshot.cpp | 10 ++++------ .../html/{ShareSnapshot.html => SnapshotReview.html} | 0 scripts/system/snapshot.js | 10 ++++------ 7 files changed, 15 insertions(+), 17 deletions(-) rename scripts/system/html/{ShareSnapshot.html => SnapshotReview.html} (100%) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 089bec07f4..b61b4111d6 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -73,8 +73,7 @@ Window { property var allStories: []; property int cardWidth: 200; property int cardHeight: 152; - property string metaverseBase: "https://metaverse.highfidelity.com/api/v1/"; - //property string metaverseBase: "http://10.0.0.241:3000/api/v1/"; + property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; property bool useHTML: false; // fixme: remove this and all false branches after the server is updated AddressBarDialog { diff --git a/interface/src/Application.h b/interface/src/Application.h index 667969abf1..0ce1aac566 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -266,7 +266,7 @@ signals: void activeDisplayPluginChanged(); void uploadRequest(QString path); - void receivedHifiSchemeURL(QString path); + void receivedHifiSchemeURL(const QString& url); public slots: QVector pasteEntities(float x, float y, float z); diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index ca82753598..7a01be7fac 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -63,7 +63,7 @@ signals: void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode); void snapshotTaken(const QString& path, bool notify); - void snapshotShared(bool success); + void snapshotShared(const QString& error); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index 68a12d4eb0..3197770433 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -14,6 +14,7 @@ #define hifi_AddressBarDialog_h #include +#include class AddressBarDialog : public OffscreenQmlDialog { Q_OBJECT @@ -21,6 +22,7 @@ class AddressBarDialog : public OffscreenQmlDialog { Q_PROPERTY(bool backEnabled READ backEnabled NOTIFY backEnabledChanged) Q_PROPERTY(bool forwardEnabled READ forwardEnabled NOTIFY forwardEnabledChanged) Q_PROPERTY(bool useFeed READ useFeed WRITE setUseFeed NOTIFY useFeedChanged) + Q_PROPERTY(QString metaverseServerUrl READ metaverseServerUrl) public: AddressBarDialog(QQuickItem* parent = nullptr); @@ -28,12 +30,13 @@ public: bool forwardEnabled() { return _forwardEnabled; } bool useFeed() { return _useFeed; } void setUseFeed(bool useFeed) { if (_useFeed != useFeed) { _useFeed = useFeed; emit useFeedChanged(); } } + QString metaverseServerUrl() { return NetworkingConstants::METAVERSE_SERVER_URL.toString(); } signals: void backEnabledChanged(); void forwardEnabledChanged(); void useFeedChanged(); - void receivedHifiSchemeURL(QString url); + void receivedHifiSchemeURL(const QString& url); protected: void displayAddressOfflineMessage(); diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index b7cdb1a126..4513894f5a 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -215,20 +215,18 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonDocument(rootObject).toJson()); } else { - emit DependencyManager::get()->snapshotShared(false); + emit DependencyManager::get()->snapshotShared(contents); } } void SnapshotUploader::uploadFailure(QNetworkReply& reply) { - // TODO: parse response, potentially helpful for logging (?) - emit DependencyManager::get()->snapshotShared(false); + emit DependencyManager::get()->snapshotShared(reply.readAll()); } void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { - emit DependencyManager::get()->snapshotShared(true); + emit DependencyManager::get()->snapshotShared(""); } void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { - // TODO: parse response, potentially helpful for logging (?) - emit DependencyManager::get()->snapshotShared(false); + emit DependencyManager::get()->snapshotShared(reply.readAll()); } diff --git a/scripts/system/html/ShareSnapshot.html b/scripts/system/html/SnapshotReview.html similarity index 100% rename from scripts/system/html/ShareSnapshot.html rename to scripts/system/html/SnapshotReview.html diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 5c893fad4a..fab26a70e6 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -30,7 +30,7 @@ function showFeedWindow() { var outstanding; function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot Review', Script.resolvePath("html/ShareSnapshot.html"), 800, 470); + var dialog = new OverlayWebWindow('Snapshot Review', Script.resolvePath("html/SnapshotReview.html"), 800, 470); function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) @@ -82,13 +82,11 @@ function confirmShare(data) { dialog.raise(); } -function snapshotShared(success) { - if (success) { - // for now just print status +function snapshotShared(errorMessage) { + if (!errorMessage) { print('snapshot uploaded and shared'); } else { - // for now just print an error. - print('snapshot upload/share failed'); + print(errorMessage); } if ((--outstanding <= 0) && shouldOpenFeedAfterShare()) { showFeedWindow(); From 1e08914f8d608e790dabcddabccc520bb705d143 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Fri, 19 Aug 2016 14:32:15 -0700 Subject: [PATCH 63/81] simplify --- scripts/system/snapshot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index fab26a70e6..9ce3cb366e 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -137,7 +137,7 @@ function resetButtons(path, notify) { confirmShare([ { localPath: path }, { - canShare: Boolean(Window.location.placename), + canShare: Boolean(location.placename), isLoggedIn: true, // Just have the dialog act as though we are. To be removed at both ends later. openFeedAfterShare: shouldOpenFeedAfterShare() } From 3d2afaac77df4addd18d559cdd503b036ec21c0a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 20 Aug 2016 11:19:30 +1200 Subject: [PATCH 64/81] Size and position snapshot dialog content blocks --- scripts/system/html/SnapshotReview.html | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index 9b9403cf78..8d73e5295f 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -3,6 +3,41 @@ Share + - - + diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css new file mode 100644 index 0000000000..4faa17e7a3 --- /dev/null +++ b/scripts/system/html/css/SnapshotReview.css @@ -0,0 +1,131 @@ +/* +// edit-style.css +// +// Created by Howard Stearns for David Rowe 8/22/2016. +// Copyright 2016 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 +*/ + + +.snapshot-container { + width: 100%; + padding-top: 3px; +} + +.snapshot-column-left { + width: 320px; + position: absolute; + padding-top: 8px; +} + +.snapshot-column-right { + margin-left: 342px; +} + +.snapshot-column-right > div > img { + width: 100%; +} + +@media (max-width: 768px) { + .snapshot-column-left { + position: initial; + width: 100%; + } + .snapshot-column-right { + margin-left: 0; + width: 100%; + } + .snapshot-column-right > div > img { + margin-top: 18px !important; + } +} + +.snapshot-column-right > div { + position: relative; + padding: 2px; +} + +.snapshot-column-right > div > img { + border: 2px solid #575757; + margin: -2px; +} + +hr { + padding-left: 0; + padding-right: 0; + margin: 21px 0; +} + +.snapsection { + text-align: center; +} + +.title { + text-transform: uppercase; + font-size: 12px; +} + +.prompt { + font-family: Raleway-SemiBold; + font-size: 14px; +} + +div.button { + padding-top: 21px; +} + +.compound-button { + position: relative; + height: auto; +} + +.compound-button input { + padding-left: 40px; +} + +.compound-button .glyph { + display: inline-block; + position: absolute; + left: 12px; + top: 16px; + width: 23px; + height: 23px; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaGVpZ2h0PSI0MCIKICAgd2lkdGg9IjQwIgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgdmlld0JveD0iMCAwIDQwIDQwIgogICB5PSIwcHgiCiAgIHg9IjBweCIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGEzNCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48ZGM6dGl0bGU+PC9kYzp0aXRsZT48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczMyIiAvPjxzdHlsZQogICAgIGlkPSJzdHlsZTQiCiAgICAgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM0MTQwNDI7fQoJLnN0MXtmaWxsOiNDQ0NDQ0M7fQoJLnN0MntmaWxsOiMxMzk4QkI7fQoJLnN0M3tmaWxsOiMzMUQ4RkY7fQo8L3N0eWxlPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMSI+PGNpcmNsZQogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJjaXJjbGUxMyIKICAgICAgIHI9IjQuNDQwMDAwMSIKICAgICAgIGN5PSIxMjYuMTciCiAgICAgICBjeD0iMjAuNTQwMDAxIgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTUiCiAgICAgICBkPSJtIDI4Ljg3LDEzOS4yNiBjIDAuMDEsLTAuMDEgMC4wMiwtMC4wMiAwLjAzLC0wLjAzIGwgMCwtMS44NiBjIDAsLTIuNjggLTIuMzMsLTQuNzcgLTUsLTQuNzcgbCAtNi40MiwwIGMgLTIuNjgsMCAtNC44NSwyLjA5IC00Ljg1LDQuNzcgbCAwLDEuODggMTYuMjQsMCB6IgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTciCiAgICAgICBkPSJtIDM4LjE3LDEyMy40MiBjIDAsLTMuOTcgLTMuMjIsLTcuMTkgLTcuMTksLTcuMTkgbCAtMjAuMzEsMCBjIC0zLjk3LDAgLTcuMTksMy4yMiAtNy4xOSw3LjE5IGwgMCwxNC4xOCBjIDAsMy45NyAzLjIyLDcuMTkgNy4xOSw3LjE5IGwgMjAuMzEsMCBjIDMuOTcsMCA3LjE5LC0zLjIyIDcuMTksLTcuMTkgbCAwLC0xNC4xOCB6IG0gLTEuNzgsMTQuMjcgYyAwLDMuMDMgLTIuNDYsNS40OSAtNS40OSw1LjQ5IGwgLTIwLjMyLDAgYyAtMy4wMywwIC01LjQ5LC0yLjQ2IC01LjQ5LC01LjQ5IGwgMCwtMTQuMTkgYyAwLC0zLjAzIDIuNDYsLTUuNDkgNS40OSwtNS40OSBsIDIwLjMzLDAgYyAzLjAzLDAgNS40OSwyLjQ2IDUuNDksNS40OSBsIDAsMTQuMTkgeiIKICAgICAgIGNsYXNzPSJzdDEiIC8+PC9nPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMiIgLz48L3N2Zz4=); + background-repeat: no-repeat; + background-size: 23px 23px; +} + +.setting { + display: inline-table; + height: 28px; +} + +.setting label { + display: table-cell; + vertical-align: middle; + font-family: Raleway-SemiBold; + font-size: 14px; +} + +.setting + .setting { + margin-left: 18px; +} + +input[type=button].naked { + font-size: 40px; + line-height: 40px; + width: 30px; + padding: 0; + margin: 0 0 -6px 0; + position: relative; + top: -6px; + left: -8px; + background: none; +} + +input[type=button].naked:hover { + color: #00b4ef; + background: none; +} diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js new file mode 100644 index 0000000000..a6515df825 --- /dev/null +++ b/scripts/system/html/js/SnapshotReview.js @@ -0,0 +1,77 @@ +"use strict"; +// +// SnapshotReview.js +// scripts/system/html/js/ +// +// Created by Howard Stearns 8/22/2016 +// Copyright 2016 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 +// + +var paths = [], idCounter = 0, useCheckboxes; +function addImage(data) { + var div = document.createElement("DIV"), + input = document.createElement("INPUT"), + label = document.createElement("LABEL"), + img = document.createElement("IMG"), + id = "p" + idCounter++; + function toggle() { data.share = input.checked; } + img.src = data.localPath; + div.appendChild(img); + data.share = true; + if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. + // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. + label.setAttribute('for', id); // cannot do label.for = + input.id = id; + input.type = "checkbox"; + input.checked = true; + input.addEventListener('change', toggle); + div.class = "property checkbox"; + div.appendChild(input); + div.appendChild(label); + } + document.getElementById("snapshot-images").appendChild(div); + paths.push(data); + +} +function handleShareButtons(shareMsg) { + var openFeed = document.getElementById('openFeed'); + openFeed.checked = shareMsg.openFeedAfterShare; + openFeed.onchange = function () { EventBridge.emitWebEvent(openFeed.checked ? 'setOpenFeedTrue' : 'setOpenFeedFalse'); }; + if (!shareMsg.canShare) { + // this means you may or may not be logged in, but can't share + // because you are not in a public place. + document.getElementById("sharing").innerHTML = "

Snapshots can be shared when they're taken in shareable places."; + } +} +window.onload = function () { + // Something like the following will allow testing in a browser. + //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); + //addImage({localPath: 'http://lorempixel.com/1512/1680'}); + openEventBridge(function () { + // Set up a handler for receiving the data, and tell the .js we are ready to receive it. + EventBridge.scriptEventReceived.connect(function (message) { + // last element of list contains a bool for whether or not we can share stuff + var shareMsg = message.pop(); + handleShareButtons(shareMsg); + + // rest are image paths which we add + useCheckboxes = message.length > 1; + message.forEach(addImage); + }); + EventBridge.emitWebEvent('ready'); + }); + +}; +// beware of bug: Cannot send objects at top level. (Nested in arrays is fine.) +function shareSelected() { + EventBridge.emitWebEvent(paths); +}; +function doNotShare() { + EventBridge.emitWebEvent([]); +}; +function snapshotSettings() { + EventBridge.emitWebEvent("openSettings"); +}; From bc90b0bc43fc37be9d26cc755130fd429859dab7 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 22 Aug 2016 16:09:09 -0700 Subject: [PATCH 79/81] pr review --- interface/resources/qml/AddressBarDialog.qml | 12 ++++++------ interface/resources/qml/hifi/Card.qml | 6 +++++- scripts/system/html/css/SnapshotReview.css | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7be7c2c83b..f2bdc5ba6d 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -273,9 +273,9 @@ Window { 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 + // Calls to iterator are overlapped and may 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.) + // 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(null, results); @@ -287,7 +287,7 @@ Window { iterator(element, function (error, mapped) { results[index] = mapped; if (error || !--count) { - count = 1; // don't cb multiple times if error + count = 0; // don't cb multiple times if error cb(error, results); } }); @@ -397,9 +397,9 @@ Window { // 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 + // TBD: should determine if place is actually running? + 'restriction=open', // Not by whitelist, etc. TBD: If logged in, add hifi to the restriction options, in order to include places that require login? + // TBD: add maturity? 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), 'sort_by=users', 'sort_order=desc', diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index fe162349c1..53829eed9e 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -37,6 +37,7 @@ Rectangle { property int textSizeSmall: 18; property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); HifiConstants { id: hifi } + function pastTime(timestamp) { // Answer a descriptive string timestamp = new Date(timestamp); var then = timestamp.getTime(), @@ -77,7 +78,7 @@ Rectangle { anchors.left: parent.left; onStatusChanged: { if (status == Image.Error) { - console.log("source: " + source + ": failed to load " + hfiUrl); + console.log("source: " + source + ": failed to load " + hifiUrl); source = defaultThumbnail; } } @@ -129,6 +130,9 @@ Rectangle { margins: textPadding; } } + // These two can be supplied to provide hover behavior. + // For example, AddressBarDialog provides functions that set the current list view item + // to that which is being hovered over. property var hoverThunk: function () { }; property var unhoverThunk: function () { }; MouseArea { diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css index 4faa17e7a3..c2965f92e1 100644 --- a/scripts/system/html/css/SnapshotReview.css +++ b/scripts/system/html/css/SnapshotReview.css @@ -1,5 +1,5 @@ /* -// edit-style.css +// SnapshotReview.css // // Created by Howard Stearns for David Rowe 8/22/2016. // Copyright 2016 High Fidelity, Inc. From f7389dc059f12d23a892e467ac1fb46a3b4795f1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 23 Aug 2016 16:28:32 +1200 Subject: [PATCH 80/81] Fix server card window scale --- interface/resources/qml/AddressBarDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index f2bdc5ba6d..8e68906406 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -219,6 +219,7 @@ Window { Window { width: 750; height: 500; + scale: 0.8 // Reset scale of Window to 1.0 (counteract address bar's scale value of 1.25) HifiControls.WebView { anchors.fill: parent; id: storyCardHTML; From 7601876641f41f90c28c8cfdb3f38b7664a2175e Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 23 Aug 2016 09:44:28 -0700 Subject: [PATCH 81/81] Scale the activity card back up to match the window being scaled down, so that it covers the address bar. --- interface/resources/qml/AddressBarDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 8e68906406..2536fdade9 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -217,8 +217,8 @@ Window { } Window { - width: 750; - height: 500; + width: 938; + height: 625; scale: 0.8 // Reset scale of Window to 1.0 (counteract address bar's scale value of 1.25) HifiControls.WebView { anchors.fill: parent;