diff --git a/interface/resources/images/address-bar.svg b/interface/resources/images/address-bar-856.svg
similarity index 52%
rename from interface/resources/images/address-bar.svg
rename to interface/resources/images/address-bar-856.svg
index 39926b017d..678e1aaf95 100644
--- a/interface/resources/images/address-bar.svg
+++ b/interface/resources/images/address-bar-856.svg
@@ -2,17 +2,17 @@
diff --git a/interface/resources/images/concurrency.svg b/interface/resources/images/concurrency.svg
deleted file mode 100644
index b9e76e7d55..0000000000
--- a/interface/resources/images/concurrency.svg
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
diff --git a/interface/resources/images/info-icon-2-state.svg b/interface/resources/images/info-icon-2-state.svg
new file mode 100644
index 0000000000..cb6c802315
--- /dev/null
+++ b/interface/resources/images/info-icon-2-state.svg
@@ -0,0 +1,24 @@
+
+
+
diff --git a/interface/resources/images/snap-icon.svg b/interface/resources/images/snap-icon.svg
new file mode 100644
index 0000000000..8c6c042bec
--- /dev/null
+++ b/interface/resources/images/snap-icon.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/interface/resources/images/snapshot.svg b/interface/resources/images/snapshot.svg
deleted file mode 100644
index 7b3da80f3c..0000000000
--- a/interface/resources/images/snapshot.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
diff --git a/interface/resources/images/swipe-chevron.svg b/interface/resources/images/swipe-chevron.svg
new file mode 100644
index 0000000000..b70c8c31e2
--- /dev/null
+++ b/interface/resources/images/swipe-chevron.svg
@@ -0,0 +1,39 @@
+
+
+
diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml
index 941099b7cc..bfb5295512 100644
--- a/interface/resources/qml/AddressBarDialog.qml
+++ b/interface/resources/qml/AddressBarDialog.qml
@@ -24,7 +24,7 @@ Window {
HifiStyles.HifiConstants { id: hifiStyleConstants }
objectName: "AddressBarDialog"
- title: "Go To"
+ title: "Go To:"
shown: false
destroyOnHidden: false
@@ -33,6 +33,7 @@ Window {
width: addressBarDialog.implicitWidth
height: addressBarDialog.implicitHeight
+ property int gap: 14
onShownChanged: {
addressBarDialog.keyboardEnabled = HMD.active;
@@ -65,7 +66,7 @@ Window {
clearAddressLineTimer.start();
}
property var allStories: [];
- property int cardWidth: 200;
+ property int cardWidth: 212;
property int cardHeight: 152;
property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/";
property bool isCursorVisible: false // Override default cursor visibility.
@@ -78,7 +79,7 @@ Window {
property bool punctuationMode: false
implicitWidth: backgroundImage.width
- implicitHeight: backgroundImage.height + (keyboardEnabled ? keyboard.height : 0) + cardHeight;
+ implicitHeight: scroll.height + gap + backgroundImage.height + (keyboardEnabled ? keyboard.height : 0);
// 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;
@@ -92,13 +93,14 @@ Window {
ListView {
id: scroll
- width: backgroundImage.width;
- height: cardHeight;
- spacing: hifi.layout.spacing;
+ height: cardHeight + scroll.stackedCardShadowHeight
+ property int stackedCardShadowHeight: 10;
+ spacing: gap;
clip: true;
anchors {
+ left: backgroundImage.left
+ right: swipe.left
bottom: backgroundImage.top
- horizontalCenter: backgroundImage.horizontalCenter
}
model: suggestions;
orientation: ListView.Horizontal;
@@ -114,29 +116,66 @@ Window {
timestamp: model.created_at;
onlineUsers: model.online_users;
storyId: model.metaverseId;
+ drillDownToPlace: model.drillDownToPlace;
+ shadowHeight: scroll.stackedCardShadowHeight;
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 by about the same amount as the last item peeks through on the other side.
- rightMargin: 50;
+ highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; }
}
Image { // Just a visual indicator that the user can swipe the cards over to see more.
- source: "../images/Swipe-Icon-single.svg"
- width: 50;
+ id: swipe;
+ source: "../images/swipe-chevron.svg";
+ width: 72;
visible: suggestions.count > 3;
anchors {
- right: scroll.right;
- verticalCenter: scroll.verticalCenter;
+ right: backgroundImage.right;
+ top: scroll.top;
+ }
+ MouseArea {
+ anchors.fill: parent
+ onClicked: scroll.currentIndex = (scroll.currentIndex < 0) ? 3 : (scroll.currentIndex + 3)
+ }
+ }
+
+ Row {
+ spacing: 2 * hifi.layout.spacing;
+ anchors {
+ top: parent.top;
+ left: parent.left;
+ leftMargin: 150;
+ topMargin: -30;
+ }
+ property var selected: allTab;
+ TextButton {
+ id: allTab;
+ text: "ALL";
+ property string includeActions: 'snapshot,concurrency';
+ selected: allTab === selectedTab;
+ action: tabSelect;
+ }
+ TextButton {
+ id: placeTab;
+ text: "PLACES";
+ property string includeActions: 'concurrency';
+ selected: placeTab === selectedTab;
+ action: tabSelect;
+ }
+ TextButton {
+ id: snapsTab;
+ text: "SNAPS";
+ property string includeActions: 'snapshot';
+ selected: snapsTab === selectedTab;
+ action: tabSelect;
}
}
Image {
id: backgroundImage
- source: "../images/address-bar.svg"
- width: 720
+ source: "../images/address-bar-856.svg"
+ width: 856
height: 100
anchors {
bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom;
@@ -362,6 +401,7 @@ Window {
tags: tags,
description: description,
online_users: data.details.concurrency || 0,
+ drillDownToPlace: false,
searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
}
@@ -371,38 +411,54 @@ Window {
return true;
}
return (place.place_name !== AddressManager.hostname); // Not our entry, but do show other entry points to current domain.
- // could also require right protocolVersion
}
+ 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 = [
- 'include_actions=snapshot,concurrency',
+ 'include_actions=' + selectedTab.includeActions,
'protocol=' + encodeURIComponent(AddressManager.protocolVersion()),
'page=' + pageNumber
];
var url = metaverseBase + 'user_stories?' + options.join('&');
+ var thisRequestId = ++requestId;
getRequest(url, function (error, data) {
- if (handleError(url, error, data, cb)) {
+ 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);
- if (!addressLine.text) { // Don't add if the user is already filtering
- stories.forEach(function (story) {
- if (suggestable(story)) {
- suggestions.append(story);
- }
- });
- }
+ 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 filterChoicesByText() {
- suggestions.clear();
+ 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) {
@@ -413,16 +469,22 @@ Window {
return place.searchText.indexOf(word) >= 0;
});
}
- data.forEach(function (place) {
+ return function (place) {
if (matches(place)) {
- suggestions.append(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);
});
diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml
index 70eab82910..9e9b1dff51 100644
--- a/interface/resources/qml/hifi/Card.qml
+++ b/interface/resources/qml/hifi/Card.qml
@@ -18,6 +18,7 @@ import "toolbars"
import "../styles-uit"
Rectangle {
+ id: root;
property string userName: "";
property string placeName: "";
property string action: "";
@@ -27,13 +28,22 @@ Rectangle {
property var goFunction: null;
property string storyId: "";
+ property bool drillDownToPlace: false;
+ property bool showPlace: isConcurrency;
+ property string messageColor: hifi.colors.blueAccent;
property string timePhrase: pastTime(timestamp);
property int onlineUsers: 0;
+ property bool isConcurrency: action === 'concurrency';
+ property bool isStacked: !isConcurrency && drillDownToPlace;
property int textPadding: 10;
+ property int smallMargin: 4;
+ property int messageHeight: 40;
property int textSize: 24;
property int textSizeSmall: 18;
+ property int stackShadowNarrowing: 5;
property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif");
+ property int shadowHeight: 20;
HifiConstants { id: hifi }
function pastTime(timestamp) { // Answer a descriptive string
@@ -59,13 +69,16 @@ Rectangle {
Image {
id: lobby;
- width: parent.width;
- height: parent.height;
+ width: parent.width - (isConcurrency ? 0 : (2 * smallMargin));
+ height: parent.height - messageHeight - (isConcurrency ? 0 : smallMargin);
source: thumbnail || defaultThumbnail;
fillMode: Image.PreserveAspectCrop;
// source gets filled in later
- anchors.verticalCenter: parent.verticalCenter;
- anchors.left: parent.left;
+ anchors {
+ horizontalCenter: parent.horizontalCenter;
+ top: parent.top;
+ topMargin: isConcurrency ? 0 : smallMargin;
+ }
onStatusChanged: {
if (status == Image.Error) {
console.log("source: " + source + ": failed to load " + hifiUrl);
@@ -73,13 +86,41 @@ Rectangle {
}
}
}
+ Rectangle {
+ id: shadow1;
+ visible: isStacked;
+ width: parent.width - stackShadowNarrowing;
+ height: shadowHeight / 2;
+ anchors {
+ top: parent.bottom;
+ horizontalCenter: parent.horizontalCenter;
+ }
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: "gray" }
+ GradientStop { position: 1.0; color: "white" }
+ }
+ }
+ Rectangle {
+ id: shadow2;
+ visible: isStacked;
+ width: shadow1.width - stackShadowNarrowing;
+ height: shadowHeight / 2;
+ anchors {
+ top: shadow1.bottom;
+ horizontalCenter: parent.horizontalCenter;
+ }
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: "gray" }
+ GradientStop { position: 1.0; color: "white" }
+ }
+ }
property int dropHorizontalOffset: 0;
property int dropVerticalOffset: 1;
property int dropRadius: 2;
property int dropSamples: 9;
property int dropSpread: 0;
DropShadow {
- visible: desktop.gradientsSupported;
+ visible: showPlace && desktop.gradientsSupported;
source: place;
anchors.fill: place;
horizontalOffset: dropHorizontalOffset;
@@ -89,37 +130,57 @@ Rectangle {
color: hifi.colors.black;
spread: dropSpread;
}
- DropShadow {
- visible: users.visible && desktop.gradientsSupported;
- source: users;
- anchors.fill: users;
- horizontalOffset: dropHorizontalOffset;
- verticalOffset: dropVerticalOffset;
- radius: dropRadius;
- samples: dropSamples;
- color: hifi.colors.black;
- spread: dropSpread;
- }
RalewaySemiBold {
id: place;
+ visible: showPlace;
text: placeName;
color: hifi.colors.white;
size: textSize;
+ elide: Text.ElideRight; // requires constrained width
anchors {
top: parent.top;
left: parent.left;
+ right: parent.right;
margins: textPadding;
}
}
- FiraSansRegular {
- id: users;
- text: (action === 'concurrency') ? onlineUsers : 'snapshot';
- size: (action === 'concurrency') ? textSize : textSizeSmall;
- color: hifi.colors.white;
+ Row {
+ 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`
+ 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;
+ bottomMargin: parent.spacing;
+ }
+ }
+ spacing: textPadding;
+ height: messageHeight;
anchors {
- verticalCenter: usersImage.verticalCenter;
- right: usersImage.left;
- margins: textPadding;
+ bottom: parent.bottom;
+ left: parent.left;
+ leftMargin: textPadding;
}
}
// These two can be supplied to provide hover behavior.
@@ -128,7 +189,6 @@ Rectangle {
property var hoverThunk: function () { };
property var unhoverThunk: function () { };
MouseArea {
- id: zmouseArea;
anchors.fill: parent;
acceptedButtons: Qt.LeftButton;
onClicked: goFunction("hifi://" + hifiUrl);
@@ -136,18 +196,26 @@ Rectangle {
onEntered: hoverThunk();
onExited: unhoverThunk();
}
- ToolbarButton {
- id: usersImage;
- imageURL: "../../images/" + action + ".svg";
+ StateImage {
+ id: actionIcon;
+ imageURL: "../../images/info-icon-2-state.svg";
size: 32;
- onClicked: goFunction("/user_stories/" + storyId);
- buttonState: 0;
- defaultState: 0;
- hoverState: 1;
+ buttonState: messageArea.containsMouse ? 1 : 0;
anchors {
bottom: parent.bottom;
right: parent.right;
- margins: textPadding;
+ margins: smallMargin;
}
}
+ MouseArea {
+ id: messageArea;
+ width: parent.width;
+ height: messageHeight;
+ anchors {
+ top: lobby.bottom;
+ }
+ acceptedButtons: Qt.LeftButton;
+ onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId));
+ hoverEnabled: true;
+ }
}
diff --git a/interface/resources/qml/hifi/TextButton.qml b/interface/resources/qml/hifi/TextButton.qml
new file mode 100644
index 0000000000..02e49d86e4
--- /dev/null
+++ b/interface/resources/qml/hifi/TextButton.qml
@@ -0,0 +1,56 @@
+//
+// TextButton.qml
+//
+// Created by Howard Stearns 11/12/16
+// 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.4
+import "../styles-uit"
+
+Rectangle {
+ property alias text: label.text;
+ property alias pixelSize: label.font.pixelSize;
+ property bool selected: false;
+ property int spacing: 2;
+ property var action: function () { };
+ property string highlightColor: hifi.colors.blueHighlight;
+ width: label.width + 64;
+ height: 32;
+ color: "transparent";
+ HifiConstants { id: hifi; }
+ RalewaySemiBold {
+ id: label;
+ color: hifi.colors.white;
+ font.pixelSize: 20;
+ anchors {
+ horizontalCenter: parent.horizontalCenter;
+ verticalCenter: parent.verticalCenter;
+ }
+ }
+ Rectangle {
+ // This is crazy. All of this stuff (except the opacity) ought to be in the parent, with the label drawn on top.
+ // But there's a bug in QT such that if you select this TextButton, AND THEN enter the area of
+ // a TextButton created before this one, AND THEN enter a ListView with a highlight, then our label
+ // will draw as though it on the bottom. (If the phase of the moon is right, it will do this for a
+ // about half a second and then render normally. But if you're not lucky it just stays this way.)
+ // So.... here we deliberately put the rectangle on TOP of the text so that you can't tell when the bug
+ // is happening.
+ anchors.fill: parent;
+ radius: height / 2;
+ border.width: 4;
+ border.color: clickArea.containsMouse ? highlightColor : "transparent";
+ color: clickArea.containsPress ? hifi.colors.darkGray : (selected ? hifi.colors.blueAccent : "transparent");
+ opacity: (clickArea.containsMouse && !clickArea.containsPress) ? 0.8 : 0.5;
+ }
+ MouseArea {
+ id: clickArea;
+ anchors.fill: parent;
+ acceptedButtons: Qt.LeftButton;
+ onClicked: action(parent);
+ hoverEnabled: true;
+ }
+}
diff --git a/interface/resources/qml/hifi/toolbars/StateImage.qml b/interface/resources/qml/hifi/toolbars/StateImage.qml
new file mode 100644
index 0000000000..44eaa6f7fd
--- /dev/null
+++ b/interface/resources/qml/hifi/toolbars/StateImage.qml
@@ -0,0 +1,34 @@
+import QtQuick 2.5
+import QtQuick.Controls 1.4
+
+Item {
+ property alias imageURL: image.source
+ property alias alpha: image.opacity
+ property var subImage;
+ property int yOffset: 0
+ property int buttonState: 0
+ property real size: 50
+ width: size; height: size
+ property bool pinned: false
+ clip: true
+
+ function updateYOffset() { yOffset = size * buttonState; }
+ onButtonStateChanged: updateYOffset();
+
+ Component.onCompleted: {
+ if (subImage) {
+ if (subImage.y) {
+ yOffset = subImage.y;
+ return;
+ }
+ }
+ updateYOffset();
+ }
+
+ Image {
+ id: image
+ y: -parent.yOffset;
+ width: parent.width
+ }
+}
+
diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml
index aed90cd433..bc035ca19c 100644
--- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml
+++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml
@@ -1,41 +1,13 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
-Item {
+StateImage {
id: button
- property alias imageURL: image.source
- property alias alpha: image.opacity
- property var subImage;
- property int yOffset: 0
- property int buttonState: 0
property int hoverState: -1
property int defaultState: -1
- property var toolbar;
- property real size: 50 // toolbar ? toolbar.buttonSize : 50
- width: size; height: size
- property bool pinned: false
- clip: true
-
- onButtonStateChanged: {
- yOffset = size * buttonState;
- }
-
- Component.onCompleted: {
- if (subImage) {
- if (subImage.y) {
- yOffset = subImage.y;
- }
- }
- }
signal clicked()
- Image {
- id: image
- y: -button.yOffset;
- width: parent.width
- }
-
Timer {
id: asyncClickSender
interval: 10
diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
index f426f4a816..5290f3df19 100644
--- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
@@ -31,7 +31,7 @@
const float METERS_TO_INCHES = 39.3701f;
static uint32_t _currentWebCount { 0 };
// Don't allow more than 100 concurrent web views
-static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 100;
+static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20;
// If a web-view hasn't been rendered for 30 seconds, de-allocate the framebuffer
static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND;
@@ -69,8 +69,6 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer
qWarning() << "Too many concurrent web views to create new view";
return false;
}
- qDebug() << "Building web surface";
-
QString javaScriptToInject;
QFile webChannelFile(":qtwebchannel/qwebchannel.js");
QFile createGlobalEventBridgeFile(PathUtils::resourcesPath() + "/html/createGlobalEventBridge.js");
@@ -85,12 +83,15 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer
qCWarning(entitiesrenderer) << "unable to find qwebchannel.js or createGlobalEventBridge.js";
}
- ++_currentWebCount;
// Save the original GL context, because creating a QML surface will create a new context
QOpenGLContext * currentContext = QOpenGLContext::currentContext();
if (!currentContext) {
return false;
}
+
+ ++_currentWebCount;
+ qDebug() << "Building web surface: " << getID() << ", #" << _currentWebCount << ", url = " << _sourceUrl;
+
QSurface * currentSurface = currentContext->surface();
auto deleter = [](OffscreenQmlSurface* webSurface) {
@@ -356,6 +357,8 @@ void RenderableWebEntityItem::destroyWebSurface() {
QObject::disconnect(_hoverLeaveConnection);
_hoverLeaveConnection = QMetaObject::Connection();
_webSurface.reset();
+
+ qDebug() << "Delete web surface: " << getID() << ", #" << _currentWebCount << ", url = " << _sourceUrl;
}
}