Merge pull request #10253 from howard-stearns/new-address-bar

New address bar
This commit is contained in:
Howard Stearns 2017-04-27 14:00:24 -07:00 committed by GitHub
commit 5d6e24b495
3 changed files with 309 additions and 298 deletions

View file

@ -35,6 +35,7 @@ Rectangle {
property string timePhrase: pastTime(timestamp);
property int onlineUsers: 0;
property bool isConcurrency: action === 'concurrency';
property bool isAnnouncement: action === 'announcement';
property bool isStacked: !isConcurrency && drillDownToPlace;
property int textPadding: 10;
@ -80,7 +81,7 @@ Rectangle {
id: lobby;
visible: !hasGif || (animation.status !== Image.Ready);
width: parent.width - (isConcurrency ? 0 : (2 * smallMargin));
height: parent.height - (isConcurrency ? 0 : smallMargin);
height: parent.height - messageHeight - (isConcurrency ? 0 : smallMargin);
source: thumbnail || defaultThumbnail;
fillMode: Image.PreserveAspectCrop;
anchors {
@ -129,7 +130,7 @@ Rectangle {
property int dropSamples: 9;
property int dropSpread: 0;
DropShadow {
visible: true;
visible: showPlace; // Do we have to check for whatever the modern equivalent is for desktop.gradientsSupported?
source: place;
anchors.fill: place;
horizontalOffset: dropHorizontalOffset;
@ -139,12 +140,12 @@ Rectangle {
color: hifi.colors.black;
spread: dropSpread;
}
RalewayLight {
RalewaySemiBold {
id: place;
visible: showPlace;
text: placeName;
color: hifi.colors.white;
size: 38;
size: textSize;
elide: Text.ElideRight; // requires constrained width
anchors {
top: parent.top;
@ -153,57 +154,44 @@ Rectangle {
margins: textPadding;
}
}
Rectangle {
id: rectRow
z: 1
width: message.width + (users.visible ? users.width + bottomRow.spacing : 0)
+ (icon.visible ? icon.width + bottomRow.spacing: 0) + bottomRow.spacing;
height: messageHeight + 1;
radius: 25
anchors {
bottom: parent.bottom
left: parent.left
leftMargin: textPadding
bottomMargin: textPadding
Row {
FiraSansRegular {
id: users;
visible: isConcurrency || isAnnouncement;
text: onlineUsers;
size: textSize;
color: messageColor;
anchors.verticalCenter: message.verticalCenter;
}
Row {
id: bottomRow
FiraSansRegular {
id: users;
visible: isConcurrency;
text: onlineUsers;
size: textSize;
color: messageColor;
anchors.verticalCenter: message.verticalCenter;
}
Image {
id: icon;
source: "../../images/snap-icon.svg"
width: 40;
height: 40;
visible: action === 'snapshot';
}
RalewayRegular {
id: message;
text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName));
size: textSizeSmall;
color: messageColor;
elide: Text.ElideRight; // requires a width to be specified`
anchors {
bottom: parent.bottom;
bottomMargin: parent.spacing;
}
}
spacing: textPadding;
height: messageHeight;
Image {
id: icon;
source: "../../images/snap-icon.svg"
width: 40;
height: 40;
visible: action === 'snapshot';
}
RalewayRegular {
id: message;
text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (isAnnouncement ? "connections" : (drillDownToPlace ? "snapshots" : ("by " + userName)));
size: textSizeSmall;
color: messageColor;
elide: Text.ElideRight; // requires a width to be specified`
width: root.width - textPadding
- (users.visible ? users.width + parent.spacing : 0)
- (icon.visible ? icon.width + parent.spacing : 0)
- (actionIcon.width + (2 * smallMargin));
anchors {
bottom: parent.bottom;
left: parent.left;
leftMargin: 4
bottomMargin: parent.spacing;
}
}
spacing: textPadding;
height: messageHeight;
anchors {
bottom: parent.bottom;
left: parent.left;
leftMargin: textPadding;
}
}
// These two can be supplied to provide hover behavior.
// For example, AddressBarDialog provides functions that set the current list view item
@ -218,37 +206,22 @@ Rectangle {
onEntered: hoverThunk();
onExited: unhoverThunk();
}
Rectangle {
id: rectIcon
z: 1
width: 32
height: 32
radius: 15
StateImage {
id: actionIcon;
imageURL: "../../images/info-icon-2-state.svg";
size: 32;
buttonState: messageArea.containsMouse ? 1 : 0;
anchors {
bottom: parent.bottom;
right: parent.right;
bottomMargin: textPadding;
rightMargin: textPadding;
}
StateImage {
id: actionIcon;
imageURL: "../../images/info-icon-2-state.svg";
size: 32;
buttonState: messageArea.containsMouse ? 1 : 0;
anchors {
bottom: parent.bottom;
right: parent.right;
//margins: smallMargin;
}
margins: smallMargin;
}
}
MouseArea {
id: messageArea;
width: rectIcon.width;
height: rectIcon.height;
anchors.fill: rectIcon
width: parent.width;
height: messageHeight;
anchors.top: lobby.bottom;
acceptedButtons: Qt.LeftButton;
onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId));
hoverEnabled: true;

View file

@ -0,0 +1,201 @@
//
// Feed.qml
// qml/hifi
//
// Displays a particular type of feed
//
// Created by Howard Stearns on 4/18/2017
// 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 QtGraphicalEffects 1.0
import "toolbars"
import "../styles-uit"
Column {
id: root;
visible: false;
property int cardWidth: 212;
property int cardHeight: 152;
property int stackedCardShadowHeight: 10;
property string metaverseServerUrl: '';
property string actions: 'snapshot';
onActionsChanged: fillDestinations();
Component.onCompleted: fillDestinations();
property string labelText: actions;
property string filter: '';
onFilterChanged: filterChoicesByText();
property var goFunction: null;
HifiConstants { id: hifi }
ListModel { id: suggestions; }
function resolveUrl(url) {
return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url;
}
function makeModelData(data) { // create a new obj from data
// ListModel elements will only ever have those properties that are defined by the first obj that is added.
// 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 = data.place_name,
tags = data.tags || [data.action, data.username],
description = data.description || "",
thumbnail_url = data.thumbnail_url || "";
if (actions === 'concurrency,snapshot') {
// A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements.
data.details.connections = 4;
data.action = 'announcement';
}
return {
place_name: name,
username: data.username || "",
path: data.path || "",
created_at: data.created_at || "",
action: data.action || "",
thumbnail_url: resolveUrl(thumbnail_url),
image_url: resolveUrl(data.details && data.details.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.details.connections || data.details.concurrency || 0,
drillDownToPlace: false,
searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
}
}
property var allStories: [];
property var placeMap: ({}); // Used for making stacks.
property int requestId: 0;
function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model
// If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness.
var options = [
'now=' + new Date().toISOString(),
'include_actions=' + actions,
'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
'require_online=true',
'protocol=' + encodeURIComponent(AddressManager.protocolVersion()),
'page=' + pageNumber
];
var url = metaverseBase + 'user_stories?' + options.join('&');
var thisRequestId = ++requestId;
getRequest(url, function (error, data) {
if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) {
return; // abandon stale requests
}
allStories = allStories.concat(data.user_stories.map(makeModelData));
if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now
if ((pageNumber === 1) && cb1) {
cb1();
}
return getUserStoryPage(pageNumber + 1, cb);
}
cb();
});
}
function fillDestinations() { // Public
var filter = makeFilteredStoryProcessor(), counter = 0;
allStories = [];
suggestions.clear();
placeMap = {};
getUserStoryPage(1, function (error) {
allStories.slice(counter).forEach(filter);
console.log('user stories query', actions, error || 'ok', allStories.length, 'filtered to', suggestions.count);
root.visible = !!suggestions.count;
}, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed.
allStories.forEach(function (story) {
counter++;
filter(story);
root.visible = !!suggestions.count;
});
});
}
function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches
var words = filter.toUpperCase().split(/\s+/).filter(identity);
function suggestable(story) { // fixme add to makeFilteredStoryProcessor
if (story.action === 'snapshot') {
return true;
}
return (story.place_name !== AddressManager.placename); // Not our entry, but do show other entry points to current domain.
}
function matches(story) {
if (!words.length) {
return suggestable(story);
}
return words.every(function (word) {
return story.searchText.indexOf(word) >= 0;
});
}
function addToSuggestions(place) {
var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement');
if (collapse) {
var existing = placeMap[place.place_name];
if (existing) {
existing.drillDownToPlace = true;
return;
}
}
suggestions.append(place);
if (collapse) {
placeMap[place.place_name] = suggestions.get(suggestions.count - 1);
} else if (place.action === 'concurrency') {
suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories).
}
}
return function (story) {
if (matches(story)) {
addToSuggestions(story);
}
};
}
function filterChoicesByText() {
suggestions.clear();
placeMap = {};
allStories.forEach(makeFilteredStoryProcessor());
root.visible = !!suggestions.count;
}
RalewayLight {
id: label;
text: labelText;
color: hifi.colors.white;
size: 28;
}
ListView {
id: scroll;
clip: true;
model: suggestions;
orientation: ListView.Horizontal;
highlightMoveDuration: -1;
highlightMoveVelocity: -1;
highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; }
spacing: 14;
width: parent.width;
height: cardHeight + stackedCardShadowHeight;
delegate: Card {
width: cardWidth;
height: cardHeight;
goFunction: root.goFunction;
userName: model.username;
placeName: model.place_name;
hifiUrl: model.place_name + model.path;
thumbnail: model.thumbnail_url;
imageUrl: model.image_url;
action: model.action;
timestamp: model.created_at;
onlineUsers: model.online_users;
storyId: model.metaverseId;
drillDownToPlace: model.drillDownToPlace;
shadowHeight: stackedCardShadowHeight;
hoverThunk: function () { scroll.currentIndex = index; }
unhoverThunk: function () { scroll.currentIndex = -1; }
}
}
}

View file

@ -30,18 +30,15 @@ StackView {
width: parent !== null ? parent.width : undefined
height: parent !== null ? parent.height : undefined
property var eventBridge;
property var allStories: [];
property int cardWidth: 460;
property int cardHeight: 320;
property int cardWidth: 212;
property int cardHeight: 152;
property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/";
property var tablet: null;
Component { id: tabletWebView; TabletWebView {} }
Component.onCompleted: {
fillDestinations();
updateLocationText(false);
fillDestinations();
addressLine.focus = !HMD.active;
root.parentChanged.connect(center);
center();
@ -190,7 +187,6 @@ StackView {
}
font.pixelSize: hifi.fonts.pixelSize * 0.75
onTextChanged: {
filterChoicesByText();
updateLocationText(text.length > 0);
}
onAccepted: {
@ -225,109 +221,68 @@ StackView {
}
}
}
Rectangle {
id: topBar
height: 37
color: hifiStyleConstants.colors.white
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.topMargin: 0
anchors.top: addressBar.bottom
Row {
id: thing
spacing: 5 * hifi.layout.spacing
anchors {
top: parent.top;
left: parent.left
leftMargin: 25
}
TabletTextButton {
id: allTab;
text: "ALL";
property string includeActions: 'snapshot,concurrency';
selected: allTab === selectedTab;
action: tabSelect;
}
TabletTextButton {
id: placeTab;
text: "PLACES";
property string includeActions: 'concurrency';
selected: placeTab === selectedTab;
action: tabSelect;
}
TabletTextButton {
id: snapTab;
text: "SNAP";
property string includeActions: 'snapshot';
selected: snapTab === selectedTab;
action: tabSelect;
}
id: bgMain;
color: hifiStyleConstants.colors.faintGray50;
anchors {
top: addressBar.bottom;
bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom;
left: parent.left;
right: parent.right;
}
}
Rectangle {
id: bgMain
color: hifiStyleConstants.colors.white
anchors.bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom
anchors.bottomMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.top: topBar.bottom
anchors.topMargin: 0
ListModel { id: suggestions }
ListView {
id: scroll
property int stackedCardShadowHeight: 0;
clip: true
spacing: 14
anchors {
bottom: parent.bottom
top: parent.top
left: parent.left
right: parent.right
leftMargin: 10
ScrollView {
anchors.fill: bgMain;
horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff;
verticalScrollBarPolicy: Qt.ScrollBarAsNeeded;
Rectangle { // Column margins require QtQuick 2.7, which we don't use yet.
id: column;
property real pad: 10;
width: bgMain.width - column.pad;
height: stack.height;
color: "transparent";
anchors {
left: parent.left;
leftMargin: column.pad;
topMargin: column.pad;
}
Column {
id: stack;
width: column.width;
spacing: column.pad;
Feed {
id: happeningNow;
width: parent.width;
property real cardScale: 1.5;
cardWidth: places.cardWidth * happeningNow.cardScale;
cardHeight: places.cardHeight * happeningNow.cardScale;
metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'Happening Now';
//actions: 'concurrency,snapshot'; // uncomment this line instead of next to produce fake announcement data for testing.
actions: 'announcement';
filter: addressLine.text;
goFunction: goCard;
}
Feed {
id: places;
width: parent.width;
metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'Places';
actions: 'concurrency';
filter: addressLine.text;
goFunction: goCard;
}
Feed {
id: snapshots;
width: parent.width;
metaverseServerUrl: addressBarDialog.metaverseServerUrl;
labelText: 'Recent Activity';
actions: 'snapshot';
filter: addressLine.text;
goFunction: goCard;
}
}
}
model: suggestions
orientation: ListView.Vertical
delegate: Card {
width: cardWidth;
height: cardHeight;
goFunction: goCard;
userName: model.username;
placeName: model.place_name;
hifiUrl: model.place_name + model.path;
thumbnail: model.thumbnail_url;
imageUrl: model.image_url;
action: model.action;
timestamp: model.created_at;
onlineUsers: model.online_users;
storyId: model.metaverseId;
drillDownToPlace: model.drillDownToPlace;
shadowHeight: scroll.stackedCardShadowHeight;
hoverThunk: function () { scroll.currentIndex = index; }
unhoverThunk: function () { scroll.currentIndex = -1; }
}
highlightMoveDuration: -1;
highlightMoveVelocity: -1;
highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; }
}
}
@ -409,124 +364,6 @@ StackView {
return true;
}
function resolveUrl(url) {
return (url.indexOf('/') === 0) ? (addressBarDialog.metaverseServerUrl + url) : url;
}
function makeModelData(data) { // create a new obj from data
// ListModel elements will only ever have those properties that are defined by the first obj that is added.
// 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 = data.place_name,
tags = data.tags || [data.action, data.username],
description = data.description || "",
thumbnail_url = data.thumbnail_url || "";
return {
place_name: name,
username: data.username || "",
path: data.path || "",
created_at: data.created_at || "",
action: data.action || "",
thumbnail_url: resolveUrl(thumbnail_url),
image_url: resolveUrl(data.details.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.details.concurrency || 0,
drillDownToPlace: false,
searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
}
}
function suggestable(place) {
if (place.action === 'snapshot') {
return true;
}
return (place.place_name !== AddressManager.placename); // Not our entry, but do show other entry points to current domain.
}
property var selectedTab: allTab;
function tabSelect(textButton) {
selectedTab = textButton;
fillDestinations();
}
property var placeMap: ({});
function addToSuggestions(place) {
var collapse = allTab.selected && (place.action !== 'concurrency');
if (collapse) {
var existing = placeMap[place.place_name];
if (existing) {
existing.drillDownToPlace = true;
return;
}
}
suggestions.append(place);
if (collapse) {
placeMap[place.place_name] = suggestions.get(suggestions.count - 1);
} else if (place.action === 'concurrency') {
suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories).
}
}
property int requestId: 0;
function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model
var options = [
'now=' + new Date().toISOString(),
'include_actions=' + selectedTab.includeActions,
'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
'require_online=true',
'protocol=' + encodeURIComponent(AddressManager.protocolVersion()),
'page=' + pageNumber
];
var url = metaverseBase + 'user_stories?' + options.join('&');
var thisRequestId = ++requestId;
getRequest(url, function (error, data) {
if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) {
return;
}
var stories = data.user_stories.map(function (story) { // explicit single-argument function
return makeModelData(story, url);
});
allStories = allStories.concat(stories);
stories.forEach(makeFilteredPlaceProcessor());
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 makeFilteredPlaceProcessor() { // answer a function(placeData) that adds it to suggestions if it matches
var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity),
data = allStories;
function matches(place) {
if (!words.length) {
return suggestable(place);
}
return words.every(function (word) {
return place.searchText.indexOf(word) >= 0;
});
}
return function (place) {
if (matches(place)) {
addToSuggestions(place);
}
};
}
function filterChoicesByText() {
suggestions.clear();
placeMap = {};
allStories.forEach(makeFilteredPlaceProcessor());
}
function fillDestinations() {
allStories = [];
suggestions.clear();
placeMap = {};
getUserStoryPage(1, function (error) {
console.log('user stories query', error || 'ok', allStories.length);
});
}
function updateLocationText(enteringAddress) {
if (enteringAddress) {
notice.text = "Go To a place, @user, path, or network address:";