diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 7688036c94..b4e0e3a244 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -128,7 +128,7 @@ target_link_libraries(${TARGET_NAME} ${BULLET_LIBRARIES}) # link required hifi libraries link_hifi_libraries(shared octree environment gpu model fbx networking entities avatars audio audio-client animation script-engine physics - render-utils entities-renderer) + render-utils entities-renderer ui) add_dependency_external_projects(sdl2) diff --git a/interface/resources/fonts/fontawesome-webfont.ttf b/interface/resources/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000..ed9372f8ea Binary files /dev/null and b/interface/resources/fonts/fontawesome-webfont.ttf differ diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index ec1480928a..8a2bc3e2b4 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -1,10 +1,9 @@ import Hifi 1.0 import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Window 2.2 -import QtQuick.Controls.Styles 1.3 +import "controls" +import "styles" -CustomDialog { +Dialog { title: "Go to..." objectName: "AddressBarDialog" height: 128 @@ -36,19 +35,20 @@ CustomDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - CustomBorder { + Border { height: 64 anchors.left: parent.left anchors.leftMargin: 0 anchors.right: goButton.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter - CustomTextInput { + TextInput { id: addressLine anchors.fill: parent helperText: "domain, location, @user, /x,y,z" anchors.margins: 8 onAccepted: { + event.accepted addressBarDialog.loadAddress(addressLine.text) } } @@ -72,5 +72,20 @@ CustomDialog { } } } + + Keys.onEscapePressed: { + enabled = false; + } + + function toggleOrGo() { + if (addressLine.text == "") { + enabled = false + } else { + addressBarDialog.loadAddress(addressLine.text) + } + } + + Keys.onReturnPressed: toggleOrGo() + Keys.onEnterPressed: toggleOrGo() } diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index d7e08fbd97..a439f9114c 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,12 +1,10 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtQuick.Window 2.2 -import QtQuick.Dialogs 1.2 -import QtQuick.Controls.Styles 1.3 import QtWebKit 3.0 +import "controls" -CustomDialog { - title: "Test Dlg" +Dialog { + title: "Browser Window" id: testDialog objectName: "Browser" width: 1280 @@ -18,7 +16,6 @@ CustomDialog { anchors.fill: parent anchors.margins: parent.margins anchors.topMargin: parent.topMargin - ScrollView { anchors.fill: parent @@ -30,16 +27,4 @@ CustomDialog { } } - - } - - -/* - -// This is the behavior, and it applies a NumberAnimation to any attempt to set the x property - -MouseArea { - anchors.fill: parent -} -*/ diff --git a/interface/resources/qml/CustomButton.qml b/interface/resources/qml/CustomButton.qml deleted file mode 100644 index ce57d7ce5e..0000000000 --- a/interface/resources/qml/CustomButton.qml +++ /dev/null @@ -1,23 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.3 -import QtQuick.Window 2.2 -import QtQuick.Controls.Styles 1.3 - -Button { - SystemPalette { id: myPalette; colorGroup: SystemPalette.Active } - text: "Text" - width: 128 - height: 64 - style: ButtonStyle { - background: CustomBorder { - anchors.fill: parent - } - label: CustomText { - renderType: Text.NativeRendering - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - text: control.text - color: control.enabled ? myPalette.text : myPalette.dark - } - } -} diff --git a/interface/resources/qml/CustomTextArea.qml b/interface/resources/qml/CustomTextArea.qml deleted file mode 100644 index cf3308e2b7..0000000000 --- a/interface/resources/qml/CustomTextArea.qml +++ /dev/null @@ -1,10 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 - -TextArea { - font.family: "Helvetica" - font.pointSize: 18 - backgroundVisible: false - readOnly: true -} - diff --git a/interface/resources/qml/HifiAction.qml b/interface/resources/qml/HifiAction.qml new file mode 100644 index 0000000000..0cecff91d7 --- /dev/null +++ b/interface/resources/qml/HifiAction.qml @@ -0,0 +1,20 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.3 + +Action { + property string name + objectName: name + "HifiAction" + text: qsTr(name) + + signal triggeredByName(string name); + signal toggledByName(string name); + + onTriggered: { + triggeredByName(name); + } + + onToggled: { + toggledByName(name, checked); + } +} + diff --git a/interface/resources/qml/HifiMenu.qml b/interface/resources/qml/HifiMenu.qml new file mode 100644 index 0000000000..1afa39c5d1 --- /dev/null +++ b/interface/resources/qml/HifiMenu.qml @@ -0,0 +1,272 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.4 +import QtQuick.Controls 1.3 +import QtQuick.Controls.Styles 1.3 +import "controls" +import "styles" + +Hifi.HifiMenu { + id: root + anchors.fill: parent + objectName: "HifiMenu" + enabled: false + opacity: 0.0 + property int animationDuration: 200 + HifiPalette { id: hifiPalette } + z: 10000 + + onEnabledChanged: { + if (enabled && columns.length == 0) { + pushColumn(rootMenu.items); + } + opacity = enabled ? 1.0 : 0.0 + if (enabled) { + forceActiveFocus() + } + } + + // The actual animator + Behavior on opacity { + NumberAnimation { + duration: root.animationDuration + easing.type: Easing.InOutBounce + } + } + + onOpacityChanged: { + visible = (opacity != 0.0); + } + + onVisibleChanged: { + if (!visible) reset(); + } + + + property var menu: Menu {} + property var models: [] + property var columns: [] + property var itemBuilder: Component { + Text { + SystemPalette { id: sp; colorGroup: SystemPalette.Active } + id: thisText + x: 32 + property var source + property var root + property var listViewIndex + property var listView + text: typedText() + height: implicitHeight + width: implicitWidth + color: source.enabled ? "black" : "gray" + + onImplicitWidthChanged: { + if (listView) { + listView.minWidth = Math.max(listView.minWidth, implicitWidth + 64); + listView.recalculateSize(); + } + } + + FontAwesome { + visible: source.type == 1 && source.checkable + x: -32 + text: (source.type == 1 && source.checked) ? "\uF05D" : "\uF10C" + } + + FontAwesome { + visible: source.type == 2 + x: listView.width - 64 + text: "\uF0DA" + } + + + function typedText() { + switch(source.type) { + case 2: + return source.title; + case 1: + return source.text; + case 0: + return "-----" + } + } + + MouseArea { + id: mouseArea + acceptedButtons: Qt.LeftButton + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + width: listView.width + onClicked: { + listView.currentIndex = listViewIndex + parent.root.selectItem(parent.source); + } + } + } + } + + + property var menuBuilder: Component { + Border { + SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active } + x: root.models.length == 1 ? + (root.width / 2 - width / 2) : + root.columns[root.models.length - 2].x + 60; + anchors.verticalCenter: parent.verticalCenter + border.color: hifiPalette.hifiBlue + color: sysPalette.window + + ListView { + spacing: 6 + property int outerMargin: 8 + property real minWidth: 0 + anchors.fill: parent + anchors.margins: outerMargin + id: listView + height: root.height + currentIndex: -1 + + onCountChanged: { + recalculateSize() + } + + function recalculateSize() { + var newHeight = 0 + var newWidth = minWidth; + for (var i = 0; i < children.length; ++i) { + var item = children[i]; + newHeight += item.height + } + parent.height = newHeight + outerMargin * 2; + parent.width = newWidth + outerMargin * 2 + } + + highlight: Rectangle { + width: listView.minWidth; height: 32 + color: sysPalette.highlight + y: (listView.currentItem) ? listView.currentItem.y : 0; + x: 32 + Behavior on y { + NumberAnimation { + duration: 100 + easing.type: Easing.InOutQuint + } + } + } + + + property int columnIndex: root.models.length - 1 + model: root.models[columnIndex] + delegate: Loader { + id: loader + sourceComponent: root.itemBuilder + Binding { + target: loader.item + property: "root" + value: root + when: loader.status == Loader.Ready + } + Binding { + target: loader.item + property: "source" + value: modelData + when: loader.status == Loader.Ready + } + Binding { + target: loader.item + property: "listViewIndex" + value: index + when: loader.status == Loader.Ready + } + Binding { + target: loader.item + property: "listView" + value: listView + when: loader.status == Loader.Ready + } + } + + } + + } + } + + + function lastColumn() { + return columns[root.columns.length - 1]; + } + + function pushColumn(items) { + models.push(items) + if (columns.length) { + var oldColumn = lastColumn(); + oldColumn.enabled = false; + oldColumn.opacity = 0.5; + } + var newColumn = menuBuilder.createObject(root); + columns.push(newColumn); + newColumn.forceActiveFocus(); + } + + function popColumn() { + if (columns.length > 0) { + var curColumn = columns.pop(); + console.log(curColumn); + curColumn.visible = false; + curColumn.destroy(); + models.pop(); + } + + if (columns.length == 0) { + enabled = false; + return; + } + + curColumn = lastColumn(); + curColumn.enabled = true; + curColumn.opacity = 1.0; + curColumn.forceActiveFocus(); + } + + function selectItem(source) { + switch (source.type) { + case 2: + pushColumn(source.items) + break; + case 1: + source.trigger() + enabled = false + break; + case 0: + break; + } + } + + function reset() { + while (columns.length > 0) { + popColumn(); + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Escape: + root.popColumn() + event.accepted = true; + } + } + + MouseArea { + anchors.fill: parent + id: mouseArea + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if (mouse.button == Qt.RightButton) { + root.popColumn(); + } else { + root.enabled = false; + } + } + } +} diff --git a/interface/resources/qml/Icon.qml b/interface/resources/qml/Icon.qml deleted file mode 100644 index 0d60afb2b7..0000000000 --- a/interface/resources/qml/Icon.qml +++ /dev/null @@ -1,8 +0,0 @@ -import QtQuick 1.0 - -Image { -id: icon -width: 64 -height: 64 -source: "file.svg" -} \ No newline at end of file diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index be69b65ef7..b3b926bbe3 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -1,12 +1,12 @@ import Hifi 1.0 import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Window 2.2 import QtQuick.Controls.Styles 1.3 -import "hifiConstants.js" as HifiConstants +import "controls" +import "styles" -CustomDialog { +Dialog { title: "Login" + HifiPalette { id: hifiPalette } SystemPalette { id: myPalette; colorGroup: SystemPalette.Active } objectName: "LoginDialog" height: 512 @@ -50,11 +50,11 @@ CustomDialog { source: "../images/hifi-logo.svg" } - CustomBorder { + Border { width: 304 height: 64 anchors.horizontalCenter: parent.horizontalCenter - CustomTextInput { + TextInput { id: username anchors.fill: parent helperText: "Username or Email" @@ -67,11 +67,11 @@ CustomDialog { } } - CustomBorder { + Border { width: 304 height: 64 anchors.horizontalCenter: parent.horizontalCenter - CustomTextInput { + TextInput { id: password anchors.fill: parent echoMode: TextInput.Password @@ -94,7 +94,7 @@ CustomDialog { } } - CustomText { + Text { anchors.horizontalCenter: parent.horizontalCenter textFormat: Text.StyledText width: parent.width @@ -117,7 +117,7 @@ CustomDialog { width: 192 height: 64 anchors.horizontalCenter: parent.horizontalCenter - color: HifiConstants.color + color: hifiPalette.hifiBlue border.width: 0 radius: 10 @@ -142,7 +142,7 @@ CustomDialog { width: 32 source: "../images/login.svg" } - CustomText { + Text { text: "Login" color: "white" width: 64 @@ -152,7 +152,7 @@ CustomDialog { } - CustomText { + Text { width: parent.width height: 24 horizontalAlignment: Text.AlignHCenter @@ -160,7 +160,7 @@ CustomDialog { text:"Create Account" font.pointSize: 12 font.bold: true - color: HifiConstants.color + color: hifiPalette.hifiBlue MouseArea { anchors.fill: parent @@ -170,14 +170,14 @@ CustomDialog { } } - CustomText { + Text { width: parent.width height: 24 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pointSize: 12 text: "Recover Password" - color: HifiConstants.color + color: hifiPalette.hifiBlue MouseArea { anchors.fill: parent diff --git a/interface/resources/qml/MarketplaceDialog.qml b/interface/resources/qml/MarketplaceDialog.qml new file mode 100644 index 0000000000..58bb3e6183 --- /dev/null +++ b/interface/resources/qml/MarketplaceDialog.qml @@ -0,0 +1,50 @@ +import Hifi 1.0 +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.3 +import QtWebKit 3.0 +import "controls" + +Dialog { + title: "Test Dlg" + id: testDialog + objectName: "Browser" + width: 720 + height: 720 + resizable: true + + MarketplaceDialog { + id: marketplaceDialog + } + + Item { + id: clientArea + // The client area + anchors.fill: parent + anchors.margins: parent.margins + anchors.topMargin: parent.topMargin + + + ScrollView { + anchors.fill: parent + WebView { + objectName: "WebView" + id: webview + url: "https://metaverse.highfidelity.com/marketplace" + anchors.fill: parent + onNavigationRequested: { + console.log(request.url) + if (!marketplaceDialog.navigationRequested(request.url)) { + console.log("Application absorbed the request") + request.action = WebView.IgnoreRequest; + return; + } + console.log("Application passed on the request") + request.action = WebView.AcceptRequest; + return; + } + } + } + + } +} diff --git a/interface/resources/qml/MessageDialog.qml b/interface/resources/qml/MessageDialog.qml new file mode 100644 index 0000000000..315af8c6c9 --- /dev/null +++ b/interface/resources/qml/MessageDialog.qml @@ -0,0 +1,356 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.2 +import QtQuick.Controls 1.2 +import QtQuick.Dialogs 1.2 +import "controls" + +Dialog { + id: root + property real spacing: 8 + property real outerSpacing: 16 + + + destroyOnCloseButton: true + destroyOnInvisible: true + implicitHeight: content.implicitHeight + outerSpacing * 2 + 48 + implicitWidth: Math.min(200, Math.max(mainText.implicitWidth, content.buttonsRowImplicitWidth) + outerSpacing * 2); + + onImplicitHeightChanged: root.height = implicitHeight + onImplicitWidthChanged: root.width = implicitWidth + + SystemPalette { id: palette } + + function calculateImplicitWidth() { + if (buttons.visibleChildren.length < 2) + return; + var calcWidth = 0; + for (var i = 0; i < buttons.visibleChildren.length; ++i) { + calcWidth += Math.max(100, buttons.visibleChildren[i].implicitWidth) + root.spacing + } + content.buttonsRowImplicitWidth = outerSpacing + calcWidth + 48 + } + + Component.onCompleted: { + enabled = true + } + + onEnabledChanged: { + if (enabled) { + root.forceActiveFocus(); + } + } + + Hifi.MessageDialog { + id: content + clip: true + anchors.fill: parent + anchors.topMargin: parent.topMargin + root.outerSpacing + anchors.leftMargin: parent.margins + root.outerSpacing + anchors.rightMargin: parent.margins + root.outerSpacing + anchors.bottomMargin: parent.margins + root.outerSpacing + implicitHeight: contentColumn.implicitHeight + outerSpacing * 2 + implicitWidth: Math.max(mainText.implicitWidth, buttonsRowImplicitWidth); + property real buttonsRowImplicitWidth: Screen.pixelDensity * 50 + + onImplicitWidthChanged: root.width = implicitWidth + + Component.onCompleted: { + root.title = title + } + + onTitleChanged: { + root.title = title + } + + Column { + id: contentColumn + spacing: root.outerSpacing + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + Item { + width: parent.width + height: Math.max(icon.height, mainText.height + informativeText.height + root.spacing) + + FontAwesome { + id: icon + width: content.icon ? 48 : 0 + height: content.icon ? 48 : 0 + visible: content.icon ? true : false + font.pixelSize: 48 + verticalAlignment: Text.AlignTop + horizontalAlignment: Text.AlignLeft + color: iconColor() + text: iconSymbol() + + function iconSymbol() { + switch (content.icon) { + case Hifi.MessageDialog.Information: + return "\uF05A" + case Hifi.MessageDialog.Question: + return "\uF059" + case Hifi.MessageDialog.Warning: + return "\uF071" + case Hifi.MessageDialog.Critical: + return "\uF057" + default: + break; + } + return content.icon; + } + function iconColor() { + switch (content.icon) { + case Hifi.MessageDialog.Information: + case Hifi.MessageDialog.Question: + return "blue" + case Hifi.MessageDialog.Warning: + case Hifi.MessageDialog.Critical: + return "red" + default: + break + } + return "black" + } + } + + Text { + id: mainText + anchors { + left: icon.right + leftMargin: root.spacing + right: parent.right + } + text: content.text + font.pointSize: 14 + font.weight: Font.Bold + wrapMode: Text.WordWrap + } + + Text { + id: informativeText + anchors { + left: icon.right + right: parent.right + top: mainText.bottom + leftMargin: root.spacing + topMargin: root.spacing + } + text: content.informativeText + font.pointSize: 14 + wrapMode: Text.WordWrap + } + } + + + Flow { + id: buttons + spacing: root.spacing + layoutDirection: Qt.RightToLeft + width: parent.width + Button { + id: okButton + text: qsTr("OK") + onClicked: content.click(StandardButton.Ok) + visible: content.standardButtons & StandardButton.Ok + } + Button { + id: openButton + text: qsTr("Open") + onClicked: content.click(StandardButton.Open) + visible: content.standardButtons & StandardButton.Open + } + Button { + id: saveButton + text: qsTr("Save") + onClicked: content.click(StandardButton.Save) + visible: content.standardButtons & StandardButton.Save + } + Button { + id: saveAllButton + text: qsTr("Save All") + onClicked: content.click(StandardButton.SaveAll) + visible: content.standardButtons & StandardButton.SaveAll + } + Button { + id: retryButton + text: qsTr("Retry") + onClicked: content.click(StandardButton.Retry) + visible: content.standardButtons & StandardButton.Retry + } + Button { + id: ignoreButton + text: qsTr("Ignore") + onClicked: content.click(StandardButton.Ignore) + visible: content.standardButtons & StandardButton.Ignore + } + Button { + id: applyButton + text: qsTr("Apply") + onClicked: content.click(StandardButton.Apply) + visible: content.standardButtons & StandardButton.Apply + } + Button { + id: yesButton + text: qsTr("Yes") + onClicked: content.click(StandardButton.Yes) + visible: content.standardButtons & StandardButton.Yes + } + Button { + id: yesAllButton + text: qsTr("Yes to All") + onClicked: content.click(StandardButton.YesToAll) + visible: content.standardButtons & StandardButton.YesToAll + } + Button { + id: noButton + text: qsTr("No") + onClicked: content.click(StandardButton.No) + visible: content.standardButtons & StandardButton.No + } + Button { + id: noAllButton + text: qsTr("No to All") + onClicked: content.click(StandardButton.NoToAll) + visible: content.standardButtons & StandardButton.NoToAll + } + Button { + id: discardButton + text: qsTr("Discard") + onClicked: content.click(StandardButton.Discard) + visible: content.standardButtons & StandardButton.Discard + } + Button { + id: resetButton + text: qsTr("Reset") + onClicked: content.click(StandardButton.Reset) + visible: content.standardButtons & StandardButton.Reset + } + Button { + id: restoreDefaultsButton + text: qsTr("Restore Defaults") + onClicked: content.click(StandardButton.RestoreDefaults) + visible: content.standardButtons & StandardButton.RestoreDefaults + } + Button { + id: cancelButton + text: qsTr("Cancel") + onClicked: content.click(StandardButton.Cancel) + visible: content.standardButtons & StandardButton.Cancel + } + Button { + id: abortButton + text: qsTr("Abort") + onClicked: content.click(StandardButton.Abort) + visible: content.standardButtons & StandardButton.Abort + } + Button { + id: closeButton + text: qsTr("Close") + onClicked: content.click(StandardButton.Close) + visible: content.standardButtons & StandardButton.Close + } + Button { + id: moreButton + text: qsTr("Show Details...") + onClicked: content.state = (content.state === "" ? "expanded" : "") + visible: content.detailedText.length > 0 + } + Button { + id: helpButton + text: qsTr("Help") + onClicked: content.click(StandardButton.Help) + visible: content.standardButtons & StandardButton.Help + } + onVisibleChildrenChanged: root.calculateImplicitWidth() + } + } + + Item { + id: details + width: parent.width + implicitHeight: detailedText.implicitHeight + root.spacing + height: 0 + clip: true + + anchors { + left: parent.left + right: parent.right + top: contentColumn.bottom + topMargin: root.spacing + leftMargin: root.outerSpacing + rightMargin: root.outerSpacing + } + + Flickable { + id: flickable + contentHeight: detailedText.height + anchors.fill: parent + anchors.topMargin: root.spacing + anchors.bottomMargin: root.outerSpacing + TextEdit { + id: detailedText + text: content.detailedText + width: details.width + wrapMode: Text.WordWrap + readOnly: true + selectByMouse: true + } + } + } + + states: [ + State { + name: "expanded" + PropertyChanges { + target: details + height: root.height - contentColumn.height - root.spacing - root.outerSpacing + } + PropertyChanges { + target: content + implicitHeight: contentColumn.implicitHeight + root.spacing * 2 + + detailedText.implicitHeight + root.outerSpacing * 2 + } + PropertyChanges { + target: moreButton + text: qsTr("Hide Details") + } + } + ] + } + + Keys.onPressed: { + 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: + event.accepted = true + content.reject() + break + + case Qt.Key_Enter: + case Qt.Key_Return: + console.log("Accepting"); + event.accepted = true + content.accept() + break + } + } +} diff --git a/interface/resources/qml/Palettes.qml b/interface/resources/qml/Palettes.qml index c4b0953df7..2bdf6eba8b 100644 --- a/interface/resources/qml/Palettes.qml +++ b/interface/resources/qml/Palettes.qml @@ -1,8 +1,5 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtQuick.Window 2.2 -import QtQuick.Dialogs 1.2 -import QtQuick.Controls.Styles 1.3 Rectangle { color: "teal" @@ -150,87 +147,4 @@ Rectangle { Rectangle { height: parent.height; width: 16; color: spd.highlightedText} } } - - -/* - CustomDialog { - title: "Test Dlg" - anchors.fill: parent - - Rectangle { - property int d: 100 - id: square - objectName: "testRect" - width: d - height: d - anchors.centerIn: parent - color: "red" - NumberAnimation on rotation { from: 0; to: 360; duration: 2000; loops: Animation.Infinite; } - } - - - CustomTextEdit { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - clip: true - text: "test edit" - anchors.top: parent.top - anchors.topMargin: parent.titleSize + 12 - } - - CustomButton { - x: 128 - y: 192 - anchors.bottom: parent.bottom - anchors.bottomMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - onClicked: { - console.log("Click"); - if (square.visible) { - square.visible = false - } else { - square.visible = true - } - } - } - - CustomButton { - id: customButton2 - y: 192 - text: "Close" - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.bottom: parent.bottom - anchors.bottomMargin: 12 - onClicked: { - onClicked: testDialog.x == 0 ? testDialog.x = 200 : testDialog.x = 0 - } - } - - Keys.onPressed: { - console.log("Key " + event.key); - switch (event.key) { - case Qt.Key_Q: - if (Qt.ControlModifier == event.modifiers) { - event.accepted = true; - break; - } - } - } - } -*/ - } - - -/* - -// This is the behavior, and it applies a NumberAnimation to any attempt to set the x property - -MouseArea { - anchors.fill: parent -} -*/ diff --git a/interface/resources/qml/Root.qml b/interface/resources/qml/Root.qml index 9422ef123d..9b2e5af6b5 100644 --- a/interface/resources/qml/Root.qml +++ b/interface/resources/qml/Root.qml @@ -1,9 +1,14 @@ import Hifi 1.0 import QtQuick 2.3 +// This is our primary 'window' object to which all dialogs and controls will +// be childed. Root { id: root - width: 1280 - height: 720 + anchors.fill: parent + + onParentChanged: { + forceActiveFocus(); + } } diff --git a/interface/resources/qml/RootMenu.qml b/interface/resources/qml/RootMenu.qml new file mode 100644 index 0000000000..b8c81a6589 --- /dev/null +++ b/interface/resources/qml/RootMenu.qml @@ -0,0 +1,9 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.3 + +Item { + Menu { + id: root + objectName: "rootMenu" + } +} diff --git a/interface/resources/qml/TestDialog.qml b/interface/resources/qml/TestDialog.qml index 1fe8676bc6..15bd790c22 100644 --- a/interface/resources/qml/TestDialog.qml +++ b/interface/resources/qml/TestDialog.qml @@ -1,10 +1,9 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtQuick.Window 2.2 -import QtQuick.Dialogs 1.2 import QtQuick.Controls.Styles 1.3 +import "controls" -CustomDialog { +Dialog { title: "Test Dialog" id: testDialog objectName: "TestDialog" @@ -37,7 +36,7 @@ CustomDialog { } - CustomTextEdit { + TextEdit { id: edit anchors.left: parent.left anchors.leftMargin: 12 @@ -49,7 +48,7 @@ CustomDialog { anchors.topMargin: 12 } - CustomButton { + Button { x: 128 y: 192 text: "Test" @@ -68,7 +67,7 @@ CustomDialog { } } - CustomButton { + Button { id: customButton2 y: 192 text: "Move" @@ -92,15 +91,4 @@ CustomDialog { } } } - } - - -/* - -// This is the behavior, and it applies a NumberAnimation to any attempt to set the x property - -MouseArea { - anchors.fill: parent -} -*/ diff --git a/interface/resources/qml/TestRoot.qml b/interface/resources/qml/TestRoot.qml index 158c0b7a54..cb53d34ce2 100644 --- a/interface/resources/qml/TestRoot.qml +++ b/interface/resources/qml/TestRoot.qml @@ -1,12 +1,17 @@ import Hifi 1.0 import QtQuick 2.3 +import QtQuick.Controls 1.3 +// Import local folder last so that our own control customizations override +// the built in ones +import "controls" Root { - id: root - width: 1280 - height: 720 - - CustomButton { + id: root + anchors.fill: parent + onParentChanged: { + forceActiveFocus(); + } + Button { id: messageBox anchors.right: createDialog.left anchors.rightMargin: 24 @@ -19,8 +24,7 @@ Root { console.log("Bar") } } - - CustomButton { + Button { id: createDialog anchors.right: parent.right anchors.rightMargin: 24 @@ -28,8 +32,12 @@ Root { anchors.bottomMargin: 24 text: "Create" onClicked: { - root.loadChild("TestDialog.qml"); + root.loadChild("MenuTest.qml"); } } + + Keys.onPressed: { + console.log("Key press root") + } } diff --git a/interface/resources/qml/componentCreation.js b/interface/resources/qml/componentCreation.js deleted file mode 100644 index 15a828d6f8..0000000000 --- a/interface/resources/qml/componentCreation.js +++ /dev/null @@ -1,29 +0,0 @@ -var component; -var instance; -var parent; - -function createObject(parentObject, url) { - parent = parentObject; - component = Qt.createComponent(url); - if (component.status == Component.Ready) - finishCreation(); - else - component.statusChanged.connect(finishCreation); -} - -function finishCreation() { - if (component.status == Component.Ready) { - instance = component.createObject(parent, {"x": 100, "y": 100}); - if (instance == null) { - // Error Handling - console.log("Error creating object"); - } else { - instance.enabled = true - } - } else if (component.status == Component.Error) { - // Error Handling - console.log("Error loading component:", component.errorString()); - } else { - console.log("Unknown component status: " + component.status); - } -} \ No newline at end of file diff --git a/interface/resources/qml/controls/Button.qml b/interface/resources/qml/controls/Button.qml new file mode 100644 index 0000000000..215e0542f7 --- /dev/null +++ b/interface/resources/qml/controls/Button.qml @@ -0,0 +1,10 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.3 as Original +import QtQuick.Controls.Styles 1.3 +import "." +import "../styles" + +Original.Button { + style: ButtonStyle { + } +} diff --git a/interface/resources/qml/CustomDialog.qml b/interface/resources/qml/controls/Dialog.qml similarity index 55% rename from interface/resources/qml/CustomDialog.qml rename to interface/resources/qml/controls/Dialog.qml index 1e0351af4f..d722d5264a 100644 --- a/interface/resources/qml/CustomDialog.qml +++ b/interface/resources/qml/controls/Dialog.qml @@ -1,40 +1,79 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtQuick.Window 2.2 -import QtQuick.Dialogs 1.2 -import QtQuick.Controls.Styles 1.3 -import "hifiConstants.js" as HifiConstants +import "." +import "../styles" +/* + * FIXME Need to create a client property here so that objects can be + * placed in it without having to think about positioning within the outer + * window. + * + * Examine the QML ApplicationWindow.qml source for how it does this + * + */ Item { - SystemPalette { id: myPalette; colorGroup: SystemPalette.Active } - id: dialog - width: 256 - height: 256 - scale: 0.0 - enabled: false + id: root + + HifiPalette { id: hifiPalette } + SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active } + x: parent ? parent.width / 2 - width / 2 : 0 + y: parent ? parent.height / 2 - height / 2 : 0 + property int animationDuration: 400 property bool destroyOnInvisible: false property bool destroyOnCloseButton: true property bool resizable: false property int minX: 256 property int minY: 256 + property int topMargin: root.height - clientBorder.height + 8 + property int margins: 8 + property string title + property int titleSize: titleBorder.height + 12 + property string frameColor: hifiPalette.hifiBlue + property string backgroundColor: sysPalette.window + property string headerBackgroundColor: sysPalette.dark clip: true + + /* + * Support for animating the dialog in and out. + */ + enabled: false + scale: 0.0 + // The offscreen UI will enable an object, rather than manipulating it's + // visibility, so that we can do animations in both directions. Because + // visibility and enabled are boolean flags, they cannot be animated. So when + // enabled is change, we modify a property that can be animated, like scale or + // opacity. onEnabledChanged: { scale = enabled ? 1.0 : 0.0 } - + + // The actual animator + Behavior on scale { + NumberAnimation { + duration: root.animationDuration + easing.type: Easing.InOutBounce + } + } + + // We remove any load the dialog might have on the QML by toggling it's + // visibility based on the state of the animated property onScaleChanged: { visible = (scale != 0.0); } + // Some dialogs should be destroyed when they become invisible, so handle that onVisibleChanged: { if (!visible && destroyOnInvisible) { - console.log("Destroying closed component"); destroy(); } } + // our close function performs the same way as the OffscreenUI class: + // don't do anything but manipulate the enabled flag and let the other + // mechanisms decide if the window should be destoryed after the close + // animation completes function close() { if (destroyOnCloseButton) { destroyOnInvisible = true @@ -42,102 +81,14 @@ Item { enabled = false; } + /* + * Resize support + */ function deltaSize(dx, dy) { width = Math.max(width + dx, minX) height = Math.max(height + dy, minY) } - Behavior on scale { - NumberAnimation { - //This specifies how long the animation takes - duration: dialog.animationDuration - //This selects an easing curve to interpolate with, the default is Easing.Linear - easing.type: Easing.InOutBounce - } - } - - property int topMargin: dialog.height - clientBorder.height + 8 - property int margins: 8 - property string title - property int titleSize: titleBorder.height + 12 - property string frameColor: HifiConstants.color - property string backgroundColor: myPalette.window - property string headerBackgroundColor: myPalette.dark - - CustomBorder { - id: windowBorder - anchors.fill: parent - border.color: dialog.frameColor - color: dialog.backgroundColor - - CustomBorder { - id: titleBorder - height: 48 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - border.color: dialog.frameColor - color: dialog.headerBackgroundColor - - CustomText { - id: titleText - color: "white" - text: dialog.title - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - } - - MouseArea { - id: titleDrag - anchors.right: closeButton.left - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.top: parent.top - anchors.rightMargin: 4 - drag { - target: dialog - minimumX: 0 - minimumY: 0 - maximumX: dialog.parent ? dialog.parent.width - dialog.width : 0 - maximumY: dialog.parent ? dialog.parent.height - dialog.height : 0 - } - } - Image { - id: closeButton - x: 360 - height: 16 - anchors.verticalCenter: parent.verticalCenter - width: 16 - anchors.right: parent.right - anchors.rightMargin: 12 - source: "../styles/close.svg" - MouseArea { - anchors.fill: parent - onClicked: { - dialog.close(); - } - } - } - } // header border - - CustomBorder { - id: clientBorder - border.color: dialog.frameColor - color: "#00000000" - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - anchors.top: titleBorder.bottom - anchors.topMargin: -titleBorder.border.width - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - clip: true - } // client border - } // window border - MouseArea { id: sizeDrag property int startX @@ -152,11 +103,91 @@ Item { startY = mouseY } onPositionChanged: { - if (pressed && dialog.resizable) { - dialog.deltaSize((mouseX - startX), (mouseY - startY)) + if (pressed && root.resizable) { + root.deltaSize((mouseX - startX), (mouseY - startY)) startX = mouseX startY = mouseY } } } + + /* + * Window decorations, with a title bar and frames + */ + Border { + id: windowBorder + anchors.fill: parent + border.color: root.frameColor + color: root.backgroundColor + + Border { + id: titleBorder + height: 48 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + border.color: root.frameColor + color: root.headerBackgroundColor + + Text { + id: titleText + // FIXME move all constant colors to our own palette class HifiPalette + color: "white" + text: root.title + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + } + + MouseArea { + id: titleDrag + anchors.right: closeButton.left + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + anchors.rightMargin: 4 + drag { + target: root + minimumX: 0 + minimumY: 0 + maximumX: root.parent ? root.parent.width - root.width : 0 + maximumY: root.parent ? root.parent.height - root.height : 0 + } + } + Image { + id: closeButton + x: 360 + height: 16 + anchors.verticalCenter: parent.verticalCenter + width: 16 + anchors.right: parent.right + anchors.rightMargin: 12 + source: "../../styles/close.svg" + MouseArea { + anchors.fill: parent + onClicked: { + root.close(); + } + } + } + } // header border + + Border { + id: clientBorder + border.color: root.frameColor + // FIXME move all constant colors to our own palette class HifiPalette + color: "#00000000" + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: titleBorder.bottom + anchors.topMargin: -titleBorder.border.width + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + clip: true + } // client border + } // window border + } diff --git a/interface/resources/qml/controls/FontAwesome.qml b/interface/resources/qml/controls/FontAwesome.qml new file mode 100644 index 0000000000..e975c0342b --- /dev/null +++ b/interface/resources/qml/controls/FontAwesome.qml @@ -0,0 +1,16 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.3 +import QtQuick.Controls.Styles 1.3 + +Text { + id: root + FontLoader { id: iconFont; source: "../../fonts/fontawesome-webfont.ttf"; } + property int size: 32 + width: size + height: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + font.family: iconFont.name + font.pointSize: 18 +} + diff --git a/interface/resources/qml/IconControl.qml b/interface/resources/qml/controls/IconButton.qml similarity index 100% rename from interface/resources/qml/IconControl.qml rename to interface/resources/qml/controls/IconButton.qml diff --git a/interface/resources/qml/controls/MenuButton.qml b/interface/resources/qml/controls/MenuButton.qml new file mode 100644 index 0000000000..740995a199 --- /dev/null +++ b/interface/resources/qml/controls/MenuButton.qml @@ -0,0 +1,5 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.3 as Original +import "../styles" +import "../controls" + diff --git a/interface/resources/qml/controls/README.md b/interface/resources/qml/controls/README.md new file mode 100644 index 0000000000..7f05f32a63 --- /dev/null +++ b/interface/resources/qml/controls/README.md @@ -0,0 +1,2 @@ +These are our own custom controls with the same names as existing controls, but +customized for readability / usability in VR. diff --git a/interface/resources/qml/CustomTextEdit.qml b/interface/resources/qml/controls/Text.qml similarity index 54% rename from interface/resources/qml/CustomTextEdit.qml rename to interface/resources/qml/controls/Text.qml index 0602bbc150..a9c19e70b4 100644 --- a/interface/resources/qml/CustomTextEdit.qml +++ b/interface/resources/qml/controls/Text.qml @@ -1,6 +1,6 @@ -import QtQuick 2.3 +import QtQuick 2.3 as Original -TextEdit { +Original.Text { font.family: "Helvetica" font.pointSize: 18 } diff --git a/interface/resources/qml/CustomText.qml b/interface/resources/qml/controls/TextArea.qml similarity index 52% rename from interface/resources/qml/CustomText.qml rename to interface/resources/qml/controls/TextArea.qml index 83229b783e..dfa177bcb6 100644 --- a/interface/resources/qml/CustomText.qml +++ b/interface/resources/qml/controls/TextArea.qml @@ -1,6 +1,6 @@ -import QtQuick 2.3 +import QtQuick 2.3 as Original -Text { +Original.TextArea { font.family: "Helvetica" font.pointSize: 18 } diff --git a/interface/resources/qml/controls/TextEdit.qml b/interface/resources/qml/controls/TextEdit.qml new file mode 100644 index 0000000000..28551bb171 --- /dev/null +++ b/interface/resources/qml/controls/TextEdit.qml @@ -0,0 +1,7 @@ +import QtQuick 2.3 as Original + +Original.TextEdit { + font.family: "Helvetica" + font.pointSize: 18 +} + diff --git a/interface/resources/qml/CustomTextInput.qml b/interface/resources/qml/controls/TextInput.qml similarity index 90% rename from interface/resources/qml/CustomTextInput.qml rename to interface/resources/qml/controls/TextInput.qml index a706187376..8ce3d85d81 100644 --- a/interface/resources/qml/CustomTextInput.qml +++ b/interface/resources/qml/controls/TextInput.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 1.2 TextInput { SystemPalette { id: myPalette; colorGroup: SystemPalette.Active } - property string helperText: "" + property string helperText font.family: "Helvetica" font.pointSize: 18 width: 256 @@ -24,7 +24,7 @@ TextInput { id: helperText anchors.fill: parent font.pointSize: parent.font.pointSize - font.family: "Helvetica" + font.family: parent.font.family verticalAlignment: TextInput.AlignVCenter text: parent.helperText color: myPalette.dark diff --git a/interface/resources/qml/hifiConstants.js b/interface/resources/qml/hifiConstants.js deleted file mode 100644 index 860226c963..0000000000 --- a/interface/resources/qml/hifiConstants.js +++ /dev/null @@ -1,4 +0,0 @@ -var color = "#0e7077" -var Colors = { - hifiBlue: "#0e7077" -} diff --git a/interface/resources/qml/CustomBorder.qml b/interface/resources/qml/styles/Border.qml similarity index 99% rename from interface/resources/qml/CustomBorder.qml rename to interface/resources/qml/styles/Border.qml index 1bb30d1ebc..7d38e7d277 100644 --- a/interface/resources/qml/CustomBorder.qml +++ b/interface/resources/qml/styles/Border.qml @@ -1,6 +1,5 @@ import QtQuick 2.3 - Rectangle { SystemPalette { id: myPalette; colorGroup: SystemPalette.Active } property int margin: 5 diff --git a/interface/resources/qml/styles/ButtonStyle.qml b/interface/resources/qml/styles/ButtonStyle.qml new file mode 100644 index 0000000000..8d866390a0 --- /dev/null +++ b/interface/resources/qml/styles/ButtonStyle.qml @@ -0,0 +1,24 @@ +import QtQuick 2.4 as Original +import QtQuick.Controls.Styles 1.3 as OriginalStyles +import "." +import "../controls" + +OriginalStyles.ButtonStyle { + Original.SystemPalette { id: myPalette; colorGroup: Original.SystemPalette.Active } + padding { + top: 8 + left: 12 + right: 12 + bottom: 8 + } + background: Border { + anchors.fill: parent + } + label: Text { + renderType: Original.Text.NativeRendering + verticalAlignment: Original.Text.AlignVCenter + horizontalAlignment: Original.Text.AlignHCenter + text: control.text + color: control.enabled ? myPalette.text : myPalette.dark + } +} diff --git a/interface/resources/qml/styles/HifiPalette.qml b/interface/resources/qml/styles/HifiPalette.qml new file mode 100644 index 0000000000..46ef0ef14e --- /dev/null +++ b/interface/resources/qml/styles/HifiPalette.qml @@ -0,0 +1,5 @@ +import QtQuick 2.4 + +QtObject { + property string hifiBlue: "#0e7077" +} \ No newline at end of file diff --git a/interface/resources/qml/styles/IconButtonStyle.qml b/interface/resources/qml/styles/IconButtonStyle.qml new file mode 100644 index 0000000000..b341e5d6dd --- /dev/null +++ b/interface/resources/qml/styles/IconButtonStyle.qml @@ -0,0 +1,15 @@ +ButtonStyle { + background: Item { anchors.fill: parent } + label: Text { + id: icon + width: height + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + font.family: iconFont.name + font.pointSize: 18 + property alias unicode: icon.text + FontLoader { id: iconFont; source: "../../fonts/fontawesome-webfont.ttf"; } + text: control.text + color: control.enabled ? "white" : "dimgray" + } +} diff --git a/interface/resources/qml/styles/MenuButtonStyle.qml b/interface/resources/qml/styles/MenuButtonStyle.qml new file mode 100644 index 0000000000..fd21e88d86 --- /dev/null +++ b/interface/resources/qml/styles/MenuButtonStyle.qml @@ -0,0 +1,22 @@ +import QtQuick 2.4 +import QtQuick.Controls.Styles 1.3 +import "../controls" +import "." + +ButtonStyle { + HifiPalette { id: hifiPalette } + padding { + top: 2 + left: 4 + right: 4 + bottom: 2 + } + background: Item {} + label: Text { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: control.text + color: control.enabled ? "yellow" : "brown" + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 95700f3257..957c3892db 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -82,6 +82,7 @@ #include #include #include +#include #include @@ -144,6 +145,23 @@ extern "C" { } #endif +enum CustomEventTypes { + Lambda = QEvent::User + 1 +}; + +class LambdaEvent : public QEvent { + std::function _fun; +public: + LambdaEvent(const std::function & fun) : + QEvent(static_cast(Lambda)), _fun(fun) { + } + LambdaEvent(std::function && fun) : + QEvent(static_cast(Lambda)), _fun(fun) { + } + void call() { _fun(); } +}; + + using namespace std; // Starfield information @@ -707,6 +725,13 @@ void Application::initializeGL() { initDisplay(); qCDebug(interfaceapp, "Initialized Display."); + // The UI can't be created until the primary OpenGL + // context is created, because it needs to share + // texture resources + initializeUi(); + qCDebug(interfaceapp, "Initialized Offscreen UI."); + _glWidget->makeCurrent(); + init(); qCDebug(interfaceapp, "init() complete."); @@ -735,17 +760,13 @@ void Application::initializeGL() { // update before the first render update(1.0f / _fps); - // The UI can't be created until the primary OpenGL - // context is created, because it needs to share - // texture resources - initializeUi(); - InfoView::showFirstTime(INFO_HELP_PATH); } void Application::initializeUi() { AddressBarDialog::registerType(); LoginDialog::registerType(); + MessageDialog::registerType(); auto offscreenUi = DependencyManager::get(); offscreenUi->create(_glWidget->context()->contextHandle()); @@ -753,6 +774,7 @@ void Application::initializeUi() { offscreenUi->setProxyWindow(_window->windowHandle()); offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); offscreenUi->load("Root.qml"); + offscreenUi->load("RootMenu.qml"); offscreenUi->setMouseTranslator([this](const QPointF& p){ if (OculusManager::isConnected()) { glm::vec2 pos = _applicationOverlay.screenToOverlay(toGlm(p)); @@ -964,6 +986,10 @@ bool Application::importSVOFromURL(const QString& urlString) { bool Application::event(QEvent* event) { switch (event->type()) { + case Lambda: + ((LambdaEvent*)event)->call(); + return true; + case QEvent::MouseMove: mouseMoveEvent((QMouseEvent*)event); return true; @@ -1060,6 +1086,11 @@ void Application::keyPressEvent(QKeyEvent* event) { bool isOption = event->modifiers().testFlag(Qt::AltModifier); switch (event->key()) { break; + case Qt::Key_Enter: + case Qt::Key_Return: + Menu::getInstance()->triggerOption(MenuOption::AddressBar); + break; + case Qt::Key_L: if (isShifted && isMeta) { Menu::getInstance()->triggerOption(MenuOption::Log); diff --git a/interface/src/Application.h b/interface/src/Application.h index 89de18dde5..688cf23876 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -147,6 +148,8 @@ public: Application(int& argc, char** argv, QElapsedTimer &startup_time); ~Application(); + void postLambdaEvent(std::function f); + void loadScripts(); QString getPreviousScriptLocation(); void setPreviousScriptLocation(const QString& previousScriptLocation); diff --git a/interface/src/ui/AddressBarDialog.cpp b/interface/src/ui/AddressBarDialog.cpp index 6b5c92c194..837702d253 100644 --- a/interface/src/ui/AddressBarDialog.cpp +++ b/interface/src/ui/AddressBarDialog.cpp @@ -15,7 +15,7 @@ #include "DependencyManager.h" #include "AddressManager.h" -QML_DIALOG_DEF(AddressBarDialog) +HIFI_QML_DEF(AddressBarDialog) AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(parent) { auto addressManager = DependencyManager::get(); diff --git a/interface/src/ui/AddressBarDialog.h b/interface/src/ui/AddressBarDialog.h index 28056edd30..395cf3c725 100644 --- a/interface/src/ui/AddressBarDialog.h +++ b/interface/src/ui/AddressBarDialog.h @@ -17,7 +17,7 @@ class AddressBarDialog : public OffscreenQmlDialog { Q_OBJECT - QML_DIALOG_DECL + HIFI_QML_DECL public: AddressBarDialog(QQuickItem* parent = nullptr); diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index bf93a3e7d2..8704a61261 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -322,7 +322,7 @@ void ApplicationOverlay::displayOverlayTextureOculus(Camera& whichCamera) { //Render magnifier, but dont show border for mouse magnifier glm::vec2 projection = screenToOverlay(glm::vec2(_reticlePosition[MOUSE].x(), _reticlePosition[MOUSE].y())); - with_each_texture(_overlays.getTexture(), _newUiTexture, [&] { + with_each_texture(_overlays.getTexture(), 0, [&] { renderMagnifier(projection, _magSizeMult[i], i != MOUSE); }); } diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 4df42c0f15..b452f153f0 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -15,7 +15,7 @@ #include "Menu.h" #include -QML_DIALOG_DEF(LoginDialog) +HIFI_QML_DEF(LoginDialog) LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent), _rootUrl(NetworkingConstants::METAVERSE_SERVER_URL.toString()) { connect(&AccountManager::getInstance(), &AccountManager::loginComplete, @@ -37,7 +37,9 @@ void LoginDialog::toggleAction() { } else { // change the menu item to login loginAction->setText("Login"); - connect(loginAction, &QAction::triggered, &LoginDialog::show); + connect(loginAction, &QAction::triggered, [] { + LoginDialog::show(); + }); } } diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index b6d505943e..e9ae0a1c16 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -17,7 +17,7 @@ class LoginDialog : public OffscreenQmlDialog { Q_OBJECT - QML_DIALOG_DECL + HIFI_QML_DECL Q_PROPERTY(QString statusText READ statusText WRITE setStatusText NOTIFY statusTextChanged) Q_PROPERTY(QString rootUrl READ rootUrl) diff --git a/libraries/render-utils/src/MatrixStack.h b/libraries/render-utils/src/MatrixStack.h index a71bfcbc4a..feeda44058 100644 --- a/libraries/render-utils/src/MatrixStack.h +++ b/libraries/render-utils/src/MatrixStack.h @@ -38,7 +38,7 @@ public: push(glm::mat4()); } - explicit MatrixStack(const MatrixStack & other) : std::stack() { + explicit MatrixStack(const MatrixStack& other) : std::stack() { *((std::stack*)this) = *((std::stack*)&other); } @@ -173,12 +173,12 @@ public: } template - static void withPush(MatrixStack & stack, Function f) { + static void withPush(MatrixStack& stack, Function f) { stack.withPush(f); } template - static void withPush(MatrixStack & stack1, MatrixStack & stack2, Function f) { + static void withPush(MatrixStack& stack1, MatrixStack& stack2, Function f) { stack1.withPush([&]{ stack2.withPush(f); }); diff --git a/libraries/render-utils/src/OffscreenQmlDialog.h b/libraries/render-utils/src/OffscreenQmlDialog.h deleted file mode 100644 index eca82261c0..0000000000 --- a/libraries/render-utils/src/OffscreenQmlDialog.h +++ /dev/null @@ -1,56 +0,0 @@ -// -// OffscreenQmlDialog.h -// -// Created by Bradley Austin Davis on 2015/04/14 -// 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 -// - -#pragma once -#ifndef hifi_OffscreenQmlDialog_h -#define hifi_OffscreenQmlDialog_h - -#include -#include "OffscreenUi.h" - -#define QML_DIALOG_DECL \ -private: \ - static const QString NAME; \ - static const QUrl QML; \ -public: \ - static void registerType(); \ - static void show(); \ - static void toggle(); \ -private: - -#define QML_DIALOG_DEF(x) \ - const QUrl x::QML = QUrl(#x ".qml"); \ - const QString x::NAME = #x; \ - \ - void x::registerType() { \ - qmlRegisterType("Hifi", 1, 0, NAME.toLocal8Bit().constData()); \ - } \ - \ - void x::show() { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->show(QML, NAME); \ - } \ - \ - void x::toggle() { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->toggle(QML, NAME); \ - } - -class OffscreenQmlDialog : public QQuickItem -{ - Q_OBJECT -public: - OffscreenQmlDialog(QQuickItem* parent = nullptr); - -protected: - void hide(); -}; - -#endif diff --git a/libraries/render-utils/src/OffscreenUi.h b/libraries/render-utils/src/OffscreenUi.h deleted file mode 100644 index c64d0d833c..0000000000 --- a/libraries/render-utils/src/OffscreenUi.h +++ /dev/null @@ -1,137 +0,0 @@ -// -// OffscreenUi.h -// interface/src/entities -// -// Created by Bradley Austin Davis on 2015-04-04 -// 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 -// -#pragma once -#ifndef hifi_OffscreenUi_h -#define hifi_OffscreenUi_h - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -#include "OffscreenGlCanvas.h" -#include "FboCache.h" -#include - - -class OffscreenUi : public OffscreenGlCanvas, public Dependency { - Q_OBJECT - - class QMyQuickRenderControl : public QQuickRenderControl { - protected: - QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{ - if (nullptr == _renderWindow) { - return QQuickRenderControl::renderWindow(offset); - } - if (nullptr != offset) { - offset->rx() = offset->ry() = 0; - } - return _renderWindow; - } - - private: - QWindow* _renderWindow{ nullptr }; - friend class OffscreenUi; - }; - -public: - using MouseTranslator = std::function; - OffscreenUi(); - virtual ~OffscreenUi(); - void create(QOpenGLContext* context); - void resize(const QSize& size); - void load(const QUrl& qmlSource, std::function f = [](QQmlContext*) {}); - void load(const QString& qmlSourceFile, std::function f = [](QQmlContext*) {}) { - load(QUrl(qmlSourceFile), f); - } - void show(const QUrl& url, const QString& name); - void toggle(const QUrl& url, const QString& name); - void setBaseUrl(const QUrl& baseUrl); - void addImportPath(const QString& path); - QQmlContext* qmlContext(); - - void pause(); - void resume(); - bool isPaused() const; - void setProxyWindow(QWindow* window); - bool shouldSwallowShortcut(QEvent* event); - QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); - virtual bool eventFilter(QObject* originalDestination, QEvent* event); - void setMouseTranslator(MouseTranslator mouseTranslator) { - _mouseTranslator = mouseTranslator; - } - - - // Messagebox replacement functions - using ButtonCallback = std::function; - static ButtonCallback NO_OP_CALLBACK; - - static void messageBox(const QString& title, const QString& text, - QMessageBox::Icon icon, - QMessageBox::StandardButtons buttons, - ButtonCallback f); - - static void information(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons = QMessageBox::Ok, - ButtonCallback callback = NO_OP_CALLBACK); - - static void question(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons = QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), - ButtonCallback callback = [](QMessageBox::StandardButton) {}); - - static void warning(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons = QMessageBox::Ok, - ButtonCallback callback = [](QMessageBox::StandardButton) {}); - - static void critical(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons = QMessageBox::Ok, - ButtonCallback callback = [](QMessageBox::StandardButton) {}); - -protected: - -private slots: - void updateQuick(); - void finishQmlLoad(); - -public slots: - void requestUpdate(); - void requestRender(); - void lockTexture(int texture); - void releaseTexture(int texture); - -signals: - void textureUpdated(GLuint texture); - -private: - QMyQuickRenderControl* _renderControl{ new QMyQuickRenderControl }; - QQuickWindow* _quickWindow{ nullptr }; - QQmlEngine* _qmlEngine{ nullptr }; - QQmlComponent* _qmlComponent{ nullptr }; - QQuickItem* _rootItem{ nullptr }; - QTimer _updateTimer; - FboCache _fboCache; - bool _polish{ true }; - bool _paused{ true }; - MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; -}; - -#endif diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index bf846c0bf2..47e5659c60 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -14,16 +14,26 @@ #include #include #include - +#include #include "PathUtils.h" QString& PathUtils::resourcesPath() { +#ifdef DEBUG + static QString staticResourcePath; + if (staticResourcePath.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + staticResourcePath = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; + } +#else #ifdef Q_OS_MAC static QString staticResourcePath = QCoreApplication::applicationDirPath() + "/../Resources/"; #else static QString staticResourcePath = QCoreApplication::applicationDirPath() + "/resources/"; #endif +#endif + return staticResourcePath; } diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt new file mode 100644 index 0000000000..36a0a1a846 --- /dev/null +++ b/libraries/ui/CMakeLists.txt @@ -0,0 +1,12 @@ +set(TARGET_NAME ui) + +# use setup_hifi_library macro to setup our project and link appropriate Qt modules +setup_hifi_library(OpenGL Network Qml Quick Script) + +link_hifi_libraries(render-utils shared) + +add_dependency_external_projects(glm) +find_package(GLM REQUIRED) +target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS}) + + diff --git a/libraries/ui/src/HifiMenu.cpp b/libraries/ui/src/HifiMenu.cpp new file mode 100644 index 0000000000..3edcaebead --- /dev/null +++ b/libraries/ui/src/HifiMenu.cpp @@ -0,0 +1,289 @@ +// +// HifiMenu.cpp +// +// Created by Bradley Austin Davis on 2015/04/21 +// 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 +// + +#include "HifiMenu.h" +#include + +// FIXME can this be made a class member? +static const QString MENU_SUFFIX{ "__Menu" }; + +HIFI_QML_DEF_LAMBDA(HifiMenu, [=](QQmlContext* context, QObject* newItem) { + auto offscreenUi = DependencyManager::get(); + QObject * rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); + Q_ASSERT(rootMenu); + static_cast(newItem)->setRootMenu(rootMenu); + context->setContextProperty("rootMenu", rootMenu); +}); + +HifiMenu::HifiMenu(QQuickItem* parent) : QQuickItem(parent), _triggerMapper(this), _toggleMapper(this) { + this->setEnabled(false); + connect(&_triggerMapper, SIGNAL(mapped(QString)), this, SLOT(onTriggeredByName(const QString &))); + connect(&_toggleMapper, SIGNAL(mapped(QString)), this, SLOT(onToggledByName(const QString &))); +} + +void HifiMenu::onTriggeredByName(const QString & name) { + qDebug() << name << " triggered"; + if (_triggerActions.count(name)) { + _triggerActions[name](); + } +} + +void HifiMenu::onToggledByName(const QString & name) { + qDebug() << name << " toggled"; + if (_toggleActions.count(name)) { + QObject* menu = findMenuObject(name); + bool checked = menu->property("checked").toBool(); + _toggleActions[name](checked); + } +} + +void HifiMenu::setToggleAction(const QString & name, std::function f) { + _toggleActions[name] = f; +} + +void HifiMenu::setTriggerAction(const QString & name, std::function f) { + _triggerActions[name] = f; +} + +QObject* addMenu(QObject* parent, const QString & text) { + // FIXME add more checking here to ensure no name conflicts + QVariant returnedValue; + QMetaObject::invokeMethod(parent, "addMenu", Qt::DirectConnection, + Q_RETURN_ARG(QVariant, returnedValue), + Q_ARG(QVariant, text)); + QObject* result = returnedValue.value(); + if (result) { + result->setObjectName(text + MENU_SUFFIX); + } + return result; +} + +class QQuickMenuItem; +QObject* addItem(QObject* parent, const QString& text) { + // FIXME add more checking here to ensure no name conflicts + QQuickMenuItem* returnedValue{ nullptr }; + bool invokeResult = QMetaObject::invokeMethod(parent, "addItem", Qt::DirectConnection, + Q_RETURN_ARG(QQuickMenuItem*, returnedValue), + Q_ARG(QString, text)); + Q_ASSERT(invokeResult); + QObject* result = reinterpret_cast(returnedValue); + return result; +} + +const QObject* HifiMenu::findMenuObject(const QString & menuOption) const { + if (menuOption.isEmpty()) { + return _rootMenu; + } + const QObject* result = _rootMenu->findChild(menuOption + MENU_SUFFIX); + return result; +} + +QObject* HifiMenu::findMenuObject(const QString & menuOption) { + if (menuOption.isEmpty()) { + return _rootMenu; + } + QObject* result = _rootMenu->findChild(menuOption + MENU_SUFFIX); + return result; +} + +void HifiMenu::addMenu(const QString & parentMenu, const QString & menuOption) { + QObject* parent = findMenuObject(parentMenu); + QObject* result = ::addMenu(parent, menuOption); + Q_ASSERT(result); + result->setObjectName(menuOption + MENU_SUFFIX); + Q_ASSERT(findMenuObject(menuOption)); +} + +void HifiMenu::removeMenu(const QString& menuName) { + QObject* menu = findMenuObject(menuName); + Q_ASSERT(menu); + Q_ASSERT(menu != _rootMenu); + QMetaObject::invokeMethod(menu->parent(), "removeItem", + Q_ARG(QVariant, QVariant::fromValue(menu))); +} + +bool HifiMenu::menuExists(const QString& menuName) const { + return findMenuObject(menuName); +} + +void HifiMenu::addSeparator(const QString& parentMenu, const QString& separatorName) { + QObject * parent = findMenuObject(parentMenu); + bool invokeResult = QMetaObject::invokeMethod(parent, "addSeparator", Qt::DirectConnection); + Q_ASSERT(invokeResult); + addItem(parentMenu, separatorName); + enableItem(separatorName, false); +} + +void HifiMenu::removeSeparator(const QString& parentMenu, const QString& separatorName) { +} + +void HifiMenu::addItem(const QString & parentMenu, const QString & menuOption) { + QObject* parent = findMenuObject(parentMenu); + Q_ASSERT(parent); + QObject* result = ::addItem(parent, menuOption); + Q_ASSERT(result); + result->setObjectName(menuOption + MENU_SUFFIX); + Q_ASSERT(findMenuObject(menuOption)); + + _triggerMapper.setMapping(result, menuOption); + connect(result, SIGNAL(triggered()), &_triggerMapper, SLOT(map())); + + _toggleMapper.setMapping(result, menuOption); + connect(result, SIGNAL(toggled(bool)), &_toggleMapper, SLOT(map())); +} + +void HifiMenu::addItem(const QString & parentMenu, const QString & menuOption, std::function f) { + setTriggerAction(menuOption, f); + addItem(parentMenu, menuOption); +} + +void HifiMenu::addItem(const QString & parentMenu, const QString & menuOption, QObject* receiver, const char* slot) { + addItem(parentMenu, menuOption); + connectItem(menuOption, receiver, slot); +} + +void HifiMenu::removeItem(const QString& menuOption) { + removeMenu(menuOption); +} + +bool HifiMenu::itemExists(const QString& menuName, const QString& menuitem) const { + return findMenuObject(menuName); +} + +void HifiMenu::triggerItem(const QString& menuOption) { + QObject* menuItem = findMenuObject(menuOption); + Q_ASSERT(menuItem); + Q_ASSERT(menuItem != _rootMenu); + QMetaObject::invokeMethod(menuItem, "trigger"); +} + +QHash warned; +void warn(const QString & menuOption) { + if (!warned.contains(menuOption)) { + warned[menuOption] = menuOption; + qWarning() << "No menu item: " << menuOption; + } +} + +bool HifiMenu::isChecked(const QString& menuOption) const { + const QObject* menuItem = findMenuObject(menuOption); + if (!menuItem) { + warn(menuOption); + return false; + } + return menuItem->property("checked").toBool(); +} + +void HifiMenu::setChecked(const QString& menuOption, bool isChecked) { + QObject* menuItem = findMenuObject(menuOption); + if (!menuItem) { + warn(menuOption); + return; + } + if (menuItem->property("checked").toBool() != isChecked) { + menuItem->setProperty("checked", QVariant::fromValue(isChecked)); + Q_ASSERT(menuItem->property("checked").toBool() == isChecked); + } +} + +void HifiMenu::setCheckable(const QString& menuOption, bool checkable) { + QObject* menuItem = findMenuObject(menuOption); + if (!menuItem) { + warn(menuOption); + return; + } + + menuItem->setProperty("checkable", QVariant::fromValue(checkable)); + Q_ASSERT(menuItem->property("checkable").toBool() == checkable); +} + +void HifiMenu::setItemText(const QString& menuOption, const QString& text) { + QObject* menuItem = findMenuObject(menuOption); + if (!menuItem) { + warn(menuOption); + return; + } + if (menuItem->property("type").toInt() == 2) { + menuItem->setProperty("title", QVariant::fromValue(text)); + } else { + menuItem->setProperty("text", QVariant::fromValue(text)); + } +} + +void HifiMenu::setRootMenu(QObject* rootMenu) { + _rootMenu = rootMenu; +} + +void HifiMenu::enableItem(const QString & menuOption, bool enabled) { + QObject* menuItem = findMenuObject(menuOption); + if (!menuItem) { + warn(menuOption); + return; + } + menuItem->setProperty("enabled", QVariant::fromValue(enabled)); +} + +void HifiMenu::addCheckableItem(const QString& parentMenu, const QString& menuOption, bool checked) { + addItem(parentMenu, menuOption); + setCheckable(menuOption); + if (checked) { + setChecked(menuOption, checked); + } +} + +void HifiMenu::addCheckableItem(const QString& parentMenu, const QString& menuOption, bool checked, std::function f) { + setToggleAction(menuOption, f); + addCheckableItem(parentMenu, menuOption, checked); +} + +void HifiMenu::setItemVisible(const QString& menuOption, bool visible) { + QObject* result = findMenuObject(menuOption); + if (result) { + result->setProperty("visible", visible); + } +} + +bool HifiMenu::isItemVisible(const QString& menuOption) { + QObject* result = findMenuObject(menuOption); + if (result) { + return result->property("visible").toBool(); + } + return false; +} + +void HifiMenu::setItemShortcut(const QString& menuOption, const QString& shortcut) { + QObject* result = findMenuObject(menuOption); + if (result) { + result->setProperty("shortcut", shortcut); + } +} + +QString HifiMenu::getItemShortcut(const QString& menuOption) { + QObject* result = findMenuObject(menuOption); + if (result) { + return result->property("shortcut").toString(); + } + return QString(); +} + +void HifiMenu::addCheckableItem(const QString& parentMenu, const QString& menuOption, bool checked, QObject* receiver, const char* slot) { + addCheckableItem(parentMenu, menuOption, checked); + connectItem(menuOption, receiver, slot); +} + +void HifiMenu::connectCheckable(const QString& menuOption, QObject* receiver, const char* slot) { + QObject* result = findMenuObject(menuOption); + connect(result, SIGNAL(toggled(bool)), receiver, slot); +} + +void HifiMenu::connectItem(const QString& menuOption, QObject* receiver, const char* slot) { + QObject* result = findMenuObject(menuOption); + connect(result, SIGNAL(triggered()), receiver, slot); +} diff --git a/libraries/ui/src/HifiMenu.h b/libraries/ui/src/HifiMenu.h new file mode 100644 index 0000000000..0adc485211 --- /dev/null +++ b/libraries/ui/src/HifiMenu.h @@ -0,0 +1,79 @@ +// +// HifiMenu.h +// +// Created by Bradley Austin Davis on 2015/04/21 +// 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 +// + +#pragma once +#ifndef hifi_HifiMenu_h +#define hifi_HifiMenu_h + +#include +#include +#include +#include +#include "OffscreenUi.h" + +class HifiMenu : public QQuickItem { + Q_OBJECT + HIFI_QML_DECL_LAMBDA + +public: + HifiMenu(QQuickItem* parent = nullptr); + + void setToggleAction(const QString& name, std::function f); + void setTriggerAction(const QString& name, std::function f); + + void addMenu(const QString& parentMenu, const QString& menuOption); + void removeMenu(const QString& menuName); + bool menuExists(const QString& menuName) const; + + void addSeparator(const QString& menuName, const QString& separatorName); + void removeSeparator(const QString& menuName, const QString& separatorName); + + void addItem(const QString& parentMenu, const QString& menuOption); + void addItem(const QString& parentMenu, const QString& menuOption, std::function f); + void addItem(const QString& parentMenu, const QString& menuOption, QObject* receiver, const char* slot); + + void addCheckableItem(const QString& parentMenu, const QString& menuOption, bool checked = false); + void addCheckableItem(const QString& parentMenu, const QString& menuOption, bool checked, std::function f); + void addCheckableItem(const QString& parentMenu, const QString& menuOption, bool checked, QObject* receiver, const char* slot); + void connectCheckable(const QString& menuOption, QObject* receiver, const char* slot); + void connectItem(const QString& menuOption, QObject* receiver, const char* slot); + + void removeItem(const QString& menuitem); + bool itemExists(const QString& menuName, const QString& menuitem) const; + void triggerItem(const QString& menuOption); + void enableItem(const QString& menuOption, bool enabled = true); + bool isChecked(const QString& menuOption) const; + void setChecked(const QString& menuOption, bool checked = true); + void setCheckable(const QString& menuOption, bool checkable = true); + void setExclusiveGroup(const QString& menuOption, const QString& groupName); + void setItemText(const QString& menuOption, const QString& text); + void setItemVisible(const QString& menuOption, bool visible = true); + bool isItemVisible(const QString& menuOption); + + void setItemShortcut(const QString& menuOption, const QString& shortcut); + QString getItemShortcut(const QString& menuOption); + + void setRootMenu(QObject* rootMenu); + +private slots: + void onTriggeredByName(const QString& name); + void onToggledByName(const QString& name); + +protected: + QHash> _triggerActions; + QHash> _toggleActions; + QObject* findMenuObject(const QString& name); + const QObject* findMenuObject(const QString& name) const; + QObject* _rootMenu{ nullptr }; + QSignalMapper _triggerMapper; + QSignalMapper _toggleMapper; +}; + +#endif // hifi_HifiMenu_h diff --git a/libraries/ui/src/MessageDialog.cpp b/libraries/ui/src/MessageDialog.cpp new file mode 100644 index 0000000000..695f87552a --- /dev/null +++ b/libraries/ui/src/MessageDialog.cpp @@ -0,0 +1,119 @@ +// +// MessageDialog.cpp +// +// Created by Bradley Austin Davis on 2015/04/14 +// 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 +// +#include "MessageDialog.h" + +HIFI_QML_DEF(MessageDialog) + +MessageDialog::MessageDialog(QQuickItem* parent) : OffscreenQmlDialog(parent) { + _buttons = StandardButtons(Ok | Cancel); +} + +MessageDialog::~MessageDialog() { +} + +QString MessageDialog::text() const { + return _text; +} + +QString MessageDialog::informativeText() const { + return _informativeText; +} + +QString MessageDialog::detailedText() const { + return _detailedText; +} + +MessageDialog::Icon MessageDialog::icon() const { + return _icon; +} + +void MessageDialog::setVisible(bool v) { + OffscreenQmlDialog::setVisible(v); +} + +void MessageDialog::setText(const QString& arg) { + if (arg != _text) { + _text = arg; + emit textChanged(); + } +} + +void MessageDialog::setInformativeText(const QString& arg) { + if (arg != _informativeText) { + _informativeText = arg; + emit informativeTextChanged(); + } +} + +void MessageDialog::setDetailedText(const QString& arg) { + if (arg != _detailedText) { + _detailedText = arg; + emit detailedTextChanged(); + } +} + +void MessageDialog::setIcon(MessageDialog::Icon icon) { + if (icon != _icon) { + _icon = icon; + emit iconChanged(); + } +} + +void MessageDialog::setStandardButtons(StandardButtons buttons) { + if (buttons != _buttons) { + _buttons = buttons; + emit standardButtonsChanged(); + } +} + +void MessageDialog::click(StandardButton button) { + // FIXME try to do it more like the standard dialog + click(StandardButton(button), ButtonRole::NoRole); +} + +MessageDialog::StandardButtons MessageDialog::standardButtons() const { + return _buttons; +} + +MessageDialog::StandardButton MessageDialog::clickedButton() const { + return _clickedButton; +} + +void MessageDialog::click(StandardButton button, ButtonRole) { + _clickedButton = button; + if (_resultCallback) { + _resultCallback(QMessageBox::StandardButton(_clickedButton)); + } + hide(); +} + +void MessageDialog::accept() { + // enter key is treated like OK + if (_clickedButton == NoButton) + _clickedButton = Ok; + if (_resultCallback) { + _resultCallback(QMessageBox::StandardButton(_clickedButton)); + } + OffscreenQmlDialog::accept(); +} + +void MessageDialog::reject() { + // escape key is treated like cancel + if (_clickedButton == NoButton) + _clickedButton = Cancel; + if (_resultCallback) { + _resultCallback(QMessageBox::StandardButton(_clickedButton)); + } + OffscreenQmlDialog::reject(); +} + +void MessageDialog::setResultCallback(OffscreenUi::ButtonCallback callback) { + _resultCallback = callback; +} diff --git a/libraries/ui/src/MessageDialog.h b/libraries/ui/src/MessageDialog.h new file mode 100644 index 0000000000..461bca8251 --- /dev/null +++ b/libraries/ui/src/MessageDialog.h @@ -0,0 +1,100 @@ +// +// MessageDialog.h +// +// Created by Bradley Austin Davis on 2015/04/14 +// 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 +// + +#pragma once +#ifndef hifi_MessageDialog_h +#define hifi_MessageDialog_h + +#include "OffscreenQmlDialog.h" + +class MessageDialog : public OffscreenQmlDialog +{ + Q_OBJECT + HIFI_QML_DECL + +private: + Q_ENUMS(Icon) + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + Q_PROPERTY(QString informativeText READ informativeText WRITE setInformativeText NOTIFY informativeTextChanged) + Q_PROPERTY(QString detailedText READ detailedText WRITE setDetailedText NOTIFY detailedTextChanged) + Q_PROPERTY(Icon icon READ icon WRITE setIcon NOTIFY iconChanged) + Q_PROPERTY(StandardButtons standardButtons READ standardButtons WRITE setStandardButtons NOTIFY standardButtonsChanged) + Q_PROPERTY(StandardButton clickedButton READ clickedButton NOTIFY buttonClicked) + +public: + enum Icon { NoIcon, Information, Warning, Critical, Question }; + + enum ButtonRole { + // keep this in sync with QDialogButtonBox::ButtonRole and QPlatformDialogHelper::ButtonRole + InvalidRole = -1, + AcceptRole, + RejectRole, + DestructiveRole, + ActionRole, + HelpRole, + YesRole, + NoRole, + ResetRole, + ApplyRole, + + NRoles + }; + + MessageDialog(QQuickItem* parent = 0); + virtual ~MessageDialog(); + + QString text() const; + QString informativeText() const; + QString detailedText() const; + Icon icon() const; + +public slots: + virtual void setVisible(bool v); + void setText(const QString& arg); + void setInformativeText(const QString& arg); + void setDetailedText(const QString& arg); + void setIcon(Icon icon); + void setStandardButtons(StandardButtons buttons); + void setResultCallback(OffscreenUi::ButtonCallback callback); + void click(StandardButton button); + StandardButtons standardButtons() const; + StandardButton clickedButton() const; + +signals: + void textChanged(); + void informativeTextChanged(); + void detailedTextChanged(); + void iconChanged(); + void standardButtonsChanged(); + void buttonClicked(); + void discard(); + void help(); + void yes(); + void no(); + void apply(); + void reset(); + +protected slots: + virtual void click(StandardButton button, ButtonRole); + virtual void accept(); + virtual void reject(); + +private: + QString _title; + QString _text; + QString _informativeText; + QString _detailedText; + Icon _icon{ Information }; + StandardButtons _buttons; + StandardButton _clickedButton{ NoButton }; + OffscreenUi::ButtonCallback _resultCallback; +}; + +#endif // hifi_MessageDialog_h diff --git a/libraries/render-utils/src/OffscreenQmlDialog.cpp b/libraries/ui/src/OffscreenQmlDialog.cpp similarity index 54% rename from libraries/render-utils/src/OffscreenQmlDialog.cpp rename to libraries/ui/src/OffscreenQmlDialog.cpp index d1e060245d..eba81f708b 100644 --- a/libraries/render-utils/src/OffscreenQmlDialog.cpp +++ b/libraries/ui/src/OffscreenQmlDialog.cpp @@ -13,6 +13,30 @@ OffscreenQmlDialog::OffscreenQmlDialog(QQuickItem* parent) : QQuickItem(parent) { } +OffscreenQmlDialog::~OffscreenQmlDialog() { +} + void OffscreenQmlDialog::hide() { static_cast(parent())->setEnabled(false); } + +QString OffscreenQmlDialog::title() const { + return _title; +} + +void OffscreenQmlDialog::setTitle(const QString& title) { + if (title != _title) { + _title = title; + emit titleChanged(); + } +} + +void OffscreenQmlDialog::accept() { + hide(); + emit accepted(); +} + +void OffscreenQmlDialog::reject() { + hide(); + emit rejected(); +} diff --git a/libraries/ui/src/OffscreenQmlDialog.h b/libraries/ui/src/OffscreenQmlDialog.h new file mode 100644 index 0000000000..33201c385d --- /dev/null +++ b/libraries/ui/src/OffscreenQmlDialog.h @@ -0,0 +1,76 @@ +// +// OffscreenQmlDialog.h +// +// Created by Bradley Austin Davis on 2015/04/14 +// 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 +// + +#pragma once +#ifndef hifi_OffscreenQmlDialog_h +#define hifi_OffscreenQmlDialog_h + +#include + +#include "OffscreenUi.h" + +class OffscreenQmlDialog : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) + Q_ENUMS(StandardButton) + Q_FLAGS(StandardButtons) + +public: + OffscreenQmlDialog(QQuickItem* parent = nullptr); + virtual ~OffscreenQmlDialog(); + + enum StandardButton { + // keep this in sync with QDialogButtonBox::StandardButton and QMessageBox::StandardButton + NoButton = 0x00000000, + Ok = 0x00000400, + Save = 0x00000800, + SaveAll = 0x00001000, + Open = 0x00002000, + Yes = 0x00004000, + YesToAll = 0x00008000, + No = 0x00010000, + NoToAll = 0x00020000, + Abort = 0x00040000, + Retry = 0x00080000, + Ignore = 0x00100000, + Close = 0x00200000, + Cancel = 0x00400000, + Discard = 0x00800000, + Help = 0x01000000, + Apply = 0x02000000, + Reset = 0x04000000, + RestoreDefaults = 0x08000000, + NButtons + }; + Q_DECLARE_FLAGS(StandardButtons, StandardButton) + +protected: + void hide(); + virtual void accept(); + virtual void reject(); + +public: + QString title() const; + void setTitle(const QString& title); + +signals: + void accepted(); + void rejected(); + void titleChanged(); + +private: + QString _title; + +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(OffscreenQmlDialog::StandardButtons) + +#endif diff --git a/libraries/render-utils/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp similarity index 74% rename from libraries/render-utils/src/OffscreenUi.cpp rename to libraries/ui/src/OffscreenUi.cpp index ac78317703..95e847d24b 100644 --- a/libraries/render-utils/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -13,6 +13,11 @@ #include #include #include +#include "MessageDialog.h" + + +Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) +Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") // Time between receiving a request to render the offscreen UI actually triggering // the render. Could possibly be increased depending on the framerate we expect to @@ -92,10 +97,10 @@ void OffscreenUi::create(QOpenGLContext* shareContext) { #ifdef DEBUG connect(_quickWindow, &QQuickWindow::focusObjectChanged, [this]{ - qDebug() << "New focus item " << _quickWindow->focusObject(); + qCDebug(offscreenFocus) << "New focus item " << _quickWindow->focusObject(); }); connect(_quickWindow, &QQuickWindow::activeFocusItemChanged, [this] { - qDebug() << "New active focus item " << _quickWindow->activeFocusItem(); + qCDebug(offscreenFocus) << "New active focus item " << _quickWindow->activeFocusItem(); }); #endif @@ -117,37 +122,40 @@ void OffscreenUi::resize(const QSize& newSize) { qDebug() << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio; _fboCache.setSize(newSize * pixelRatio); + if (_quickWindow) { + _quickWindow->setGeometry(QRect(QPoint(), newSize)); + } + + _quickWindow->contentItem()->setSize(newSize); + // Update our members if (_rootItem) { _rootItem->setSize(newSize); } - if (_quickWindow) { - _quickWindow->setGeometry(QRect(QPoint(), newSize)); - } - doneCurrent(); } -QQmlContext* OffscreenUi::qmlContext() { - if (nullptr == _rootItem) { - return _qmlComponent->creationContext(); - } - return QQmlEngine::contextForObject(_rootItem); +QQuickItem* OffscreenUi::getRootItem() { + return _rootItem; } void OffscreenUi::setBaseUrl(const QUrl& baseUrl) { _qmlEngine->setBaseUrl(baseUrl); } -void OffscreenUi::load(const QUrl& qmlSource, std::function f) { +QObject* OffscreenUi::load(const QUrl& qmlSource, std::function f) { qDebug() << "Loading QML from URL " << qmlSource; _qmlComponent->loadUrl(qmlSource); if (_qmlComponent->isLoading()) { - connect(_qmlComponent, &QQmlComponent::statusChanged, this, []{}); - } else { - finishQmlLoad(); + connect(_qmlComponent, &QQmlComponent::statusChanged, this, + [this, f](QQmlComponent::Status){ + finishQmlLoad(f); + }); + return nullptr; } + + return finishQmlLoad(f); } void OffscreenUi::requestUpdate() { @@ -163,53 +171,60 @@ void OffscreenUi::requestRender() { } } -void OffscreenUi::finishQmlLoad() { - disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, &OffscreenUi::finishQmlLoad); +QObject* OffscreenUi::finishQmlLoad(std::function f) { + disconnect(_qmlComponent, &QQmlComponent::statusChanged, this, 0); if (_qmlComponent->isError()) { QList errorList = _qmlComponent->errors(); - foreach(const QQmlError &error, errorList) { + foreach(const QQmlError& error, errorList) { qWarning() << error.url() << error.line() << error; } - return; + return nullptr; } - QObject* newObject = _qmlComponent->create(); + QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp); + QObject* newObject = _qmlComponent->beginCreate(newContext); if (_qmlComponent->isError()) { QList errorList = _qmlComponent->errors(); - foreach(const QQmlError &error, errorList) + foreach(const QQmlError& error, errorList) qWarning() << error.url() << error.line() << error; if (!_rootItem) { qFatal("Unable to finish loading QML root"); } - return; + return nullptr; } + f(newContext, newObject); + _qmlComponent->completeCreate(); + + + // All quick items should be focusable QQuickItem* newItem = qobject_cast(newObject); - if (!newItem) { - qWarning("run: Not a QQuickItem"); - delete newObject; - if (!_rootItem) { - qFatal("Unable to find root QQuickItem"); - } - return; + if (newItem) { + // Make sure we make items focusable (critical for + // supporting keyboard shortcuts) + newItem->setFlag(QQuickItem::ItemIsFocusScope, true); } - // Make sure we make items focusable (critical for - // supporting keyboard shortcuts) - newItem->setFlag(QQuickItem::ItemIsFocusScope, true); - - if (!_rootItem) { - // The root item is ready. Associate it with the window. - _rootItem = newItem; - _rootItem->setParentItem(_quickWindow->contentItem()); - _rootItem->setSize(_quickWindow->renderTargetSize()); - } else { + // If we already have a root, just set a couple of flags and the ancestry + if (_rootItem) { // Allow child windows to be destroyed from JS - QQmlEngine::setObjectOwnership(newItem, QQmlEngine::JavaScriptOwnership); - newItem->setParent(_rootItem); - newItem->setParentItem(_rootItem); - newItem->setEnabled(true); + QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); + newObject->setParent(_rootItem); + if (newItem) { + newItem->setParentItem(_rootItem); + } + return newObject; } + + if (!newItem) { + qFatal("Could not load object as root item"); + return nullptr; + } + // The root item is ready. Associate it with the window. + _rootItem = newItem; + _rootItem->setParentItem(_quickWindow->contentItem()); + _rootItem->setSize(_quickWindow->renderTargetSize()); + return _rootItem; } @@ -272,9 +287,9 @@ QPointF OffscreenUi::mapWindowToUi(const QPointF& sourcePosition, QObject* sourc // // However, the problem may go away once we switch to the new menu system, // so I think it's OK for the time being. -bool OffscreenUi::shouldSwallowShortcut(QEvent * event) { +bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { Q_ASSERT(event->type() == QEvent::ShortcutOverride); - QObject * focusObject = _quickWindow->focusObject(); + QObject* focusObject = _quickWindow->focusObject(); if (focusObject != _quickWindow && focusObject != _rootItem) { //qDebug() << "Swallowed shortcut " << static_cast(event)->key(); event->accept(); @@ -297,7 +312,7 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { #ifdef DEBUG // Don't intercept our own events, or we enter an infinite recursion - QObject * recurseTest = originalDestination; + QObject* recurseTest = originalDestination; while (recurseTest) { Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow); recurseTest = recurseTest->parent(); @@ -388,54 +403,72 @@ void OffscreenUi::setProxyWindow(QWindow* window) { _renderControl->_renderWindow = window; } -void OffscreenUi::show(const QUrl& url, const QString& name) { +void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { QQuickItem* item = _rootItem->findChild(name); // First load? if (!item) { - load(url); - return; + load(url, f); + item = _rootItem->findChild(name); + } + if (item) { + item->setEnabled(true); } - item->setEnabled(true); } -void OffscreenUi::toggle(const QUrl& url, const QString& name) { +void OffscreenUi::toggle(const QUrl& url, const QString& name, std::function f) { QQuickItem* item = _rootItem->findChild(name); // First load? if (!item) { - load(url); - return; + load(url, f); + item = _rootItem->findChild(name); + } + if (item) { + item->setEnabled(!item->isEnabled()); } - item->setEnabled(!item->isEnabled()); } void OffscreenUi::messageBox(const QString& title, const QString& text, + ButtonCallback callback, QMessageBox::Icon icon, - QMessageBox::StandardButtons buttons, - ButtonCallback f) { + QMessageBox::StandardButtons buttons) { + MessageDialog* pDialog{ nullptr }; + MessageDialog::show([&](QQmlContext* ctx, QObject* item) { + pDialog = item->findChild(); + pDialog->setIcon((MessageDialog::Icon)icon); + pDialog->setTitle(title); + pDialog->setText(text); + pDialog->setStandardButtons(MessageDialog::StandardButtons(static_cast(buttons))); + pDialog->setResultCallback(callback); + }); + pDialog->setEnabled(true); } void OffscreenUi::information(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons, - ButtonCallback callback) { - callback(QMessageBox::information(nullptr, title, text, buttons)); + ButtonCallback callback, + QMessageBox::StandardButtons buttons) { + messageBox(title, text, callback, + static_cast(MessageDialog::Information), buttons); } void OffscreenUi::question(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons, - ButtonCallback callback) { - callback(QMessageBox::question(nullptr, title, text, buttons)); + ButtonCallback callback, + QMessageBox::StandardButtons buttons) { + messageBox(title, text, callback, + static_cast(MessageDialog::Question), buttons); } void OffscreenUi::warning(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons, - ButtonCallback callback) { - callback(QMessageBox::warning(nullptr, title, text, buttons)); + ButtonCallback callback, + QMessageBox::StandardButtons buttons) { + messageBox(title, text, callback, + static_cast(MessageDialog::Warning), buttons); } void OffscreenUi::critical(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons, - ButtonCallback callback) { - callback(QMessageBox::critical(nullptr, title, text, buttons)); + ButtonCallback callback, + QMessageBox::StandardButtons buttons) { + messageBox(title, text, callback, + static_cast(MessageDialog::Critical), buttons); } diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h new file mode 100644 index 0000000000..84626cab52 --- /dev/null +++ b/libraries/ui/src/OffscreenUi.h @@ -0,0 +1,200 @@ +// +// OffscreenUi.h +// interface/src/entities +// +// Created by Bradley Austin Davis on 2015-04-04 +// 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 +// +#pragma once +#ifndef hifi_OffscreenUi_h +#define hifi_OffscreenUi_h + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "OffscreenGlCanvas.h" +#include "FboCache.h" +#include + +#define HIFI_QML_DECL \ +private: \ + static const QString NAME; \ + static const QUrl QML; \ +public: \ + static void registerType(); \ + static void show(std::function f = [](QQmlContext*, QObject*) {}); \ + static void toggle(std::function f = [](QQmlContext*, QObject*) {}); \ + static void load(std::function f = [](QQmlContext*, QObject*) {}); \ +private: + +#define HIFI_QML_DECL_LAMBDA \ +protected: \ + static const QString NAME; \ + static const QUrl QML; \ +public: \ + static void registerType(); \ + static void show(); \ + static void toggle(); \ + static void load(); \ +private: + +#define HIFI_QML_DEF(x) \ + const QUrl x::QML = QUrl(#x ".qml"); \ + const QString x::NAME = #x; \ + \ + void x::registerType() { \ + qmlRegisterType("Hifi", 1, 0, NAME.toLocal8Bit().constData()); \ + } \ + \ + void x::show(std::function f) { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->show(QML, NAME, f); \ + } \ + \ + void x::toggle(std::function f) { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->toggle(QML, NAME, f); \ + } \ + void x::load(std::function f) { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->load(QML, f); \ + } + +#define HIFI_QML_DEF_LAMBDA(x, f) \ + const QUrl x::QML = QUrl(#x ".qml"); \ + const QString x::NAME = #x; \ + \ + void x::registerType() { \ + qmlRegisterType("Hifi", 1, 0, NAME.toLocal8Bit().constData()); \ + } \ + void x::show() { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->show(QML, NAME, f); \ + } \ + void x::toggle() { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->toggle(QML, NAME, f); \ + } \ + void x::load() { \ + auto offscreenUi = DependencyManager::get(); \ + offscreenUi->load(QML, f); \ + } + +class OffscreenUi : public OffscreenGlCanvas, public Dependency { + Q_OBJECT + + class QMyQuickRenderControl : public QQuickRenderControl { + protected: + QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{ + if (nullptr == _renderWindow) { + return QQuickRenderControl::renderWindow(offset); + } + if (nullptr != offset) { + offset->rx() = offset->ry() = 0; + } + return _renderWindow; + } + + private: + QWindow* _renderWindow{ nullptr }; + friend class OffscreenUi; + }; + +public: + using MouseTranslator = std::function; + OffscreenUi(); + virtual ~OffscreenUi(); + void create(QOpenGLContext* context); + void resize(const QSize& size); + QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + return load(QUrl(qmlSourceFile), f); + } + void show(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); + void toggle(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); + void setBaseUrl(const QUrl& baseUrl); + void addImportPath(const QString& path); + //QQmlContext* getQmlContext(); + QQuickItem* getRootItem(); + void pause(); + void resume(); + bool isPaused() const; + void setProxyWindow(QWindow* window); + bool shouldSwallowShortcut(QEvent* event); + QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); + virtual bool eventFilter(QObject* originalDestination, QEvent* event); + void setMouseTranslator(MouseTranslator mouseTranslator) { + _mouseTranslator = mouseTranslator; + } + + + // Messagebox replacement functions + using ButtonCallback = std::function; + static ButtonCallback NO_OP_CALLBACK; + + static void messageBox(const QString& title, const QString& text, + ButtonCallback f, + QMessageBox::Icon icon, + QMessageBox::StandardButtons buttons); + + static void information(const QString& title, const QString& text, + ButtonCallback callback = NO_OP_CALLBACK, + QMessageBox::StandardButtons buttons = QMessageBox::Ok); + + static void question(const QString& title, const QString& text, + ButtonCallback callback = NO_OP_CALLBACK, + QMessageBox::StandardButtons buttons = QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No)); + + static void warning(const QString& title, const QString& text, + ButtonCallback callback = NO_OP_CALLBACK, + QMessageBox::StandardButtons buttons = QMessageBox::Ok); + + static void critical(const QString& title, const QString& text, + ButtonCallback callback = NO_OP_CALLBACK, + QMessageBox::StandardButtons buttons = QMessageBox::Ok); + +protected: + +private slots: + void updateQuick(); + QObject* finishQmlLoad(std::function f); + +public slots: + void requestUpdate(); + void requestRender(); + void lockTexture(int texture); + void releaseTexture(int texture); + +signals: + void textureUpdated(GLuint texture); + +private: + QMyQuickRenderControl* _renderControl{ new QMyQuickRenderControl }; + QQuickWindow* _quickWindow{ nullptr }; + QQmlEngine* _qmlEngine{ nullptr }; + QQmlComponent* _qmlComponent{ nullptr }; + QQuickItem* _rootItem{ nullptr }; + QTimer _updateTimer; + FboCache _fboCache; + bool _polish{ true }; + bool _paused{ true }; + MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p; } }; +}; + +#endif diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index 9d7363c241..5e45bf23a2 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -10,7 +10,6 @@ #include "TextRenderer.h" #include "MatrixStack.h" -#include "OffscreenUi.h" #include #include @@ -27,6 +26,7 @@ #include #include #include + #include #include #include @@ -80,6 +80,7 @@ const QString& getQmlDir() { } return dir; } + // Create a simple OpenGL window that renders text in various ways class QTestWindow : public QWindow { Q_OBJECT @@ -88,24 +89,17 @@ class QTestWindow : public QWindow { QSize _size; TextRenderer* _textRenderer[4]; RateCounter fps; - int testQmlTexture{ 0 }; - //ProgramPtr _planeProgam; - //ShapeWrapperPtr _planeShape; protected: - void renderText(); - void renderQml(); private: void resizeWindow(const QSize& size) { _size = size; - DependencyManager::get()->resize(_size); } public: QTestWindow() { - DependencyManager::set(); setSurfaceType(QSurface::OpenGLSurface); QSurfaceFormat format; @@ -165,30 +159,10 @@ public: glClearColor(0.2f, 0.2f, 0.2f, 1); glDisable(GL_DEPTH_TEST); - auto offscreenUi = DependencyManager::get(); - offscreenUi->create(_context); - // FIXME, need to switch to a QWindow for mouse and keyboard input to work - offscreenUi->setProxyWindow(this); - // "#0e7077" + makeCurrent(); + setFramePosition(QPoint(-1000, 0)); resize(QSize(800, 600)); - - offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); - offscreenUi->load(QUrl("TestRoot.qml")); - offscreenUi->addImportPath(getQmlDir()); - offscreenUi->addImportPath("."); - - connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - offscreenUi->lockTexture(textureId); - assert(!glGetError()); - GLuint oldTexture = testQmlTexture; - testQmlTexture = textureId; - if (oldTexture) { - offscreenUi->releaseTexture(oldTexture); - } - }); - installEventFilter(offscreenUi.data()); - offscreenUi->resume(); } virtual ~QTestWindow() { @@ -204,28 +178,6 @@ protected: void resizeEvent(QResizeEvent* ev) override { resizeWindow(ev->size()); } - - - void keyPressEvent(QKeyEvent* event) { - switch (event->key()) { - case Qt::Key_L: - if (event->modifiers() & Qt::CTRL) { - DependencyManager::get()->toggle(QString("TestDialog.qml"), "TestDialog"); - } - break; - } - QWindow::keyPressEvent(event); - } - - void moveEvent(QMoveEvent* event) { - static qreal oldPixelRatio = 0.0; - if (devicePixelRatio() != oldPixelRatio) { - oldPixelRatio = devicePixelRatio(); - resizeWindow(size()); - } - - QWindow::moveEvent(event); - } }; #ifndef SERIF_FONT_FAMILY @@ -282,39 +234,16 @@ void QTestWindow::renderText() { } } -void QTestWindow::renderQml() { - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - if (testQmlTexture > 0) { - glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, testQmlTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - glBegin(GL_QUADS); - { - glTexCoord2f(0, 0); - glVertex2f(-1, -1); - glTexCoord2f(0, 1); - glVertex2f(-1, 1); - glTexCoord2f(1, 1); - glVertex2f(1, 1); - glTexCoord2f(1, 0); - glVertex2f(1, -1); - } - glEnd(); -} - void QTestWindow::draw() { + if (!isVisible()) { + return; + } + makeCurrent(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); - //renderText(); - renderQml(); + renderText(); _context->swapBuffers(this); glFinish(); @@ -327,10 +256,8 @@ void QTestWindow::draw() { } int main(int argc, char** argv) { - QApplication app(argc, argv); - //QLoggingCategory::setFilterRules("qt.quick.mouse.debug = true"); + QGuiApplication app(argc, argv); QTestWindow window; - QTimer timer; timer.setInterval(1); app.connect(&timer, &QTimer::timeout, &app, [&] { diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt new file mode 100644 index 0000000000..3ff8555fa2 --- /dev/null +++ b/tests/ui/CMakeLists.txt @@ -0,0 +1,15 @@ +set(TARGET_NAME ui-tests) + +setup_hifi_project(Widgets OpenGL Network Qml Quick Script) + +if (WIN32) + add_dependency_external_projects(glew) + find_package(GLEW REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${GLEW_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${GLEW_LIBRARIES} wsock32.lib opengl32.lib Winmm.lib) +endif() + +# link in the shared libraries +link_hifi_libraries(ui render-utils gpu shared) + +copy_dlls_beside_windows_executable() \ No newline at end of file diff --git a/tests/ui/main.qml b/tests/ui/main.qml new file mode 100644 index 0000000000..168b9fb291 --- /dev/null +++ b/tests/ui/main.qml @@ -0,0 +1,161 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Controls 1.2 +import "qml/UI.js" as UI +import "qml" +//import "/Users/bdavis/Git/hifi/interface/resources/qml" + +Item { + anchors.fill: parent + visible: true + //title: "Qt Quick Controls Gallery" + + MessageDialog { + id: aboutDialog + icon: StandardIcon.Information + title: "About" + text: "Qt Quick Controls Gallery" + informativeText: "This example demonstrates most of the available Qt Quick Controls." + } + + Action { + id: copyAction + text: "&Copy" + shortcut: StandardKey.Copy + iconName: "edit-copy" + enabled: (!!activeFocusItem && !!activeFocusItem["copy"]) + onTriggered: activeFocusItem.copy() + } + + Action { + id: cutAction + text: "Cu&t" + shortcut: StandardKey.Cut + iconName: "edit-cut" + enabled: (!!activeFocusItem && !!activeFocusItem["cut"]) + onTriggered: activeFocusItem.cut() + } + + Action { + id: pasteAction + text: "&Paste" + shortcut: StandardKey.Paste + iconName: "edit-paste" + enabled: (!!activeFocusItem && !!activeFocusItem["paste"]) + onTriggered: activeFocusItem.paste() + } + +// toolBar: ToolBar { +// RowLayout { +// anchors.fill: parent +// anchors.margins: spacing +// Label { +// text: UI.label +// } +// Item { Layout.fillWidth: true } +// CheckBox { +// id: enabler +// text: "Enabled" +// checked: true +// } +// } +// } + +// menuBar: MenuBar { +// Menu { +// title: "&File" +// MenuItem { +// text: "E&xit" +// shortcut: StandardKey.Quit +// onTriggered: Qt.quit() +// } +// } +// Menu { +// title: "&Edit" +// visible: tabView.currentIndex == 2 +// MenuItem { action: cutAction } +// MenuItem { action: copyAction } +// MenuItem { action: pasteAction } +// } +// Menu { +// title: "&Help" +// MenuItem { +// text: "About..." +// onTriggered: aboutDialog.open() +// } +// } +// } + + TabView { + id: tabView + + anchors.fill: parent + anchors.margins: UI.margin + tabPosition: UI.tabPosition + + Layout.minimumWidth: 360 + Layout.minimumHeight: 360 + Layout.preferredWidth: 480 + Layout.preferredHeight: 640 + + Tab { + title: "Buttons" + ButtonPage { + enabled: enabler.checked + } + } + Tab { + title: "Progress" + ProgressPage { + enabled: enabler.checked + } + } + Tab { + title: "Input" + InputPage { + enabled: enabler.checked + } + } + } +} diff --git a/tests/ui/qml/ButtonPage.qml b/tests/ui/qml/ButtonPage.qml new file mode 100644 index 0000000000..0ed7e2d6ad --- /dev/null +++ b/tests/ui/qml/ButtonPage.qml @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.2 + +ScrollView { + id: page + + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + Item { + id: content + + width: Math.max(page.viewport.width, grid.implicitWidth + 2 * grid.rowSpacing) + height: Math.max(page.viewport.height, grid.implicitHeight + 2 * grid.columnSpacing) + + GridLayout { + id: grid + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: grid.rowSpacing + anchors.rightMargin: grid.rowSpacing + anchors.topMargin: grid.columnSpacing + + columns: page.width < page.height ? 1 : 2 + + GroupBox { + title: "Button" + Layout.fillWidth: true + Layout.columnSpan: grid.columns + RowLayout { + anchors.fill: parent + Button { text: "OK"; isDefault: true } + Button { text: "Cancel" } + Item { Layout.fillWidth: true } + Button { + text: "Attach" + menu: Menu { + MenuItem { text: "Image" } + MenuItem { text: "Document" } + } + } + } + } + + GroupBox { + title: "CheckBox" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + CheckBox { text: "E-mail"; checked: true } + CheckBox { text: "Calendar"; checked: true } + CheckBox { text: "Contacts" } + } + } + + GroupBox { + title: "RadioButton" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + ExclusiveGroup { id: radioGroup } + RadioButton { text: "Portrait"; exclusiveGroup: radioGroup } + RadioButton { text: "Landscape"; exclusiveGroup: radioGroup } + RadioButton { text: "Automatic"; exclusiveGroup: radioGroup; checked: true } + } + } + + GroupBox { + title: "Switch" + Layout.fillWidth: true + Layout.columnSpan: grid.columns + ColumnLayout { + anchors.fill: parent + RowLayout { + Label { text: "Wi-Fi"; Layout.fillWidth: true } + Switch { checked: true } + } + RowLayout { + Label { text: "Bluetooth"; Layout.fillWidth: true } + Switch { checked: false } + } + } + } + } + } +} diff --git a/tests/ui/qml/InputPage.qml b/tests/ui/qml/InputPage.qml new file mode 100644 index 0000000000..cb1878d023 --- /dev/null +++ b/tests/ui/qml/InputPage.qml @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.2 + +ScrollView { + id: page + + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + Item { + id: content + + width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) + height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) + + ColumnLayout { + id: column + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: column.spacing + + GroupBox { + title: "TextField" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + TextField { placeholderText: "..."; Layout.fillWidth: true; z: 1 } + TextField { placeholderText: "Password"; echoMode: TextInput.Password; Layout.fillWidth: true } + } + } + + GroupBox { + title: "ComboBox" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + ComboBox { + model: Qt.fontFamilies() + Layout.fillWidth: true + } + ComboBox { + editable: true + model: ListModel { + id: listModel + ListElement { text: "Apple" } + ListElement { text: "Banana" } + ListElement { text: "Coconut" } + ListElement { text: "Orange" } + } + onAccepted: { + if (find(currentText) === -1) { + listModel.append({text: editText}) + currentIndex = find(editText) + } + } + Layout.fillWidth: true + } + } + } + + GroupBox { + title: "SpinBox" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + SpinBox { value: 99; Layout.fillWidth: true; z: 1 } + SpinBox { decimals: 2; Layout.fillWidth: true } + } + } + } + } +} diff --git a/tests/ui/qml/ProgressPage.qml b/tests/ui/qml/ProgressPage.qml new file mode 100644 index 0000000000..a1fa596f79 --- /dev/null +++ b/tests/ui/qml/ProgressPage.qml @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 1.2 + +ScrollView { + id: page + + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + Item { + id: content + + width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) + height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) + + ColumnLayout { + id: column + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: column.spacing + + GroupBox { + title: "ProgressBar" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + ProgressBar { indeterminate: true; Layout.fillWidth: true } + ProgressBar { value: slider.value; Layout.fillWidth: true } + } + } + + GroupBox { + title: "Slider" + Layout.fillWidth: true + ColumnLayout { + anchors.fill: parent + Slider { id: slider; value: 0.5; Layout.fillWidth: true } + } + } + + GroupBox { + title: "BusyIndicator" + Layout.fillWidth: true + BusyIndicator { running: true } + } + } + } +} diff --git a/tests/ui/qml/UI.js b/tests/ui/qml/UI.js new file mode 100644 index 0000000000..0286ac56a6 --- /dev/null +++ b/tests/ui/qml/UI.js @@ -0,0 +1,45 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +.pragma library + +var margin = 2 +var tabPosition = Qt.TopEdge +var label = "" diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp new file mode 100644 index 0000000000..2ad2fa639c --- /dev/null +++ b/tests/ui/src/main.cpp @@ -0,0 +1,347 @@ +// +// main.cpp +// tests/render-utils/src +// +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "OffscreenUi.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "MessageDialog.h" +#include "HifiMenu.h" + +class RateCounter { + std::vector times; + QElapsedTimer timer; +public: + RateCounter() { + timer.start(); + } + + void reset() { + times.clear(); + } + + unsigned int count() const { + return times.size() - 1; + } + + float elapsed() const { + if (times.size() < 1) { + return 0.0f; + } + float elapsed = *times.rbegin() - *times.begin(); + return elapsed; + } + + void increment() { + times.push_back(timer.elapsed() / 1000.0f); + } + + float rate() const { + if (elapsed() == 0.0f) { + return NAN; + } + return (float) count() / elapsed(); + } +}; + + +const QString & getQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/qml/")) + "/"; + qDebug() << "Qml Path: " << dir; + } + return dir; +} + +const QString & getTestQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Qml Test Path: " << dir; + } + return dir; +} + +// Create a simple OpenGL window that renders text in various ways +class QTestWindow : public QWindow, private QOpenGLFunctions { + Q_OBJECT + + QOpenGLContext * _context{ nullptr }; + QSize _size; + bool _altPressed{ false }; + RateCounter fps; + QTimer _timer; + int testQmlTexture{ 0 }; + +public: + QObject * rootMenu; + + QTestWindow() { + _timer.setInterval(1); + connect(&_timer, &QTimer::timeout, [=] { + draw(); + }); + + DependencyManager::set(); + setSurfaceType(QSurface::OpenGLSurface); + + QSurfaceFormat format; + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + format.setVersion(4, 1); + format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile); + format.setOption(QSurfaceFormat::DebugContext); + + setFormat(format); + + _context = new QOpenGLContext; + _context->setFormat(format); + if (!_context->create()) { + qFatal("Could not create OpenGL context"); + } + + show(); + makeCurrent(); + initializeOpenGLFunctions(); + + { + QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this); + logger->initialize(); // initializes in the current context, i.e. ctx + logger->enableMessages(); + connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { + qDebug() << debugMessage; + }); + // logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); + } + + qDebug() << (const char*)this->glGetString(GL_VERSION); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glClearColor(0.2f, 0.2f, 0.2f, 1); + glDisable(GL_DEPTH_TEST); + + MessageDialog::registerType(); + HifiMenu::registerType(); + + auto offscreenUi = DependencyManager::get(); + offscreenUi->create(_context); + connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { + offscreenUi->lockTexture(textureId); + assert(!glGetError()); + GLuint oldTexture = testQmlTexture; + testQmlTexture = textureId; + if (oldTexture) { + offscreenUi->releaseTexture(oldTexture); + } + }); + + makeCurrent(); + + offscreenUi->setProxyWindow(this); + setFramePosition(QPoint(-1000, 0)); + resize(QSize(800, 600)); + +#ifdef QML_CONTROL_GALLERY + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getTestQmlDir())); + offscreenUi->load(QUrl("main.qml")); +#else + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); + offscreenUi->load(QUrl("TestRoot.qml")); + offscreenUi->load(QUrl("RootMenu.qml")); + HifiMenu::load(); + QObject* menuObject = offscreenUi->getRootItem()->findChild("HifiMenu"); + HifiMenu* menu = offscreenUi->getRootItem()->findChild(); + menu->addMenu("", "File"); + menu->addItem("File", "Quit", []{ + QApplication::quit(); + }); + menu->addCheckableItem("File", "Toggle", false, [](bool toggled) { + qDebug() << "Toggle is " << toggled; + }); + menu->addMenu("", "Edit"); + menu->addItem("Edit", "Undo"); + menu->addItem("Edit", "Redo"); + menu->addItem("Edit", "Copy"); + menu->addItem("Edit", "Cut"); + menu->addItem("Edit", "Paste"); + menu->addMenu("", "Long Menu Name..."); +#endif + installEventFilter(offscreenUi.data()); + offscreenUi->resume(); + _timer.start(); + } + + virtual ~QTestWindow() { + DependencyManager::destroy(); + } + +private: + void draw() { + if (!isVisible()) { + return; + } + + makeCurrent(); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); + + renderQml(); + + _context->swapBuffers(this); + glFinish(); + + fps.increment(); + if (fps.elapsed() >= 2.0f) { + qDebug() << "FPS: " << fps.rate(); + fps.reset(); + } + } + + void makeCurrent() { + _context->makeCurrent(this); + } + + void renderQml(); + + void resizeWindow(const QSize & size) { + _size = size; + DependencyManager::get()->resize(_size); + } + + +protected: + void resizeEvent(QResizeEvent * ev) override { + resizeWindow(ev->size()); + } + + + void keyPressEvent(QKeyEvent *event) { + _altPressed = Qt::Key_Alt == event->key(); + switch (event->key()) { + case Qt::Key_L: + if (event->modifiers() & Qt::CTRL) { + auto offscreenUi = DependencyManager::get(); + HifiMenu * menu = offscreenUi->findChild(); + menu->addItem("", "Test 3"); + menu->addItem("File", "Test 3"); + } + break; + case Qt::Key_K: + if (event->modifiers() & Qt::CTRL) { + OffscreenUi::question("Message title", "Message contents", [](QMessageBox::Button b){ + qDebug() << b; + }); + } + break; + case Qt::Key_J: + if (event->modifiers() & Qt::CTRL) { + auto offscreenUi = DependencyManager::get(); + rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); + QMetaObject::invokeMethod(rootMenu, "popup"); + } + break; + } + QWindow::keyPressEvent(event); + } + QQmlContext* menuContext{ nullptr }; + void keyReleaseEvent(QKeyEvent *event) { + if (_altPressed && Qt::Key_Alt == event->key()) { + HifiMenu::toggle(); + } + } + + void moveEvent(QMoveEvent *event) { + static qreal oldPixelRatio = 0.0; + if (devicePixelRatio() != oldPixelRatio) { + oldPixelRatio = devicePixelRatio(); + resizeWindow(size()); + } + QWindow::moveEvent(event); + } +}; + +void QTestWindow::renderQml() { + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + if (testQmlTexture > 0) { + glEnable(GL_TEXTURE_2D); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, testQmlTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + glBegin(GL_QUADS); + { + glTexCoord2f(0, 0); + glVertex2f(-1, -1); + glTexCoord2f(0, 1); + glVertex2f(-1, 1); + glTexCoord2f(1, 1); + glVertex2f(1, 1); + glTexCoord2f(1, 0); + glVertex2f(1, -1); + } + glEnd(); +} + + +const char * LOG_FILTER_RULES = R"V0G0N( +*.debug=false +qt.quick.mouse.debug=false +)V0G0N"; + +//int main(int argc, char *argv[]) { +// QGuiApplication app(argc, argv); +// QQmlApplicationEngine engine; +// engine.setBaseUrl(QUrl::fromLocalFile(getQmlDir())); +// engine.load(QUrl("Main.qml")); +// return app.exec(); +//} + + + +int main(int argc, char** argv) { + QGuiApplication app(argc, argv); +// QLoggingCategory::setFilterRules(LOG_FILTER_RULES); + QTestWindow window; + app.exec(); + return 0; +} + +#include "main.moc"