diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index ecdf14ebec..f72832f902 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -453,7 +453,7 @@ void EntityServer::domainSettingsRequestFailed() { void EntityServer::startDynamicDomainVerification() { qCDebug(entities) << "Starting Dynamic Domain Verification..."; - QString thisDomainID = DependencyManager::get()->getDomainId().remove(QRegExp("\\{|\\}")); + QString thisDomainID = DependencyManager::get()->getDomainID().remove(QRegExp("\\{|\\}")); EntityTreePointer tree = std::static_pointer_cast(_tree); QHash localMap(tree->getEntityCertificateIDMap()); diff --git a/cmake/macros/AddCrashpad.cmake b/cmake/macros/AddCrashpad.cmake index bf59418f37..7d161be7f0 100644 --- a/cmake/macros/AddCrashpad.cmake +++ b/cmake/macros/AddCrashpad.cmake @@ -51,8 +51,8 @@ macro(add_crashpad) ) install( PROGRAMS ${CRASHPAD_HANDLER_EXE_PATH} - DESTINATION ${CLIENT_COMPONENT} - COMPONENT ${INTERFACE_INSTALL_DIR} + DESTINATION ${INTERFACE_INSTALL_DIR} + COMPONENT ${CLIENT_COMPONENT} ) endif () endmacro() diff --git a/cmake/macros/AddCustomQrcPath.cmake b/cmake/macros/AddCustomQrcPath.cmake new file mode 100644 index 0000000000..6bd54baa4d --- /dev/null +++ b/cmake/macros/AddCustomQrcPath.cmake @@ -0,0 +1,7 @@ +# adds a custom path and local path to the inserted CUSTOM_PATHS_VAR list which +# can be given to the GENERATE_QRC command + +function(ADD_CUSTOM_QRC_PATH CUSTOM_PATHS_VAR IMPORT_PATH LOCAL_PATH) + list(APPEND ${CUSTOM_PATHS_VAR} "${IMPORT_PATH}=${LOCAL_PATH}") + set(${CUSTOM_PATHS_VAR} ${${CUSTOM_PATHS_VAR}} PARENT_SCOPE) +endfunction() diff --git a/cmake/macros/FindNPM.cmake b/cmake/macros/FindNPM.cmake new file mode 100644 index 0000000000..c66114f878 --- /dev/null +++ b/cmake/macros/FindNPM.cmake @@ -0,0 +1,14 @@ +# +# FindNPM.cmake +# cmake/macros +# +# Created by Thijs Wenker on 01/23/18. +# Copyright 2018 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 +# + +macro(find_npm) + find_program(NPM_EXECUTABLE "npm") +endmacro() diff --git a/cmake/macros/GenerateQrc.cmake b/cmake/macros/GenerateQrc.cmake index 9bf530b2a2..5e2be71c82 100644 --- a/cmake/macros/GenerateQrc.cmake +++ b/cmake/macros/GenerateQrc.cmake @@ -1,7 +1,7 @@ function(GENERATE_QRC) set(oneValueArgs OUTPUT PREFIX PATH) - set(multiValueArgs GLOBS) + set(multiValueArgs CUSTOM_PATHS GLOBS) cmake_parse_arguments(GENERATE_QRC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) if ("${GENERATE_QRC_PREFIX}" STREQUAL "") set(QRC_PREFIX_PATH /) @@ -20,6 +20,13 @@ function(GENERATE_QRC) endforeach() endforeach() + foreach(CUSTOM_PATH ${GENERATE_QRC_CUSTOM_PATHS}) + string(REPLACE "=" ";" CUSTOM_PATH ${CUSTOM_PATH}) + list(GET CUSTOM_PATH 0 IMPORT_PATH) + list(GET CUSTOM_PATH 1 LOCAL_PATH) + set(QRC_CONTENTS "${QRC_CONTENTS}${IMPORT_PATH}\n") + endforeach() + set(GENERATE_QRC_DEPENDS ${ALL_FILES} PARENT_SCOPE) configure_file("${HF_CMAKE_DIR}/templates/resources.qrc.in" ${GENERATE_QRC_OUTPUT}) endfunction() diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 8abb202bd4..c1bfebe2c4 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -823,6 +823,7 @@ Section "-Core installation" Delete "$INSTDIR\server-console.exe" RMDir /r "$INSTDIR\locales" RMDir /r "$INSTDIR\resources\app" + RMDir /r "$INSTDIR\client" Delete "$INSTDIR\resources\atom.asar" Delete "$INSTDIR\build-info.json" Delete "$INSTDIR\content_resources_200_percent.pak" diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index ee2997e216..7a5d890d2a 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -11,9 +11,17 @@ function(JOIN VALUES GLUE OUTPUT) set (${OUTPUT} "${_TMP_STR}" PARENT_SCOPE) endfunction() +set(CUSTOM_INTERFACE_QRC_PATHS "") + +find_npm() + +if (BUILD_TOOLS AND NPM_EXECUTABLE) + add_custom_qrc_path(CUSTOM_INTERFACE_QRC_PATHS "${CMAKE_SOURCE_DIR}/tools/jsdoc/out/hifiJSDoc.json" "auto-complete/hifiJSDoc.json") +endif () + set(RESOURCES_QRC ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc) set(RESOURCES_RCC ${CMAKE_CURRENT_SOURCE_DIR}/compiledResources/resources.rcc) -generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources GLOBS *) +generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources CUSTOM_PATHS ${CUSTOM_INTERFACE_QRC_PATHS} GLOBS *) add_custom_command( OUTPUT ${RESOURCES_RCC} @@ -163,6 +171,11 @@ else () add_executable(${TARGET_NAME} ${INTERFACE_SRCS} ${QM}) endif () +if (BUILD_TOOLS AND NPM_EXECUTABLE) + # require JSDoc to be build before interface is deployed (Console Auto-complete) + add_dependencies(resources jsdoc) +endif() + add_dependencies(${TARGET_NAME} resources) if (WIN32) @@ -290,6 +303,7 @@ if (APPLE) set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_APP_PATH}/Contents/Resources") + # copy script files beside the executable add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml index 02c6181952..926e9c4fe5 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controls-uit/Button.qml @@ -32,6 +32,12 @@ Original.Button { Tablet.playSound(TabletEnums.ButtonHover); } } + + onFocusChanged: { + if (focus) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } onClicked: { Tablet.playSound(TabletEnums.ButtonClick); @@ -59,6 +65,8 @@ Original.Button { hifi.buttons.pressedColor[control.color] } else if (control.hovered) { hifi.buttons.hoveredColor[control.color] + } else if (!control.hovered && control.focus) { + hifi.buttons.focusedColor[control.color] } else { hifi.buttons.colorStart[control.color] } @@ -73,6 +81,8 @@ Original.Button { hifi.buttons.pressedColor[control.color] } else if (control.hovered) { hifi.buttons.hoveredColor[control.color] + } else if (!control.hovered && control.focus) { + hifi.buttons.focusedColor[control.color] } else { hifi.buttons.colorFinish[control.color] } diff --git a/interface/resources/qml/controls-uit/GlyphButton.qml b/interface/resources/qml/controls-uit/GlyphButton.qml index 9c23171ee1..024c131a09 100644 --- a/interface/resources/qml/controls-uit/GlyphButton.qml +++ b/interface/resources/qml/controls-uit/GlyphButton.qml @@ -31,6 +31,12 @@ Original.Button { } } + onFocusChanged: { + if (focus) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + onClicked: { Tablet.playSound(TabletEnums.ButtonClick); } @@ -50,6 +56,8 @@ Original.Button { hifi.buttons.pressedColor[control.color] } else if (control.hovered) { hifi.buttons.hoveredColor[control.color] + } else if (!control.hovered && control.focus) { + hifi.buttons.focusedColor[control.color] } else { hifi.buttons.colorStart[control.color] } @@ -64,6 +72,8 @@ Original.Button { hifi.buttons.pressedColor[control.color] } else if (control.hovered) { hifi.buttons.hoveredColor[control.color] + } else if (!control.hovered && control.focus) { + hifi.buttons.focusedColor[control.color] } else { hifi.buttons.colorFinish[control.color] } diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index 1443cd5d6e..a3e4113d08 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -93,7 +93,7 @@ TableView { size: hifi.fontSizes.tableHeadingIcon anchors { left: titleText.right - leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 5 : 0) + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 15 : 10) right: parent.right rightMargin: hifi.dimensions.tablePadding verticalCenter: titleText.verticalCenter diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 7b1a177c86..0b5da8a5f5 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -110,7 +110,17 @@ ModalWindow { } }); - fileTableView.forceActiveFocus(); + focusTimer.start(); + } + + Timer { + id: focusTimer + interval: 10 + running: false + repeat: false + onTriggered: { + fileTableView.contentItem.forceActiveFocus(); + } } Item { @@ -130,7 +140,9 @@ ModalWindow { drag.target: root onClicked: { d.clearSelection(); - frame.forceActiveFocus(); // Defocus text field so that the keyboard gets hidden. + // Defocus text field so that the keyboard gets hidden. + // Clicking also breaks keyboard navigation apart from backtabbing to cancel + frame.forceActiveFocus(); } } @@ -150,6 +162,11 @@ ModalWindow { size: 30 enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" onClicked: d.navigateUp(); + Keys.onReturnPressed: { d.navigateUp(); } + KeyNavigation.tab: homeButton + KeyNavigation.backtab: upButton + KeyNavigation.left: upButton + KeyNavigation.right: homeButton } GlyphButton { @@ -160,6 +177,10 @@ ModalWindow { width: height enabled: d.homeDestination ? true : false onClicked: d.navigateHome(); + Keys.onReturnPressed: { d.navigateHome(); } + KeyNavigation.tab: fileTableView.contentItem + KeyNavigation.backtab: upButton + KeyNavigation.left: upButton } } @@ -228,9 +249,15 @@ ModalWindow { d.currentSelectionUrl = helper.pathToUrl(currentText); } fileTableModel.folder = folder; - fileTableView.forceActiveFocus(); } } + + KeyNavigation.up: fileTableView.contentItem + KeyNavigation.down: fileTableView.contentItem + KeyNavigation.tab: fileTableView.contentItem + KeyNavigation.backtab: fileTableView.contentItem + KeyNavigation.left: fileTableView.contentItem + KeyNavigation.right: fileTableView.contentItem } QtObject { @@ -483,7 +510,6 @@ ModalWindow { } headerVisible: !selectDirectory onDoubleClicked: navigateToRow(row); - focus: true Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); @@ -560,7 +586,7 @@ ModalWindow { resizable: true } TableViewColumn { - id: fileMofifiedColumn + id: fileModifiedColumn role: "fileModified" title: "Date" width: 0.3 * fileTableView.width @@ -571,7 +597,7 @@ ModalWindow { TableViewColumn { role: "fileSize" title: "Size" - width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + width: fileTableView.width - fileNameColumn.width - fileModifiedColumn.width movable: false resizable: true visible: !selectDirectory @@ -649,6 +675,8 @@ ModalWindow { break; } } + + KeyNavigation.tab: root.saveDialog ? currentSelection : openButton } TextField { @@ -665,6 +693,10 @@ ModalWindow { activeFocusOnTab: !readOnly onActiveFocusChanged: if (activeFocus) { selectAll(); } onAccepted: okAction.trigger(); + KeyNavigation.up: fileTableView.contentItem + KeyNavigation.down: openButton + KeyNavigation.tab: openButton + KeyNavigation.backtab: fileTableView.contentItem } FileTypeSelection { @@ -675,8 +707,6 @@ ModalWindow { right: parent.right } visible: !selectDirectory && filtersCount > 1 - KeyNavigation.left: fileTableView - KeyNavigation.right: openButton } Keyboard { @@ -704,18 +734,18 @@ ModalWindow { color: hifi.buttons.blue action: okAction Keys.onReturnPressed: okAction.trigger() - KeyNavigation.up: selectionType - KeyNavigation.left: selectionType KeyNavigation.right: cancelButton + KeyNavigation.up: root.saveDialog ? currentSelection : fileTableView.contentItem + KeyNavigation.tab: cancelButton } Button { id: cancelButton action: cancelAction - KeyNavigation.up: selectionType + Keys.onReturnPressed: { cancelAction.trigger() } KeyNavigation.left: openButton - KeyNavigation.right: fileTableView.contentItem - Keys.onReturnPressed: { canceled(); root.enabled = false } + KeyNavigation.up: root.saveDialog ? currentSelection : fileTableView.contentItem + KeyNavigation.backtab: openButton } } diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 40c5a01e15..351df6dc8a 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -37,6 +37,16 @@ ModalWindow { return OffscreenUi.waitForMessageBoxResult(root); } + Keys.onRightPressed: if (defaultButton === OriginalDialogs.StandardButton.Yes) { + yesButton.forceActiveFocus() + } else if (defaultButton === OriginalDialogs.StandardButton.Ok) { + okButton.forceActiveFocus() + } + Keys.onTabPressed: if (defaultButton === OriginalDialogs.StandardButton.Yes) { + yesButton.forceActiveFocus() + } else if (defaultButton === OriginalDialogs.StandardButton.Ok) { + okButton.forceActiveFocus() + } property alias detailedText: detailedText.text property alias text: mainTextContainer.text property alias informativeText: informativeTextContainer.text @@ -47,7 +57,6 @@ ModalWindow { onIconChanged: updateIcon(); property int defaultButton: OriginalDialogs.StandardButton.NoButton; property int clickedButton: OriginalDialogs.StandardButton.NoButton; - focus: defaultButton === OriginalDialogs.StandardButton.NoButton property int titleWidth: 0 onTitleWidthChanged: d.resize(); @@ -134,16 +143,35 @@ ModalWindow { MessageDialogButton { dialog: root; text: qsTr("Reset"); button: OriginalDialogs.StandardButton.Reset; } MessageDialogButton { dialog: root; text: qsTr("Discard"); button: OriginalDialogs.StandardButton.Discard; } MessageDialogButton { dialog: root; text: qsTr("No to All"); button: OriginalDialogs.StandardButton.NoToAll; } - MessageDialogButton { dialog: root; text: qsTr("No"); button: OriginalDialogs.StandardButton.No; } + MessageDialogButton { + id: noButton + dialog: root + text: qsTr("No") + button: OriginalDialogs.StandardButton.No + KeyNavigation.left: yesButton + KeyNavigation.backtab: yesButton + } MessageDialogButton { dialog: root; text: qsTr("Yes to All"); button: OriginalDialogs.StandardButton.YesToAll; } - MessageDialogButton { dialog: root; text: qsTr("Yes"); button: OriginalDialogs.StandardButton.Yes; } + MessageDialogButton { + id: yesButton + dialog: root + text: qsTr("Yes") + button: OriginalDialogs.StandardButton.Yes + KeyNavigation.right: noButton + KeyNavigation.tab: noButton + } MessageDialogButton { dialog: root; text: qsTr("Apply"); button: OriginalDialogs.StandardButton.Apply; } MessageDialogButton { dialog: root; text: qsTr("Ignore"); button: OriginalDialogs.StandardButton.Ignore; } MessageDialogButton { dialog: root; text: qsTr("Retry"); button: OriginalDialogs.StandardButton.Retry; } MessageDialogButton { dialog: root; text: qsTr("Save All"); button: OriginalDialogs.StandardButton.SaveAll; } MessageDialogButton { dialog: root; text: qsTr("Save"); button: OriginalDialogs.StandardButton.Save; } MessageDialogButton { dialog: root; text: qsTr("Open"); button: OriginalDialogs.StandardButton.Open; } - MessageDialogButton { dialog: root; text: qsTr("OK"); button: OriginalDialogs.StandardButton.Ok; } + MessageDialogButton { + id: okButton + dialog: root + text: qsTr("OK") + button: OriginalDialogs.StandardButton.Ok + } Button { id: moreButton @@ -230,12 +258,6 @@ ModalWindow { event.accepted = true root.click(OriginalDialogs.StandardButton.Cancel) break - - case Qt.Key_Enter: - case Qt.Key_Return: - event.accepted = true - root.click(root.defaultButton) - break } } } diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index 9a38c3f0d6..b5de5362f2 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -95,19 +95,19 @@ ModalWindow { TextField { id: textResult label: root.label - focus: items ? false : true visible: items ? false : true anchors { left: parent.left; right: parent.right; bottom: parent.bottom } + KeyNavigation.down: acceptButton + KeyNavigation.tab: acceptButton } ComboBox { id: comboBox label: root.label - focus: true visible: items ? true : false anchors { left: parent.left @@ -115,6 +115,8 @@ ModalWindow { bottom: parent.bottom } model: items ? items : [] + KeyNavigation.down: acceptButton + KeyNavigation.tab: acceptButton } } @@ -135,7 +137,6 @@ ModalWindow { Flow { id: buttons - focus: true spacing: hifi.dimensions.contentSpacing.x onHeightChanged: d.resize(); onWidthChanged: d.resize(); layoutDirection: Qt.RightToLeft @@ -145,8 +146,21 @@ ModalWindow { margins: 0 bottomMargin: hifi.dimensions.contentSpacing.y } - Button { action: cancelAction } - Button { action: acceptAction } + Button { + id: cancelButton + action: cancelAction + KeyNavigation.left: acceptButton + KeyNavigation.up: items ? comboBox : textResult + KeyNavigation.backtab: acceptButton + } + Button { + id: acceptButton + action: acceptAction + KeyNavigation.right: cancelButton + KeyNavigation.up: items ? comboBox : textResult + KeyNavigation.tab: cancelButton + KeyNavigation.backtab: items ? comboBox : textResult + } } Action { @@ -184,7 +198,13 @@ ModalWindow { case Qt.Key_Return: case Qt.Key_Enter: - acceptAction.trigger() + if (acceptButton.focus) { + acceptAction.trigger() + } else if (cancelButton.focus) { + cancelAction.trigger() + } else if (comboBox.focus || comboBox.popup.focus) { + comboBox.showList() + } event.accepted = true; break; } @@ -194,6 +214,10 @@ ModalWindow { keyboardEnabled = HMD.active; updateIcon(); d.resize(); - textResult.forceActiveFocus(); + if (items) { + comboBox.forceActiveFocus() + } else { + textResult.forceActiveFocus() + } } } diff --git a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml index b7ff9354eb..bdb42aba61 100644 --- a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml +++ b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml @@ -16,10 +16,21 @@ import "../../controls-uit" Button { property var dialog; - property int button: StandardButton.NoButton; + property int button: StandardButton.Ok; - color: dialog.defaultButton === button ? hifi.buttons.blue : hifi.buttons.white - focus: dialog.defaultButton === button + color: focus ? hifi.buttons.blue : hifi.buttons.white onClicked: dialog.click(button) visible: dialog.buttons & button + Keys.onPressed: { + if (!focus) { + return + } + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + dialog.click(button) + break + } + } } diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index 524bbf5f4d..cfff3a3273 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -127,7 +127,7 @@ Item { GridView { id: gridView - + flickableDirection: Flickable.AutoFlickIfNeeded keyNavigationEnabled: false highlightFollowsCurrentItem: false @@ -144,23 +144,20 @@ Item { bottomMargin: 0 } - function setButtonState(buttonIndex, buttonstate) { - if (buttonIndex < 0 || gridView.contentItem === undefined - || gridView.contentItem.children.length - 1 < buttonIndex) { - return; - } - var itemat = gridView.contentItem.children[buttonIndex].children[0]; - if (itemat.isActive) { - itemat.state = "active state"; - } else { - itemat.state = buttonstate; - } + onCurrentIndexChanged: { + previousGridIndex = currentIndex } - onCurrentIndexChanged: { - setButtonState(previousGridIndex, "base state"); - setButtonState(currentIndex, "hover state"); - previousGridIndex = currentIndex + onMovementStarted: { + if (currentIndex < 0 || gridView.currentItem === undefined || gridView.contentItem.children.length - 1 < currentIndex) { + return; + } + var button = gridView.contentItem.children[currentIndex].children[0]; + if (button.isActive) { + button.state = "active state"; + } else { + button.state = "base state"; + } } cellWidth: width/3 diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index cf800cb62e..5da587ea57 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -219,6 +219,7 @@ Item { readonly property var colorFinish: [ colors.lightGrayText, colors.blueAccent, "#94132e", colors.black, Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0), Qt.rgba(0, 0, 0, 0) ] readonly property var hoveredColor: [ colorStart[white], colorStart[blue], colorStart[red], colorFinish[black], colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] readonly property var pressedColor: [ colorFinish[white], colorFinish[blue], colorFinish[red], colorStart[black], colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] + readonly property var focusedColor: [ colors.lightGray50, colors.blueAccent, colors.redAccent, colors.darkGray, colorStart[none], colorStart[noneBorderless], colorStart[noneBorderlessWhite], colorStart[noneBorderlessGray] ] readonly property var disabledColorStart: [ colorStart[white], colors.baseGrayHighlight] readonly property var disabledColorFinish: [ colorFinish[white], colors.baseGrayShadow] readonly property var disabledTextColor: [ colors.lightGrayText, colors.baseGrayShadow] diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 8587ddc9c2..ed391f750a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -81,7 +81,7 @@ class MyAvatar : public Avatar { * and MyAvatar.customListenOrientation properties. * @property customListenPosition {Vec3} If MyAvatar.audioListenerMode == MyAvatar.audioListenerModeHead, then this determines the position * of audio spatialization listener. - * @property customListenOreintation {Quat} If MyAvatar.audioListenerMode == MyAvatar.audioListenerModeHead, then this determines the orientation + * @property customListenOrientation {Quat} If MyAvatar.audioListenerMode == MyAvatar.audioListenerModeHead, then this determines the orientation * of the audio spatialization listener. * @property audioListenerModeHead {number} READ-ONLY. When passed to MyAvatar.audioListenerMode, it will set the audio listener * around the avatar's head. diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index fc45555bf9..d419d7484b 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -105,14 +105,14 @@ void WindowScriptingInterface::raiseMainWindow() { /// \param const QString& message message to display /// \return QScriptValue::UndefinedValue void WindowScriptingInterface::alert(const QString& message) { - OffscreenUi::asyncWarning("", message); + OffscreenUi::asyncWarning("", message, QMessageBox::Ok, QMessageBox::Ok); } /// Display a confirmation box with the options 'Yes' and 'No' /// \param const QString& message message to display /// \return QScriptValue `true` if 'Yes' was clicked, `false` otherwise QScriptValue WindowScriptingInterface::confirm(const QString& message) { - return QScriptValue((QMessageBox::Yes == OffscreenUi::question("", message, QMessageBox::Yes | QMessageBox::No))); + return QScriptValue((QMessageBox::Yes == OffscreenUi::question("", message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes))); } /// Display a prompt with a text box @@ -354,7 +354,7 @@ QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } -/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid +/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid /// directory the browser will start at the root directory. /// \param const QString& title title of the window /// \param const QString& directory directory to start the asset browser at diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index c5f8b54ebd..1ca1ac2842 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -12,10 +12,12 @@ #include "JSConsole.h" #include -#include #include #include #include +#include +#include +#include #include #include @@ -38,6 +40,21 @@ const QString RESULT_ERROR_STYLE = "color: #d13b22;"; const QString GUTTER_PREVIOUS_COMMAND = "<"; const QString GUTTER_ERROR = "X"; +const QString JSDOC_LINE_SEPARATOR = "\r"; + +const QString JSDOC_STYLE = + ""; + const QString JSConsole::_consoleFileName { "about:console" }; const QString JSON_KEY = "entries"; @@ -49,7 +66,7 @@ QList _readLines(const QString& filename) { // TODO: check root["version"] return root[JSON_KEY].toVariant().toStringList(); } - + void _writeLines(const QString& filename, const QList& lines) { QFile file(filename); file.open(QFile::WriteOnly); @@ -61,12 +78,71 @@ void _writeLines(const QString& filename, const QList& lines) { QTextStream(&file) << json; } +QString _jsdocTypeToString(QJsonValue jsdocType) { + return jsdocType.toObject().value("names").toVariant().toStringList().join("/"); +} + +void JSConsole::readAPI() { + QFile file(PathUtils::resourcesPath() + "auto-complete/hifiJSDoc.json"); + file.open(QFile::ReadOnly); + auto json = QTextStream(&file).readAll().toUtf8(); + _apiDocs = QJsonDocument::fromJson(json).array(); +} + +QStandardItem* getAutoCompleteItem(QJsonValue propertyObject) { + auto propertyItem = new QStandardItem(propertyObject.toObject().value("name").toString()); + propertyItem->setData(propertyObject.toVariant()); + return propertyItem; +} + +QStandardItemModel* JSConsole::getAutoCompleteModel(const QString& memberOf) { + QString memberOfProperty = nullptr; + + auto model = new QStandardItemModel(this); + + if (memberOf != nullptr) { + foreach(auto doc, _apiDocs) { + auto object = doc.toObject(); + if (object.value("name").toString() == memberOf && object.value("scope").toString() == "global" && + object.value("kind").toString() == "namespace") { + + memberOfProperty = object.value("longname").toString(); + + auto properties = doc.toObject().value("properties").toArray(); + foreach(auto propertyObject, properties) { + model->appendRow(getAutoCompleteItem(propertyObject)); + } + } + } + if (memberOfProperty == nullptr) { + return nullptr; + } + } + + foreach(auto doc, _apiDocs) { + auto object = doc.toObject(); + auto scope = object.value("scope"); + if ((memberOfProperty == nullptr && scope.toString() == "global" && object.value("kind").toString() == "namespace") || + (memberOfProperty != nullptr && object.value("memberof").toString() == memberOfProperty && + object.value("kind").toString() != "typedef")) { + + model->appendRow(getAutoCompleteItem(doc)); + } + } + model->sort(0); + return model; +} + JSConsole::JSConsole(QWidget* parent, const ScriptEnginePointer& scriptEngine) : QWidget(parent), _ui(new Ui::Console), _currentCommandInHistory(NO_CURRENT_HISTORY_COMMAND), _savedHistoryFilename(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + HISTORY_FILENAME), - _commandHistory(_readLines(_savedHistoryFilename)) { + _commandHistory(_readLines(_savedHistoryFilename)), + _completer(new QCompleter(this)) { + + readAPI(); + _ui->setupUi(this); _ui->promptTextEdit->setLineWrapMode(QTextEdit::NoWrap); _ui->promptTextEdit->setWordWrapMode(QTextOption::NoWrap); @@ -78,38 +154,174 @@ JSConsole::JSConsole(QWidget* parent, const ScriptEnginePointer& scriptEngine) : setStyleSheet(styleSheet.readAll()); } - connect(_ui->scrollArea->verticalScrollBar(), SIGNAL(rangeChanged(int, int)), this, SLOT(scrollToBottom())); - connect(_ui->promptTextEdit, SIGNAL(textChanged()), this, SLOT(resizeTextInput())); + connect(_ui->scrollArea->verticalScrollBar(), &QScrollBar::rangeChanged, this, &JSConsole::scrollToBottom); + connect(_ui->promptTextEdit, &QTextEdit::textChanged, this, &JSConsole::resizeTextInput); + + _completer->setWidget(_ui->promptTextEdit); + _completer->setModel(getAutoCompleteModel(nullptr)); + _completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); + _completer->setMaxVisibleItems(12); + _completer->setFilterMode(Qt::MatchStartsWith); + _completer->setWrapAround(false); + _completer->setCompletionMode(QCompleter::PopupCompletion); + _completer->setCaseSensitivity(Qt::CaseSensitive); + + QListView *listView = new QListView(); + listView->setEditTriggers(QAbstractItemView::NoEditTriggers); + listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listView->setSelectionBehavior(QAbstractItemView::SelectRows); + listView->setSelectionMode(QAbstractItemView::SingleSelection); + listView->setModelColumn(_completer->completionColumn()); + + _completer->setPopup(listView); + _completer->popup()->installEventFilter(this); + QObject::connect(_completer, static_cast(&QCompleter::activated), this, + &JSConsole::insertCompletion); + + QObject::connect(_completer, static_cast(&QCompleter::highlighted), this, + &JSConsole::highlightedCompletion); setScriptEngine(scriptEngine); resizeTextInput(); - connect(&_executeWatcher, SIGNAL(finished()), this, SLOT(commandFinished())); + connect(&_executeWatcher, &QFutureWatcher::finished, this, &JSConsole::commandFinished); +} + +void JSConsole::insertCompletion(const QModelIndex& completion) { + auto jsdocObject = QJsonValue::fromVariant(completion.data(Qt::UserRole + 1)).toObject(); + auto kind = jsdocObject.value("kind").toString(); + auto completionString = completion.data().toString(); + if (kind == "function") { + auto params = jsdocObject.value("params").toArray(); + // automatically add the parenthesis/parentheses for the functions + completionString += params.isEmpty() ? "()" : "("; + } + QTextCursor textCursor = _ui->promptTextEdit->textCursor(); + int extra = completionString.length() - _completer->completionPrefix().length(); + textCursor.movePosition(QTextCursor::Left); + textCursor.movePosition(QTextCursor::EndOfWord); + textCursor.insertText(completionString.right(extra)); + _ui->promptTextEdit->setTextCursor(textCursor); +} + +void JSConsole::highlightedCompletion(const QModelIndex& completion) { + auto jsdocObject = QJsonValue::fromVariant(completion.data(Qt::UserRole + 1)).toObject(); + QString memberOf = ""; + if (!_completerModule.isEmpty()) { + memberOf = _completerModule + "."; + } + auto name = memberOf + "" + jsdocObject.value("name").toString() + ""; + auto description = jsdocObject.value("description").toString(); + auto examples = jsdocObject.value("examples").toArray(); + auto kind = jsdocObject.value("kind").toString(); + QString returnTypeText = ""; + + QString paramsTable = ""; + if (kind == "function") { + auto params = jsdocObject.value("params").toArray(); + auto returns = jsdocObject.value("returns"); + if (!returns.isUndefined()) { + returnTypeText = _jsdocTypeToString(jsdocObject.value("returns").toArray().at(0).toObject().value("type")) + " "; + } + name += "("; + if (!params.isEmpty()) { + bool hasDefaultParam = false; + bool hasOptionalParam = false; + bool firstItem = true; + foreach(auto param, params) { + auto paramObject = param.toObject(); + if (!hasOptionalParam && paramObject.value("optional").toBool(false)) { + hasOptionalParam = true; + name += "["; + } + if (!firstItem) { + name += ", "; + } else { + firstItem = false; + } + name += paramObject.value("name").toString(); + if (!hasDefaultParam && !paramObject.value("defaultvalue").isUndefined()) { + hasDefaultParam = true; + } + } + if (hasOptionalParam) { + name += "]"; + } + + paramsTable += ""; + if (hasDefaultParam) { + paramsTable += ""; + } + paramsTable += ""; + foreach(auto param, params) { + auto paramObject = param.toObject(); + paramsTable += ""; + } + + paramsTable += "
NameTypeDefaultDescription
" + paramObject.value("name").toString() + "" + + _jsdocTypeToString(paramObject.value("type")) + ""; + if (hasDefaultParam) { + paramsTable += paramObject.value("defaultvalue").toVariant().toString() + ""; + } + paramsTable += paramObject.value("description").toString() + "
"; + } + name += ")"; + } else if (!jsdocObject.value("type").isUndefined()){ + returnTypeText = _jsdocTypeToString(jsdocObject.value("type")) + " "; + } + auto popupText = JSDOC_STYLE + "" + returnTypeText + name + ""; + auto descriptionText = "

" + description.replace(JSDOC_LINE_SEPARATOR, "
") + "

"; + + popupText += descriptionText; + popupText += paramsTable; + auto returns = jsdocObject.value("returns"); + if (!returns.isUndefined()) { + foreach(auto returnEntry, returns.toArray()) { + auto returnsObject = returnEntry.toObject(); + auto returnsDescription = returnsObject.value("description").toString().replace(JSDOC_LINE_SEPARATOR, "
"); + popupText += "

Returns

" + returnsDescription + "

Type
" +
+                _jsdocTypeToString(returnsObject.value("type")) + "
"; + } + } + + if (!examples.isEmpty()) { + popupText += "

Examples

"; + foreach(auto example, examples) { + auto exampleText = example.toString(); + auto exampleLines = exampleText.split(JSDOC_LINE_SEPARATOR); + foreach(auto exampleLine, exampleLines) { + if (exampleLine.contains("")) { + popupText += exampleLine.replace("caption>", "h5>"); + } else { + popupText += "
" + exampleLine + "\n
"; + } + } + } + } + + QToolTip::showText(QPoint(_completer->popup()->pos().x() + _completer->popup()->width(), _completer->popup()->pos().y()), + popupText, _completer->popup()); } JSConsole::~JSConsole() { if (_scriptEngine) { - disconnect(_scriptEngine.data(), SIGNAL(printedMessage(const QString&)), this, SLOT(handlePrint(const QString&))); - disconnect(_scriptEngine.data(), SIGNAL(errorMessage(const QString&)), this, SLOT(handleError(const QString&))); + disconnect(_scriptEngine.data(), nullptr, this, nullptr); _scriptEngine.reset(); } delete _ui; } void JSConsole::setScriptEngine(const ScriptEnginePointer& scriptEngine) { - if (_scriptEngine == scriptEngine && scriptEngine != NULL) { + if (_scriptEngine == scriptEngine && scriptEngine != nullptr) { return; } - if (_scriptEngine != NULL) { - disconnect(_scriptEngine.data(), &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); - disconnect(_scriptEngine.data(), &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); - disconnect(_scriptEngine.data(), &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); - disconnect(_scriptEngine.data(), &ScriptEngine::errorMessage, this, &JSConsole::handleError); + if (_scriptEngine != nullptr) { + disconnect(_scriptEngine.data(), nullptr, this, nullptr); _scriptEngine.reset(); } - // if scriptEngine is NULL then create one and keep track of it using _ownScriptEngine + // if scriptEngine is nullptr then create one and keep track of it using _ownScriptEngine if (scriptEngine.isNull()) { _scriptEngine = DependencyManager::get()->loadScript(_consoleFileName, false); } else { @@ -199,45 +411,121 @@ void JSConsole::showEvent(QShowEvent* event) { } bool JSConsole::eventFilter(QObject* sender, QEvent* event) { - if (sender == _ui->promptTextEdit) { - if (event->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(event); - int key = keyEvent->key(); + if ((sender == _ui->promptTextEdit || sender == _completer->popup()) && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + int key = keyEvent->key(); - if ((key == Qt::Key_Return || key == Qt::Key_Enter)) { - if (keyEvent->modifiers() & Qt::ShiftModifier) { - // If the shift key is being used then treat it as a regular return/enter. If this isn't done, - // a new QTextBlock isn't created. - keyEvent->setModifiers(keyEvent->modifiers() & ~Qt::ShiftModifier); - } else { - QString command = _ui->promptTextEdit->toPlainText().replace("\r\n","\n").trimmed(); - - if (!command.isEmpty()) { - QTextCursor cursor = _ui->promptTextEdit->textCursor(); - _ui->promptTextEdit->clear(); - - executeCommand(command); - } - - return true; - } - } else if (key == Qt::Key_Down) { - // Go to the next command in history if the cursor is at the last line of the current command. - int blockNumber = _ui->promptTextEdit->textCursor().blockNumber(); - int blockCount = _ui->promptTextEdit->document()->blockCount(); - if (blockNumber == blockCount - 1) { - setToNextCommandInHistory(); - return true; - } - } else if (key == Qt::Key_Up) { - // Go to the previous command in history if the cursor is at the first line of the current command. - int blockNumber = _ui->promptTextEdit->textCursor().blockNumber(); - if (blockNumber == 0) { - setToPreviousCommandInHistory(); - return true; - } + if (_completer->popup()->isVisible()) { + // The following keys are forwarded by the completer to the widget + switch (key) { + case Qt::Key_Space: + case Qt::Key_Enter: + case Qt::Key_Return: + insertCompletion(_completer->popup()->currentIndex()); + _completer->popup()->hide(); + return true; + default: + return false; } } + + if (key == Qt::Key_Return || key == Qt::Key_Enter) { + if (keyEvent->modifiers() & Qt::ShiftModifier) { + // If the shift key is being used then treat it as a regular return/enter. If this isn't done, + // a new QTextBlock isn't created. + keyEvent->setModifiers(keyEvent->modifiers() & ~Qt::ShiftModifier); + } else { + QString command = _ui->promptTextEdit->toPlainText().replace("\r\n", "\n").trimmed(); + + if (!command.isEmpty()) { + QTextCursor cursor = _ui->promptTextEdit->textCursor(); + _ui->promptTextEdit->clear(); + + executeCommand(command); + } + + return true; + } + } else if (key == Qt::Key_Down) { + // Go to the next command in history if the cursor is at the last line of the current command. + int blockNumber = _ui->promptTextEdit->textCursor().blockNumber(); + int blockCount = _ui->promptTextEdit->document()->blockCount(); + if (blockNumber == blockCount - 1) { + setToNextCommandInHistory(); + return true; + } + } else if (key == Qt::Key_Up) { + // Go to the previous command in history if the cursor is at the first line of the current command. + int blockNumber = _ui->promptTextEdit->textCursor().blockNumber(); + if (blockNumber == 0) { + setToPreviousCommandInHistory(); + return true; + } + } + } else if ((sender == _ui->promptTextEdit || sender == _completer->popup()) && event->type() == QEvent::KeyRelease) { + QKeyEvent* keyEvent = static_cast(event); + int key = keyEvent->key(); + + // completer shortcut (CTRL + SPACE) + bool isCompleterShortcut = ((keyEvent->modifiers() & Qt::ControlModifier) && key == Qt::Key_Space) || + key == Qt::Key_Period; + if (_completer->popup()->isVisible() || isCompleterShortcut) { + + const bool ctrlOrShift = keyEvent->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); + if (ctrlOrShift && keyEvent->text().isEmpty()) { + return false; + } + + static QString eow("~!@#$%^&*()+{}|:\"<>?,/;'[]\\-="); // end of word + + if (!isCompleterShortcut && (!keyEvent->text().isEmpty() && eow.contains(keyEvent->text().right(1)))) { + _completer->popup()->hide(); + return false; + } + + auto textCursor = _ui->promptTextEdit->textCursor(); + + textCursor.select(QTextCursor::WordUnderCursor); + + QString completionPrefix = textCursor.selectedText(); + + auto leftOfCursor = _ui->promptTextEdit->toPlainText().left(textCursor.position()); + + // RegEx [3] [4] + // (Module.subModule).(property/subModule) + + const int MODULE_INDEX = 3; + const int PROPERTY_INDEX = 4; + // TODO: disallow invalid characters on left of property + QRegExp regExp("((([A-Za-z0-9_\\.]+)\\.)|(?!\\.))([a-zA-Z0-9_]*)$"); + regExp.indexIn(leftOfCursor); + auto rexExpCapturedTexts = regExp.capturedTexts(); + auto memberOf = rexExpCapturedTexts[MODULE_INDEX]; + completionPrefix = rexExpCapturedTexts[PROPERTY_INDEX]; + bool switchedModule = false; + if (memberOf != _completerModule) { + _completerModule = memberOf; + auto autoCompleteModel = getAutoCompleteModel(memberOf); + if (autoCompleteModel == nullptr) { + _completer->popup()->hide(); + return false; + } + _completer->setModel(autoCompleteModel); + _completer->popup()->installEventFilter(this); + switchedModule = true; + } + + if (switchedModule || completionPrefix != _completer->completionPrefix()) { + _completer->setCompletionPrefix(completionPrefix); + _completer->popup()->setCurrentIndex(_completer->completionModel()->index(0, 0)); + } + auto cursorRect = _ui->promptTextEdit->cursorRect(); + cursorRect.setWidth(_completer->popup()->sizeHintForColumn(0) + + _completer->popup()->verticalScrollBar()->sizeHint().width()); + _completer->complete(cursorRect); + highlightedCompletion(_completer->popup()->currentIndex()); + return false; + } } return false; } @@ -321,7 +609,7 @@ void JSConsole::appendMessage(const QString& gutter, const QString& message) { void JSConsole::clear() { QLayoutItem* item; - while ((item = _ui->logArea->layout()->takeAt(0)) != NULL) { + while ((item = _ui->logArea->layout()->takeAt(0)) != nullptr) { delete item->widget(); delete item; } diff --git a/interface/src/ui/JSConsole.h b/interface/src/ui/JSConsole.h index 4b6409a76f..eeb3601886 100644 --- a/interface/src/ui/JSConsole.h +++ b/interface/src/ui/JSConsole.h @@ -12,12 +12,11 @@ #ifndef hifi_JSConsole_h #define hifi_JSConsole_h -#include -#include #include #include -#include #include +#include +#include #include "ui_console.h" #include "ScriptEngine.h" @@ -54,12 +53,20 @@ protected slots: void handleError(const QString& message, const QString& scriptName); void commandFinished(); +private slots: + void insertCompletion(const QModelIndex& completion); + void highlightedCompletion(const QModelIndex& completion); + private: void appendMessage(const QString& gutter, const QString& message); void setToNextCommandInHistory(); void setToPreviousCommandInHistory(); void resetCurrentCommandHistory(); + void readAPI(); + + QStandardItemModel* getAutoCompleteModel(const QString& memberOf = nullptr); + QFutureWatcher _executeWatcher; Ui::Console* _ui; int _currentCommandInHistory; @@ -68,6 +75,9 @@ private: QString _rootCommand; ScriptEnginePointer _scriptEngine; static const QString _consoleFileName; + QJsonArray _apiDocs; + QCompleter* _completer; + QString _completerModule {""}; }; diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 4c4e2ffbfd..d1b321dbca 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -139,7 +139,7 @@ public slots: Q_INVOKABLE bool canRezTmpCertified(); /**jsdoc - * @function Entities.canWriteAsseets + * @function Entities.canWriteAssets * @return {bool} `true` if the DomainServer will allow this Node/Avatar to write to the asset server */ Q_INVOKABLE bool canWriteAssets(); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 765cdda6eb..bf29f3bec9 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1679,14 +1679,17 @@ void EntityTree::entityChanged(EntityItemPointer entity) { } } - void EntityTree::fixupNeedsParentFixups() { PROFILE_RANGE(simulation_physics, "FixupParents"); MovingEntitiesOperator moveOperator; + QVector entitiesToFixup; + { + QWriteLocker locker(&_needsParentFixupLock); + entitiesToFixup = _needsParentFixup; + _needsParentFixup.clear(); + } - QWriteLocker locker(&_needsParentFixupLock); - - QMutableVectorIterator iter(_needsParentFixup); + QMutableVectorIterator iter(entitiesToFixup); while (iter.hasNext()) { EntityItemWeakPointer entityWP = iter.next(); EntityItemPointer entity = entityWP.lock(); @@ -1749,6 +1752,12 @@ void EntityTree::fixupNeedsParentFixups() { PerformanceTimer perfTimer("recurseTreeWithOperator"); recurseTreeWithOperator(&moveOperator); } + + { + QWriteLocker locker(&_needsParentFixupLock); + // add back the entities that did not get fixup + _needsParentFixup.append(entitiesToFixup); + } } void EntityTree::deleteDescendantsOfAvatar(QUuid avatarID) { diff --git a/libraries/gl/src/gl/Context.cpp b/libraries/gl/src/gl/Context.cpp index 8acbada5aa..309839808e 100644 --- a/libraries/gl/src/gl/Context.cpp +++ b/libraries/gl/src/gl/Context.cpp @@ -23,25 +23,31 @@ #include #include #include "GLLogging.h" -#include "Config.h" - -#ifdef Q_OS_WIN - -#if defined(DEBUG) || defined(USE_GLES) -static bool enableDebugLogger = true; -#else -static const QString DEBUG_FLAG("HIFI_DEBUG_OPENGL"); -static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); -#endif - -#endif - #include "Config.h" #include "GLHelpers.h" using namespace gl; +bool Context::enableDebugLogger() { +#if defined(DEBUG) || defined(USE_GLES) + static bool enableDebugLogger = true; +#else + static const QString DEBUG_FLAG("HIFI_DEBUG_OPENGL"); + static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +#endif + static std::once_flag once; + std::call_once(once, [&] { + // If the previous run crashed, force GL debug logging on + if (qApp->property(hifi::properties::CRASHED).toBool()) { + enableDebugLogger = true; + } + }); + return enableDebugLogger; +} + + + std::atomic Context::_totalSwapchainMemoryUsage { 0 }; size_t Context::getSwapchainMemoryUsage() { return _totalSwapchainMemoryUsage.load(); } @@ -245,10 +251,6 @@ void Context::create() { // Create a temporary context to initialize glew static std::once_flag once; std::call_once(once, [&] { - // If the previous run crashed, force GL debug logging on - if (qApp->property(hifi::properties::CRASHED).toBool()) { - enableDebugLogger = true; - } auto hdc = GetDC(hwnd); setupPixelFormatSimple(hdc); auto glrc = wglCreateContext(hdc); @@ -328,7 +330,7 @@ void Context::create() { contextAttribs.push_back(WGL_CONTEXT_CORE_PROFILE_BIT_ARB); #endif contextAttribs.push_back(WGL_CONTEXT_FLAGS_ARB); - if (enableDebugLogger) { + if (enableDebugLogger()) { contextAttribs.push_back(WGL_CONTEXT_DEBUG_BIT_ARB); } else { contextAttribs.push_back(0); @@ -350,7 +352,7 @@ void Context::create() { if (!makeCurrent()) { throw std::runtime_error("Could not make context current"); } - if (enableDebugLogger) { + if (enableDebugLogger()) { glDebugMessageCallback(debugMessageCallback, NULL); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB); } diff --git a/libraries/gl/src/gl/Context.h b/libraries/gl/src/gl/Context.h index 4f0747a86f..b6160cbd6c 100644 --- a/libraries/gl/src/gl/Context.h +++ b/libraries/gl/src/gl/Context.h @@ -47,6 +47,7 @@ namespace gl { Context(const Context& other); public: + static bool enableDebugLogger(); Context(); Context(QWindow* window); void release(); diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 8a476c45ad..380ba085cc 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "Context.h" #include "GLHelpers.h" @@ -68,9 +69,32 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { } #endif + if (gl::Context::enableDebugLogger()) { + _context->makeCurrent(_offscreenSurface); + QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this); + connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OffscreenGLCanvas::onMessageLogged); + logger->initialize(); + logger->enableMessages(); + logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); + _context->doneCurrent(); + } + return true; } +void OffscreenGLCanvas::onMessageLogged(const QOpenGLDebugMessage& debugMessage) { + auto severity = debugMessage.severity(); + switch (severity) { + case QOpenGLDebugMessage::NotificationSeverity: + case QOpenGLDebugMessage::LowSeverity: + return; + default: + break; + } + qDebug(glLogging) << debugMessage; + return; +} + bool OffscreenGLCanvas::makeCurrent() { bool result = _context->makeCurrent(_offscreenSurface); std::call_once(_reportOnce, []{ diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h index 583aa7c60f..be0e0d9678 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.h +++ b/libraries/gl/src/gl/OffscreenGLCanvas.h @@ -17,7 +17,7 @@ class QOpenGLContext; class QOffscreenSurface; -class QOpenGLDebugLogger; +class QOpenGLDebugMessage; class OffscreenGLCanvas : public QObject { public: @@ -32,6 +32,9 @@ public: } QObject* getContextObject(); +private slots: + void onMessageLogged(const QOpenGLDebugMessage &debugMessage); + protected: std::once_flag _reportOnce; QOpenGLContext* _context{ nullptr }; diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index fa5507fcf7..4e1354d3aa 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -776,7 +776,7 @@ void AddressManager::copyPath() { QApplication::clipboard()->setText(currentPath()); } -QString AddressManager::getDomainId() const { +QString AddressManager::getDomainID() const { return DependencyManager::get()->getDomainHandler().getUUID().toString(); } diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 0173a1fad7..6c9f06d2dd 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -35,9 +35,11 @@ const QString GET_PLACE = "/api/v1/places/%1"; * The location API provides facilities related to your current location in the metaverse. * * @namespace location - * @property {Uuid} domainId - A UUID uniquely identifying the domain you're visiting. Is {@link Uuid|Uuid.NULL} if you're not + * @property {Uuid} domainID - A UUID uniquely identifying the domain you're visiting. Is {@link Uuid|Uuid.NULL} if you're not * connected to the domain. * Read-only. + * @property {Uuid} domainId - Synonym for domainId. Read-only. Deprecated: This property + * is deprecated and will soon be removed. * @property {string} hostname - The name of the domain for your current metaverse address (e.g., "AvatarIsland", * localhost, or an IP address). * Read-only. @@ -66,7 +68,8 @@ class AddressManager : public QObject, public Dependency { Q_PROPERTY(QString hostname READ getHost) Q_PROPERTY(QString pathname READ currentPath) Q_PROPERTY(QString placename READ getPlaceName) - Q_PROPERTY(QString domainId READ getDomainId) + Q_PROPERTY(QString domainID READ getDomainID) + Q_PROPERTY(QString domainId READ getDomainID) public: /**jsdoc @@ -164,7 +167,7 @@ public: const QUuid& getRootPlaceID() const { return _rootPlaceID; } const QString& getPlaceName() const { return _shareablePlaceName.isEmpty() ? _placeName : _shareablePlaceName; } - QString getDomainId() const; + QString getDomainID() const; const QString& getHost() const { return _host; } diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index a3ac995bcf..7ba7cca96d 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -340,14 +340,6 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& return resource; } - if (QThread::currentThread() != thread()) { - qCDebug(networking) << "Fetching asynchronously:" << url; - QMetaObject::invokeMethod(this, "getResource", - Q_ARG(QUrl, url), Q_ARG(QUrl, fallback)); - // Cannot use extra parameter as it might be freed before the invocation - return QSharedPointer(); - } - if (!url.isValid() && !url.isEmpty() && fallback.isValid()) { return getResource(fallback, QUrl()); } @@ -358,6 +350,7 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& extra); resource->setSelf(resource); resource->setCache(this); + resource->moveToThread(qApp->thread()); connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); { QWriteLocker locker(&_resourcesLock); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 2d33bbca14..a4e741a797 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -150,7 +150,7 @@ signals: /**jsdoc * Notifies scripts that a user has disconnected from the domain - * @function Users.avatar.avatarDisconnected + * @function Users.avatarDisconnected * @param {nodeID} NodeID The session ID of the avatar that has disconnected */ void avatarDisconnected(const QUuid& nodeID); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index db3df34dc5..6ff0a7d416 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -543,6 +543,7 @@ void OffscreenQmlSurface::cleanup() { void OffscreenQmlSurface::render() { #if !defined(DISABLE_QML) + if (nsightActive()) { return; } @@ -569,14 +570,18 @@ void OffscreenQmlSurface::render() { { // If the most recent texture was unused, we can directly recycle it - if (_latestTextureAndFence.first) { - offscreenTextures.releaseTexture(_latestTextureAndFence); - _latestTextureAndFence = { 0, 0 }; - } - - _latestTextureAndFence = { texture, glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0) }; + auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); // Fence will be used in another thread / context, so a flush is required glFlush(); + + { + Lock lock(_latestTextureAndFenceMutex); + if (_latestTextureAndFence.first) { + offscreenTextures.releaseTexture(_latestTextureAndFence); + _latestTextureAndFence = { 0, 0 }; + } + _latestTextureAndFence = { texture, fence}; + } } _quickWindow->resetOpenGLState(); @@ -588,13 +593,21 @@ void OffscreenQmlSurface::render() { bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) { textureAndFence = { 0, 0 }; + // Lock free early check if (0 == _latestTextureAndFence.first) { return false; } // Ensure writes to the latest texture are complete before before returning it for reading - textureAndFence = _latestTextureAndFence; - _latestTextureAndFence = { 0, 0 }; + { + Lock lock(_latestTextureAndFenceMutex); + // Double check inside the lock + if (0 == _latestTextureAndFence.first) { + return false; + } + textureAndFence = _latestTextureAndFence; + _latestTextureAndFence = { 0, 0 }; + } return true; } @@ -813,10 +826,13 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { // Release hold on the textures of the old size if (uvec2() != _size) { - // If the most recent texture was unused, we can directly recycle it - if (_latestTextureAndFence.first) { - offscreenTextures.releaseTexture(_latestTextureAndFence); - _latestTextureAndFence = { 0, 0 }; + { + Lock lock(_latestTextureAndFenceMutex); + // If the most recent texture was unused, we can directly recycle it + if (_latestTextureAndFence.first) { + offscreenTextures.releaseTexture(_latestTextureAndFence); + _latestTextureAndFence = { 0, 0 }; + } } offscreenTextures.releaseSize(_size); } diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 7aeac8d7c3..08c7ca6bf8 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -167,6 +167,9 @@ public slots: bool handlePointerEvent(const PointerEvent& event, class QTouchDevice& device, bool release = false); private: + using Mutex = std::mutex; + using Lock = std::unique_lock; + QQuickWindow* _quickWindow { nullptr }; QQmlContext* _qmlContext { nullptr }; QQuickItem* _rootItem { nullptr }; @@ -188,6 +191,7 @@ private: #endif // Texture management + Mutex _latestTextureAndFenceMutex; TextureAndFence _latestTextureAndFence { 0, 0 }; bool _render { false }; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 5a656c4ba0..647497335e 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -493,7 +493,7 @@ function populateNearbyUserList(selectData, oldAudioData) { data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); }); - getConnectionData(false, location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). + getConnectionData(false, location.domainID); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). conserveResources = Object.keys(avatarsOfInterest).length > 20; sendToQml({ method: 'nearbyUsers', params: data }); if (selectData) { diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index dad642075f..ae8ef52a15 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -337,7 +337,7 @@ function fillImageDataFromPrevious() { containsGif: previousAnimatedSnapPath !== "", processingGif: false, shouldUpload: false, - canBlast: location.domainId === Settings.getValue("previousSnapshotDomainID"), + canBlast: location.domainID === Settings.getValue("previousSnapshotDomainID"), isLoggedIn: isLoggedIn }; imageData = []; @@ -416,7 +416,7 @@ function snapshotUploaded(isError, reply) { } isUploadingPrintableStill = false; } -var href, domainId; +var href, domainID; function takeSnapshot() { tablet.emitScriptEvent(JSON.stringify({ type: "snapshot", @@ -443,11 +443,11 @@ function takeSnapshot() { MyAvatar.setClearOverlayWhenMoving(false); // We will record snapshots based on the starting location. That could change, e.g., when recording a .gif. - // Even the domainId could change (e.g., if the user falls into a teleporter while recording). + // Even the domainID could change (e.g., if the user falls into a teleporter while recording). href = location.href; Settings.setValue("previousSnapshotHref", href); - domainId = location.domainId; - Settings.setValue("previousSnapshotDomainID", domainId); + domainID = location.domainID; + Settings.setValue("previousSnapshotDomainID", domainID); maybeDeleteSnapshotStories(); @@ -548,7 +548,7 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { } HMD.openTablet(); - isDomainOpen(domainId, function (canShare) { + isDomainOpen(domainID, function (canShare) { snapshotOptions = { containsGif: false, processingGif: false, @@ -594,7 +594,7 @@ function processingGifStarted(pathStillSnapshot) { } HMD.openTablet(); - isDomainOpen(domainId, function (canShare) { + isDomainOpen(domainID, function (canShare) { snapshotOptions = { containsGif: true, processingGif: true, @@ -622,13 +622,13 @@ function processingGifCompleted(pathAnimatedSnapshot) { Settings.setValue("previousAnimatedSnapPath", pathAnimatedSnapshot); - isDomainOpen(domainId, function (canShare) { + isDomainOpen(domainID, function (canShare) { snapshotOptions = { containsGif: true, processingGif: false, canShare: canShare, isLoggedIn: isLoggedIn, - canBlast: location.domainId === Settings.getValue("previousSnapshotDomainID"), + canBlast: location.domainID === Settings.getValue("previousSnapshotDomainID"), }; imageData = [{ localPath: pathAnimatedSnapshot, href: href }]; tablet.emitScriptEvent(JSON.stringify({ diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 16446c5071..53d7fc2836 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -2,6 +2,12 @@ add_subdirectory(scribe) set_target_properties(scribe PROPERTIES FOLDER "Tools") +find_npm() +if (NPM_EXECUTABLE) + add_subdirectory(jsdoc) + set_target_properties(jsdoc PROPERTIES FOLDER "Tools") +endif() + if (BUILD_TOOLS) add_subdirectory(udt-test) set_target_properties(udt-test PROPERTIES FOLDER "Tools") @@ -27,4 +33,3 @@ if (BUILD_TOOLS) add_subdirectory(auto-tester) set_target_properties(auto-tester PROPERTIES FOLDER "Tools") endif() - diff --git a/tools/jsdoc/CMakeLists.txt b/tools/jsdoc/CMakeLists.txt new file mode 100644 index 0000000000..292523a813 --- /dev/null +++ b/tools/jsdoc/CMakeLists.txt @@ -0,0 +1,17 @@ +set(TARGET_NAME jsdoc) + +add_custom_target(${TARGET_NAME}) + +find_npm() + +set(JSDOC_WORKING_DIR ${CMAKE_SOURCE_DIR}/tools/jsdoc) +file(TO_NATIVE_PATH ${JSDOC_WORKING_DIR}/node_modules/.bin/jsdoc JSDOC_PATH) +file(TO_NATIVE_PATH ${JSDOC_WORKING_DIR}/config.json JSDOC_CONFIG_PATH) +file(TO_NATIVE_PATH ${JSDOC_WORKING_DIR}/out OUTPUT_DIR) +file(TO_NATIVE_PATH ${JSDOC_WORKING_DIR} NATIVE_JSDOC_WORKING_DIR) + +add_custom_command(TARGET ${TARGET_NAME} + COMMAND ${NPM_EXECUTABLE} --no-progress install && ${JSDOC_PATH} ${NATIVE_JSDOC_WORKING_DIR} -c ${JSDOC_CONFIG_PATH} -d ${OUTPUT_DIR} + WORKING_DIRECTORY ${JSDOC_WORKING_DIR} + COMMENT "generate the JSDoc JSON for the JSConsole auto-completer" +) diff --git a/tools/jsdoc/config.json b/tools/jsdoc/config.json index 0fb833d015..a24e248661 100644 --- a/tools/jsdoc/config.json +++ b/tools/jsdoc/config.json @@ -4,5 +4,8 @@ "outputSourceFiles": false } }, - "plugins": ["plugins/hifi"] + "plugins": [ + "plugins/hifi", + "plugins/hifiJSONExport" + ] } diff --git a/tools/jsdoc/package.json b/tools/jsdoc/package.json new file mode 100644 index 0000000000..215ceec177 --- /dev/null +++ b/tools/jsdoc/package.json @@ -0,0 +1,7 @@ +{ + "name": "hifiJSDoc", + "dependencies": { + "jsdoc": "^3.5.5" + }, + "private": true +} diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index 3af5fbeee3..bb556814e8 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -10,6 +10,8 @@ function endsWith(path, exts) { exports.handlers = { beforeParse: function(e) { + const pathTools = require('path'); + var rootFolder = pathTools.dirname(e.filename); console.log("Scanning hifi source for jsdoc comments..."); // directories to scan for jsdoc comments @@ -34,9 +36,10 @@ exports.handlers = { const fs = require('fs'); dirList.forEach(function (dir) { - var files = fs.readdirSync(dir) + var joinedDir = pathTools.join(rootFolder, dir); + var files = fs.readdirSync(joinedDir) files.forEach(function (file) { - var path = dir + "/" + file; + var path = pathTools.join(joinedDir, file); if (fs.lstatSync(path).isFile() && endsWith(path, exts)) { var data = fs.readFileSync(path, "utf8"); var reg = /(\/\*\*jsdoc(.|[\r\n])*?\*\/)/gm; diff --git a/tools/jsdoc/plugins/hifiJSONExport.js b/tools/jsdoc/plugins/hifiJSONExport.js new file mode 100644 index 0000000000..cd14c9faad --- /dev/null +++ b/tools/jsdoc/plugins/hifiJSONExport.js @@ -0,0 +1,19 @@ +exports.handlers = { + processingComplete: function(e) { + const pathTools = require('path'); + var outputFolder = pathTools.join(__dirname, '../out'); + var doclets = e.doclets.map(doclet => Object.assign({}, doclet)); + const fs = require('fs'); + if (!fs.existsSync(outputFolder)) { + fs.mkdirSync(outputFolder); + } + doclets.map(doclet => {delete doclet.meta; delete doclet.comment}); + fs.writeFile(pathTools.join(outputFolder, "hifiJSDoc.json"), JSON.stringify(doclets, null, 4), function(err) { + if (err) { + return console.log(err); + } + + console.log("The Hifi JSDoc JSON was saved!"); + }); + } +};