diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index a622931db7..fd4e629568 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -16,8 +16,6 @@ Windows.Window { destroyOnCloseButton: false property alias source: webview.url - function raiseWindow() { Desktop.raise(root) } - Controls.WebView { id: webview url: "about:blank" diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 2a8d8f60d9..63efd0bc2e 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -21,16 +21,10 @@ Windows.Window { destroyOnCloseButton: false property alias source: pageLoader.source - function raiseWindow() { Desktop.raise(root) } - Loader { id: pageLoader objectName: "Loader" focus: true property var dialog: root - - Keys.onPressed: { - console.log("QmlWindow pageLoader keypress") - } } } // dialog diff --git a/interface/resources/qml/VrMenu.qml b/interface/resources/qml/VrMenu.qml deleted file mode 100644 index 689171f9e9..0000000000 --- a/interface/resources/qml/VrMenu.qml +++ /dev/null @@ -1,229 +0,0 @@ -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.VrMenu { - id: root - HifiConstants { id: hifi } - - anchors.fill: parent - - objectName: "VrMenu" - enabled: false - opacity: 0.0 - z: 10000 - - property int animationDuration: 200 - property var models: [] - property var columns: [] - - onEnabledChanged: { - if (enabled && columns.length == 0) { - pushColumn(rootMenu.items); - } - opacity = enabled ? 1.0 : 0.0 - offscreenFlags.navigationFocused = enabled; - } - - // 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 menuBuilder: Component { - VrMenuView { - property int menuDepth: root.models.length - 1 - model: root.models[menuDepth] - - function fit(position, size, maxposition) { - var padding = 8; - if (position < padding) { - position = padding; - } else if (position + size + padding > maxposition) { - position = maxposition - (size + padding); - } - return position; - } - - Component.onCompleted: { - if (menuDepth === 0) { - x = lastMousePosition.x - 20 - y = lastMousePosition.y - 20 - } else { - var lastColumn = root.columns[menuDepth - 1] - x = lastColumn.x + 64; - y = lastMousePosition.y - height / 2; - } - x = fit(x, width, parent.width); - y = fit(y, height, parent.height); - } - - onSelected: { - root.selectItem(menuDepth, item) - } - } - } - - function lastColumn() { - return columns[root.columns.length - 1]; - } - - function pushColumn(items) { - models.push(itemsToModel(items)) - if (columns.length) { - var oldColumn = lastColumn(); - //oldColumn.enabled = false - } - var newColumn = menuBuilder.createObject(root); - columns.push(newColumn); - forceActiveFocus(); - } - - function popColumn() { - if (columns.length > 0) { - var curColumn = columns.pop(); - 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 itemsToModel(items) { - var newListModel = Qt.createQmlObject('import QtQuick 2.2; ListModel {}', root); - for (var i = 0; i < items.length; ++i) { - var item = items[i]; - switch (item.type) { - case 2: - newListModel.append({"type":item.type, "name": item.title, "item": item}) - break; - case 1: - newListModel.append({"type":item.type, "name": item.text, "item": item}) - break; - case 0: - newListModel.append({"type":item.type, "name": "-----", "item": item}) - break; - } - } - return newListModel; - } - - function selectItem(depth, source) { - var popped = false; - while (depth + 1 < columns.length) { - popColumn() - popped = true - } - - switch (source.type) { - case 2: - lastColumn().enabled = false - pushColumn(source.items) - break; - case 1: - if (!popped) source.trigger() - enabled = false - break; - case 0: - break; - } - } - - function reset() { - while (columns.length > 0) { - popColumn(); - } - } - - MouseArea { - anchors.fill: parent - id: mouseArea - acceptedButtons: Qt.LeftButton | Qt.RightButton - onClicked: { - if (mouse.button == Qt.RightButton) { - root.popColumn(); - } else { - root.enabled = false; - } - } - } - - function addMenu(menu, newMenu) { - return menu.addMenu(newMenu); - } - - function addItem(menu, newMenuItem) { - return menu.addItem(newMenuItem); - } - - function insertItem(menu, beforeItem, newMenuItem) { - for (var i = 0; i < menu.items.length; ++i) { - if (menu.items[i] === beforeItem) { - return menu.insertItem(i, newMenuItem); - } - } - return addItem(menu, newMenuItem); - } - - function removeItem(menu, menuItem) { - menu.removeItem(menuItem); - } - - function previousItem() { - if (columns.length) { - lastColumn().incrementCurrentIndex() - } - } - - function nextItem() { - if (columns.length) { - lastColumn().decrementCurrentIndex() - } - } - - function selectCurrentItem() { - if (columns.length) { - var depth = columns.length - 1; - var index = lastColumn().currentIndex; - if (index >= 0) { - var model = models[depth]; - var item = model.get(index).item; - selectItem(depth, item); - } - } - } - - Keys.onDownPressed: previousItem(); - Keys.onUpPressed: nextItem(); - Keys.onSpacePressed: selectCurrentItem(); - Keys.onReturnPressed: selectCurrentItem(); - Keys.onRightPressed: selectCurrentItem(); - Keys.onLeftPressed: popColumn(); - Keys.onEscapePressed: popColumn(); -} diff --git a/interface/resources/qml/VrMenuView.qml b/interface/resources/qml/VrMenuView.qml deleted file mode 100644 index b00e21ba93..0000000000 --- a/interface/resources/qml/VrMenuView.qml +++ /dev/null @@ -1,77 +0,0 @@ -import QtQuick 2.4 -import QtQuick.Controls 1.3 -import QtQuick.Controls.Styles 1.3 - -import "styles" - -ListView { - id: root - HifiConstants { id: hifi } - width: 128 - height: count * 32 - onEnabledChanged: recalcSize(); - onVisibleChanged: recalcSize(); - onCountChanged: recalcSize(); - - signal selected(var item) - - highlight: Rectangle { - width: root.currentItem ? root.currentItem.width : 0 - height: root.currentItem ? root.currentItem.height : 0 - color: "lightsteelblue"; radius: 3 - } - - delegate: VrMenuItem { - text: name - source: item - onImplicitHeightChanged: root.recalcSize() - onImplicitWidthChanged: root.recalcSize() - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: root.currentIndex = index - onClicked: root.selected(item) - } - } - - function recalcSize() { - if (model.count !== count || !visible) { - return; - } - - var originalIndex = currentIndex; - var maxWidth = width; - var newHeight = 0; - for (var i = 0; i < count; ++i) { - currentIndex = i; - if (!currentItem) { - continue; - } - if (currentItem && currentItem.implicitWidth > maxWidth) { - maxWidth = currentItem.implicitWidth - } - if (currentItem.visible) { - newHeight += currentItem.implicitHeight - } - } - if (maxWidth > width) { - width = maxWidth; - } - if (newHeight > height) { - height = newHeight - } - currentIndex = originalIndex; - } - - Border { - id: border - anchors.fill: parent - anchors.margins: -8 - z: parent.z - 1 - border.color: hifi.colors.hifiBlue - color: hifi.colors.window - } -} - - diff --git a/interface/resources/qml/Root.qml b/interface/resources/qml/desktop/Desktop.qml similarity index 81% rename from interface/resources/qml/Root.qml rename to interface/resources/qml/desktop/Desktop.qml index e1431de967..ee3277e4c4 100644 --- a/interface/resources/qml/Root.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -2,74 +2,34 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs; -import "dialogs" +import "../dialogs" +import "../menus" // This is our primary 'desktop' object to which all VR dialogs and // windows will be childed. FocusScope { id: desktop anchors.fill: parent; - - // Debugging help for figuring out focus issues - property var offscreenWindow; - onOffscreenWindowChanged: offscreenWindow.activeFocusItemChanged.connect(onWindowFocusChanged); - function onWindowFocusChanged() { - console.log("Focus item is " + offscreenWindow.activeFocusItem); - var focusedItem = offscreenWindow.activeFocusItem ; - if (DebugQML && focusedItem) { - var rect = desktop.mapToItem(desktop, focusedItem.x, focusedItem.y, focusedItem.width, focusedItem.height); - focusDebugger.visible = true - focusDebugger.x = rect.x; - focusDebugger.y = rect.y; - focusDebugger.width = rect.width - focusDebugger.height = rect.height - } - } - - Rectangle { - id: focusDebugger; - z: 9999; visible: false; color: "red" - ColorAnimation on color { from: "#7fffff00"; to: "#7f0000ff"; duration: 1000; loops: 9999 } - } + objectName: "desktop" // Allows QML/JS to find the desktop through the parent chain property bool desktopRoot: true // The VR version of the primary menu - property var rootMenu: Menu { objectName: "rootMenu" } - - // The tool window, one instance - property alias toolWindow: toolWindow - ToolWindow { id: toolWindow } - - // FIXME support always on top flags - function raise(item) { - d.raiseWindow(item); - } - - Component { - id: messageDialogBuilder - MessageDialog { } - } - - Component { - id: nativeMessageDialogBuilder - OriginalDialogs.MessageDialog { } - } - - function messageBox(properties) { - // Debugging: native message dialog for comparison - // nativeMessageDialogBuilder.createObject(desktop, properties); - return messageDialogBuilder.createObject(desktop, properties); + property var rootMenu: Menu { + id: rootMenu; objectName: "rootMenu" + Component.onCompleted: { + console.log("ROOT_MENU " + rootMenu); + } } QtObject { id: d - readonly property int zBasisNormal: 0 readonly property int zBasisAlwaysOnTop: 4096 readonly property int zBasisModal: 8192 - + readonly property var messageDialogBuilder: Component { MessageDialog { } } + readonly property var nativeMessageDialogBuilder: Component { OriginalDialogs.MessageDialog { } } function findChild(item, name) { for (var i = 0; i < item.children.length; ++i) { @@ -203,6 +163,43 @@ FocusScope { } } + MenuMouseHandler { id: menuPopperUpper } + + function raise(item) { + d.raiseWindow(item); + } + + function messageBox(properties) { + // Debugging: native message dialog for comparison + // d.nativeMessageDialogBuilder.createObject(desktop, properties); + return d.messageDialogBuilder.createObject(desktop, properties); + } + + function popupMenu(point) { + menuPopperUpper.popup(desktop, rootMenu.items, point); + } + + function toggleMenu(point) { + menuPopperUpper.toggle(desktop, rootMenu.items, point); + } + + Keys.onEscapePressed: { + if (menuPopperUpper.closeLastMenu()) { + event.accepted = true; + return; + } + event.accepted = false; + } + + Keys.onLeftPressed: { + if (menuPopperUpper.closeLastMenu()) { + event.accepted = true; + return; + } + event.accepted = false; + } + + function unfocusWindows() { var windows = d.getTopLevelWindows(); for (var i = 0; i < windows.length; ++i) { @@ -210,6 +207,27 @@ FocusScope { } desktop.focus = true; } + + // Debugging help for figuring out focus issues + property var offscreenWindow; + onOffscreenWindowChanged: offscreenWindow.activeFocusItemChanged.connect(onWindowFocusChanged); + function onWindowFocusChanged() { + console.log("Focus item is " + offscreenWindow.activeFocusItem); + var focusedItem = offscreenWindow.activeFocusItem ; + if (DebugQML && focusedItem) { + var rect = desktop.mapToItem(null, focusedItem.x, focusedItem.y, focusedItem.width, focusedItem.height); + focusDebugger.visible = true + focusDebugger.x = rect.x; + focusDebugger.y = rect.y; + focusDebugger.width = rect.width + focusDebugger.height = rect.height + } + } + Rectangle { + id: focusDebugger; + z: 9999; visible: false; color: "red" + ColorAnimation on color { from: "#7fffff00"; to: "#7f0000ff"; duration: 1000; loops: 9999 } + } } diff --git a/interface/resources/qml/dialogs/RunningScripts.qml b/interface/resources/qml/dialogs/RunningScripts.qml index 0d771b79cd..7668d4e197 100644 --- a/interface/resources/qml/dialogs/RunningScripts.qml +++ b/interface/resources/qml/dialogs/RunningScripts.qml @@ -14,6 +14,7 @@ Window { resizable: true destroyOnInvisible: true x: 40; y: 40 + implicitWidth: 384; implicitHeight: 640 property var scripts: ScriptDiscoveryService; property var scriptsModel: scripts.scriptsModelFilter @@ -77,7 +78,7 @@ Window { } function loadFromFile() { - var fileDialog = fileDialogBuilder.createObject(Desktop, { filterModel: fileFilters }); + var fileDialog = fileDialogBuilder.createObject(desktop, { filterModel: fileFilters }); fileDialog.canceled.connect(function(){ console.debug("Cancelled file open") }) @@ -90,7 +91,7 @@ Window { Rectangle { color: "white" - implicitWidth: 384; implicitHeight: 640 + anchors.fill: parent Item { anchors { fill: parent; margins: 8 } diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml new file mode 100644 index 0000000000..1655a32989 --- /dev/null +++ b/interface/resources/qml/hifi/Desktop.qml @@ -0,0 +1,15 @@ +import QtQuick 2.5 + +import "../desktop" +import ".." + +Desktop { + id: desktop + + // The tool window, one instance + property alias toolWindow: toolWindow + ToolWindow { id: toolWindow } +} + + + diff --git a/interface/resources/qml/menus/MenuMouseHandler.qml b/interface/resources/qml/menus/MenuMouseHandler.qml new file mode 100644 index 0000000000..c73730773f --- /dev/null +++ b/interface/resources/qml/menus/MenuMouseHandler.qml @@ -0,0 +1,149 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "." + +Item { + id: root + property int zBasis: 8192 - 1024 + anchors.fill: parent + + MouseArea { + id: menuRoot; + anchors.fill: parent + enabled: d.topMenu !== null + onClicked: { + d.clearMenus(); + } + } + + QtObject { + id: d + property var menuStack: [] + property var topMenu: null; + property var modelMaker: Component { ListModel { } } + property var menuViewMaker: Component { + VrMenuView { + id: subMenu + onSelected: d.handleSelection(subMenu, currentItem, item) + } + } + + function toModel(items) { + var result = modelMaker.createObject(desktop); + for (var i = 0; i < items.length; ++i) { + var item = items[i]; + if (!item.visible) continue; + switch (item.type) { + case MenuItemType.Menu: + result.append({"name": item.title, "item": item}) + break; + case MenuItemType.Item: + result.append({"name": item.text, "item": item}) + break; + case MenuItemType.Separator: + result.append({"name": "", "item": item}) + break; + } + } + return result; + } + + function popMenu() { + if (menuStack.length) { + menuStack.pop().destroy(); + } + if (menuStack.length) { + topMenu = menuStack[menuStack.length - 1]; + topMenu.focus = true; + } else { + topMenu = null; + offscreenFlags.navigationFocused = false; + menuRoot.enabled = false; + } + } + + function pushMenu(newMenu) { + menuStack.push(newMenu); + topMenu = newMenu; + topMenu.focus = true; + offscreenFlags.navigationFocused = true; + } + + function clearMenus() { + while (menuStack.length) { + popMenu() + } + } + + function clampMenuPosition(menu) { + var margins = 0; + if (menu.x < margins) { + menu.x = margins + } else if ((menu.x + menu.width + margins) > root.width) { + menu.x = root.width - (menu.width + margins); + } + + if (menu.y < 0) { + menu.y = margins + } else if ((menu.y + menu.height + margins) > root.height) { + menu.y = root.height - (menu.height + margins); + } + } + + function buildMenu(items, targetPosition) { + var model = toModel(items); + var newMenu = menuViewMaker.createObject(menuRoot, { model: model, z: topMenu ? topMenu.z + 1 : zBasis }); + if (targetPosition) { + newMenu.x = targetPosition.x + newMenu.y = targetPosition.y - newMenu.height / 3 * 1 + } + clampMenuPosition(newMenu); + pushMenu(newMenu); + return newMenu; + } + + function handleSelection(parentMenu, selectedItem, item) { + while (topMenu && topMenu !== parentMenu) { + popMenu(); + } + + switch (item.type) { + case MenuItemType.Menu: + var target = Qt.vector2d(topMenu.x, topMenu.y).plus(Qt.vector2d(selectedItem.x + 96, selectedItem.y)); + buildMenu(item.items, target).objectName = item.title; + break; + + case MenuItemType.Item: + console.log("Triggering " + item.text) + item.trigger(); + clearMenus(); + break; + } + } + + } + + function popup(parent, items, point) { + d.clearMenus(); + menuRoot.enabled = true; + d.buildMenu(items, point); + } + + function toggle(parent, items, point) { + if (d.topMenu) { + d.clearMenus(); + return; + } + popup(parent, items, point); + } + + function closeLastMenu() { + if (d.menuStack.length) { + d.popMenu(); + return true; + } + return false; + } + +} diff --git a/interface/resources/qml/VrMenuItem.qml b/interface/resources/qml/menus/VrMenuItem.qml similarity index 94% rename from interface/resources/qml/VrMenuItem.qml rename to interface/resources/qml/menus/VrMenuItem.qml index 2b1a4a3b5a..c23a54a2c7 100644 --- a/interface/resources/qml/VrMenuItem.qml +++ b/interface/resources/qml/menus/VrMenuItem.qml @@ -1,16 +1,13 @@ import QtQuick 2.4 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 -import "controls" -import "styles" + +import "../controls" +import "../styles" Item { id: root - HifiConstants { - id: hifi - } - - // The model object + HifiConstants { id: hifi } property alias text: label.text property var source diff --git a/interface/resources/qml/menus/VrMenuView.qml b/interface/resources/qml/menus/VrMenuView.qml new file mode 100644 index 0000000000..bbb9bd706e --- /dev/null +++ b/interface/resources/qml/menus/VrMenuView.qml @@ -0,0 +1,100 @@ +import QtQuick 2.4 +import QtQuick.Controls 1.3 +import QtQuick.Controls.Styles 1.3 + +import "../styles" + + +FocusScope { + id: root + implicitHeight: border.height + implicitWidth: border.width + + property alias currentItem: listView.currentItem + property alias model: listView.model + signal selected(var item) + + + Border { + id: border + anchors.fill: listView + anchors.margins: -8 + border.color: hifi.colors.hifiBlue + color: hifi.colors.window + // color: "#7f7f7f7f" + } + + ListView { + id: listView + x: 8; y: 8 + HifiConstants { id: hifi } + width: 128 + height: count * 32 + onEnabledChanged: recalcSize(); + onVisibleChanged: recalcSize(); + onCountChanged: recalcSize(); + focus: true + + highlight: Rectangle { + width: listView.currentItem ? listView.currentItem.width : 0 + height: listView.currentItem ? listView.currentItem.height : 0 + color: "lightsteelblue"; radius: 3 + } + + delegate: VrMenuItem { + text: name + source: item + onImplicitHeightChanged: listView.recalcSize() + onImplicitWidthChanged: listView.recalcSize() + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: listView.currentIndex = index + onClicked: root.selected(item) + } + } + + function recalcSize() { + if (model.count !== count || !visible) { + return; + } + + var originalIndex = currentIndex; + var maxWidth = width; + var newHeight = 0; + for (var i = 0; i < count; ++i) { + currentIndex = i; + if (!currentItem) { + continue; + } + if (currentItem && currentItem.implicitWidth > maxWidth) { + maxWidth = currentItem.implicitWidth + } + if (currentItem.visible) { + newHeight += currentItem.implicitHeight + } + } + if (maxWidth > width) { + width = maxWidth; + } + if (newHeight > height) { + height = newHeight + } + currentIndex = originalIndex; + } + + function previousItem() { currentIndex = (currentIndex + count - 1) % count; } + function nextItem() { currentIndex = (currentIndex + count + 1) % count; } + function selectCurrentItem() { if (currentIndex != -1) root.selected(currentItem.source); } + + Keys.onUpPressed: previousItem(); + Keys.onDownPressed: nextItem(); + Keys.onSpacePressed: selectCurrentItem(); + Keys.onRightPressed: selectCurrentItem(); + Keys.onReturnPressed: selectCurrentItem(); + } +} + + + diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 6bc8a2bab5..25bb87deab 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -114,7 +114,7 @@ Fadable { function raise() { if (visible && parent) { - Desktop.raise(window) + desktop.raise(window) if (!focus) { focus = true; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fa730c5d40..88202ff2cb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -793,7 +793,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { cycleCamera(); } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { - VrMenu::toggle(); // show context menu even on non-stereo displays + offscreenUi->toggleMenu(_glWidget->mapFromGlobal(QCursor::pos())); } else if (action == controller::toInt(controller::Action::RETICLE_X)) { auto oldPos = QCursor::pos(); auto newPos = oldPos; @@ -1176,7 +1176,6 @@ void Application::initializeUi() { AddressBarDialog::registerType(); ErrorDialog::registerType(); LoginDialog::registerType(); - VrMenu::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); @@ -1186,7 +1185,7 @@ void Application::initializeUi() { offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to // support the window management and scripting proxies for VR use - offscreenUi->createDesktop(); + offscreenUi->createDesktop(QString("hifi/Desktop.qml")); // FIXME either expose so that dialogs can set this themselves or // do better detection in the offscreen UI of what has focus @@ -1244,8 +1243,6 @@ void Application::initializeUi() { rootContext->setContextProperty("Render", DependencyManager::get().data()); _glWidget->installEventFilter(offscreenUi.data()); - VrMenu::load(); - VrMenu::executeQueuedLambdas(); offscreenUi->setMouseTranslator([=](const QPointF& pt) { QPointF result = pt; auto displayPlugin = getActiveDisplayPlugin(); @@ -2063,7 +2060,8 @@ void Application::keyPressEvent(QKeyEvent* event) { void Application::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) { - VrMenu::toggle(); // show context menu even on non-stereo displays + auto offscreenUi = DependencyManager::get(); + offscreenUi->toggleMenu(_glWidget->mapFromGlobal(QCursor::pos())); } _keysPressed.remove(event->key()); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 32cb1131f2..813386132c 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -628,3 +628,30 @@ QQmlContext* OffscreenQmlSurface::getRootContext() { return _qmlEngine->rootContext(); } +Q_DECLARE_METATYPE(std::function); +static auto VoidLambdaType = qRegisterMetaType>(); +Q_DECLARE_METATYPE(std::function); +static auto VariantLambdaType = qRegisterMetaType>(); + + +void OffscreenQmlSurface::executeOnUiThread(std::function function, bool blocking ) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "executeOnUiThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, + Q_ARG(std::function, function)); + return; + } + + function(); +} + +QVariant OffscreenQmlSurface::returnFromUiThread(std::function function) { + if (QThread::currentThread() != thread()) { + QVariant result; + QMetaObject::invokeMethod(this, "returnFromUiThread", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVariant, result), + Q_ARG(std::function, function)); + return result; + } + + return function(); +} diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 608e811b4b..fb916178ad 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -45,6 +45,9 @@ public: return load(QUrl(qmlSourceFile), f); } + Q_INVOKABLE void executeOnUiThread(std::function function, bool blocking = false); + Q_INVOKABLE QVariant returnFromUiThread(std::function function); + void setMaxFps(uint8_t maxFps) { _maxFps = maxFps; } // Optional values for event handling void setProxyWindow(QWindow* window); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index e215fcc067..7849bf5b95 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -16,6 +16,8 @@ #include #include +#include "VrMenu.h" + // Needs to match the constants in resources/qml/Global.js class OffscreenFlags : public QObject { Q_OBJECT @@ -192,7 +194,7 @@ QMessageBox::StandardButton OffscreenUi::messageBox(QMessageBox::Icon icon, cons map.insert("buttons", buttons.operator int()); map.insert("defaultButton", defaultButton); QVariant result; - bool invokeResult = QMetaObject::invokeMethod(getDesktop(), "messageBox", + bool invokeResult = QMetaObject::invokeMethod(_desktop, "messageBox", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, QVariant::fromValue(map))); @@ -231,16 +233,22 @@ void OffscreenUi::setNavigationFocused(bool focused) { offscreenFlags->setNavigationFocused(focused); } -void OffscreenUi::createDesktop() { +void OffscreenUi::createDesktop(const QUrl& url) { if (_desktop) { qDebug() << "Desktop already created"; + return; } getRootContext()->setContextProperty("DebugQML", QVariant(false)); - _desktop = dynamic_cast(load("Root.qml")); + _desktop = dynamic_cast(load(url)); Q_ASSERT(_desktop); - getRootContext()->setContextProperty("Desktop", _desktop); + getRootContext()->setContextProperty("desktop", _desktop); + + // Enable focus debugging _desktop->setProperty("offscreenWindow", QVariant::fromValue(getWindow())); + _toolWindow = _desktop->findChild("ToolWindow"); + + new VrMenu(this); } QQuickItem* OffscreenUi::getDesktop() { @@ -251,38 +259,15 @@ QQuickItem* OffscreenUi::getToolWindow() { return _toolWindow; } -Q_DECLARE_METATYPE(std::function); -static auto VoidLambdaType = qRegisterMetaType>(); -Q_DECLARE_METATYPE(std::function); -static auto VariantLambdaType = qRegisterMetaType>(); - - -void OffscreenUi::executeOnUiThread(std::function function) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "executeOnUiThread", Qt::QueuedConnection, - Q_ARG(std::function, function)); - return; - } - - function(); -} - -QVariant OffscreenUi::returnFromUiThread(std::function function) { - if (QThread::currentThread() != thread()) { - QVariant result; - QMetaObject::invokeMethod(this, "returnFromUiThread", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QVariant, result), - Q_ARG(std::function, function)); - return result; - } - - return function(); -} - void OffscreenUi::unfocusWindows() { bool invokeResult = QMetaObject::invokeMethod(_desktop, "unfocusWindows"); Q_ASSERT(invokeResult); } +void OffscreenUi::toggleMenu(const QPoint& screenPosition) { + auto virtualPos = mapToVirtualScreen(screenPosition, nullptr); + QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, virtualPos)); +} + #include "OffscreenUi.moc" diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index fa4d7aaeaf..fd54bc6c2b 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -27,18 +27,18 @@ class OffscreenUi : public OffscreenQmlSurface, public Dependency { public: OffscreenUi(); virtual void create(QOpenGLContext* context) override; - void createDesktop(); + void createDesktop(const QUrl& url); 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*) {}); bool shouldSwallowShortcut(QEvent* event); bool navigationFocused(); void setNavigationFocused(bool focused); void unfocusWindows(); + void toggleMenu(const QPoint& screenCoordinates); + QQuickItem* getDesktop(); QQuickItem* getToolWindow(); - Q_INVOKABLE void executeOnUiThread(std::function function); - Q_INVOKABLE QVariant returnFromUiThread(std::function function); /// Same design as QMessageBox::critical(), will block, returns result static QMessageBox::StandardButton critical(void* ignored, const QString& title, const QString& text, diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 4a827f57db..3874b85a12 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -345,7 +345,7 @@ void QmlWindowClass::hasClosed() { } void QmlWindowClass::raise() { - QMetaObject::invokeMethod(asQuickItem(), "raiseWindow", Qt::QueuedConnection); + QMetaObject::invokeMethod(asQuickItem(), "raise", Qt::QueuedConnection); } #include "QmlWindowClass.moc" diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 2d9dd57ba9..103eb7660e 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -36,7 +36,6 @@ public: private: MenuUserData(const MenuUserData&); - void init(QObject* widgetObject, QObject* qmlObject) { widgetObject->setUserData(USER_DATA_ID, this); qmlObject->setUserData(USER_DATA_ID, this); @@ -48,14 +47,6 @@ private: const int MenuUserData::USER_DATA_ID = QObject::registerUserData(); -HIFI_QML_DEF_LAMBDA(VrMenu, [&](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); -}); - VrMenu* VrMenu::_instance{ nullptr }; static QQueue> queuedLambdas; @@ -70,19 +61,18 @@ void VrMenu::executeOrQueue(std::function f) { } } -void VrMenu::executeQueuedLambdas() { - Q_ASSERT(_instance); + +VrMenu::VrMenu(QObject* parent) : QObject(parent) { + _instance = this; + auto offscreenUi = DependencyManager::get(); + _rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); + offscreenUi->getRootContext()->setContextProperty("rootMenu", _rootMenu); foreach(std::function f, queuedLambdas) { - f(_instance); + f(this); } queuedLambdas.clear(); } -VrMenu::VrMenu(QQuickItem* parent) : QQuickItem(parent) { - _instance = this; - this->setEnabled(false); -} - QObject* VrMenu::findMenuObject(const QString& menuOption) { if (menuOption.isEmpty()) { return _rootMenu; @@ -91,10 +81,6 @@ QObject* VrMenu::findMenuObject(const QString& menuOption) { return result; } -void VrMenu::setRootMenu(QObject* rootMenu) { - _rootMenu = rootMenu; -} - void updateQmlItemFromAction(QObject* target, QAction* source) { target->setProperty("checkable", source->isCheckable()); target->setProperty("enabled", source->isEnabled()); @@ -116,9 +102,8 @@ void VrMenu::addMenu(QMenu* menu) { Q_ASSERT(false); } QVariant returnedValue; - bool invokeResult = QMetaObject::invokeMethod(this, "addMenu", Qt::DirectConnection, + bool invokeResult = QMetaObject::invokeMethod(qmlParent, "addMenu", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue), - Q_ARG(QVariant, QVariant::fromValue(qmlParent)), Q_ARG(QVariant, QVariant::fromValue(menu->title()))); Q_ASSERT(invokeResult); Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x @@ -147,21 +132,24 @@ void bindActionToQmlAction(QObject* qmlAction, QAction* action) { QObject::connect(qmlAction, SIGNAL(triggered()), action, SLOT(trigger())); } +class QQuickMenuItem; + void VrMenu::addAction(QMenu* menu, QAction* action) { Q_ASSERT(!MenuUserData::forObject(action)); Q_ASSERT(MenuUserData::forObject(menu)); MenuUserData* userData = MenuUserData::forObject(menu); QObject* menuQml = findMenuObject(userData->uuid.toString()); Q_ASSERT(menuQml); - QVariant returnedValue; + QQuickMenuItem* returnedValue { nullptr }; - bool invokeResult = QMetaObject::invokeMethod(this, "addItem", Qt::DirectConnection, - Q_RETURN_ARG(QVariant, returnedValue), - Q_ARG(QVariant, QVariant::fromValue(menuQml)), - Q_ARG(QVariant, QVariant::fromValue(action->text()))); + qDebug() << menuQml; + bool invokeResult = QMetaObject::invokeMethod(menuQml, "addItem", Qt::DirectConnection, + Q_RETURN_ARG(QQuickMenuItem*, returnedValue), + Q_ARG(QString, action->text())); + Q_ASSERT(invokeResult); Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x - QObject* result = returnedValue.value(); + QObject* result = reinterpret_cast(returnedValue); // returnedValue.value(); Q_ASSERT(result); // Bind the QML and Widget together bindActionToQmlAction(result, action); @@ -175,19 +163,19 @@ void VrMenu::insertAction(QAction* before, QAction* action) { beforeQml = findMenuObject(beforeUserData->uuid.toString()); } QObject* menu = beforeQml->parent(); - QVariant returnedValue; - bool invokeResult = QMetaObject::invokeMethod(this, "insertItem", Qt::DirectConnection, - Q_RETURN_ARG(QVariant, returnedValue), - Q_ARG(QVariant, QVariant::fromValue(menu)), - Q_ARG(QVariant, QVariant::fromValue(beforeQml)), - Q_ARG(QVariant, QVariant::fromValue(action->text()))); + QQuickMenuItem* returnedValue { nullptr }; + // FIXME this needs to find the index of the beforeQml item and call insertItem(int, object) + bool invokeResult = QMetaObject::invokeMethod(menu, "addItem", Qt::DirectConnection, + Q_RETURN_ARG(QQuickMenuItem*, returnedValue), + Q_ARG(QString, action->text())); Q_ASSERT(invokeResult); - Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x - QObject* result = returnedValue.value(); + QObject* result = reinterpret_cast(returnedValue); // returnedValue.value(); Q_ASSERT(result); bindActionToQmlAction(result, action); } +class QQuickMenuBase; + void VrMenu::removeAction(QAction* action) { if (!action) { qWarning("Attempted to remove invalid menu action"); @@ -198,12 +186,12 @@ void VrMenu::removeAction(QAction* action) { qWarning("Attempted to remove menu action with no found QML object"); return; } + QObject* item = findMenuObject(userData->uuid.toString()); QObject* menu = item->parent(); // Proxy QuickItem requests through the QML layer - bool invokeResult = QMetaObject::invokeMethod(this, "removeItem", Qt::DirectConnection, - Q_ARG(QVariant, QVariant::fromValue(menu)), - Q_ARG(QVariant, QVariant::fromValue(item))); + QQuickMenuBase* qmlItem = reinterpret_cast(item); + bool invokeResult = QMetaObject::invokeMethod(menu, "removeItem", Qt::DirectConnection, + Q_ARG(QQuickMenuBase*, qmlItem)); Q_ASSERT(invokeResult); - Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x } diff --git a/libraries/ui/src/VrMenu.h b/libraries/ui/src/VrMenu.h index 38e3b54739..b092b63566 100644 --- a/libraries/ui/src/VrMenu.h +++ b/libraries/ui/src/VrMenu.h @@ -21,21 +21,16 @@ #include "OffscreenUi.h" // FIXME break up the rendering code (VrMenu) and the code for mirroring a Widget based menu in QML -class VrMenu : public QQuickItem { +class VrMenu : public QObject { Q_OBJECT - HIFI_QML_DECL_LAMBDA - public: static void executeOrQueue(std::function f); - static void executeQueuedLambdas(); - VrMenu(QQuickItem* parent = nullptr); + VrMenu(QObject* parent = nullptr); void addMenu(QMenu* menu); void addAction(QMenu* parent, QAction* action); void insertAction(QAction* before, QAction* action); void removeAction(QAction* action); - void setRootMenu(QObject* rootMenu); - protected: QObject* _rootMenu{ nullptr }; QObject* findMenuObject(const QString& name); diff --git a/interface/resources/qml/ScrollingGraph.qml b/tests/ui/qml/ScrollingGraph.qml similarity index 100% rename from interface/resources/qml/ScrollingGraph.qml rename to tests/ui/qml/ScrollingGraph.qml diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 0acb88c36d..ae9d93e0a6 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -6,6 +6,8 @@ import Qt.labs.settings 1.0 import "../../../interface/resources/qml" import "../../../interface/resources/qml/windows" import "../../../interface/resources/qml/dialogs" +import "../../../interface/resources/qml/hifi" +import "../../../interface/resources/qml/hifi/dialogs" ApplicationWindow { id: appWindow @@ -36,7 +38,7 @@ ApplicationWindow { return newListModel; } - Root { + Desktop { id: desktop anchors.fill: parent StubMenu { id: stubMenu } @@ -70,28 +72,6 @@ ApplicationWindow { blue.enabled = !blue.enabled } } -// Button { -// text: "add web tab" -// onClicked: { -// testButtons.urls.push("http://slashdot.org?" + testButtons.count++); -// testButtons.tabs.push(desktop.toolWindow.addWebTab({ title: "test", source: testButtons.urls[testButtons.urls.length - 1], width: 500, height: 720 })) -// } -// } -// Button { -// text: "toggle tab visible" -// onClicked: { -// var lastUrl = testButtons.urls[testButtons.urls.length - 1]; -// var tab = desktop.toolWindow.findTabForUrl(lastUrl); -// desktop.toolWindow.showTabForUrl(lastUrl, !tab.enabled) -// } -// } -// Button { -// text: "Remove last tab" -// onClicked: { -// testButtons.tabs.pop(); -// desktop.toolWindow.removeTabForUrl(testButtons.urls.pop()); -// } -// } Button { text: "Show Long Error" onClicked: { @@ -152,6 +132,11 @@ ApplicationWindow { console.log(appWindow.activeFocusItem); } } + Button { + text: "Preferences" + property var preferencesComponent: Component { PreferencesDialog { } } + onClicked: preferencesComponent.createObject(desktop); + } } Window { diff --git a/tests/ui/qmlscratch.pro b/tests/ui/qmlscratch.pro index e1fbb00ca6..34eca04c32 100644 --- a/tests/ui/qmlscratch.pro +++ b/tests/ui/qmlscratch.pro @@ -80,5 +80,10 @@ DISTFILES += \ ../../interface/resources/qml/VrMenu.qml \ ../../interface/resources/qml/VrMenuItem.qml \ ../../interface/resources/qml/VrMenuView.qml \ - ../../interface/resources/qml/WebEntity.qml + ../../interface/resources/qml/WebEntity.qml \ + ../../interface/resources/qml/desktop/Desktop.qml \ + ../../interface/resources/qml/hifi/Desktop.qml \ + ../../interface/resources/qml/menus/MenuMouseHandler.qml \ + ../../interface/resources/qml/menus/VrMenuItem.qml \ + ../../interface/resources/qml/menus/VrMenuView.qml