Merge branch 'master' of https://github.com/highfidelity/hifi into light

This commit is contained in:
samcake 2017-11-20 09:26:29 -08:00
commit a74372b233
33 changed files with 940 additions and 282 deletions

View file

@ -1,7 +1,7 @@
import QtQuick 2.5
import QtQuick.Controls 1.2
import QtWebChannel 1.0
import QtWebEngine 1.2
import QtWebEngine 1.5
import "controls-uit"
import "styles" as HifiStyles

View file

@ -8,7 +8,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick 2.7
import QtWebEngine 1.5
WebEngineView {

View file

@ -51,19 +51,23 @@ FocusScope {
// The VR version of the primary menu
property var rootMenu: Menu {
id: rootMenuId
objectName: "rootMenu"
// for some reasons it is not possible to use just '({})' here as it gets empty when passed to TableRoot/DesktopRoot
property var exclusionGroupsByMenuItem : ListModel {}
property var exclusionGroups: ({});
property Component exclusiveGroupMaker: Component {
ExclusiveGroup {
}
}
function addExclusionGroup(menuItem, exclusionGroup)
{
exclusionGroupsByMenuItem.append(
{
'menuItem' : menuItem.toString(),
'exclusionGroup' : exclusionGroup.toString()
}
);
function addExclusionGroup(qmlAction, exclusionGroup) {
var exclusionGroupId = exclusionGroup.toString();
if(!exclusionGroups[exclusionGroupId]) {
exclusionGroups[exclusionGroupId] = exclusiveGroupMaker.createObject(rootMenuId);
}
qmlAction.exclusiveGroup = exclusionGroups[exclusionGroupId]
}
}

View file

@ -1,6 +1,6 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtWebEngine 1.1;
import QtWebEngine 1.5;
import Qt.labs.settings 1.0
import "../desktop" as OriginalDesktop

View file

@ -1,3 +1,4 @@
//
// WebBrowser.qml
//
@ -9,12 +10,12 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.5 as QQControls
import QtQuick 2.7
import QtQuick.Controls 2.2 as QQControls
import QtQuick.Layouts 1.3
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
import QtWebEngine 1.2
import QtWebEngine 1.5
import QtWebChannel 1.0
import "../styles-uit"
@ -22,6 +23,8 @@ import "../controls-uit" as HifiControls
import "../windows"
import "../controls"
import HifiWeb 1.0
Rectangle {
id: root;
@ -32,214 +35,401 @@ Rectangle {
property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false
property bool keyboardRaised: false
property bool punctuationMode: false
property var suggestionsList: []
readonly property string searchUrlTemplate: "https://www.google.com/search?client=hifibrowser&q=";
WebBrowserSuggestionsEngine {
id: searchEngine
onSuggestions: {
if (suggestions.length > 0) {
suggestionsList = []
suggestionsList.push(addressBarInput.text); //do not overwrite edit text
for(var i = 0; i < suggestions.length; i++) {
suggestionsList.push(suggestions[i]);
}
addressBar.model = suggestionsList
if (!addressBar.popup.visible) {
addressBar.popup.open();
}
}
}
}
Timer {
id: suggestionRequestTimer
interval: 200
repeat: false
onTriggered: {
if (addressBar.editText !== "") {
searchEngine.querySuggestions(addressBarInput.text);
}
}
}
color: hifi.colors.baseGray;
// only show the title if loaded through a "loader"
function goTo(url) {
//must be valid attempt to open an site with dot
var urlNew = url
if (url.indexOf(".") > 0) {
if (url.indexOf("http") < 0) {
urlNew = "http://" + url;
}
} else {
urlNew = searchUrlTemplate + url
}
addressBar.model = []
//need to rebind if binfing was broken by selecting from suggestions
addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; });
webStack.currentItem.webEngineView.url = urlNew
suggestionRequestTimer.stop();
addressBar.popup.close();
}
Column {
spacing: 2
width: parent.width;
RowLayout {
id: addressBarRow
width: parent.width;
height: 48
HifiControls.WebGlyphButton {
enabled: webEngineView.canGoBack
enabled: webStack.currentItem.webEngineView.canGoBack || webStack.depth > 1
glyph: hifi.glyphs.backward;
anchors.verticalCenter: parent.verticalCenter;
size: 38;
onClicked: {
webEngineView.goBack()
if (webStack.currentItem.webEngineView.canGoBack) {
webStack.currentItem.webEngineView.goBack();
} else if (webStack.depth > 1) {
webStack.pop();
}
}
}
HifiControls.WebGlyphButton {
enabled: webEngineView.canGoForward
enabled: webStack.currentItem.webEngineView.canGoForward
glyph: hifi.glyphs.forward;
anchors.verticalCenter: parent.verticalCenter;
size: 38;
onClicked: {
webEngineView.goForward()
webStack.currentItem.webEngineView.goForward();
}
}
QQControls.TextField {
QQControls.ComboBox {
id: addressBar
Image {
anchors.verticalCenter: addressBar.verticalCenter;
x: 5
z: 2
id: faviconImage
width: 16; height: 16
sourceSize: Qt.size(width, height)
source: webEngineView.icon
//selectByMouse: true
focus: true
editable: true
//flat: true
indicator: Item {}
background: Item {}
onActivated: {
goTo(textAt(index));
}
HifiControls.WebGlyphButton {
glyph: webEngineView.loading ? hifi.glyphs.closeSmall : hifi.glyphs.reloadSmall;
anchors.verticalCenter: parent.verticalCenter;
width: hifi.dimensions.controlLineHeight
z: 2
x: addressBar.width - 28
onClicked: {
if (webEngineView.loading) {
webEngineView.stop()
} else {
reloadTimer.start()
onHighlightedIndexChanged: {
if (highlightedIndex >= 0) {
addressBar.editText = textAt(highlightedIndex)
}
}
popup.height: webStack.height
onFocusChanged: {
if (focus) {
addressBarInput.selectAll();
}
}
contentItem: QQControls.TextField {
id: addressBarInput
leftPadding: 26
rightPadding: hifi.dimensions.controlLineHeight + 5
text: addressBar.editText
placeholderText: qsTr("Enter URL")
font: addressBar.font
selectByMouse: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
onFocusChanged: {
if (focus) {
selectAll();
}
}
Keys.onDeletePressed: {
addressBarInput.text = ""
}
Keys.onPressed: {
if (event.key === Qt.Key_Return) {
goTo(addressBarInput.text);
event.accepted = true;
}
}
Image {
anchors.verticalCenter: parent.verticalCenter;
x: 5
z: 2
id: faviconImage
width: 16; height: 16
sourceSize: Qt.size(width, height)
source: webStack.currentItem.webEngineView.icon
}
HifiControls.WebGlyphButton {
glyph: webStack.currentItem.webEngineView.loading ? hifi.glyphs.closeSmall : hifi.glyphs.reloadSmall;
anchors.verticalCenter: parent.verticalCenter;
width: hifi.dimensions.controlLineHeight
z: 2
x: addressBarInput.width - implicitWidth
onClicked: {
if (webStack.currentItem.webEngineView.loading) {
webStack.currentItem.webEngineView.stop();
} else {
webStack.currentItem.reloadTimer.start();
}
}
}
}
style: TextFieldStyle {
padding {
left: 26;
right: 26
Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i");
Keys.onPressed: {
if (event.key === Qt.Key_Return) {
goTo(addressBarInput.text);
event.accepted = true;
}
}
focus: true
onEditTextChanged: {
if (addressBar.editText !== "" && addressBar.editText !== webStack.currentItem.webEngineView.url.toString()) {
suggestionRequestTimer.restart();
} else {
addressBar.model = []
addressBar.popup.close();
}
}
Layout.fillWidth: true
text: webEngineView.url
onAccepted: webEngineView.url = text
editText: webStack.currentItem.webEngineView.url
onAccepted: goTo(addressBarInput.text);
}
HifiControls.WebGlyphButton {
checkable: true
//only QtWebEngine 1.3
//checked: webEngineView.audioMuted
checked: webStack.currentItem.webEngineView.audioMuted
glyph: checked ? hifi.glyphs.unmuted : hifi.glyphs.muted
anchors.verticalCenter: parent.verticalCenter;
width: hifi.dimensions.controlLineHeight
onClicked: {
webEngineView.triggerWebAction(WebEngineView.ToggleMediaMute)
webStack.currentItem.webEngineView.audioMuted = !webStack.currentItem.webEngineView.audioMuted
}
}
}
QQControls.ProgressBar {
id: loadProgressBar
style: ProgressBarStyle {
background: Rectangle {
color: "#6A6A6A"
}
progress: Rectangle{
background: Rectangle {
implicitHeight: 2
color: "#6A6A6A"
}
contentItem: Item {
implicitHeight: 2
Rectangle {
width: loadProgressBar.visualPosition * parent.width
height: parent.height
color: "#00B4EF"
}
}
width: parent.width;
minimumValue: 0
maximumValue: 100
value: webEngineView.loadProgress
from: 0
to: 100
value: webStack.currentItem.webEngineView.loadProgress
height: 2
}
HifiControls.BaseWebView {
id: webEngineView
focus: true
objectName: "tabletWebEngineView"
Component {
id: webViewComponent
Rectangle {
property alias webEngineView: webEngineView
property alias reloadTimer: reloadTimer
url: "http://www.highfidelity.com"
property real webViewHeight: root.height - loadProgressBar.height - 48 - 4
property WebEngineNewViewRequest request: null
width: parent.width;
height: keyboardEnabled && keyboardRaised ? webViewHeight - keyboard.height : webViewHeight
property bool isDialog: QQControls.StackView.index > 0
property real margins: isDialog ? 10 : 0
profile: HFWebEngineProfile;
color: "#d1d1d1"
property string userScriptUrl: ""
// creates a global EventBridge object.
WebEngineScript {
id: createGlobalEventBridge
sourceCode: eventBridgeJavaScriptToInject
injectionPoint: WebEngineScript.DocumentCreation
worldId: WebEngineScript.MainWorld
}
// detects when to raise and lower virtual keyboard
WebEngineScript {
id: raiseAndLowerKeyboard
injectionPoint: WebEngineScript.Deferred
sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js"
worldId: WebEngineScript.MainWorld
}
// User script.
WebEngineScript {
id: userScript
sourceUrl: webEngineView.userScriptUrl
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
settings.autoLoadImages: true
settings.javascriptEnabled: true
settings.errorPageEnabled: true
settings.pluginsEnabled: true
settings.fullScreenSupportEnabled: false
//from WebEngine 1.3
// settings.autoLoadIconsForPage: false
// settings.touchIconsEnabled: false
onCertificateError: {
error.defer();
}
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
webEngineView.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)";
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, true);
}
onNewViewRequested: {
if (!request.userInitiated) {
print("Warning: Blocked a popup window.");
}
}
onRenderProcessTerminated: {
var status = "";
switch (terminationStatus) {
case WebEngineView.NormalTerminationStatus:
status = "(normal exit)";
break;
case WebEngineView.AbnormalTerminationStatus:
status = "(abnormal exit)";
break;
case WebEngineView.CrashedTerminationStatus:
status = "(crashed)";
break;
case WebEngineView.KilledTerminationStatus:
status = "(killed)";
break;
QQControls.StackView.onActivated: {
addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; });
}
print("Render process exited with code " + exitCode + " " + status);
reloadTimer.running = true;
}
onRequestChanged: {
if (isDialog && request !== null && request !== undefined) {//is Dialog ?
request.openIn(webEngineView);
}
}
onWindowCloseRequested: {
}
HifiControls.BaseWebView {
id: webEngineView
anchors.fill: parent
anchors.margins: parent.margins
Timer {
id: reloadTimer
interval: 0
running: false
repeat: false
onTriggered: webEngineView.reload()
layer.enabled: parent.isDialog
layer.effect: DropShadow {
verticalOffset: 8
horizontalOffset: 8
color: "#330066ff"
samples: 10
spread: 0.5
}
focus: true
objectName: "tabletWebEngineView"
//profile: HFWebEngineProfile;
profile.httpUserAgent: "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0"
property string userScriptUrl: ""
onLoadingChanged: {
if (!loading) {
addressBarInput.cursorPosition = 0 //set input field cursot to beginning
suggestionRequestTimer.stop();
addressBar.popup.close();
}
}
onLinkHovered: {
//TODO: change cursor shape?
}
// creates a global EventBridge object.
WebEngineScript {
id: createGlobalEventBridge
sourceCode: eventBridgeJavaScriptToInject
injectionPoint: WebEngineScript.Deferred
worldId: WebEngineScript.MainWorld
}
// detects when to raise and lower virtual keyboard
WebEngineScript {
id: raiseAndLowerKeyboard
injectionPoint: WebEngineScript.Deferred
sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js"
worldId: WebEngineScript.MainWorld
}
// User script.
WebEngineScript {
id: userScript
sourceUrl: webEngineView.userScriptUrl
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
settings.autoLoadImages: true
settings.javascriptEnabled: true
settings.errorPageEnabled: true
settings.pluginsEnabled: true
settings.fullScreenSupportEnabled: true
settings.autoLoadIconsForPage: true
settings.touchIconsEnabled: true
onCertificateError: {
error.defer();
}
Component.onCompleted: {
webChannel.registerObject("eventBridge", eventBridge);
webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper);
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, true);
}
onNewViewRequested: {
if (request.destination == WebEngineView.NewViewInDialog) {
webStack.push(webViewComponent, {"request": request});
} else {
request.openIn(webEngineView);
}
}
onRenderProcessTerminated: {
var status = "";
switch (terminationStatus) {
case WebEngineView.NormalTerminationStatus:
status = "(normal exit)";
break;
case WebEngineView.AbnormalTerminationStatus:
status = "(abnormal exit)";
break;
case WebEngineView.CrashedTerminationStatus:
status = "(crashed)";
break;
case WebEngineView.KilledTerminationStatus:
status = "(killed)";
break;
}
console.error("Render process exited with code " + exitCode + " " + status);
reloadTimer.running = true;
}
onFullScreenRequested: {
if (request.toggleOn) {
webEngineView.state = "FullScreen";
} else {
webEngineView.state = "";
}
request.accept();
}
onWindowCloseRequested: {
webStack.pop();
}
}
Timer {
id: reloadTimer
interval: 0
running: false
repeat: false
onTriggered: webEngineView.reload()
}
}
}
QQControls.StackView {
id: webStack
width: parent.width;
property real webViewHeight: root.height - loadProgressBar.height - 48 - 4
height: keyboardEnabled && keyboardRaised ? webViewHeight - keyboard.height : webViewHeight
Component.onCompleted: webStack.push(webViewComponent, {"webEngineView.url": "https://www.highfidelity.com"});
}
}
HifiControls.Keyboard {
id: keyboard
raised: parent.keyboardEnabled && parent.keyboardRaised

View file

@ -5,7 +5,9 @@ import TabletScriptingInterface 1.0
Item {
id: tabletButton
property string captionColorOverride: ""
property color defaultCaptionColor: "#ffffff"
property color captionColor: defaultCaptionColor
property var uuid;
property string icon: "icons/tablet-icons/edit-i.svg"
property string hoverIcon: tabletButton.icon
@ -105,7 +107,7 @@ Item {
Text {
id: text
color: captionColorOverride !== "" ? captionColorOverride: "#ffffff"
color: captionColor
text: tabletButton.text
font.bold: true
font.pixelSize: 18
@ -171,7 +173,7 @@ Item {
PropertyChanges {
target: text
color: captionColorOverride !== "" ? captionColorOverride: "#ffffff"
color: captionColor
text: tabletButton.hoverText
}
@ -197,7 +199,7 @@ Item {
PropertyChanges {
target: text
color: captionColorOverride !== "" ? captionColorOverride: "#333333"
color: captionColor !== defaultCaptionColor ? captionColor : "#333333"
text: tabletButton.activeText
}
@ -228,7 +230,7 @@ Item {
PropertyChanges {
target: text
color: captionColorOverride !== "" ? captionColorOverride: "#333333"
color: captionColor !== defaultCaptionColor ? captionColor : "#333333"
text: tabletButton.activeHoverText
}

View file

@ -40,37 +40,29 @@ Item {
CheckBox {
id: checkbox
// FIXME: Should use radio buttons if source.exclusiveGroup.
width: 20
visible: source !== null ?
source.visible && source.type === 1 && source.checkable && !source.exclusiveGroup :
false
checked: setChecked()
function setChecked() {
if (!source || source.type !== 1 || !source.checkable) {
return false;
}
// FIXME this works for native QML menus but I don't think it will
// for proxied QML menus
return source.checked;
Binding on checked {
value: source.checked;
when: source && source.type === 1 && source.checkable && !source.exclusiveGroup;
}
}
RadioButton {
id: radiobutton
// FIXME: Should use radio buttons if source.exclusiveGroup.
width: 20
visible: source !== null ?
source.visible && source.type === 1 && source.checkable && source.exclusiveGroup :
false
checked: setChecked()
function setChecked() {
if (!source || source.type !== 1 || !source.checkable) {
return false;
}
// FIXME this works for native QML menus but I don't think it will
// for proxied QML menus
return source.checked;
Binding on checked {
value: source.checked;
when: source && source.type === 1 && source.checkable && source.exclusiveGroup;
}
}
}

View file

@ -66,7 +66,6 @@ Item {
function toModel(items, newMenu) {
var result = modelMaker.createObject(tabletMenu);
var exclusionGroups = {};
for (var i = 0; i < items.length; ++i) {
var item = items[i];
@ -78,28 +77,6 @@ Item {
if (item.text !== "Users Online") {
result.append({"name": item.text, "item": item})
}
for(var j = 0; j < tabletMenu.rootMenu.exclusionGroupsByMenuItem.count; ++j)
{
var entry = tabletMenu.rootMenu.exclusionGroupsByMenuItem.get(j);
if(entry.menuItem == item.toString())
{
var exclusionGroupId = entry.exclusionGroup;
console.debug('item exclusionGroupId: ', exclusionGroupId)
if(!exclusionGroups[exclusionGroupId])
{
exclusionGroups[exclusionGroupId] = exclusiveGroupMaker.createObject(newMenu);
console.debug('new exclusion group created: ', exclusionGroups[exclusionGroupId])
}
var exclusionGroup = exclusionGroups[exclusionGroupId];
item.exclusiveGroup = exclusionGroup
console.debug('item.exclusiveGroup: ', item.exclusiveGroup)
}
}
break;
case MenuItemType.Separator:
result.append({"name": "", "item": item})

View file

@ -4,7 +4,9 @@ import QtQuick.Controls 1.4
StateImage {
id: button
property string captionColorOverride: ""
property color defaultCaptionColor: "#ffffff"
property color captionColor: defaultCaptionColor
property bool buttonEnabled: true
property bool isActive: false
property bool isEntered: false
@ -98,7 +100,7 @@ StateImage {
Text {
id: caption
color: captionColorOverride !== "" ? captionColorOverride: (button.isActive ? "#000000" : "#ffffff")
color: button.isActive ? "#000000" : captionColor
text: button.isActive ? (button.isEntered ? button.activeHoverText : button.activeText) : (button.isEntered ? button.hoverText : button.text)
font.bold: false
font.pixelSize: 9

View file

@ -203,6 +203,8 @@
#include "commerce/Wallet.h"
#include "commerce/QmlCommerce.h"
#include "webbrowser/WebBrowserSuggestionsEngine.h"
// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
// FIXME seems to be broken.
#if defined(Q_OS_WIN)
@ -2223,6 +2225,7 @@ void Application::initializeUi() {
QmlCommerce::registerType();
qmlRegisterType<ResourceImageItem>("Hifi", 1, 0, "ResourceImageItem");
qmlRegisterType<Preference>("Hifi", 1, 0, "Preference");
qmlRegisterType<WebBrowserSuggestionsEngine>("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine");
auto offscreenUi = DependencyManager::get<OffscreenUi>();
offscreenUi->create();
@ -7086,6 +7089,7 @@ DisplayPluginPointer Application::getActiveDisplayPlugin() const {
return _displayPlugin;
}
static const char* EXCLUSION_GROUP_KEY = "exclusionGroup";
static void addDisplayPluginToMenu(DisplayPluginPointer displayPlugin, bool active = false) {
auto menu = Menu::getInstance();
@ -7121,6 +7125,8 @@ static void addDisplayPluginToMenu(DisplayPluginPointer displayPlugin, bool acti
action->setCheckable(true);
action->setChecked(active);
displayPluginGroup->addAction(action);
action->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(displayPluginGroup));
Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name));
}

View file

@ -56,7 +56,7 @@ Menu* Menu::getInstance() {
return dynamic_cast<Menu*>(qApp->getWindow()->menuBar());
}
const char* exclusionGroupKey = "exclusionGroup";
const char* EXCLUSION_GROUP_KEY = "exclusionGroup";
Menu::Menu() {
auto dialogsManager = DependencyManager::get<DialogsManager>();
@ -228,21 +228,21 @@ Menu::Menu() {
viewMenu, MenuOption::FirstPerson, Qt::CTRL | Qt::Key_F,
true, qApp, SLOT(cameraMenuChanged())));
firstPersonAction->setProperty(exclusionGroupKey, QVariant::fromValue(cameraModeGroup));
firstPersonAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Third Person
auto thirdPersonAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(
viewMenu, MenuOption::ThirdPerson, Qt::CTRL | Qt::Key_G,
false, qApp, SLOT(cameraMenuChanged())));
thirdPersonAction->setProperty(exclusionGroupKey, QVariant::fromValue(cameraModeGroup));
thirdPersonAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Mirror
auto viewMirrorAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(
viewMenu, MenuOption::FullscreenMirror, Qt::CTRL | Qt::Key_H,
false, qApp, SLOT(cameraMenuChanged())));
viewMirrorAction->setProperty(exclusionGroupKey, QVariant::fromValue(cameraModeGroup));
viewMirrorAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Independent [advanced]
auto viewIndependentAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu,
@ -250,7 +250,7 @@ Menu::Menu() {
false, qApp, SLOT(cameraMenuChanged()),
UNSPECIFIED_POSITION, "Advanced"));
viewIndependentAction->setProperty(exclusionGroupKey, QVariant::fromValue(cameraModeGroup));
viewIndependentAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Entity Camera [advanced]
auto viewEntityCameraAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu,
@ -258,7 +258,7 @@ Menu::Menu() {
false, qApp, SLOT(cameraMenuChanged()),
UNSPECIFIED_POSITION, "Advanced"));
viewEntityCameraAction->setProperty(exclusionGroupKey, QVariant::fromValue(cameraModeGroup));
viewEntityCameraAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
viewMenu->addSeparator();

View file

@ -216,7 +216,7 @@ QVariant Base3DOverlay::getProperty(const QString& property) {
if (property == "localRotation" || property == "localOrientation") {
return quatToVariant(getLocalOrientation());
}
if (property == "isSolid" || property == "isFilled" || property == "solid" || property == "filed") {
if (property == "isSolid" || property == "isFilled" || property == "solid" || property == "filled" || property == "filed") {
return _isSolid;
}
if (property == "isWire" || property == "wire") {

View file

@ -0,0 +1,88 @@
//
// WebBrowserSuggestionsEngine.cpp
// interface/src/webbrowser
//
// Created by Vlad Stelmahovsky on 30/10/17.
// Copyright 2017 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
//
#include "WebBrowserSuggestionsEngine.h"
#include "qregexp.h"
#include <qbuffer.h>
#include <qcoreapplication.h>
#include <qlocale.h>
#include <qnetworkrequest.h>
#include <qnetworkreply.h>
#include <qregexp.h>
#include <qstringlist.h>
#include <QUrlQuery>
#include <QJsonDocument>
#include <NetworkAccessManager.h>
static const QString GoogleSuggestionsUrl = "https://suggestqueries.google.com/complete/search?output=firefox&q=%1";
static const int SUGGESTIONS_LIST_INDEX = 1;
WebBrowserSuggestionsEngine::WebBrowserSuggestionsEngine(QObject* parent)
: QObject(parent)
, _suggestionsReply(0) {
_currentNAM = &NetworkAccessManager::getInstance();
connect(_currentNAM, &QNetworkAccessManager::finished, this, &WebBrowserSuggestionsEngine::suggestionsFinished);
}
WebBrowserSuggestionsEngine::~WebBrowserSuggestionsEngine() {
disconnect(_currentNAM, &QNetworkAccessManager::finished, this, &WebBrowserSuggestionsEngine::suggestionsFinished);
}
void WebBrowserSuggestionsEngine::querySuggestions(const QString &searchString) {
if (_suggestionsReply) {
_suggestionsReply->disconnect(this);
_suggestionsReply->abort();
_suggestionsReply->deleteLater();
_suggestionsReply = 0;
}
QString url = QString(GoogleSuggestionsUrl).arg(searchString);
_suggestionsReply = _currentNAM->get(QNetworkRequest(url));
}
void WebBrowserSuggestionsEngine::suggestionsFinished(QNetworkReply *reply) {
if (reply != _suggestionsReply) {
return; //invalid reply. ignore
}
const QByteArray response = _suggestionsReply->readAll();
_suggestionsReply->close();
_suggestionsReply->deleteLater();
_suggestionsReply = 0;
QJsonParseError err;
QJsonDocument json = QJsonDocument::fromJson(response, &err);
const QVariant res = json.toVariant();
if (err.error != QJsonParseError::NoError || res.type() != QVariant::List) {
return;
}
const QVariantList list = res.toList();
if (list.size() <= SUGGESTIONS_LIST_INDEX) {
return;
}
QStringList out;
const QVariantList& suggList = list.at(SUGGESTIONS_LIST_INDEX).toList();
foreach (const QVariant &v, suggList) {
out.append(v.toString());
}
emit suggestions(out);
}

View file

@ -0,0 +1,46 @@
//
// WebBrowserSuggestionsEngine.h
// interface/src/webbrowser
//
// Created by Vlad Stelmahovsky on 30/10/17.
// Copyright 2017 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
//
#ifndef WEBBROWSERSUGGESTIONSENGINE_H
#define WEBBROWSERSUGGESTIONSENGINE_H
#include <qpair.h>
#include <qimage.h>
#include <qmap.h>
#include <qnetworkaccessmanager.h>
#include <qstring.h>
#include <qurl.h>
class QNetworkReply;
class WebBrowserSuggestionsEngine : public QObject {
Q_OBJECT
public:
WebBrowserSuggestionsEngine(QObject* parent = 0);
virtual ~WebBrowserSuggestionsEngine();
public slots:
void querySuggestions(const QString& searchString);
signals:
void suggestions(const QStringList& suggestions);
private slots:
void suggestionsFinished(QNetworkReply *reply);
private:
QNetworkReply* _suggestionsReply;
QNetworkAccessManager* _currentNAM;
};
#endif // WEBBROWSERSUGGESTIONSENGINE_H

View file

@ -12,24 +12,25 @@
#include "EntityTreeRenderer.h"
#include <glm/gtx/quaternion.hpp>
#include <queue>
#include <QEventLoop>
#include <QScriptSyntaxCheckResult>
#include <QThreadPool>
#include <shared/QtHelpers.h>
#include <ColorUtils.h>
#include <AbstractScriptingServicesInterface.h>
#include <AbstractViewStateInterface.h>
#include <AddressManager.h>
#include <ColorUtils.h>
#include <Model.h>
#include <NetworkAccessManager.h>
#include <PerfStat.h>
#include <PrioritySortUtil.h>
#include <Rig.h>
#include <SceneScriptingInterface.h>
#include <ScriptEngine.h>
#include <AddressManager.h>
#include <Rig.h>
#include <EntitySimulation.h>
#include <AddressManager.h>
#include <ZoneRenderer.h>
#include "EntitiesRendererLogging.h"
@ -183,6 +184,7 @@ void EntityTreeRenderer::clear() {
qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown";
}
_entitiesInScene.clear();
_renderablesToUpdate.clear();
// reset the zone to the default (while we load the next scene)
_layeredZones.clear();
@ -272,7 +274,7 @@ void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, r
}
}
void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction) {
void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, const ViewFrustum& view, render::Transaction& transaction) {
PROFILE_RANGE_EX(simulation_physics, "Change", 0xffff00ff, (uint64_t)_changedEntities.size());
PerformanceTimer pt("change");
std::unordered_set<EntityItemID> changedEntities;
@ -286,21 +288,91 @@ void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene
#endif
});
for (const auto& entityId : changedEntities) {
auto renderable = renderableForEntityId(entityId);
if (!renderable) {
continue;
{
PROFILE_RANGE_EX(simulation_physics, "CopyRenderables", 0xffff00ff, (uint64_t)changedEntities.size());
for (const auto& entityId : changedEntities) {
auto renderable = renderableForEntityId(entityId);
if (renderable) {
// only add valid renderables _renderablesToUpdate
_renderablesToUpdate.insert({ entityId, renderable });
}
}
_renderablesToUpdate.insert({ entityId, renderable });
}
if (!_renderablesToUpdate.empty()) {
float expectedUpdateCost = _avgRenderableUpdateCost * _renderablesToUpdate.size();
if (expectedUpdateCost < MAX_UPDATE_RENDERABLES_TIME_BUDGET) {
// we expect to update all renderables within available time budget
PROFILE_RANGE_EX(simulation_physics, "UpdateRenderables", 0xffff00ff, (uint64_t)_renderablesToUpdate.size());
uint64_t updateStart = usecTimestampNow();
for (const auto& entry : _renderablesToUpdate) {
const auto& renderable = entry.second;
assert(renderable); // only valid renderables are added to _renderablesToUpdate
renderable->updateInScene(scene, transaction);
}
size_t numRenderables = _renderablesToUpdate.size() + 1; // add one to avoid divide by zero
_renderablesToUpdate.clear();
// compute average per-renderable update cost
float cost = (float)(usecTimestampNow() - updateStart) / (float)(numRenderables);
const float blend = 0.1f;
_avgRenderableUpdateCost = (1.0f - blend) * _avgRenderableUpdateCost + blend * cost;
} else {
// we expect the cost to updating all renderables to exceed available time budget
// so we first sort by priority and update in order until out of time
class SortableRenderer: public PrioritySortUtil::Sortable {
public:
SortableRenderer(const EntityRendererPointer& renderer) : _renderer(renderer) { }
glm::vec3 getPosition() const override { return _renderer->getEntity()->getPosition(); }
float getRadius() const override { return 0.5f * _renderer->getEntity()->getQueryAACube().getScale(); }
uint64_t getTimestamp() const override { return _renderer->getUpdateTime(); }
const EntityRendererPointer& getRenderer() const { return _renderer; }
private:
EntityRendererPointer _renderer;
};
// prioritize and sort the renderables
uint64_t sortStart = usecTimestampNow();
PrioritySortUtil::PriorityQueue<SortableRenderer> sortedRenderables(view);
{
PROFILE_RANGE_EX(simulation_physics, "SortRenderables", 0xffff00ff, (uint64_t)_renderablesToUpdate.size());
std::unordered_map<EntityItemID, EntityRendererPointer>::iterator itr = _renderablesToUpdate.begin();
while (itr != _renderablesToUpdate.end()) {
assert(itr->second); // only valid renderables are added to _renderablesToUpdate
sortedRenderables.push(SortableRenderer(itr->second));
++itr;
}
}
{
PROFILE_RANGE_EX(simulation_physics, "UpdateRenderables", 0xffff00ff, sortedRenderables.size());
// compute remaining time budget
uint64_t updateStart = usecTimestampNow();
uint64_t timeBudget = MIN_SORTED_UPDATE_RENDERABLES_TIME_BUDGET;
uint64_t sortCost = updateStart - sortStart;
if (sortCost < MAX_UPDATE_RENDERABLES_TIME_BUDGET - MIN_SORTED_UPDATE_RENDERABLES_TIME_BUDGET) {
timeBudget = MAX_UPDATE_RENDERABLES_TIME_BUDGET - sortCost;
}
uint64_t expiry = updateStart + timeBudget;
// process the sorted renderables
std::unordered_map<EntityItemID, EntityRendererPointer>::iterator itr;
size_t numSorted = sortedRenderables.size();
while (!sortedRenderables.empty() && usecTimestampNow() < expiry) {
const EntityRendererPointer& renderable = sortedRenderables.top().getRenderer();
renderable->updateInScene(scene, transaction);
_renderablesToUpdate.erase(renderable->getEntity()->getID());
sortedRenderables.pop();
}
// compute average per-renderable update cost
size_t numUpdated = numSorted - sortedRenderables.size() + 1; // add one to avoid divide by zero
float cost = (float)(usecTimestampNow() - updateStart) / (float)(numUpdated);
const float blend = 0.1f;
_avgRenderableUpdateCost = (1.0f - blend) * _avgRenderableUpdateCost + blend * cost;
}
}
}
@ -319,7 +391,9 @@ void EntityTreeRenderer::update(bool simulate) {
if (scene) {
render::Transaction transaction;
addPendingEntities(scene, transaction);
updateChangedEntities(scene, transaction);
ViewFrustum view;
_viewState->copyCurrentViewFrustum(view);
updateChangedEntities(scene, view, transaction);
scene->enqueueTransaction(transaction);
}
}
@ -749,7 +823,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
}
void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) {
// If it's in the pending queue, remove it
// If it's in a pending queue, remove it
_renderablesToUpdate.erase(entityID);
_entitiesToAdd.erase(entityID);
auto itr = _entitiesInScene.find(entityID);
@ -757,7 +832,7 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) {
// Not in the scene, and no longer potentially in the pending queue, we're done
return;
}
if (_tree && !_shuttingDown && _entitiesScriptEngine) {
_entitiesScriptEngine->unloadEntityScript(entityID, true);
}

View file

@ -144,7 +144,7 @@ protected:
private:
void addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction);
void updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction);
void updateChangedEntities(const render::ScenePointer& scene, const ViewFrustum& view, render::Transaction& transaction);
EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); }
render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); }
@ -235,11 +235,12 @@ private:
NetworkTexturePointer _skyboxTexture;
QString _ambientTextureURL;
QString _skyboxTextureURL;
float _avgRenderableUpdateCost { 0.0f };
bool _pendingAmbientTexture { false };
bool _pendingSkyboxTexture { false };
quint64 _lastZoneCheck { 0 };
const quint64 ZONE_CHECK_INTERVAL = USECS_PER_MSEC * 100; // ~10hz
uint64_t _lastZoneCheck { 0 };
const uint64_t ZONE_CHECK_INTERVAL = USECS_PER_MSEC * 100; // ~10hz
const float ZONE_CHECK_DISTANCE = 0.001f;
ReadWriteLockable _changedEntitiesGuard;

View file

@ -59,7 +59,7 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St
const QUuid& myNodeID = nodeList->getSessionUUID();
statusGetters.push_back([entity]() -> render::Item::Status::Value {
quint64 delta = usecTimestampNow() - entity->getLastEditedFromRemote();
uint64_t delta = usecTimestampNow() - entity->getLastEditedFromRemote();
const float WAIT_THRESHOLD_INV = 1.0f / (0.2f * USECS_PER_SECOND);
float normalizedDelta = delta * WAIT_THRESHOLD_INV;
// Status icon will scale from 1.0f down to 0.0f after WAIT_THRESHOLD
@ -71,7 +71,7 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St
});
statusGetters.push_back([entity] () -> render::Item::Status::Value {
quint64 delta = usecTimestampNow() - entity->getLastBroadcast();
uint64_t delta = usecTimestampNow() - entity->getLastBroadcast();
const float WAIT_THRESHOLD_INV = 1.0f / (0.4f * USECS_PER_SECOND);
float normalizedDelta = delta * WAIT_THRESHOLD_INV;
// Status icon will scale from 1.0f down to 0.0f after WAIT_THRESHOLD
@ -278,6 +278,7 @@ void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& trans
if (!isValidRenderItem()) {
return;
}
_updateTime = usecTimestampNow();
// FIXME is this excessive?
if (!needsRenderUpdate()) {

View file

@ -52,6 +52,8 @@ public:
void clearSubRenderItemIDs();
void setSubRenderItemIDs(const render::ItemIDs& ids);
const uint64_t& getUpdateTime() const { return _updateTime; }
protected:
virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); }
virtual void onAddToScene(const EntityItemPointer& entity);
@ -100,7 +102,6 @@ protected:
return result;
}
signals:
void requestRenderUpdate();
@ -113,14 +114,15 @@ protected:
static std::function<bool()> _entitiesShouldFadeFunction;
const Transform& getModelTransform() const;
Item::Bound _bound;
SharedSoundPointer _collisionSound;
QUuid _changeHandlerId;
ItemID _renderItemID{ Item::INVALID_ITEM_ID };
ItemIDs _subRenderItemIDs;
quint64 _fadeStartTime{ usecTimestampNow() };
uint64_t _fadeStartTime{ usecTimestampNow() };
uint64_t _updateTime{ usecTimestampNow() }; // used when sorting/throttling render updates
bool _isFading{ _entitiesShouldFadeFunction() };
bool _prevIsTransparent { false };
Item::Bound _bound;
bool _visible { false };
bool _moving { false };
// Only touched on the rendering thread

View file

@ -93,6 +93,11 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
QByteArray bufferOut(NLPacket::maxPayloadSize(type), 0);
if (type == PacketType::EntityAdd) {
auto MAX_ADD_DATA_SIZE = NLPacket::maxPayloadSize(type) * 10; // a really big buffer
bufferOut.resize(MAX_ADD_DATA_SIZE);
}
OctreeElement::AppendState encodeResult = OctreeElement::PARTIAL; // start the loop assuming there's more to send
auto nodeList = DependencyManager::get<NodeList>();
@ -115,6 +120,7 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
qCDebug(entities) << " id:" << entityItemID;
qCDebug(entities) << " properties:" << properties;
#endif
queueOctreeEditMessage(type, bufferOut);
if (type == PacketType::EntityAdd && !properties.getCertificateID().isEmpty()) {
emit addingEntityWithCertificate(properties.getCertificateID(), DependencyManager::get<AddressManager>()->getPlaceName());

View file

@ -40,6 +40,9 @@ const quint64 DOMAIN_SERVER_CHECK_IN_MSECS = 1 * 1000;
const int MAX_SILENT_DOMAIN_SERVER_CHECK_INS = 5;
using PacketOrPacketList = std::pair<std::unique_ptr<NLPacket>, std::unique_ptr<NLPacketList>>;
using NodePacketOrPacketListPair = std::pair<SharedNodePointer, PacketOrPacketList>;
using NodePacketPair = std::pair<SharedNodePointer, std::unique_ptr<NLPacket>>;
using NodeSharedPacketPair = std::pair<SharedNodePointer, QSharedPointer<NLPacket>>;
using NodeSharedReceivedMessagePair = std::pair<SharedNodePointer, QSharedPointer<ReceivedMessage>>;

View file

@ -53,7 +53,19 @@ void PacketSender::queuePacketForSending(const SharedNodePointer& destinationNod
_totalBytesQueued += packet->getDataSize();
lock();
_packets.push_back({destinationNode, std::move(packet)});
_packets.push_back({destinationNode, PacketOrPacketList { std::move(packet), nullptr} });
unlock();
// Make sure to wake our actual processing thread because we now have packets for it to process.
_hasPackets.wakeAll();
}
void PacketSender::queuePacketListForSending(const SharedNodePointer& destinationNode, std::unique_ptr<NLPacketList> packetList) {
_totalPacketsQueued += packetList->getNumPackets();
_totalBytesQueued += packetList->getMessageSize();
lock();
_packets.push_back({ destinationNode, PacketOrPacketList { nullptr, std::move(packetList)} });
unlock();
// Make sure to wake our actual processing thread because we now have packets for it to process.
@ -178,8 +190,8 @@ bool PacketSender::nonThreadedProcess() {
float averagePacketsPerCall = 0; // might be less than 1, if our caller calls us more frequently than the target PPS
int packetsSentThisCall = 0;
int packetsToSendThisCall = 0;
size_t packetsSentThisCall = 0;
size_t packetsToSendThisCall = 0;
// Since we're in non-threaded mode, we need to determine how many packets to send per call to process
// based on how often we get called... We do this by keeping a running average of our call times, and we determine
@ -265,24 +277,32 @@ bool PacketSender::nonThreadedProcess() {
while ((packetsSentThisCall < packetsToSendThisCall) && (packetsLeft > 0)) {
lock();
NodePacketPair packetPair = std::move(_packets.front());
NodePacketOrPacketListPair packetPair = std::move(_packets.front());
_packets.pop_front();
packetsLeft = _packets.size();
unlock();
// send the packet through the NodeList...
DependencyManager::get<NodeList>()->sendUnreliablePacket(*packetPair.second, *packetPair.first);
//PacketOrPacketList packetOrList = packetPair.second;
bool sendAsPacket = packetPair.second.first.get();
size_t packetSize = sendAsPacket ? packetPair.second.first->getDataSize() : packetPair.second.second->getMessageSize();
size_t packetCount = sendAsPacket ? 1 : packetPair.second.second->getNumPackets();
packetsSentThisCall++;
_packetsOverCheckInterval++;
_totalPacketsSent++;
if (sendAsPacket) {
DependencyManager::get<NodeList>()->sendUnreliablePacket(*packetPair.second.first, *packetPair.first);
} else {
DependencyManager::get<NodeList>()->sendPacketList(std::move(packetPair.second.second), *packetPair.first);
}
packetsSentThisCall += packetCount;
_packetsOverCheckInterval += packetCount;
_totalPacketsSent += packetCount;
int packetSize = packetPair.second->getDataSize();
_totalBytesSent += packetSize;
emit packetSent(packetSize);
emit packetSent(packetSize); // FIXME should include number of packets?
_lastSendTime = now;
}
return isStillRunning();

View file

@ -39,6 +39,7 @@ public:
/// Add packet to outbound queue.
void queuePacketForSending(const SharedNodePointer& destinationNode, std::unique_ptr<NLPacket> packet);
void queuePacketListForSending(const SharedNodePointer& destinationNode, std::unique_ptr<NLPacketList> packetList);
void setPacketsPerSecond(int packetsPerSecond);
int getPacketsPerSecond() const { return _packetsPerSecond; }
@ -99,14 +100,14 @@ protected:
SimpleMovingAverage _averageProcessCallTime;
private:
std::list<NodePacketPair> _packets;
std::list<NodePacketOrPacketListPair> _packets;
quint64 _lastSendTime;
bool threadedProcess();
bool nonThreadedProcess();
quint64 _lastPPSCheck;
int _packetsOverCheckInterval;
size_t _packetsOverCheckInterval;
quint64 _started;
quint64 _totalPacketsSent;

View file

@ -53,7 +53,6 @@ ReceivedMessage::ReceivedMessage(QByteArray byteArray, PacketType packetType, Pa
_senderSockAddr(senderSockAddr),
_isComplete(true)
{
}
void ReceivedMessage::setFailed() {

View file

@ -115,6 +115,22 @@ void OctreeEditPacketSender::queuePacketToNode(const QUuid& nodeUUID, std::uniqu
});
}
// This method is called when the edit packet layer has determined that it has a fully formed packet destined for
// a known nodeID.
void OctreeEditPacketSender::queuePacketListToNode(const QUuid& nodeUUID, std::unique_ptr<NLPacketList> packetList) {
DependencyManager::get<NodeList>()->eachNode([&](const SharedNodePointer& node) {
// only send to the NodeTypes that are getMyNodeType()
if (node->getType() == getMyNodeType()
&& ((node->getUUID() == nodeUUID) || (nodeUUID.isNull()))
&& node->getActiveSocket()) {
// NOTE: unlike packets, the packet lists don't get rewritten sequence numbers.
// or do history for resend
queuePacketListForSending(node, std::move(packetList));
}
});
}
void OctreeEditPacketSender::processPreServerExistsPackets() {
assert(serversExist()); // we should only be here if we have jurisdictions
@ -247,33 +263,65 @@ void OctreeEditPacketSender::queueOctreeEditMessage(PacketType type, QByteArray&
});
}
if (isMyJurisdiction) {
std::unique_ptr<NLPacket>& bufferedPacket = _pendingEditPackets[nodeUUID];
if (!bufferedPacket) {
bufferedPacket = initializePacket(type, node->getClockSkewUsec());
} else {
// If we're switching type, then we send the last one and start over
if ((type != bufferedPacket->getType() && bufferedPacket->getPayloadSize() > 0) ||
(editMessage.size() >= bufferedPacket->bytesAvailableForWrite())) {
// for edit messages, we will attempt to combine multiple edit commands where possible, we
// don't do this for add because we send those reliably
if (type == PacketType::EntityAdd) {
// create the new packet and swap it with the packet in _pendingEditPackets
auto packetToRelease = initializePacket(type, node->getClockSkewUsec());
bufferedPacket.swap(packetToRelease);
auto newPacket = NLPacketList::create(type, QByteArray(), true, true);
auto nodeClockSkew = node->getClockSkewUsec();
// release the previously buffered packet
releaseQueuedPacket(nodeUUID, std::move(packetToRelease));
// pack sequence number
quint16 sequence = _outgoingSequenceNumbers[nodeUUID]++;
newPacket->writePrimitive(sequence);
// pack in timestamp
quint64 now = usecTimestampNow() + nodeClockSkew;
newPacket->writePrimitive(now);
// We call this virtual function that allows our specific type of EditPacketSender to
// fixup the buffer for any clock skew
if (nodeClockSkew != 0) {
adjustEditPacketForClockSkew(type, editMessage, nodeClockSkew);
}
}
// This is really the first time we know which server/node this particular edit message
// is going to, so we couldn't adjust for clock skew till now. But here's our chance.
// We call this virtual function that allows our specific type of EditPacketSender to
// fixup the buffer for any clock skew
if (node->getClockSkewUsec() != 0) {
adjustEditPacketForClockSkew(type, editMessage, node->getClockSkewUsec());
}
newPacket->write(editMessage);
bufferedPacket->write(editMessage);
// release the new packet
releaseQueuedPacketList(nodeUUID, std::move(newPacket));
} else {
std::unique_ptr<NLPacket>& bufferedPacket = _pendingEditPackets[nodeUUID].first; //only a NLPacket for now
if (!bufferedPacket) {
bufferedPacket = initializePacket(type, node->getClockSkewUsec());
} else {
// If we're switching type, then we send the last one and start over
if ((type != bufferedPacket->getType() && bufferedPacket->getPayloadSize() > 0) ||
(editMessage.size() >= bufferedPacket->bytesAvailableForWrite())) {
// create the new packet and swap it with the packet in _pendingEditPackets
auto packetToRelease = initializePacket(type, node->getClockSkewUsec());
bufferedPacket.swap(packetToRelease);
// release the previously buffered packet
releaseQueuedPacket(nodeUUID, std::move(packetToRelease));
}
}
// This is really the first time we know which server/node this particular edit message
// is going to, so we couldn't adjust for clock skew till now. But here's our chance.
// We call this virtual function that allows our specific type of EditPacketSender to
// fixup the buffer for any clock skew
if (node->getClockSkewUsec() != 0) {
adjustEditPacketForClockSkew(type, editMessage, node->getClockSkewUsec());
}
bufferedPacket->write(editMessage);
}
}
}
});
@ -291,15 +339,24 @@ void OctreeEditPacketSender::releaseQueuedMessages() {
} else {
_packetsQueueLock.lock();
for (auto& i : _pendingEditPackets) {
if (i.second) {
if (i.second.first) {
// construct a null unique_ptr to an NL packet
std::unique_ptr<NLPacket> releasedPacket;
// swap the null ptr with the packet we want to release
i.second.swap(releasedPacket);
i.second.first.swap(releasedPacket);
// move and release the queued packet
releaseQueuedPacket(i.first, std::move(releasedPacket));
} else if (i.second.second) {
// construct a null unique_ptr to an NLPacketList
std::unique_ptr<NLPacketList> releasedPacketList;
// swap the null ptr with the NLPacketList we want to release
i.second.second.swap(releasedPacketList);
// move and release the queued NLPacketList
releaseQueuedPacketList(i.first, std::move(releasedPacketList));
}
}
@ -315,6 +372,14 @@ void OctreeEditPacketSender::releaseQueuedPacket(const QUuid& nodeID, std::uniqu
_releaseQueuedPacketMutex.unlock();
}
void OctreeEditPacketSender::releaseQueuedPacketList(const QUuid& nodeID, std::unique_ptr<NLPacketList> packetList) {
_releaseQueuedPacketMutex.lock();
if (packetList->getMessageSize() > 0 && packetList->getType() != PacketType::Unknown) {
queuePacketListToNode(nodeID, std::move(packetList));
}
_releaseQueuedPacketMutex.unlock();
}
std::unique_ptr<NLPacket> OctreeEditPacketSender::initializePacket(PacketType type, qint64 nodeClockSkew) {
auto newPacket = NLPacket::create(type);

View file

@ -87,15 +87,18 @@ protected:
bool _shouldSend;
void queuePacketToNode(const QUuid& nodeID, std::unique_ptr<NLPacket> packet);
void queuePacketListToNode(const QUuid& nodeUUID, std::unique_ptr<NLPacketList> packetList);
void queuePendingPacketToNodes(std::unique_ptr<NLPacket> packet);
void queuePacketToNodes(std::unique_ptr<NLPacket> packet);
std::unique_ptr<NLPacket> initializePacket(PacketType type, qint64 nodeClockSkew);
void releaseQueuedPacket(const QUuid& nodeUUID, std::unique_ptr<NLPacket> packetBuffer); // releases specific queued packet
void releaseQueuedPacketList(const QUuid& nodeID, std::unique_ptr<NLPacketList> packetList);
void processPreServerExistsPackets();
// These are packets which are destined from know servers but haven't been released because they're still too small
std::unordered_map<QUuid, std::unique_ptr<NLPacket>> _pendingEditPackets;
std::unordered_map<QUuid, PacketOrPacketList> _pendingEditPackets;
// These are packets that are waiting to be processed because we don't yet know if there are servers
int _maxPendingMessages;

View file

@ -35,7 +35,13 @@ OctreePacketData::OctreePacketData(bool enableCompression, int targetSize) {
void OctreePacketData::changeSettings(bool enableCompression, unsigned int targetSize) {
_enableCompression = enableCompression;
_targetSize = std::min(MAX_OCTREE_UNCOMRESSED_PACKET_SIZE, targetSize);
_targetSize = targetSize;
_uncompressedByteArray.resize(_targetSize);
_compressedByteArray.resize(_targetSize);
_uncompressed = (unsigned char*)_uncompressedByteArray.data();
_compressed = (unsigned char*)_compressedByteArray.data();
reset();
}
@ -689,6 +695,8 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto
uint16_t length;
memcpy(&length, dataBytes, sizeof(uint16_t));
dataBytes += sizeof(length);
// FIXME - this size check is wrong if we allow larger packets
if (length * sizeof(glm::vec3) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) {
result.resize(0);
return sizeof(uint16_t);
@ -702,6 +710,8 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto
uint16_t length;
memcpy(&length, dataBytes, sizeof(uint16_t));
dataBytes += sizeof(length);
// FIXME - this size check is wrong if we allow larger packets
if (length * sizeof(glm::quat) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) {
result.resize(0);
return sizeof(uint16_t);
@ -720,6 +730,8 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto
uint16_t length;
memcpy(&length, dataBytes, sizeof(uint16_t));
dataBytes += sizeof(length);
// FIXME - this size check is wrong if we allow larger packets
if (length * sizeof(float) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) {
result.resize(0);
return sizeof(uint16_t);
@ -733,6 +745,8 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto
uint16_t length;
memcpy(&length, dataBytes, sizeof(uint16_t));
dataBytes += sizeof(length);
// FIXME - this size check is wrong if we allow larger packets
if (length / 8 > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) {
result.resize(0);
return sizeof(uint16_t);

View file

@ -279,7 +279,8 @@ private:
unsigned int _targetSize;
bool _enableCompression;
unsigned char _uncompressed[MAX_OCTREE_UNCOMRESSED_PACKET_SIZE];
QByteArray _uncompressedByteArray;
unsigned char* _uncompressed { nullptr };
int _bytesInUse;
int _bytesAvailable;
int _subTreeAt;
@ -288,7 +289,8 @@ private:
bool compressContent();
unsigned char _compressed[MAX_OCTREE_UNCOMRESSED_PACKET_SIZE];
QByteArray _compressedByteArray;
unsigned char* _compressed { nullptr };
int _compressedBytes;
int _bytesInUseLastCheck;
bool _dirty;

View file

@ -87,7 +87,7 @@ namespace indexed_container {
if (index < (Index) _elements.size()) {
_elements[index] = e;
} else {
assert(index == _elements.size());
assert(index == (Index)_elements.size());
_elements.emplace_back(e);
}
}
@ -159,4 +159,4 @@ namespace indexed_container {
};
};
}
#endif
#endif

View file

@ -0,0 +1,141 @@
//
// PrioritySortUtil.h
//
// Created by Andrew Meadows on 2017-11-08
// Copyright 2017 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
//
#pragma once
#ifndef hifi_PrioritySortUtil_h
#define hifi_PrioritySortUtil_h
#include <glm/glm.hpp>
#include "ViewFrustum.h"
/* PrioritySortUtil is a helper for sorting 3D things relative to a ViewFrustum. To use:
(1) Derive a class from pure-virtual PrioritySortUtil::Sortable that wraps a copy of
the Thing you want to prioritize and sort:
class SortableWrapper: public PrioritySortUtil::Sortable {
public:
SortableWrapper(const Thing& thing) : _thing(thing) { }
glm::vec3 getPosition() const override { return _thing->getPosition(); }
float getRadius() const override { return 0.5f * _thing->getBoundingRadius(); }
uint64_t getTimestamp() const override { return _thing->getLastTime(); }
const Thing& getThing() const { return _thing; }
private:
Thing _thing;
};
(2) Make a PrioritySortUtil::PriorityQueue<Thing> and add them to the queue:
PrioritySortUtil::Prioritizer prioritizer(viewFrustum);
std::priority_queue< PrioritySortUtil::Sortable<Thing> > sortedThings;
for (thing in things) {
float priority = prioritizer.computePriority(PrioritySortUtil::PrioritizableThing(thing));
sortedThings.push(PrioritySortUtil::Sortable<Thing> entry(thing, priority));
}
(3) Loop over your priority queue and do timeboxed work:
uint64_t cutoffTime = usecTimestampNow() + TIME_BUDGET;
while (!sortedThings.empty()) {
const Thing& thing = sortedThings.top();
// ...do work on thing...
sortedThings.pop();
if (usecTimestampNow() > cutoffTime) {
break;
}
}
*/
namespace PrioritySortUtil {
constexpr float DEFAULT_ANGULAR_COEF { 1.0f };
constexpr float DEFAULT_CENTER_COEF { 0.5f };
constexpr float DEFAULT_AGE_COEF { 0.25f / (float)(USECS_PER_SECOND) };
class Sortable {
public:
virtual glm::vec3 getPosition() const = 0;
virtual float getRadius() const = 0;
virtual uint64_t getTimestamp() const = 0;
void setPriority(float priority) { _priority = priority; }
bool operator<(const Sortable& other) const { return _priority < other._priority; }
private:
float _priority { 0.0f };
};
template <typename T>
class PriorityQueue {
public:
PriorityQueue() = delete;
PriorityQueue(const ViewFrustum& view) : _view(view) { }
PriorityQueue(const ViewFrustum& view, float angularWeight, float centerWeight, float ageWeight)
: _view(view), _angularWeight(angularWeight), _centerWeight(centerWeight), _ageWeight(ageWeight)
{ }
void setView(const ViewFrustum& view) { _view = view; }
void setWeights(float angularWeight, float centerWeight, float ageWeight) {
_angularWeight = angularWeight;
_centerWeight = centerWeight;
_ageWeight = ageWeight;
}
size_t size() const { return _queue.size(); }
void push(T thing) {
thing.setPriority(computePriority(thing));
_queue.push(thing);
}
const T& top() const { return _queue.top(); }
void pop() { return _queue.pop(); }
bool empty() const { return _queue.empty(); }
private:
float computePriority(const T& thing) const {
// priority = weighted linear combination of multiple values:
// (a) angular size
// (b) proximity to center of view
// (c) time since last update
// where the relative "weights" are tuned to scale the contributing values into units of "priority".
glm::vec3 position = thing.getPosition();
glm::vec3 offset = position - _view.getPosition();
float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
float radius = thing.getRadius();
float priority = _angularWeight * (radius / distance)
+ _centerWeight * (glm::dot(offset, _view.getDirection()) / distance)
+ _ageWeight * (float)(usecTimestampNow() - thing.getTimestamp());
// decrement priority of things outside keyhole
if (distance - radius > _view.getCenterRadius()) {
if (!_view.sphereIntersectsFrustum(position, radius)) {
constexpr float OUT_OF_VIEW_PENALTY = -10.0f;
priority += OUT_OF_VIEW_PENALTY;
}
}
return priority;
}
ViewFrustum _view;
std::priority_queue<T> _queue;
float _angularWeight { DEFAULT_ANGULAR_COEF };
float _centerWeight { DEFAULT_CENTER_COEF };
float _ageWeight { DEFAULT_AGE_COEF };
};
} // namespace PrioritySortUtil
// for now we're keeping hard-coded sorted time budgets in one spot
const uint64_t MAX_UPDATE_RENDERABLES_TIME_BUDGET = 2000; // usec
const uint64_t MIN_SORTED_UPDATE_RENDERABLES_TIME_BUDGET = 1000; // usec
#endif // hifi_PrioritySortUtil_h

View file

@ -328,7 +328,8 @@ void TabletProxy::initialScreen(const QVariant& url) {
pushOntoStack(url);
} else {
_initialScreen = true;
_initialPath = url;
_initialPath.first = url;
_initialPath.second = State::QML;
}
}
@ -416,10 +417,18 @@ void TabletProxy::setQmlTabletRoot(OffscreenQmlSurface* qmlOffscreenSurface) {
});
if (_initialScreen) {
if (!_showRunningScripts) {
pushOntoStack(_initialPath);
if (!_showRunningScripts && _initialPath.second == State::QML) {
pushOntoStack(_initialPath.first);
} else if (_initialPath.second == State::Web) {
QVariant webUrl = _initialPath.first;
QVariant scriptUrl = _initialWebPathParams.first;
gotoWebScreen(webUrl.toString(), scriptUrl.toString(), _initialWebPathParams.second);
}
_initialScreen = false;
_initialPath.first = "";
_initialPath.second = State::Uninitialized;
_initialWebPathParams.first = "";
_initialWebPathParams.second = false;
}
if (_showRunningScripts) {
@ -685,6 +694,14 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS
QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(false)));
}
QMetaObject::invokeMethod(root, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl)));
} else {
// tablet is not initialized yet, save information and load when
// the tablet root is set
_initialPath.first = url;
_initialPath.second = State::Web;
_initialWebPathParams.first = injectedJavaScriptUrl;
_initialWebPathParams.second = loadOtherBase;
_initialScreen = true;
}
_state = State::Web;
emit screenChanged(QVariant("Web"), QVariant(url));

View file

@ -248,7 +248,6 @@ protected:
void removeButtonsFromToolbar();
bool _initialScreen { false };
QVariant _initialPath { "" };
QVariant _currentPathLoaded { "" };
QString _name;
std::vector<QSharedPointer<TabletButtonProxy>> _tabletButtonProxies;
@ -260,6 +259,8 @@ protected:
enum class State { Uninitialized, Home, Web, Menu, QML };
State _state { State::Uninitialized };
std::pair<QVariant, State> _initialPath { "", State::Uninitialized };
std::pair<QVariant, bool> _initialWebPathParams;
bool _landscape { false };
bool _showRunningScripts { false };
};

View file

@ -419,7 +419,7 @@ var toolBar = (function () {
var createButtonIconRsrc = (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON);
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
activeButton = tablet.addButton({
captionColorOverride: hasRezPermissions ? "" : "#888888",
captionColor: hasRezPermissions ? "#ffffff" : "#888888",
icon: createButtonIconRsrc,
activeIcon: "icons/tablet-icons/edit-a.svg",
text: "CREATE",
@ -792,7 +792,7 @@ function handleDomainChange() {
var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp() || Entities.canRezCertified() || Entities.canRezTmpCertified());
createButton.editProperties({
icon: (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON),
captionColorOverride: (hasRezPermissions ? "" : "#888888"),
captionColor: (hasRezPermissions ? "#ffffff" : "#888888"),
});
}

View file

@ -18,7 +18,7 @@
APP_ICON_INACTIVE = Script.resolvePath("./assets/shapes-i.svg"),
APP_ICON_ACTIVE = Script.resolvePath("./assets/shapes-a.svg"),
APP_ICON_DISABLED = Script.resolvePath("./assets/shapes-d.svg"),
ENABLED_CAPTION_COLOR_OVERRIDE = "",
ENABLED_CAPTION_COLOR_OVERRIDE = "#ffffff",
DISABLED_CAPTION_COLOR_OVERRIDE = "#888888",
START_DELAY = 2000, // ms
@ -1865,7 +1865,7 @@
}
button.editProperties({
icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED,
captionColorOverride: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE,
captionColor: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE,
isActive: isAppActive
});
}
@ -1880,7 +1880,7 @@
}
button.editProperties({
icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED,
captionColorOverride: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE,
captionColor: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE,
isActive: isAppActive
});
}
@ -1945,7 +1945,7 @@
hasRezPermissions = Entities.canRez() || Entities.canRezTmp();
button = tablet.addButton({
icon: hasRezPermissions ? APP_ICON_INACTIVE : APP_ICON_DISABLED,
captionColorOverride: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE,
captionColor: hasRezPermissions ? ENABLED_CAPTION_COLOR_OVERRIDE : DISABLED_CAPTION_COLOR_OVERRIDE,
activeIcon: APP_ICON_ACTIVE,
text: APP_NAME,
isActive: isAppActive