diff --git a/interface/resources/images/steam-sign-in.png b/interface/resources/images/steam-sign-in.png new file mode 100644 index 0000000000..148e6ab280 Binary files /dev/null and b/interface/resources/images/steam-sign-in.png differ diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index f75e83e36e..1f84024e15 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -10,296 +10,70 @@ import Hifi 1.0 import QtQuick 2.4 -import "controls" -import "styles" + +import "controls-uit" +import "styles-uit" import "windows" -ScrollingWindow { +import "LoginDialog" + +ModalWindow { id: root HifiConstants { id: hifi } objectName: "LoginDialog" - height: loginDialog.implicitHeight - width: loginDialog.implicitWidth - // FIXME make movable - anchors.centerIn: parent - destroyOnHidden: false - hideBackground: true - shown: false + implicitWidth: 520 + implicitHeight: 320 + destroyOnCloseButton: true + destroyOnHidden: true + visible: true + + property string iconText: "" + property int iconSize: 50 + + property string title: "" + property int titleWidth: 0 LoginDialog { id: loginDialog - implicitWidth: backgroundRectangle.width - implicitHeight: backgroundRectangle.height - readonly property int inputWidth: 500 - readonly property int inputHeight: 60 - readonly property int borderWidth: 30 - readonly property int closeMargin: 16 - readonly property real tan30: 0.577 // tan(30°) - readonly property int inputSpacing: 16 - Rectangle { - id: backgroundRectangle - width: loginDialog.inputWidth + loginDialog.borderWidth * 2 - height: loginDialog.inputHeight * 6 + loginDialog.closeMargin * 2 - radius: loginDialog.closeMargin * 2 - color: "#2c86b1" - opacity: 0.85 - } - - Column { - id: mainContent - width: loginDialog.inputWidth - spacing: loginDialog.inputSpacing - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - - Item { - // Offset content down a little - width: loginDialog.inputWidth - height: loginDialog.closeMargin - } - - Rectangle { - width: loginDialog.inputWidth - height: loginDialog.inputHeight - radius: height / 2 - color: "#ebebeb" - - Image { - source: "../images/login-username.svg" - width: loginDialog.inputHeight * 0.65 - height: width - sourceSize: Qt.size(width, height); - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - leftMargin: loginDialog.inputHeight / 4 - } - } - - TextInput { - id: username - anchors { - fill: parent - leftMargin: loginDialog.inputHeight - rightMargin: loginDialog.inputHeight / 2 - } - - helperText: "username or email" - color: hifi.colors.text - - KeyNavigation.tab: password - KeyNavigation.backtab: password - } - } - - Rectangle { - width: loginDialog.inputWidth - height: loginDialog.inputHeight - radius: height / 2 - color: "#ebebeb" - - Image { - source: "../images/login-password.svg" - width: loginDialog.inputHeight * 0.65 - height: width - sourceSize: Qt.size(width, height); - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - leftMargin: loginDialog.inputHeight / 4 - } - } - - TextInput { - id: password - anchors { - fill: parent - leftMargin: loginDialog.inputHeight - rightMargin: loginDialog.inputHeight / 2 - } - - helperText: "password" - echoMode: TextInput.Password - color: hifi.colors.text - - KeyNavigation.tab: username - KeyNavigation.backtab: username - onFocusChanged: { - if (password.focus) { - password.selectAll() - } - } - } - } - - Item { - width: loginDialog.inputWidth - height: loginDialog.inputHeight / 2 - - Text { - id: messageText - - visible: loginDialog.statusText != "" && loginDialog.statusText != "Logging in..." - - width: loginDialog.inputWidth - height: loginDialog.inputHeight / 2 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - text: loginDialog.statusText - color: "white" - } - - Row { - id: messageSpinner - - visible: loginDialog.statusText == "Logging in..." - onVisibleChanged: visible ? messageSpinnerAnimation.restart() : messageSpinnerAnimation.stop() - - spacing: 24 - anchors { - verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter - } - - Rectangle { - id: spinner1 - width: 10 - height: 10 - color: "#ebebeb" - opacity: 0.05 - } - - Rectangle { - id: spinner2 - width: 10 - height: 10 - color: "#ebebeb" - opacity: 0.05 - } - - Rectangle { - id: spinner3 - width: 10 - height: 10 - color: "#ebebeb" - opacity: 0.05 - } - - SequentialAnimation { - id: messageSpinnerAnimation - running: messageSpinner.visible - loops: Animation.Infinite - NumberAnimation { target: spinner1; property: "opacity"; to: 1.0; duration: 1000 } - NumberAnimation { target: spinner2; property: "opacity"; to: 1.0; duration: 1000 } - NumberAnimation { target: spinner3; property: "opacity"; to: 1.0; duration: 1000 } - NumberAnimation { target: spinner1; property: "opacity"; to: 0.05; duration: 0 } - NumberAnimation { target: spinner2; property: "opacity"; to: 0.05; duration: 0 } - NumberAnimation { target: spinner3; property: "opacity"; to: 0.05; duration: 0 } - } - } - } - - Rectangle { - width: loginDialog.inputWidth - height: loginDialog.inputHeight - radius: height / 2 - color: "#353535" - - TextInput { - anchors.fill: parent - text: "Login" - color: "white" - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - loginDialog.login(username.text, password.text) - } - } - } - - Item { - anchors { left: parent.left; right: parent.right; } - height: loginDialog.inputHeight - - Image { - id: hifiIcon - source: "../images/hifi-logo-blackish.svg" - width: loginDialog.inputHeight - height: width - sourceSize: Qt.size(width, height); - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - } - - Text { - anchors { verticalCenter: parent.verticalCenter; right: hifiIcon.left; margins: loginDialog.inputSpacing } - text: "Password?" - scale: 0.8 - font.underline: true - color: "#e0e0e0" - MouseArea { - anchors { fill: parent; margins: -loginDialog.inputSpacing / 2 } - cursorShape: Qt.PointingHandCursor - onClicked: loginDialog.openUrl(loginDialog.rootUrl + "/users/password/new") - } - } - - Text { - anchors { verticalCenter: parent.verticalCenter; left: hifiIcon.right; margins: loginDialog.inputSpacing } - text: "Register" - scale: 0.8 - font.underline: true - color: "#e0e0e0" - - MouseArea { - anchors { fill: parent; margins: -loginDialog.inputSpacing / 2 } - cursorShape: Qt.PointingHandCursor - onClicked: loginDialog.openUrl(loginDialog.rootUrl + "/signup") - } - } - - } - } - } - - onShownChanged: { - if (!shown) { - username.text = "" - password.text = "" - loginDialog.statusText = "" - } else { - username.forceActiveFocus() + Loader { + id: bodyLoader + anchors.fill: parent + source: loginDialog.isSteamRunning() ? "LoginDialog/SignInBody.qml" : "LoginDialog/LinkAccountBody.qml" } } Keys.onPressed: { - switch (event.key) { + if (!visible) { + return + } + + if (event.modifiers === Qt.ControlModifier) + switch (event.key) { + case Qt.Key_A: + event.accepted = true + detailedText.selectAll() + break + case Qt.Key_C: + event.accepted = true + detailedText.copy() + break + case Qt.Key_Period: + if (Qt.platform.os === "osx") { + event.accepted = true + content.reject() + } + break + } else switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.shown = false; - event.accepted = true; - break; + event.accepted = true + destroy() + break case Qt.Key_Enter: case Qt.Key_Return: - if (username.activeFocus) { - event.accepted = true - password.forceActiveFocus() - } else if (password.activeFocus) { - event.accepted = true - if (username.text == "") { - username.forceActiveFocus() - } else { - loginDialog.login(username.text, password.text) - } - } + event.accepted = true break } } diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml new file mode 100644 index 0000000000..fc7eac3d00 --- /dev/null +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -0,0 +1,125 @@ +// +// CompleteProfileBody.qml +// +// Created by Clement on 7/18/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 QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: completeProfileBody + clip: true + width: pane.width + height: pane.height + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, additionalTextContainer.contentWidth) + var targetHeight = 4 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + Row { + id: buttons + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + + onClicked: loginDialog.createAccountFromStream() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + + onClicked: root.destroy() + } + } + + ShortcutText { + id: additionalTextContainer + anchors { + top: buttons.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: "Already have a High Fidelity profile? Link to an existing profile here." + + wrapMode: Text.WordWrap + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: { + bodyLoader.source = "LinkAccountBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + + Component.onCompleted: { + root.title = qsTr("Complete Your Profile") + root.iconText = "<" + d.resize(); + } + + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + + bodyLoader.source = "UsernameCollisionBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } +} diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml new file mode 100644 index 0000000000..137556964f --- /dev/null +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -0,0 +1,215 @@ +// +// LinkAccountBody.qml +// +// Created by Clement on 7/18/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 QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: linkAccountBody + clip: true + width: root.pane.width + height: root.pane.height + + function login() { + mainTextContainer.visible = false + loginDialog.login(usernameField.text, passwordField.text) + } + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, form.contentWidth) + var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + + 4 * hifi.dimensions.contentSpacing.y + form.height + + 4 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + + text: qsTr("Username or password incorrect.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "User Name or Email" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Forgot Username?" + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: loginDialog.openUrl(link) + } + } + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Forgot Password?" + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: loginDialog.openUrl(link) + } + } + + } + + Row { + id: buttons + anchors { + top: form.bottom + right: parent.right + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") + color: hifi.buttons.blue + + onClicked: linkAccountBody.login() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: root.destroy() + } + } + + Component.onCompleted: { + root.title = qsTr("Sign Into High Fidelity") + root.iconText = "<" + d.resize(); + + usernameField.forceActiveFocus() + } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded, linking steam account") + + if (loginDialog.isSteamRunning()) { + loginDialog.linkSteam() + } else { + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + onHandleLoginFailed: { + console.log("Login Failed") + mainTextContainer.visible = true + } + onHandleLinkCompleted: { + console.log("Link Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLinkFailed: { + console.log("Link Failed") + + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + linkAccountBody.login() + break + } + } +} diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml new file mode 100644 index 0000000000..2da0ea856d --- /dev/null +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -0,0 +1,128 @@ +// +// SignInBody.qml +// +// Created by Clement on 7/18/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 QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: signInBody + clip: true + width: pane.width + height: pane.height + + property bool required: false + + function login() { + console.log("Trying to log in") + loginDialog.loginThroughSteam() + } + + function cancel() { + root.destroy() + } + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: required ? qsTr("This domain's owner requires that you sign in:") + : qsTr("Sign in to access your user account:") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + width: undefined // invalidate so that the image's size sets the width + height: undefined // invalidate so that the image's size sets the height + focus: true + + style: OriginalStyles.ButtonStyle { + background: Image { + id: buttonImage + source: "../../images/steam-sign-in.png" + } + } + onClicked: signInBody.login() + } + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel"); + + onClicked: signInBody.cancel() + } + } + + Component.onCompleted: { + root.title = required ? qsTr("Sign In Required") + : qsTr("Sign In") + root.iconText = "" + d.resize(); + } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginFailed: { + console.log("Login Failed") + + bodyLoader.source = "CompleteProfileBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } +} diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml new file mode 100644 index 0000000000..7e22b11f8b --- /dev/null +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -0,0 +1,173 @@ +// +// UsernameCollisionBody.qml +// +// Created by Clement on 7/18/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 QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: usernameCollisionBody + clip: true + width: root.pane.width + height: root.pane.height + + function create() { + mainTextContainer.visible = false + loginDialog.createAccountFromStream(textField.text) + } + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, Math.max(mainTextContainer.contentWidth, + termsContainer.contentWidth)) + var targetHeight = mainTextContainer.height + + 2 * hifi.dimensions.contentSpacing.y + textField.height + + 5 * hifi.dimensions.contentSpacing.y + termsContainer.height + + 1 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("Your Steam username is not available.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + + TextField { + id: textField + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + width: 250 + + placeholderText: "Choose your own" + } + + MenuItem { + id: termsContainer + anchors { + top: textField.bottom + left: parent.left + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + + text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: loginDialog.openUrl(link) + } + + Row { + id: buttons + anchors { + top: termsContainer.bottom + right: parent.right + margins: 0 + topMargin: 1 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + + onClicked: usernameCollisionBody.create() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: root.destroy() + } + } + + Component.onCompleted: { + root.title = qsTr("Complete Your Profile") + root.iconText = "<" + d.resize(); + } + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + + mainTextContainer.visible = true + mainTextContainer.text = "\"" + textField.text + qsTr("\" is invalid or already taken.") + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + usernameCollisionBody.create() + break + } + } +} diff --git a/interface/resources/qml/LoginDialog/WelcomeBody.qml b/interface/resources/qml/LoginDialog/WelcomeBody.qml new file mode 100644 index 0000000000..ecc848cdc0 --- /dev/null +++ b/interface/resources/qml/LoginDialog/WelcomeBody.qml @@ -0,0 +1,90 @@ +// +// WelcomeBody.qml +// +// Created by Clement on 7/18/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 "../controls-uit" +import "../styles-uit" + +Item { + id: welcomeBody + clip: true + width: pane.width + height: pane.height + + property bool welcomeBack: false + + function setTitle() { + root.title = (welcomeBack ? qsTr("Welcome back ") : qsTr("Welcome ")) + Account.username + qsTr("!") + root.iconText = "" + d.resize(); + } + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("You are now signed into High Fidelity") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Close"); + + onClicked: root.destroy() + } + } + + Component.onCompleted: welcomeBody.setTitle() + + Connections { + target: Account + onUsernameChanged: welcomeBody.setTitle() + } +} diff --git a/interface/resources/qml/controls-uit/HorizontalRule.qml b/interface/resources/qml/controls-uit/HorizontalRule.qml new file mode 100644 index 0000000000..425500f1d4 --- /dev/null +++ b/interface/resources/qml/controls-uit/HorizontalRule.qml @@ -0,0 +1,20 @@ +// +// HorizontalRule.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: hifi.colors.lightGray +} diff --git a/interface/resources/qml/controls-uit/HorizontalSpacer.qml b/interface/resources/qml/controls-uit/HorizontalSpacer.qml new file mode 100644 index 0000000000..545154ab44 --- /dev/null +++ b/interface/resources/qml/controls-uit/HorizontalSpacer.qml @@ -0,0 +1,21 @@ +// +// HorizontalSpacer.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 + +import "../styles-uit" + +Item { + id: root + property alias size: root.width + + width: hifi.dimensions.controlInterlineHeight + height: 1 // Must be non-zero +} diff --git a/interface/resources/qml/controls-uit/VerticalSpacer.qml b/interface/resources/qml/controls-uit/VerticalSpacer.qml index 6fc49605c0..2df65f1002 100644 --- a/interface/resources/qml/controls-uit/VerticalSpacer.qml +++ b/interface/resources/qml/controls-uit/VerticalSpacer.qml @@ -13,6 +13,9 @@ import QtQuick 2.5 import "../styles-uit" Item { + id: root + property alias size: root.height + width: 1 // Must be non-zero height: hifi.dimensions.controlInterlineHeight } diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index d390ea08bf..40c5a01e15 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -21,8 +21,6 @@ import "messageDialog" ModalWindow { id: root HifiConstants { id: hifi } - implicitWidth: 640 - implicitHeight: 320 destroyOnCloseButton: true destroyOnHidden: true visible: true @@ -70,7 +68,7 @@ ModalWindow { QtObject { id: d readonly property int minWidth: 480 - readonly property int maxWdith: 1280 + readonly property int maxWidth: 1280 readonly property int minHeight: 120 readonly property int maxHeight: 720 @@ -80,7 +78,7 @@ ModalWindow { + (informativeTextContainer.text != "" ? informativeTextContainer.contentHeight + 3 * hifi.dimensions.contentSpacing.y : 0) + buttons.height + (content.state === "expanded" ? details.implicitHeight + hifi.dimensions.contentSpacing.y : 0) - root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth) + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWidth) ? d.maxWidth : targetWidth) root.height = (targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight) } } diff --git a/interface/resources/qml/styles-uit/ButtonLabel.qml b/interface/resources/qml/styles-uit/ButtonLabel.qml new file mode 100644 index 0000000000..aade5fb439 --- /dev/null +++ b/interface/resources/qml/styles-uit/ButtonLabel.qml @@ -0,0 +1,18 @@ +// +// ButtonLabel.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayBold { + font.pixelSize: hifi.fontSizes.buttonLabel +} diff --git a/interface/resources/qml/styles-uit/IconButton.qml b/interface/resources/qml/styles-uit/IconButton.qml new file mode 100644 index 0000000000..84c1ef14c1 --- /dev/null +++ b/interface/resources/qml/styles-uit/IconButton.qml @@ -0,0 +1,20 @@ +// +// IconButton.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.iconButton + font.capitalization: Font.AllUppercase + font.letterSpacing: 1.5 +} diff --git a/interface/resources/qml/styles-uit/InputLabel.qml b/interface/resources/qml/styles-uit/InputLabel.qml new file mode 100644 index 0000000000..59657a554d --- /dev/null +++ b/interface/resources/qml/styles-uit/InputLabel.qml @@ -0,0 +1,18 @@ +// +// InputLabel.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewaySemiBold { + font.pixelSize: hifi.fontSizes.inputLabel +} diff --git a/interface/resources/qml/styles-uit/ListItem.qml b/interface/resources/qml/styles-uit/ListItem.qml new file mode 100644 index 0000000000..f707686edc --- /dev/null +++ b/interface/resources/qml/styles-uit/ListItem.qml @@ -0,0 +1,18 @@ +// +// ListItem.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.listItem +} diff --git a/interface/resources/qml/styles-uit/Logs.qml b/interface/resources/qml/styles-uit/Logs.qml new file mode 100644 index 0000000000..577fe2f8d8 --- /dev/null +++ b/interface/resources/qml/styles-uit/Logs.qml @@ -0,0 +1,18 @@ +// +// Logs.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +AnonymousProRegular { + font.pixelSize: hifi.fontSizes.logs +} diff --git a/interface/resources/qml/styles-uit/MenuItem.qml b/interface/resources/qml/styles-uit/MenuItem.qml new file mode 100644 index 0000000000..4431c357bf --- /dev/null +++ b/interface/resources/qml/styles-uit/MenuItem.qml @@ -0,0 +1,19 @@ +// +// MenuItem.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewaySemiBold { + lineHeight: 2 + font.pixelSize: hifi.fontSizes.menuItem +} diff --git a/interface/resources/qml/styles-uit/OverlayTitle.qml b/interface/resources/qml/styles-uit/OverlayTitle.qml new file mode 100644 index 0000000000..e23b9eca14 --- /dev/null +++ b/interface/resources/qml/styles-uit/OverlayTitle.qml @@ -0,0 +1,18 @@ +// +// OverlayTitle.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.overlayTitle +} diff --git a/interface/resources/qml/styles-uit/SectionName.qml b/interface/resources/qml/styles-uit/SectionName.qml new file mode 100644 index 0000000000..5438fec7bc --- /dev/null +++ b/interface/resources/qml/styles-uit/SectionName.qml @@ -0,0 +1,19 @@ +// +// SectionName.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.sectionName + font.capitalization: Font.AllUppercase +} diff --git a/interface/resources/qml/styles-uit/ShortcutText.qml b/interface/resources/qml/styles-uit/ShortcutText.qml new file mode 100644 index 0000000000..a3ab351870 --- /dev/null +++ b/interface/resources/qml/styles-uit/ShortcutText.qml @@ -0,0 +1,18 @@ +// +// ShortcutText.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayLight { + font.pixelSize: hifi.fontSizes.shortcutText +} diff --git a/interface/resources/qml/styles-uit/TabName.qml b/interface/resources/qml/styles-uit/TabName.qml new file mode 100644 index 0000000000..eb4e790e7e --- /dev/null +++ b/interface/resources/qml/styles-uit/TabName.qml @@ -0,0 +1,19 @@ +// +// TabName.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.tabName + font.capitalization: Font.AllUppercase +} diff --git a/interface/resources/qml/styles-uit/TextFieldInput.qml b/interface/resources/qml/styles-uit/TextFieldInput.qml new file mode 100644 index 0000000000..010b4d03ad --- /dev/null +++ b/interface/resources/qml/styles-uit/TextFieldInput.qml @@ -0,0 +1,18 @@ +// +// TextFieldInput.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +FiraSansSemiBold { + font.pixelSize: hifi.fontSizes.textFieldInput +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bf85c17370..76649739be 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -86,15 +86,16 @@ #include #include #include +#include #include #include #include #include #include -#include +#include #include #include -#include +#include #include #include #include @@ -1631,6 +1632,8 @@ void Application::initializeUi() { rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); + + rootContext->setContextProperty("Steam", new SteamScriptingInterface(engine)); _glWidget->installEventFilter(offscreenUi.data()); @@ -2943,6 +2946,8 @@ void Application::idle(float nsecsElapsed) { PROFILE_RANGE(__FUNCTION__); + SteamClient::runCallbacks(); + float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND; // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. @@ -3251,6 +3256,14 @@ void Application::init() { addressLookupString = arguments().value(urlIndex + 1); } + // when +connect_lobby in command line, join steam lobby + const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; + int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); + if (lobbyIndex != -1) { + QString lobbyId = arguments().value(lobbyIndex + 1); + SteamClient::joinLobby(lobbyId); + } + Setting::Handle firstRun { Settings::firstRun, true }; if (addressLookupString.isEmpty() && firstRun.get()) { qDebug() << "First run and no URL passed... attempting to go to Home or Entry..."; @@ -4837,6 +4850,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine)); } bool Application::canAcceptURL(const QString& urlString) const { diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 4051bd8a1e..dd80dadca7 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -36,11 +37,11 @@ const QString SESSION_ID_KEY = "session_id"; void DiscoverabilityManager::updateLocation() { auto accountManager = DependencyManager::get(); - - if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) { - auto addressManager = DependencyManager::get(); - DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); + auto addressManager = DependencyManager::get(); + auto& domainHandler = DependencyManager::get()->getDomainHandler(); + + if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) { // construct a QJsonObject given the user's current address information QJsonObject rootObject; @@ -48,8 +49,6 @@ void DiscoverabilityManager::updateLocation() { QString pathString = addressManager->currentPath(); - const QString LOCATION_KEY_IN_ROOT = "location"; - const QString PATH_KEY_IN_LOCATION = "path"; locationObject.insert(PATH_KEY_IN_LOCATION, pathString); @@ -90,6 +89,7 @@ void DiscoverabilityManager::updateLocation() { // we have a changed location, send it now _lastLocationObject = locationObject; + const QString LOCATION_KEY_IN_ROOT = "location"; rootObject.insert(LOCATION_KEY_IN_ROOT, locationObject); apiPath = API_USER_LOCATION_PATH; @@ -109,6 +109,9 @@ void DiscoverabilityManager::updateLocation() { accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters); } + + // Update Steam + SteamClient::updateLocation(domainHandler.getHostname(), addressManager->currentFacingAddress()); } void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index de5455fc14..57e379a9ac 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -122,11 +122,13 @@ MyAvatar::MyAvatar(RigPointer rig) : _driveKeys[i] = 0.0f; } + + // Necessary to select the correct slot + using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); + // connect to AddressManager signal for location jumps - connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, - [=](const glm::vec3& newPosition, bool hasOrientation, const glm::quat& newOrientation, bool shouldFaceLocation){ - goToLocation(newPosition, hasOrientation, newOrientation, shouldFaceLocation); - }); + connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, + this, static_cast(&MyAvatar::goToLocation)); _characterController.setEnabled(true); @@ -1859,7 +1861,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation); if (shouldFaceLocation) { - quatOrientation = newOrientation * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + quatOrientation = newOrientation * glm::angleAxis(PI, Vectors::UP); // move the user a couple units away const float DISTANCE_TO_USER = 2.0f; diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 8fc0384aee..527b7f2331 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -8,6 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include #include @@ -20,12 +22,13 @@ #include #include +#include + #include "AddressManager.h" #include "Application.h" #include "InterfaceLogging.h" #include "UserActivityLogger.h" #include "MainWindow.h" -#include #ifdef HAS_BUGSPLAT #include @@ -137,6 +140,8 @@ int main(int argc, const char* argv[]) { // or in the main window ctor, before GL startup. Application::initPlugins(arguments); + SteamClient::init(); + int exitCode; { QSettings::setDefaultFormat(QSettings::IniFormat); @@ -202,6 +207,8 @@ int main(int argc, const char* argv[]) { Application::shutdownPlugins(); + SteamClient::shutdown(); + qCDebug(interfaceapp, "Normal exit."); #if !defined(DEBUG) && !defined(Q_OS_LINUX) // HACK: exit immediately (don't handle shutdown callbacks) for Release build diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp index 1328197195..4090c99ac8 100644 --- a/interface/src/scripting/AccountScriptingInterface.cpp +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -15,6 +15,9 @@ AccountScriptingInterface* AccountScriptingInterface::getInstance() { static AccountScriptingInterface sharedInstance; + auto accountManager = DependencyManager::get(); + QObject::connect(accountManager.data(), &AccountManager::profileChanged, + &sharedInstance, &AccountScriptingInterface::usernameChanged); return &sharedInstance; } diff --git a/interface/src/scripting/AccountScriptingInterface.h b/interface/src/scripting/AccountScriptingInterface.h index 888149b836..49648781ce 100644 --- a/interface/src/scripting/AccountScriptingInterface.h +++ b/interface/src/scripting/AccountScriptingInterface.h @@ -17,6 +17,11 @@ class AccountScriptingInterface : public QObject { Q_OBJECT + Q_PROPERTY(QString username READ getUsername NOTIFY usernameChanged) + +signals: + void usernameChanged(); + public slots: static AccountScriptingInterface* getInstance(); QString getUsername(); diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 80d52b7a07..fef0a12813 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -12,8 +12,11 @@ #include "LoginDialog.h" #include +#include +#include #include +#include #include "AccountManager.h" #include "DependencyManager.h" @@ -21,9 +24,7 @@ HIFI_QML_DEF(LoginDialog) -LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent), - _rootUrl(NetworkingConstants::METAVERSE_SERVER_URL.toString()) -{ +LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::loginComplete, this, &LoginDialog::handleLoginCompleted); @@ -53,36 +54,102 @@ void LoginDialog::toggleAction() { } } -void LoginDialog::handleLoginCompleted(const QUrl&) { - hide(); +bool LoginDialog::isSteamRunning() const { + return SteamClient::isRunning(); } -void LoginDialog::handleLoginFailed() { - setStatusText("Invalid username or password"); -} - -void LoginDialog::setStatusText(const QString& statusText) { - if (statusText != _statusText) { - _statusText = statusText; - emit statusTextChanged(); - } -} - -QString LoginDialog::statusText() const { - return _statusText; -} - -QString LoginDialog::rootUrl() const { - return _rootUrl; -} - -void LoginDialog::login(const QString& username, const QString& password) { +void LoginDialog::login(const QString& username, const QString& password) const { qDebug() << "Attempting to login " << username; - setStatusText("Logging in..."); DependencyManager::get()->requestAccessToken(username, password); } -void LoginDialog::openUrl(const QString& url) { - qDebug() << url; - QDesktopServices::openUrl(url); +void LoginDialog::loginThroughSteam() { + qDebug() << "Attempting to login through Steam"; + SteamClient::requestTicket([this](Ticket ticket) { + if (ticket.isNull()) { + emit handleLoginFailed(); + return; + } + + DependencyManager::get()->requestAccessTokenWithSteam(ticket); + }); } + +void LoginDialog::linkSteam() { + qDebug() << "Attempting to link Steam account"; + SteamClient::requestTicket([this](Ticket ticket) { + if (ticket.isNull()) { + emit handleLoginFailed(); + return; + } + + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "linkCompleted"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "linkFailed"; + + const QString LINK_STEAM_PATH = "api/v1/user/steam/link"; + + QJsonObject payload; + payload.insert("steam_auth_ticket", QJsonValue::fromVariant(QVariant(ticket))); + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(LINK_STEAM_PATH, AccountManagerAuth::Required, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(payload).toJson()); + }); +} + +void LoginDialog::createAccountFromStream(QString username) { + qDebug() << "Attempting to create account from Steam info"; + SteamClient::requestTicket([this, username](Ticket ticket) { + if (ticket.isNull()) { + emit handleLoginFailed(); + return; + } + + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "createCompleted"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "createFailed"; + + const QString CREATE_ACCOUNT_FROM_STEAM_PATH = "api/v1/user/steam/create"; + + QJsonObject payload; + payload.insert("steam_auth_ticket", QJsonValue::fromVariant(QVariant(ticket))); + if (!username.isEmpty()) { + payload.insert("username", QJsonValue::fromVariant(QVariant(username))); + } + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(CREATE_ACCOUNT_FROM_STEAM_PATH, AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(payload).toJson()); + }); + +} + +void LoginDialog::openUrl(const QString& url) const { + auto offscreenUi = DependencyManager::get(); + auto browser = offscreenUi->load("Browser.qml"); + browser->setProperty("url", url); +} + +void LoginDialog::linkCompleted(QNetworkReply& reply) { + emit handleLinkCompleted(); +} + +void LoginDialog::linkFailed(QNetworkReply& reply) { + emit handleLinkFailed(reply.errorString()); +} + +void LoginDialog::createCompleted(QNetworkReply& reply) { + emit handleCreateCompleted(); +} + +void LoginDialog::createFailed(QNetworkReply& reply) { + emit handleCreateFailed(reply.errorString()); +} + diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 25ecf45898..8b6dc40302 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -16,35 +16,44 @@ #include +class QNetworkReply; + class LoginDialog : public OffscreenQmlDialog { Q_OBJECT HIFI_QML_DECL - Q_PROPERTY(QString statusText READ statusText WRITE setStatusText NOTIFY statusTextChanged) - Q_PROPERTY(QString rootUrl READ rootUrl) - public: static void toggleAction(); LoginDialog(QQuickItem* parent = nullptr); - void setStatusText(const QString& statusText); - QString statusText() const; - - QString rootUrl() const; - signals: - void statusTextChanged(); - -protected: - void handleLoginCompleted(const QUrl& authURL); + void handleLoginCompleted(); void handleLoginFailed(); - Q_INVOKABLE void login(const QString& username, const QString& password); - Q_INVOKABLE void openUrl(const QString& url); -private: - QString _statusText; - const QString _rootUrl; + void handleLinkCompleted(); + void handleLinkFailed(QString error); + + void handleCreateCompleted(); + void handleCreateFailed(QString error); + +public slots: + void linkCompleted(QNetworkReply& reply); + void linkFailed(QNetworkReply& reply); + + void createCompleted(QNetworkReply& reply); + void createFailed(QNetworkReply& reply); + +protected slots: + Q_INVOKABLE bool isSteamRunning() const; + + Q_INVOKABLE void login(const QString& username, const QString& password) const; + Q_INVOKABLE void loginThroughSteam(); + Q_INVOKABLE void linkSteam(); + Q_INVOKABLE void createAccountFromStream(QString username = QString()); + + Q_INVOKABLE void openUrl(const QString& url) const; + }; #endif // hifi_LoginDialog_h diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index c4bfae7cac..d89514b7cd 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -505,6 +505,29 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } +void AccountManager::requestAccessTokenWithSteam(QByteArray authSessionTicket) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest request; + request.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + + QUrl grantURL = _authURL; + grantURL.setPath("/oauth/token"); + + const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; + + QByteArray postData; + postData.append("grant_type=password&"); + postData.append("steam_auth_ticket=" + QUrl::toPercentEncoding(authSessionTicket) + "&"); + postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); + + request.setUrl(grantURL); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply* requestReply = networkAccessManager.post(request, postData); + connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished); + connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); +} void AccountManager::requestAccessTokenFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); @@ -545,6 +568,7 @@ void AccountManager::requestAccessTokenFinished() { void AccountManager::requestAccessTokenError(QNetworkReply::NetworkError error) { // TODO: error handling qCDebug(networking) << "AccountManager requestError - " << error; + emit loginFailed(); } void AccountManager::requestProfile() { diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 846cdb6220..eb4d224501 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -96,6 +96,7 @@ public: public slots: void requestAccessToken(const QString& login, const QString& password); + void requestAccessTokenWithSteam(QByteArray authSessionTicket); void requestAccessTokenFinished(); void requestProfileFinished(); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index ae6aad3c4f..6760d44244 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -46,7 +47,7 @@ bool AddressManager::isConnected() { return DependencyManager::get()->getDomainHandler().isConnected(); } -const QUrl AddressManager::currentAddress() const { +QUrl AddressManager::currentAddress() const { QUrl hifiURL; hifiURL.setScheme(HIFI_URL_SCHEME); @@ -61,6 +62,21 @@ const QUrl AddressManager::currentAddress() const { return hifiURL; } +QUrl AddressManager::currentFacingAddress() const { + QUrl hifiURL; + + hifiURL.setScheme(HIFI_URL_SCHEME); + hifiURL.setHost(_host); + + if (_port != 0 && _port != DEFAULT_DOMAIN_SERVER_PORT) { + hifiURL.setPort(_port); + } + + hifiURL.setPath(currentFacingPath()); + + return hifiURL; +} + void AddressManager::loadSettings(const QString& lookupString) { if (lookupString.isEmpty()) { handleUrl(currentAddressHandle.get().toString(), LookupTrigger::StartupFromSettings); @@ -97,7 +113,7 @@ void AddressManager::storeCurrentAddress() { currentAddressHandle.set(currentAddress()); } -const QString AddressManager::currentPath(bool withOrientation) const { +QString AddressManager::currentPath(bool withOrientation) const { if (_positionGetter) { QString pathString = "/" + createByteArray(_positionGetter()); @@ -121,6 +137,25 @@ const QString AddressManager::currentPath(bool withOrientation) const { } } +QString AddressManager::currentFacingPath() const { + if (_positionGetter && _orientationGetter) { + auto position = _positionGetter(); + auto orientation = _orientationGetter(); + + // move the user a couple units away + const float DISTANCE_TO_USER = 2.0f; + position += orientation * Vectors::FRONT * DISTANCE_TO_USER; + + // rotate the user by 180 degrees + orientation = orientation * glm::angleAxis(PI, Vectors::UP); + + return "/" + createByteArray(position) + "/" + createByteArray(orientation); + } else { + qCDebug(networking) << "Cannot create address path without a getter for position/orientation."; + return QString(); + } +} + const JSONCallbackParameters& AddressManager::apiCallbackParameters() { static bool hasSetupParameters = false; static JSONCallbackParameters callbackParams; diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 2e9f177137..8ccddc5975 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -57,8 +57,10 @@ public: bool isConnected(); const QString& getProtocol() { return HIFI_URL_SCHEME; }; - const QUrl currentAddress() const; - const QString currentPath(bool withOrientation = true) const; + QUrl currentAddress() const; + QUrl currentFacingAddress() const; + QString currentPath(bool withOrientation = true) const; + QString currentFacingPath() const; const QUuid& getRootPlaceID() const { return _rootPlaceID; } const QString& getPlaceName() const { return _placeName; } diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index 0f06e03672..2e8f6bd7b3 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -11,9 +11,303 @@ #include "SteamClient.h" +#include + +#include +#include +#include +#include +#include +#include + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverloaded-virtual" +#endif + #include +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + + +static const Ticket INVALID_TICKET = Ticket(); + +class SteamTicketRequests { +public: + SteamTicketRequests(); + ~SteamTicketRequests(); + + HAuthTicket startRequest(TicketRequestCallback callback); + void stopRequest(HAuthTicket authTicket); + void stopAll(); + + STEAM_CALLBACK(SteamTicketRequests, onGetAuthSessionTicketResponse, + GetAuthSessionTicketResponse_t, _getAuthSessionTicketResponse); + +private: + struct PendingTicket { + HAuthTicket authTicket; + Ticket ticket; + TicketRequestCallback callback; + }; + + std::vector _pendingTickets; +}; + +SteamTicketRequests::SteamTicketRequests() : + _getAuthSessionTicketResponse(this, &SteamTicketRequests::onGetAuthSessionTicketResponse) +{ +} + +SteamTicketRequests::~SteamTicketRequests() { + stopAll(); +} + +HAuthTicket SteamTicketRequests::startRequest(TicketRequestCallback callback) { + static const uint32 MAX_TICKET_SIZE { 1024 }; + uint32 ticketSize { 0 }; + char ticket[MAX_TICKET_SIZE]; + + auto authTicket = SteamUser()->GetAuthSessionTicket(ticket, MAX_TICKET_SIZE, &ticketSize); + qDebug() << "Got Steam auth session ticket:" << authTicket; + + if (authTicket == k_HAuthTicketInvalid) { + qWarning() << "Auth session ticket is invalid."; + callback(INVALID_TICKET); + } else { + PendingTicket pendingTicket{ authTicket, QByteArray(ticket, ticketSize).toHex(), callback }; + _pendingTickets.push_back(pendingTicket); + } + + return authTicket; +} + +void SteamTicketRequests::stopRequest(HAuthTicket authTicket) { + auto it = std::find_if(_pendingTickets.begin(), _pendingTickets.end(), [&authTicket](const PendingTicket& pendingTicket) { + return pendingTicket.authTicket == authTicket; + }); + + if (it != _pendingTickets.end()) { + SteamUser()->CancelAuthTicket(it->authTicket); + it->callback(INVALID_TICKET); + _pendingTickets.erase(it); + } +} + +void SteamTicketRequests::stopAll() { + auto steamUser = SteamUser(); + if (steamUser) { + for (const auto& pendingTicket : _pendingTickets) { + steamUser->CancelAuthTicket(pendingTicket.authTicket); + pendingTicket.callback(INVALID_TICKET); + } + } + _pendingTickets.clear(); +} + +void SteamTicketRequests::onGetAuthSessionTicketResponse(GetAuthSessionTicketResponse_t* pCallback) { + auto authTicket = pCallback->m_hAuthTicket; + + auto it = std::find_if(_pendingTickets.begin(), _pendingTickets.end(), [&authTicket](const PendingTicket& pendingTicket) { + return pendingTicket.authTicket == authTicket; + }); + + + if (it != _pendingTickets.end()) { + + if (pCallback->m_eResult == k_EResultOK) { + qDebug() << "Got steam callback, auth session ticket is valid. Send it." << authTicket; + it->callback(it->ticket); + } else { + qWarning() << "Steam auth session ticket callback encountered an error:" << pCallback->m_eResult; + it->callback(INVALID_TICKET); + } + + _pendingTickets.erase(it); + } else { + qWarning() << "Could not find steam auth session ticket in list of pending tickets:" << authTicket; + } +} + + +const QString CONNECT_PREFIX = "--url \""; +const QString CONNECT_SUFFIX = "\""; + +class SteamCallbackManager { +public: + SteamCallbackManager(); + + STEAM_CALLBACK(SteamCallbackManager, onGameRichPresenceJoinRequested, + GameRichPresenceJoinRequested_t, _gameRichPresenceJoinRequestedResponse); + + STEAM_CALLBACK(SteamCallbackManager, onLobbyCreated, + LobbyCreated_t, _lobbyCreatedResponse); + + STEAM_CALLBACK(SteamCallbackManager, onGameLobbyJoinRequested, + GameLobbyJoinRequested_t, _gameLobbyJoinRequestedResponse); + + STEAM_CALLBACK(SteamCallbackManager, onLobbyEnter, + LobbyEnter_t, _lobbyEnterResponse); + + SteamTicketRequests& getTicketRequests() { return _steamTicketRequests; } + +private: + SteamTicketRequests _steamTicketRequests; +}; + +SteamCallbackManager::SteamCallbackManager() : + _gameRichPresenceJoinRequestedResponse(this, &SteamCallbackManager::onGameRichPresenceJoinRequested), + _lobbyCreatedResponse(this, &SteamCallbackManager::onLobbyCreated), + _gameLobbyJoinRequestedResponse(this, &SteamCallbackManager::onGameLobbyJoinRequested), + _lobbyEnterResponse(this, &SteamCallbackManager::onLobbyEnter) +{ +} + +void parseUrlAndGo(QString url) { + if (url.startsWith(CONNECT_PREFIX) && url.endsWith(CONNECT_SUFFIX)) { + url.remove(0, CONNECT_PREFIX.size()); + url.remove(-CONNECT_SUFFIX.size(), CONNECT_SUFFIX.size()); + } + + qDebug() << "Joining Steam Friend at:" << url; + auto mimeData = new QMimeData(); + mimeData->setUrls(QList() << QUrl(url)); + auto event = new QDropEvent(QPointF(0, 0), Qt::MoveAction, mimeData, Qt::LeftButton, Qt::NoModifier); + + QCoreApplication::postEvent(qApp, event); +} + +void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinRequested_t* pCallback) { + auto url = QString::fromLocal8Bit(pCallback->m_rgchConnect); + + parseUrlAndGo(url); +} +void SteamCallbackManager::onLobbyCreated(LobbyCreated_t* pCallback) { + if (pCallback->m_eResult == k_EResultOK) { + qDebug() << "Inviting steam friends" << pCallback->m_ulSteamIDLobby; + auto url = SteamFriends()->GetFriendRichPresence(SteamUser()->GetSteamID(), "connect"); + SteamMatchmaking()->SetLobbyData(pCallback->m_ulSteamIDLobby, "connect", url); + SteamFriends()->ActivateGameOverlayInviteDialog(pCallback->m_ulSteamIDLobby); + } +} + +void SteamCallbackManager::onGameLobbyJoinRequested(GameLobbyJoinRequested_t* pCallback) { + qDebug() << "Joining Steam lobby" << pCallback->m_steamIDLobby.ConvertToUint64(); + SteamMatchmaking()->JoinLobby(pCallback->m_steamIDLobby); +} + +void SteamCallbackManager::onLobbyEnter(LobbyEnter_t* pCallback) { + if (pCallback->m_EChatRoomEnterResponse != k_EChatRoomEnterResponseSuccess) { + qWarning() << "An error occured while joing the Steam lobby:" << pCallback->m_EChatRoomEnterResponse; + return; + } + + qDebug() << "Entered Steam lobby" << pCallback->m_ulSteamIDLobby; + + if (SteamMatchmaking()->GetLobbyOwner(pCallback->m_ulSteamIDLobby) != SteamUser()->GetSteamID()) { + auto url = SteamMatchmaking()->GetLobbyData(pCallback->m_ulSteamIDLobby, "connect"); + qDebug() << "Jumping to" << url; + parseUrlAndGo(url); + SteamMatchmaking()->LeaveLobby(pCallback->m_ulSteamIDLobby); + } +} + + +static std::atomic_bool initialized { false }; +static SteamCallbackManager steamCallbackManager; + + +bool SteamClient::isRunning() { + return initialized; +} + +bool SteamClient::init() { + if (SteamAPI_IsSteamRunning() && !initialized) { + initialized = SteamAPI_Init(); + } + return initialized; +} + +void SteamClient::shutdown() { + if (initialized) { + SteamAPI_Shutdown(); + } + + steamCallbackManager.getTicketRequests().stopAll(); +} + +void SteamClient::runCallbacks() { + if (!initialized) { + return; + } + + auto steamPipe = SteamAPI_GetHSteamPipe(); + if (!steamPipe) { + qDebug() << "Could not get SteamPipe"; + return; + } + + Steam_RunCallbacks(steamPipe, false); +} + +void SteamClient::requestTicket(TicketRequestCallback callback) { + if (!initialized) { + if (SteamAPI_IsSteamRunning()) { + init(); + } else { + qWarning() << "Steam is not running"; + callback(INVALID_TICKET); + return; + } + } + + if (!initialized) { + qDebug() << "Steam not initialized"; + return; + } + + steamCallbackManager.getTicketRequests().startRequest(callback); +} + +void SteamClient::updateLocation(QString status, QUrl locationUrl) { + if (!initialized) { + return; + } + + auto connectStr = locationUrl.isEmpty() ? "" : CONNECT_PREFIX + locationUrl.toString() + CONNECT_SUFFIX; + + SteamFriends()->SetRichPresence("status", status.toLocal8Bit().data()); + SteamFriends()->SetRichPresence("connect", connectStr.toLocal8Bit().data()); +} + +void SteamClient::openInviteOverlay() { + if (!initialized) { + return; + } + + qDebug() << "Creating Steam lobby"; + static const int MAX_LOBBY_SIZE = 20; + SteamMatchmaking()->CreateLobby(k_ELobbyTypePrivate, MAX_LOBBY_SIZE); +} + + +void SteamClient::joinLobby(QString lobbyIdStr) { + if (!initialized) { + if (SteamAPI_IsSteamRunning()) { + init(); + } else { + qWarning() << "Steam is not running"; + return; + } + } + + qDebug() << "Trying to join Steam lobby:" << lobbyIdStr; + CSteamID lobbyId(lobbyIdStr.toULongLong()); + SteamMatchmaking()->JoinLobby(lobbyId); +} \ No newline at end of file diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index 369641b0c7..5bf0d4db56 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -13,8 +13,43 @@ #ifndef hifi_SteamClient_h #define hifi_SteamClient_h -class SteamClient { +#include +#include +#include + +using Ticket = QByteArray; +using TicketRequestCallback = std::function; + +class QUrl; + +class SteamClient { +public: + static bool isRunning(); + + static bool init(); + static void shutdown(); + + static void runCallbacks(); + + static void requestTicket(TicketRequestCallback callback); + static void updateLocation(QString status, QUrl locationUrl); + static void openInviteOverlay(); + static void joinLobby(QString lobbyId); + +}; + +class SteamScriptingInterface : public QObject { + Q_OBJECT + + Q_PROPERTY(bool isRunning READ isRunning) + +public: + SteamScriptingInterface(QObject* parent) : QObject(parent) {} + +public slots: + bool isRunning() const { return SteamClient::isRunning(); } + void openInviteOverlay() const { SteamClient::openInviteOverlay(); } }; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 4e84c6d0fa..a532261014 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -161,7 +161,10 @@ static bool isBadPose(vr::HmdMatrix34_t* mat) { bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { handleOpenVrEvents(); - + if (openVrQuitRequested()) { + QMetaObject::invokeMethod(qApp, "quit"); + return false; + } double displayFrequency = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float); double frameDuration = 1.f / displayFrequency; double vsyncToPhotons = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float); diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index c93a2178b5..ee61a07da6 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -34,6 +34,11 @@ using Lock = std::unique_lock; static int refCount { 0 }; static Mutex mutex; static vr::IVRSystem* activeHmd { nullptr }; +static bool _openVrQuitRequested { false }; + +bool openVrQuitRequested() { + return _openVrQuitRequested; +} static const uint32_t RELEASE_OPENVR_HMD_DELAY_MS = 5000; @@ -99,6 +104,7 @@ void releaseOpenVrSystem() { qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; #endif vr::VR_Shutdown(); + _openVrQuitRequested = false; activeHmd = nullptr; } } @@ -257,8 +263,8 @@ void handleOpenVrEvents() { while (activeHmd->PollNextEvent(&event, sizeof(event))) { switch (event.eventType) { case vr::VREvent_Quit: + _openVrQuitRequested = true; activeHmd->AcknowledgeQuit_Exiting(); - QMetaObject::invokeMethod(qApp, "quit"); break; case vr::VREvent_KeyboardDone: diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 85feebda11..930b3dd450 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -214,6 +214,10 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); handleOpenVrEvents(); + if (openVrQuitRequested()) { + deactivate(); + return; + } // because update mutates the internal state we need to lock userInputMapper->withLock([&, this]() { diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 0efcd0c140..cf707c4d19 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -15,6 +15,7 @@ Script.load("system/users.js"); Script.load("system/mute.js"); Script.load("system/goto.js"); Script.load("system/hmd.js"); +Script.load("system/steam.js"); Script.load("system/marketplace.js"); Script.load("system/edit.js"); Script.load("system/mod.js"); diff --git a/scripts/system/assets/images/tools/steam-invite.svg b/scripts/system/assets/images/tools/steam-invite.svg new file mode 100644 index 0000000000..ce225cca68 --- /dev/null +++ b/scripts/system/assets/images/tools/steam-invite.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/steam.js b/scripts/system/steam.js new file mode 100644 index 0000000000..a80f0072ac --- /dev/null +++ b/scripts/system/steam.js @@ -0,0 +1,31 @@ +// +// steam.js +// scripts/system/ +// +// Created by Clement on 7/28/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 +// + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + + +var steamInviteButton = toolBar.addButton({ + objectName: "steamInvite", + imageURL: Script.resolvePath("assets/images/tools/steam-invite.svg"), + visible: Steam.isRunning, + buttonState: 1, + defaultState: 1, + hoverState: 3, + alpha: 0.9 +}); + +steamInviteButton.clicked.connect(function(){ + Steam.openInviteOverlay(); +}); + +Script.scriptEnding.connect(function () { + toolBar.removeButton("steamInvite"); +}); \ No newline at end of file