From 5c5ace7c7b60d6f9b283ae1ff5c0817eae931dbb Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 8 Feb 2017 21:37:42 +0000 Subject: [PATCH 1/5] goto in tablet --- interface/resources/qml/hifi/Card.qml | 2 +- .../qml/hifi/tablet/TabletAddressDialog.qml | 557 ++++++++++++++++++ interface/src/ui/overlays/Web3DOverlay.cpp | 6 + scripts/system/goto.js | 7 +- 4 files changed, 568 insertions(+), 4 deletions(-) create mode 100644 interface/resources/qml/hifi/tablet/TabletAddressDialog.qml diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 876be740cd..7dd7c63da9 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -129,7 +129,7 @@ Rectangle { property int dropSamples: 9; property int dropSpread: 0; DropShadow { - visible: showPlace && desktop.gradientsSupported; + visible: showPlace; //&& desktop.gradientsSupported; source: place; anchors.fill: place; horizontalOffset: dropHorizontalOffset; diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml new file mode 100644 index 0000000000..011b4928bd --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -0,0 +1,557 @@ +// +// TabletAddressDialog.qml +// +// Created by Dante Ruiz on 2016/07/16 +// 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 QtGraphicalEffects 1.0 +import "../../controls" +import "../../styles" +import "../../windows" +import "../" +import "../toolbars" +import "../../styles-uit" as HifiStyles +import "../../controls-uit" as HifiControls + +Item { + id: root + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifiStyleConstants } + + width: parent.width + height: parent.height + + property var allStories: []; + property int cardWidth: 370; + property int cardHeight: 320; + property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; + + + Component.onCompleted: { + fillDestinations(); + updateLocationText(); + 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 resetAfterTeleport() { + //storyCardFrame.shown = root.shown = false; + } + function goCard(targetString) { + if (0 !== targetString.indexOf('hifi://')) { + return; + } + addressLine.text = targetString; + toggleOrGo(true); + clearAddressLineTimer.start(); + } + + property bool isCursorVisible: false // Override default cursor visibility. + + + AddressBarDialog { + id: addressBarDialog + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + width: parent.width + height: parent.height + + anchors { + right: parent.right + left: parent.left + top: parent.top + bottom: parent.bottom + } + + onMetaverseServerUrlChanged: updateLocationTextTimer.start(); + Rectangle { + id: topBar + height: 90 + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + + } + + GradientStop { + position: 1 + color: "#1e1e1e" + } + } + + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.topMargin: 0 + anchors.top: parent.top + + Row { + id: thing + spacing: 2 * hifi.layout.spacing + + anchors { + top: parent.top; + left: parent.left + } + + 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: snapTab; + text: "SNAP"; + property string includeActions: 'snapshot'; + selected: snapTab === selectedTab; + action: tabSelect; + } + } + + } + + Rectangle { + id: bgMain + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + + } + + GradientStop { + position: 1 + color: "#0f212e" + } + } + + + anchors.bottom: backgroundImage.top + 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: 10; + clip: true + spacing: 14 + anchors { + bottom: parent.bottom + top: parent.top + left: parent.left + right: parent.right + leftMargin: 50 + } + 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; } + } + } + + Rectangle { + id: backgroundImage + width: 480 + height: 70 + + gradient: Gradient { + GradientStop { + position: 0 + color: "#c2ced8" + + } + + GradientStop { + position: 1 + color: "#c2ced8" + } + } + + anchors { + bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom + right: parent.right + left: parent.left + } + + + ToolbarButton { + id: homeButton + imageURL: "../../../images/home.svg" + onClicked: { + addressBarDialog.loadHome(); + root.shown = false; + } + anchors { + left: parent.left + leftMargin: homeButton.width / 2 + verticalCenter: parent.verticalCenter + } + } + property int inputAreaHeight: 70 + property int inputAreaStep: (height - inputAreaHeight) / 2 + + HifiStyles.RalewayLight { + id: notice; + font.pixelSize: hifi.fonts.pixelSize * 0.50; + anchors { + top: parent.top + topMargin: parent.inputAreaStep + 12 + left: addressLine.left + right: addressLine.right + } + } + HifiStyles.FiraSansRegular { + id: location; + font.pixelSize: addressLine.font.pixelSize; + color: "gray"; + clip: true; + anchors.fill: addressLine; + visible: addressLine.text.length === 0 + } + + TextInput { + id: addressLine + focus: true + anchors { + bottom: parent.bottom + left: homeButton.right + right: parent.right + leftMargin: homeButton.width + rightMargin: homeButton.width / 2 + topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing) + bottomMargin: parent.inputAreaStep + } + font.pixelSize: hifi.fonts.pixelSize * 0.75 + cursorVisible: false + onTextChanged: { + filterChoicesByText(); + updateLocationText(text.length > 0); + if (!isCursorVisible && text.length > 0) { + isCursorVisible = true; + cursorVisible = true; + } + } + onAccepted: { + addressBarDialog.keyboardEnabled = false; + } + onActiveFocusChanged: { + cursorVisible = isCursorVisible && focus; + } + MouseArea { + // If user clicks in address bar show cursor to indicate ability to enter address. + anchors.fill: parent + onClicked: { + isCursorVisible = true; + //parent.cursorVisible = true; + parent.forceActiveFocus(); + addressBarDialog.keyboardEnabled = HMD.active + tabletRoot.playButtonClickSound(); + } + } + } + } + + Timer { + // Delay updating location text a bit to avoid flicker of content and so that connection status is valid. + id: updateLocationTextTimer + running: false + interval: 500 // ms + repeat: false + onTriggered: updateLocationText(false); + } + + Timer { + // Delay clearing address line so as to avoid flicker of "not connected" being displayed after entering an address. + id: clearAddressLineTimer + running: false + interval: 100 // ms + repeat: false + onTriggered: { + addressLine.text = ""; + isCursorVisible = false; + } + } + + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + + clickSound: tabletRoot.playButtonClickSound(); + } + + } + + 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 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 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"; + notice.color = hifiStyleConstants.colors.baseGrayHighlight; + } else { + notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected"; + notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.baseGrayHighlight : hifiStyleConstants.colors.redHighlight; + // Display hostname, which includes ip address, localhost, and other non-placenames. + location.text = (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''); + } + } + + onVisibleChanged: { + updateLocationText(false); + if (visible) { + addressLine.forceActiveFocus(); + fillDestinations(); + } + } + + 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 + clearAddressLineTimer.start(); + event.accepted = true + break + case Qt.Key_Enter: + case Qt.Key_Return: + toggleOrGo() + clearAddressLineTimer.start(); + event.accepted = true + break + } + } +} diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index a381b90bfb..ddf380d0b2 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -34,6 +34,9 @@ #include #include #include +#include +#include "scripting/AccountScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -160,6 +163,9 @@ void Web3DOverlay::loadSourceURL() { auto tabletScriptingInterface = DependencyManager::get(); auto flags = tabletScriptingInterface->getFlags(); _webSurface->getRootContext()->setContextProperty("offscreenFlags", flags); + _webSurface->getRootContext()->setContextProperty("AddressManager", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); + _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); // Override min fps for tablet UI, for silky smooth scrolling diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 092abd0369..9e6964b5f0 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -18,13 +18,14 @@ var button; var buttonName = "GOTO"; var toolBar = null; var tablet = null; - +var ADDRESS_DIALOG_QML_SOURCE = "TabletAddressDialog.qml"; function onAddressBarShown(visible) { - button.editProperties({isActive: visible}); + //button.editProperties({isActive: visible}); } function onClicked(){ - DialogsManager.toggleAddressBar(); + //DialogsManager.toggleAddressBar(); + tablet.loadQMLSource(ADDRESS_DIALOG_QML_SOURCE); } if (Settings.getValue("HUDUIEnabled")) { From 6bc685b02a039d0a394eedfdbfe5981e0050a93d Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 8 Feb 2017 21:38:28 +0000 Subject: [PATCH 2/5] saving work --- interface/resources/qml/hifi/tablet/TabletAddressDialog.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 011b4928bd..952a1f7faa 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -343,8 +343,6 @@ Item { left: parent.left right: parent.right } - - clickSound: tabletRoot.playButtonClickSound(); } } From 580216c3d2d316f739a05eba52cc340a4476f8ab Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 8 Feb 2017 21:56:41 +0000 Subject: [PATCH 3/5] adding tablet-goto.js --- scripts/system/goto.js | 6 ++-- scripts/system/tablet-goto.js | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 scripts/system/tablet-goto.js diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 9e6964b5f0..aea061fce4 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -18,14 +18,12 @@ var button; var buttonName = "GOTO"; var toolBar = null; var tablet = null; -var ADDRESS_DIALOG_QML_SOURCE = "TabletAddressDialog.qml"; function onAddressBarShown(visible) { - //button.editProperties({isActive: visible}); + button.editProperties({isActive: visible}); } function onClicked(){ - //DialogsManager.toggleAddressBar(); - tablet.loadQMLSource(ADDRESS_DIALOG_QML_SOURCE); + DialogsManager.toggleAddressBar(); } if (Settings.getValue("HUDUIEnabled")) { diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js new file mode 100644 index 0000000000..1a3fbab3ea --- /dev/null +++ b/scripts/system/tablet-goto.js @@ -0,0 +1,64 @@ +"use strict"; + +// +// goto.js +// scripts/system/ +// +// Created by Dante Ruiz on 8 February 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 +// + +(function() { // BEGIN LOCAL_SCOPE + var gotoQmlSource = "TabletAddressDialog.qml"; + var button; + var buttonName = "GOTO"; + var toolBar = null; + var tablet = null; + function onAddressBarShown(visible) { + if (toolBar) { + button.editProperties({isActive: visible}); + } + } + + function onClicked(){ + if (toolBar) { + DialogsManager.toggleAddressBar(); + } else { + tablet.loadQMLSource(gotoQmlSource); + } + } + if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/directory.svg"), + visible: true, + alpha: 0.9 + }); + } else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/goto-i.svg", + text: buttonName, + sortOrder: 8 + }); + } + + button.clicked.connect(onClicked); + DialogsManager.addressBarShown.connect(onAddressBarShown); + + Script.scriptEnding.connect(function () { + button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } + DialogsManager.addressBarShown.disconnect(onAddressBarShown); + }); + +}()); // END LOCAL_SCOPE From 0e83e5da0654606458756a0bb4a0b88bd1b52f38 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 8 Feb 2017 22:03:32 +0000 Subject: [PATCH 4/5] minimize diff --- scripts/system/goto.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/system/goto.js b/scripts/system/goto.js index aea061fce4..092abd0369 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -18,6 +18,7 @@ var button; var buttonName = "GOTO"; var toolBar = null; var tablet = null; + function onAddressBarShown(visible) { button.editProperties({isActive: visible}); } From ff60058cbe219a59d55286975f48b62a0d570fc3 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 9 Feb 2017 17:54:41 +0000 Subject: [PATCH 5/5] checking if desktop exists --- interface/resources/qml/hifi/Card.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 7dd7c63da9..f6f7e88d0c 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -129,7 +129,7 @@ Rectangle { property int dropSamples: 9; property int dropSpread: 0; DropShadow { - visible: showPlace; //&& desktop.gradientsSupported; + visible: showPlace && (desktop ? desktop.gradientsSupported : false) source: place; anchors.fill: place; horizontalOffset: dropHorizontalOffset;