From b6272b7824e6fb8409682751b3bf47e74dfd42c7 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Jan 2016 20:41:57 -0800 Subject: [PATCH] Migrate tool window to overlay/QML --- examples/edit.js | 6 +- examples/example/games/planky.js | 2 +- examples/html/entityList.html | 426 ++--- examples/html/entityProperties.html | 1520 +++++++++-------- examples/html/eventBridgeLoader.js | 6 +- examples/html/gridControls.html | 202 +-- examples/libraries/entityList.js | 4 +- examples/libraries/gridTool.js | 4 +- interface/resources/qml/Global.js | 35 +- interface/resources/qml/QmlWebWindow.qml | 82 +- interface/resources/qml/QmlWindow.qml | 40 +- interface/resources/qml/Root.qml | 25 +- interface/resources/qml/ToolWindow.qml | 126 ++ interface/resources/qml/controls/VrDialog.qml | 4 +- interface/resources/qml/controls/WebView.qml | 50 + .../resources/qml/dialogs/FileDialog.qml | 232 +++ interface/resources/qml/test/Stubs.qml | 311 ++++ .../resources/qml/windows/DefaultFrame.qml | 141 +- interface/resources/qml/windows/Window.qml | 197 +-- interface/src/Application.cpp | 7 +- interface/src/Application.h | 5 - interface/src/scripting/WebWindowClass.cpp | 70 +- interface/src/scripting/WebWindowClass.h | 3 +- .../scripting/WindowScriptingInterface.cpp | 5 +- .../src/scripting/WindowScriptingInterface.h | 6 +- interface/src/ui/DialogsManager.cpp | 3 +- interface/src/ui/HMDToolsDialog.cpp | 3 - interface/src/ui/PreferencesDialog.cpp | 3 +- interface/src/ui/PreferencesDialog.h | 4 +- interface/src/ui/ToolWindow.cpp | 145 -- interface/src/ui/ToolWindow.h | 44 - libraries/gl/src/gl/OffscreenQmlSurface.cpp | 15 +- libraries/ui/src/OffscreenUi.cpp | 46 +- libraries/ui/src/OffscreenUi.h | 12 + libraries/ui/src/QmlWebWindowClass.cpp | 16 +- libraries/ui/src/QmlWindowClass.cpp | 199 ++- libraries/ui/src/QmlWindowClass.h | 8 +- 37 files changed, 2313 insertions(+), 1694 deletions(-) create mode 100644 interface/resources/qml/ToolWindow.qml create mode 100644 interface/resources/qml/controls/WebView.qml create mode 100644 interface/resources/qml/dialogs/FileDialog.qml create mode 100644 interface/resources/qml/test/Stubs.qml delete mode 100644 interface/src/ui/ToolWindow.cpp delete mode 100644 interface/src/ui/ToolWindow.h diff --git a/examples/edit.js b/examples/edit.js index 6d77aa2f11..41f25cb2e5 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -1502,7 +1502,11 @@ PropertiesTool = function(opts) { var that = {}; var url = Script.resolvePath('html/entityProperties.html'); - var webView = new WebWindow('Entity Properties', url, 200, 280, true); + var webView = new OverlayWebWindow({ + title: 'Entity Properties', + source: url, + toolWindow: true + }); var visible = false; diff --git a/examples/example/games/planky.js b/examples/example/games/planky.js index 8abc697353..78e7bf9cbe 100644 --- a/examples/example/games/planky.js +++ b/examples/example/games/planky.js @@ -51,7 +51,7 @@ SettingsWindow = function() { this.plankyStack = null; this.webWindow = null; this.init = function(plankyStack) { - _this.webWindow = new WebWindow('Planky', Script.resolvePath('../../html/plankySettings.html'), 255, 500, true); + _this.webWindow = new OverlayWebWindow('Planky', Script.resolvePath('../../html/plankySettings.html'), 255, 500, true); _this.webWindow.setVisible(false); _this.webWindow.eventBridge.webEventReceived.connect(_this.onWebEventReceived); _this.plankyStack = plankyStack; diff --git a/examples/html/entityList.html b/examples/html/entityList.html index 3a1eeedf95..ad10a50c01 100644 --- a/examples/html/entityList.html +++ b/examples/html/entityList.html @@ -2,6 +2,8 @@ + + diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index ff72e95313..daf85664fb 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -5,6 +5,8 @@ + + diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js index b62e7d9384..ebfb6dc740 100644 --- a/examples/html/eventBridgeLoader.js +++ b/examples/html/eventBridgeLoader.js @@ -8,6 +8,8 @@ // void scriptEventReceived(const QString& data); // +var EventBridge; + EventBridgeConnectionProxy = function(parent) { this.parent = parent; this.realSignal = this.parent.realBridge.scriptEventReceived @@ -46,12 +48,10 @@ openEventBridge = function(callback) { socket.onopen = function() { channel = new QWebChannel(socket, function(channel) { console.log("Document url is " + document.URL); - for(var key in channel.objects){ - console.log("registered object: " + key); - } var webWindow = channel.objects[document.URL.toLowerCase()]; console.log("WebWindow is " + webWindow) eventBridgeProxy = new EventBridgeProxy(webWindow); + EventBridge = eventBridgeProxy; if (callback) { callback(eventBridgeProxy); } }); } diff --git a/examples/html/gridControls.html b/examples/html/gridControls.html index 941a4b5c2a..1a3e949446 100644 --- a/examples/html/gridControls.html +++ b/examples/html/gridControls.html @@ -1,110 +1,114 @@ + + diff --git a/examples/libraries/entityList.js b/examples/libraries/entityList.js index 1aa08fbe2d..b37ba58737 100644 --- a/examples/libraries/entityList.js +++ b/examples/libraries/entityList.js @@ -4,7 +4,9 @@ EntityListTool = function(opts) { var that = {}; var url = ENTITY_LIST_HTML_URL; - var webView = new WebWindow('Entities', url, 200, 280, true); + var webView = new OverlayWebWindow({ + title: 'Entities', source: url, toolWindow: true + }); var searchRadius = 100; diff --git a/examples/libraries/gridTool.js b/examples/libraries/gridTool.js index 35d9858ace..54f80e9c96 100644 --- a/examples/libraries/gridTool.js +++ b/examples/libraries/gridTool.js @@ -231,7 +231,9 @@ GridTool = function(opts) { var listeners = []; var url = GRID_CONTROLS_HTML_URL; - var webView = new WebWindow('Grid', url, 200, 280, true); + var webView = new OverlayWebWindow({ + title: 'Grid', source: url, toolWindow: true + }); horizontalGrid.addListener(function(data) { webView.eventBridge.emitScriptEvent(JSON.stringify(data)); diff --git a/interface/resources/qml/Global.js b/interface/resources/qml/Global.js index fadf6d6c71..3ad6352af6 100644 --- a/interface/resources/qml/Global.js +++ b/interface/resources/qml/Global.js @@ -10,18 +10,22 @@ function findChild(item, name) { return null; } -function findParent(item, name) { +function findParentMatching(item, predicate) { while (item) { - if (item.objectName === name) { - return item; + if (predicate(item)) { + break; } item = item.parent; } - return null; + return item; } -function getDesktop(item) { - return findParent(item, OFFSCREEN_ROOT_OBJECT_NAME); +function findParentByName(item, name) { + return findParentMatching(item, function(item) { + var testName = name; + var result = (item.name === testName); + return result; + }); } function findRootMenu(item) { @@ -29,6 +33,13 @@ function findRootMenu(item) { return item ? item.rootMenu : null; } +function isDesktop(item) { + return item.desktopRoot; +} + +function isTopLevelWindow(item) { + return item.topLevelWindow; +} function getTopLevelWindows(item) { var desktop = getDesktop(item); @@ -40,8 +51,7 @@ function getTopLevelWindows(item) { for (var i = 0; i < desktop.children.length; ++i) { var child = desktop.children[i]; - if ((Global.OFFSCREEN_WINDOW_OBJECT_NAME === child.objectName) || - child[Global.OFFSCREEN_WINDOW_OBJECT_NAME]) { + if (isTopLevelWindow(child)) { var windowId = child.toString(); currentWindows.push(child) } @@ -50,9 +60,12 @@ function getTopLevelWindows(item) { } +function getDesktop(item) { + return findParentMatching(item, isDesktop); +} + function getDesktopWindow(item) { - item = findParent(item, OFFSCREEN_WINDOW_OBJECT_NAME); - return item; + return findParentMatching(item, isTopLevelWindow) } function closeWindow(item) { @@ -142,7 +155,7 @@ function raiseWindow(item) { var desktop = getDesktop(targetWindow); if (!desktop) { - //console.warn("Could not find desktop for window " + targetWindow); + console.warn("Could not find desktop for window " + targetWindow); return; } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 008aaeccc3..a622931db7 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -2,86 +2,26 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebEngine 1.1 -import "controls" +import "windows" as Windows +import "controls" as Controls import "styles" -VrDialog { +Windows.Window { id: root HifiConstants { id: hifi } title: "WebWindow" resizable: true - enabled: false visible: false // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false - contentImplicitWidth: clientArea.implicitWidth - contentImplicitHeight: clientArea.implicitHeight - backgroundColor: "#7f000000" - property url source: "about:blank" + property alias source: webview.url - signal navigating(string url) - function stop() { - webview.stop(); + function raiseWindow() { Desktop.raise(root) } + + Controls.WebView { + id: webview + url: "about:blank" + anchors.fill: parent + focus: true } - - Component.onCompleted: { - // Ensure the JS from the web-engine makes it to our logging - webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); - }); - - } - - Item { - id: clientArea - implicitHeight: 600 - implicitWidth: 800 - x: root.clientX - y: root.clientY - width: root.clientWidth - height: root.clientHeight - - WebEngineView { - id: webview - url: root.source - anchors.fill: parent - focus: true - - property var originalUrl - property var lastFixupTime: 0 - - onUrlChanged: { - var currentUrl = url.toString(); - var newUrl = urlHandler.fixupUrl(currentUrl).toString(); - if (newUrl != currentUrl) { - var now = new Date().valueOf(); - if (url === originalUrl && (now - lastFixupTime < 100)) { - console.warn("URL fixup loop detected") - return; - } - originalUrl = url - lastFixupTime = now - url = newUrl; - } - } - - onLoadingChanged: { - // Required to support clicking on "hifi://" links - if (WebEngineView.LoadStartedStatus == loadRequest.status) { - var url = loadRequest.url.toString(); - if (urlHandler.canHandleUrl(url)) { - if (urlHandler.handleUrl(url)) { - webview.stop(); - } - } - } - } - - profile: WebEngineProfile { - id: webviewProfile - httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" - storageName: "qmlWebEngine" - } - } - } // item } // dialog diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index f8217371e7..2a8d8f60d9 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -5,52 +5,32 @@ import QtWebChannel 1.0 import QtWebSockets 1.0 import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel -import "Global.js" as Global - +import "windows" as Windows import "controls" import "styles" -VrDialog { +Windows.Window { id: root HifiConstants { id: hifi } title: "QmlWindow" resizable: true - enabled: false visible: false focus: true property var channel; - // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false - contentImplicitWidth: clientArea.implicitWidth - contentImplicitHeight: clientArea.implicitHeight property alias source: pageLoader.source - function raiseWindow() { - Global.raiseWindow(root) - } + function raiseWindow() { Desktop.raise(root) } - Item { - id: clientArea - implicitHeight: 600 - implicitWidth: 800 - x: root.clientX - y: root.clientY - width: root.clientWidth - height: root.clientHeight + Loader { + id: pageLoader + objectName: "Loader" focus: true - clip: true - - Loader { - id: pageLoader - objectName: "Loader" - anchors.fill: parent - focus: true - property var dialog: root + property var dialog: root - Keys.onPressed: { - console.log("QmlWindow pageLoader keypress") - } + Keys.onPressed: { + console.log("QmlWindow pageLoader keypress") } - } // item + } } // dialog diff --git a/interface/resources/qml/Root.qml b/interface/resources/qml/Root.qml index 247947b72c..08e75b7cce 100644 --- a/interface/resources/qml/Root.qml +++ b/interface/resources/qml/Root.qml @@ -7,22 +7,25 @@ import "Global.js" as Global // windows will be childed. Item { id: desktop - objectName: Global.OFFSCREEN_ROOT_OBJECT_NAME anchors.fill: parent; + onParentChanged: forceActiveFocus(); + + // 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" } + + // List of all top level windows property var windows: []; - property var rootMenu: Menu { - objectName: "rootMenu" - } + onChildrenChanged: windows = Global.getTopLevelWindows(desktop); - onChildrenChanged: { - windows = Global.getTopLevelWindows(desktop); - } - - onParentChanged: { - forceActiveFocus(); - } + // The tool window, one instance + property alias toolWindow: toolWindow + ToolWindow { id: toolWindow } function raise(item) { Global.raiseWindow(item); } + } diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml new file mode 100644 index 0000000000..3ce5b5616d --- /dev/null +++ b/interface/resources/qml/ToolWindow.qml @@ -0,0 +1,126 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebEngine 1.1 + +import Qt.labs.settings 1.0 + +import "windows" as Windows +import "controls" as Controls + +Windows.Window { + id: toolWindow + resizable: true + objectName: "ToolWindow" + destroyOnCloseButton: false + destroyOnInvisible: false + visible: false + property string newTabSource + property alias tabView: tabView + onParentChanged: { + x = desktop.width / 2 - width / 2; + y = desktop.height / 2 - height / 2; + } + + Settings { + category: "ToolWindow.Position" + property alias x: toolWindow.x + property alias y: toolWindow.y + } + + property var webTabCreator: Component { + Controls.WebView { + id: webView + property string originalUrl; + + // Both toolWindow.newTabSource and url can change, so we need + // to store the original url here, without creating any bindings + Component.onCompleted: { + originalUrl = toolWindow.newTabSource; + url = originalUrl; + } + } + } + + TabView { + id: tabView; width: 384; height: 640; + onCountChanged: { + if (0 == count) { + toolWindow.visible = false + } + } + } + + function updateVisiblity() { + var newVisible = false + console.log("Checking " + tabView.count + " children") + for (var i = 0; i < tabView.count; ++i) { + if (tabView.getTab(i).enabled) { + console.log("Tab " + i + " enabled"); + newVisible = true; + break; + } + } + console.log("Setting toolWindow visible: " + newVisible); + visible = newVisible + } + + function findIndexForUrl(source) { + for (var i = 0; i < tabView.count; ++i) { + var tab = tabView.getTab(i); + if (tab && tab.item && tab.item.originalUrl && + tab.item.originalUrl === source) { + return i; + } + } + return -1; + } + + function removeTabForUrl(source) { + var index = findIndexForUrl(source); + if (index < 0) { + console.warn("Could not find tab for " + source); + return; + } + tabView.removeTab(index); + console.log("Updating visibility based on child tab removed"); + updateVisiblity(); + } + + function addWebTab(properties) { + if (!properties.source) { + console.warn("Attempted to open Web Tool Pane without URl") + return; + } + + var existingTabIndex = findIndexForUrl(properties.source); + if (existingTabIndex >= 0) { + console.log("Existing tab " + existingTabIndex + " found with URL " + properties.source) + return tabView.getTab(existingTabIndex); + } + + var title = properties.title || "Unknown"; + newTabSource = properties.source; + console.log(typeof(properties.source)) + var newTab = tabView.addTab(title, webTabCreator); + newTab.active = true; + newTab.enabled = false; + + if (properties.width) { + tabView.width = Math.min(Math.max(tabView.width, properties.width), + toolWindow.maxSize.x); + } + + if (properties.height) { + tabView.height = Math.min(Math.max(tabView.height, properties.height), + toolWindow.maxSize.y); + } + + console.log("Updating visibility based on child tab added"); + newTab.enabledChanged.connect(function() { + console.log("Updating visibility based on child tab enabled change"); + updateVisiblity(); + }) + updateVisiblity(); + return newTab + } +} diff --git a/interface/resources/qml/controls/VrDialog.qml b/interface/resources/qml/controls/VrDialog.qml index 18cab04533..d4568cded1 100644 --- a/interface/resources/qml/controls/VrDialog.qml +++ b/interface/resources/qml/controls/VrDialog.qml @@ -3,8 +3,6 @@ import QtQuick.Controls 1.2 import "." import "../styles" -import "../Global.js" as Global - /* * 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 @@ -48,7 +46,7 @@ DialogBase { if (enabled) { visible = true; if (root.parent) { - Global.raiseWindow(root); + Desktop.raise(root); } } } diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml new file mode 100644 index 0000000000..285aa80d4e --- /dev/null +++ b/interface/resources/qml/controls/WebView.qml @@ -0,0 +1,50 @@ +import QtQuick 2.5 +import QtWebEngine 1.1 + +WebEngineView { + id: root + property var originalUrl + property int lastFixupTime: 0 + + Component.onCompleted: { + console.log("Connecting JS messaging to Hifi Logging") + // Ensure the JS from the web-engine makes it to our logging + root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); + }); + } + + + onUrlChanged: { + var currentUrl = url.toString(); + var newUrl = urlHandler.fixupUrl(currentUrl).toString(); + if (newUrl != currentUrl) { + var now = new Date().valueOf(); + if (url === originalUrl && (now - lastFixupTime < 100)) { + console.warn("URL fixup loop detected") + return; + } + originalUrl = url + lastFixupTime = now + url = newUrl; + } + } + + onLoadingChanged: { + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + profile: WebEngineProfile { + id: webviewProfile + httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" + storageName: "qmlWebEngine" + } +} diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml new file mode 100644 index 0000000000..6711475d23 --- /dev/null +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -0,0 +1,232 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.4 +import Qt.labs.folderlistmodel 2.1 +import Qt.labs.settings 1.0 + +import ".." +import "." + +// Work in progress.... +DialogBase { + id: root + Constants { id: vr } + property string settingsName: "" + + signal selectedFile(var file); + signal canceled(); + + function selectCurrentFile() { + var row = tableView.currentRow + console.log("Selecting row " + row) + var fileName = folderModel.get(row, "fileName"); + if (fileName === "..") { + folderModel.folder = folderModel.parentFolder + } else if (folderModel.isFolder(row)) { + folderModel.folder = folderModel.get(row, "fileURL"); + } else { + selectedFile(folderModel.get(row, "fileURL")); + enabled = false + } + + } + + property var folderModel: FolderListModel { + id: folderModel + showDotAndDotDot: true + showDirsFirst: true + folder: "file:///C:/"; + onFolderChanged: tableView.currentRow = 0 + } + + property var filterModel: ListModel { + ListElement { + text: "All Files (*.*)" + filter: "*" + } + ListElement { + text: "Javascript Files (*.js)" + filter: "*.js" + } + ListElement { + text: "QML Files (*.qml)" + filter: "*.qml" + } + } + + Settings { + // fixme, combine with a property to allow different saved locations + category: "FileOpenLastFolder." + settingsName + property alias folder: folderModel.folder + } + + Rectangle { + id: currentDirectoryWrapper + anchors.left: parent.left + anchors.leftMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.top: parent.top + anchors.topMargin: 8 + height: currentDirectory.implicitHeight + 8 + radius: vr.styles.radius + color: vr.controls.colors.inputBackground + + TextEdit { + enabled: false + id: currentDirectory + text: folderModel.folder + anchors { + fill: parent + leftMargin: 8 + rightMargin: 8 + topMargin: 4 + bottomMargin: 4 + } + } + } + + + TableView { + id: tableView + focus: true + model: folderModel + anchors.top: currentDirectoryWrapper.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: selectionType.top + anchors.margins: 8 + itemDelegate: Item { + Text { + anchors.verticalCenter: parent.verticalCenter + font.family: vr.fonts.lightFontName + font.pointSize: 12 + color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor + elide: styleData.elideMode + text: getText(); + function getText() { + switch (styleData.column) { + //case 1: return Date.fromLocaleString(locale, styleData.value, "yyyy-MM-dd hh:mm:ss"); + case 2: return folderModel.get(styleData.row, "fileIsDir") ? "" : formatSize(styleData.value); + default: return styleData.value; + } + } + + function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; + } + } + } + + TableViewColumn { + role: "fileName" + title: "Name" + width: 400 + } + TableViewColumn { + role: "fileModified" + title: "Date Modified" + width: 200 + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: 200 + } + + function selectItem(row) { + selectCurrentFile() + } + + + Keys.onReturnPressed: selectCurrentFile(); + onDoubleClicked: { currentRow = row; selectCurrentFile(); } + onCurrentRowChanged: currentSelection.text = model.get(currentRow, "fileName"); + KeyNavigation.left: cancelButton + KeyNavigation.right: selectionType + } + + Rectangle { + anchors.right: selectionType.left + anchors.rightMargin: 8 + anchors.top: selectionType.top + anchors.bottom: selectionType.bottom + anchors.left: parent.left + anchors.leftMargin: 8 + radius: 8 + color: "#eee" + Text { + id: currentSelection + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 8 + anchors.verticalCenter: parent.verticalCenter + font.family: vr.fonts.mainFontName + font.pointSize: 18 + } + } + + ComboBox { + id: selectionType + anchors.bottom: buttonRow.top + anchors.bottomMargin: 8 + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.left: buttonRow.left + model: filterModel + onCurrentIndexChanged: folderModel.nameFilters = [ + filterModel.get(currentIndex).filter + ] + + KeyNavigation.left: tableView + KeyNavigation.right: openButton + } + + Row { + id: buttonRow + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 + layoutDirection: Qt.RightToLeft + spacing: 8 + Button { + id: cancelButton + text: "Cancel" + KeyNavigation.up: selectionType + KeyNavigation.left: openButton + KeyNavigation.right: tableView.contentItem + Keys.onReturnPressed: { canceled(); root.enabled = false } + onClicked: { canceled(); root.enabled = false } + } + Button { + id: openButton + text: "Open" + enabled: tableView.currentRow != -1 && !folderModel.get(tableView.currentRow, "fileIsDir") + KeyNavigation.up: selectionType + KeyNavigation.left: selectionType + KeyNavigation.right: cancelButton + onClicked: selectCurrentFile(); + Keys.onReturnPressed: selectedFile(folderModel.get(tableView.currentRow, "fileURL")) + } + } + + Keys.onPressed: { + if (event.key === Qt.Key_Backspace && folderModel.parentFolder && folderModel.parentFolder != "") { + console.log("Navigating to " + folderModel.parentFolder) + folderModel.folder = folderModel.parentFolder + event.accepted = true + } + } +} + + diff --git a/interface/resources/qml/test/Stubs.qml b/interface/resources/qml/test/Stubs.qml new file mode 100644 index 0000000000..f23e5c89ee --- /dev/null +++ b/interface/resources/qml/test/Stubs.qml @@ -0,0 +1,311 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +// Stubs for the global service objects set by Interface.cpp when creating the UI +// This is useful for testing inside Qt creator where these services don't actually exist. +Item { + + Item { + objectName: "urlHandler" + function fixupUrl(url) { return url; } + function canHandleUrl(url) { return false; } + function handleUrl(url) { return true; } + } + + Item { + objectName: "Account" + function isLoggedIn() { return true; } + function getUsername() { return "Jherico"; } + } + + Item { + objectName: "ScriptDiscoveryService" + property var scriptsModelFilter: scriptsModel + signal scriptCountChanged() + property var _runningScripts:[ + { name: "wireFrameTest.js", url: "foo/wireframetest.js", path: "foo/wireframetest.js", local: true }, + { name: "edit.js", url: "foo/edit.js", path: "foo/edit.js", local: false }, + { name: "listAllScripts.js", url: "foo/listAllScripts.js", path: "foo/listAllScripts.js", local: false }, + { name: "users.js", url: "foo/users.js", path: "foo/users.js", local: false }, + ] + + function getRunning() { + return _runningScripts; + } + + } + + Item { + id: menuHelper + objectName: "MenuHelper" + + Component { + id: modelMaker + ListModel { } + } + + function toModel(menu, parent) { + if (!parent) { parent = menuHelper } + var result = modelMaker.createObject(parent); + if (menu.type !== MenuItemType.Menu) { + console.warn("Not a menu: " + menu); + return result; + } + + var items = menu.items; + for (var i = 0; i < items.length; ++i) { + var item = items[i]; + switch (item.type) { + case 2: + result.append({"name": item.title, "item": item}) + break; + case 1: + result.append({"name": item.text, "item": item}) + break; + case 0: + result.append({"name": "", "item": item}) + break; + } + } + return result; + } + + } + + Item { + objectName: "Desktop" + + property string _OFFSCREEN_ROOT_OBJECT_NAME: "desktopRoot"; + property string _OFFSCREEN_DIALOG_OBJECT_NAME: "topLevelWindow"; + + + function findChild(item, name) { + for (var i = 0; i < item.children.length; ++i) { + if (item.children[i].objectName === name) { + return item.children[i]; + } + } + return null; + } + + function findParent(item, name) { + while (item) { + if (item.objectName === name) { + return item; + } + item = item.parent; + } + return null; + } + + function findDialog(item) { + item = findParent(item, _OFFSCREEN_DIALOG_OBJECT_NAME); + return item; + } + + function closeDialog(item) { + item = findDialog(item); + if (item) { + item.enabled = false + } else { + console.warn("Could not find top level dialog") + } + } + + function getDesktop(item) { + while (item) { + if (item.desktopRoot) { + break; + } + item = item.parent; + } + return item + } + + function raise(item) { + var desktop = getDesktop(item); + if (desktop) { + desktop.raise(item); + } + } + } + + Menu { + id: root + objectName: "rootMenu" + + Menu { + title: "Audio" + } + + Menu { + title: "Avatar" + } + + Menu { + title: "Display" + ExclusiveGroup { id: displayMode } + Menu { + title: "More Stuff" + + Menu { title: "Empty" } + + MenuItem { + text: "Do Nothing" + onTriggered: console.log("Nothing") + } + } + MenuItem { + text: "Oculus" + exclusiveGroup: displayMode + checkable: true + } + MenuItem { + text: "OpenVR" + exclusiveGroup: displayMode + checkable: true + } + MenuItem { + text: "OSVR" + exclusiveGroup: displayMode + checkable: true + } + MenuItem { + text: "2D Screen" + exclusiveGroup: displayMode + checkable: true + checked: true + } + MenuItem { + text: "3D Screen (Active)" + exclusiveGroup: displayMode + checkable: true + } + MenuItem { + text: "3D Screen (Passive)" + exclusiveGroup: displayMode + checkable: true + } + } + + Menu { + title: "View" + Menu { + title: "Camera Mode" + ExclusiveGroup { id: cameraMode } + MenuItem { + exclusiveGroup: cameraMode + text: "First Person"; + onTriggered: console.log(text + " checked " + checked) + checkable: true + checked: true + } + MenuItem { + exclusiveGroup: cameraMode + text: "Third Person"; + onTriggered: console.log(text) + checkable: true + } + MenuItem { + exclusiveGroup: cameraMode + text: "Independent Mode"; + onTriggered: console.log(text) + checkable: true + } + MenuItem { + exclusiveGroup: cameraMode + text: "Entity Mode"; + onTriggered: console.log(text) + enabled: false + checkable: true + } + MenuItem { + exclusiveGroup: cameraMode + text: "Fullscreen Mirror"; + onTriggered: console.log(text) + checkable: true + } + } + } + + Menu { + title: "Edit" + + MenuItem { + text: "Undo" + shortcut: "Ctrl+Z" + enabled: false + onTriggered: console.log(text) + } + + MenuItem { + text: "Redo" + shortcut: "Ctrl+Shift+Z" + enabled: false + onTriggered: console.log(text) + } + + MenuSeparator { } + + MenuItem { + text: "Cut" + shortcut: "Ctrl+X" + onTriggered: console.log(text) + } + + MenuItem { + text: "Copy" + shortcut: "Ctrl+C" + onTriggered: console.log(text) + } + + MenuItem { + text: "Paste" + shortcut: "Ctrl+V" + visible: false + onTriggered: console.log("Paste") + } + } + + Menu { + title: "Navigate" + } + + Menu { + title: "Market" + } + + Menu { + title: "Settings" + } + + Menu { + title: "Developer" + } + + Menu { + title: "Quit" + } + + Menu { + title: "File" + + Action { + id: login + text: "Login" + } + + Action { + id: quit + text: "Quit" + shortcut: "Ctrl+Q" + onTriggered: Qt.quit(); + } + + MenuItem { action: quit } + MenuItem { action: login } + } + } + +} + diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index d810ea8c77..ed8999da04 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -4,90 +4,133 @@ import "." import "../controls" Frame { - id: root - anchors { margins: -16; topMargin: parent.closable ? -32 : -16; } + id: frame + // The frame fills the parent, which should be the size of the content. + // The frame decorations use negative anchor margins to extend beyond + anchors.fill: parent + // Size of the controls + readonly property real iconSize: 24; + // Convenience accessor for the window + property alias window: frame.parent + // FIXME needed? + property alias decoration: decoration - MouseArea { - id: controlsArea - anchors.fill: desktopControls - hoverEnabled: true - drag.target: root.parent - propagateComposedEvents: true - onClicked: { - root.raise() - mouse.accepted = false; - } - - MouseArea { - id: sizeDrag - enabled: root.parent.resizable - property int startX - property int startY - anchors.right: parent.right - anchors.bottom: parent.bottom - width: 16 - height: 16 - z: 1000 - hoverEnabled: true - onPressed: { - startX = mouseX - startY = mouseY - } - onPositionChanged: { - if (pressed) { - root.deltaSize((mouseX - startX), (mouseY - startY)) - startX = mouseX - startY = mouseY - } - } - } + Rectangle { + anchors { margins: -4 } + visible: !decoration.visible + anchors.fill: parent; + color: "#7f7f7f7f"; + radius: 3; } Rectangle { - id: desktopControls - // FIXME this doesn't work as expected - visible: root.parent.showFrame + id: decoration + anchors { margins: -iconSize; topMargin: -iconSize * (window.closable ? 2 : 1); } + visible: window.activator.containsMouse anchors.fill: parent; color: "#7f7f7f7f"; radius: 3; + // Allow dragging of the window + MouseArea { + id: dragMouseArea + anchors.fill: parent + drag { + target: window + // minimumX: (decoration.width - window.width) * -1 + // minimumY: 0 + // maximumX: (window.parent.width - window.width) - 2 * (decoration.width - window.width) + // maximumY: (window.parent.height - window.height) - 2 * (decoration.height - window.height) + } + } Row { + id: controlsRow anchors.right: parent.right anchors.top: parent.top - anchors.rightMargin: 4 - anchors.topMargin: 4 - spacing: 4 + anchors.rightMargin: iconSize + anchors.topMargin: iconSize / 2 + spacing: iconSize / 4 FontAwesome { visible: false text: "\uf08d" style: Text.Outline; styleColor: "white" - size: 16 - rotation: !root.parent ? 90 : root.parent.pinned ? 0 : 90 - color: root.pinned ? "red" : "black" + size: frame.iconSize + rotation: !frame.parent ? 90 : frame.parent.pinned ? 0 : 90 + color: frame.pinned ? "red" : "black" MouseArea { anchors.fill: parent propagateComposedEvents: true - onClicked: { root.pin(); mouse.accepted = false; } + onClicked: { frame.pin(); mouse.accepted = false; } } } FontAwesome { - visible: root.parent.closable + visible: window.closable text: closeClickArea.containsMouse ? "\uf057" : "\uf05c" style: Text.Outline; styleColor: "white" color: closeClickArea.containsMouse ? "red" : "black" - size: 16 + size: frame.iconSize MouseArea { id: closeClickArea anchors.fill: parent hoverEnabled: true - onClicked: root.close(); + onClicked: frame.close(); } } } + + // Allow sizing of the window + // FIXME works in native QML, doesn't work in Interface + MouseArea { + id: sizeDrag + width: iconSize + height: iconSize + + anchors { + right: decoration.right; + bottom: decoration.bottom + bottomMargin: iconSize * 2 + } + property vector2d pressOrigin + property vector2d sizeOrigin + property bool hid: false + onPressed: { + console.log("Pressed on size") + pressOrigin = Qt.vector2d(mouseX, mouseY) + sizeOrigin = Qt.vector2d(window.content.width, window.content.height) + hid = false; + } + onReleased: { + if (hid) { + window.content.visible = true + hid = false; + } + } + + onPositionChanged: { + if (pressed) { + if (window.content.visible) { + window.content.visible = false; + hid = true; + } + var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); + frame.deltaSize(delta.x, delta.y) + } + } + } + + FontAwesome { + visible: window.resizable + rotation: -45 + anchors { centerIn: sizeDrag } + horizontalAlignment: Text.AlignHCenter + text: "\uf07d" + size: iconSize / 3 * 2 + style: Text.Outline; styleColor: "white" + } } } diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 2e1f0e24d5..6b008f2872 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -6,78 +6,75 @@ import "../styles" FocusScope { id: window - objectName: "topLevelWindow" HifiConstants { id: hifi } - implicitHeight: frame.height - implicitWidth: frame.width + // The Window size is the size of the content, while the frame + // decorations can extend outside it. Windows should generally not be + // given explicit height / width, but rather be allowed to conform to + // their content + implicitHeight: content.height + implicitWidth: content.width property bool topLevelWindow: true property string title - property bool showFrame: true + // Should the window include a close control? property bool closable: true - property bool destroyOnInvisible: false + // Should hitting the close button hide or destroy the window? property bool destroyOnCloseButton: true - property bool pinned: false + // Should hiding the window destroy it or just hide it? + property bool destroyOnInvisible: false + // FIXME support for pinned / unpinned pending full design + // property bool pinnable: false + // property bool pinned: false property bool resizable: false - property real minX: 320 - property real minY: 240; + property vector2d minSize: Qt.vector2d(100, 100) + property vector2d maxSize: Qt.vector2d(1280, 720) + + // The content to place inside the window, determined by the client default property var content - property var frame: DefaultFrame { anchors.fill: content } - property var blur: FastBlur { anchors.fill: content; source: content; visible: false; radius: 0} - //property var hoverDetector: MouseArea { anchors.fill: frame; hoverEnabled: true; propagateComposedEvents: true; } - //property bool mouseInWindow: hoverDetector.containsMouse - children: [ frame, content, blur ] - signal windowDestroyed(); - QtObject { - id: d - property vector2d minPosition: Qt.vector2d(0, 0); - property vector2d maxPosition: Qt.vector2d(100, 100); - function clamp(value, min, max) { - return Math.min(Math.max(value, min), max); + onContentChanged: { + if (content) { + content.anchors.fill = window } - - function updateParentRect() { -// if (!frame) { return; } -// console.log(window.parent.width); -// console.log(frame.width); -// minPosition = Qt.vector2d(-frame.anchors.leftMargin, -frame.anchors.topMargin); -// maxPosition = Qt.vector2d( -// Math.max(minPosition.x, Desktop.width - frame.width + minPosition.x), -// Math.max(minPosition.y, Desktop.height - frame.height + minPosition.y)) -// console.log(maxPosition); - } - - function keepOnScreen() { - //window.x = clamp(x, minPosition.x, maxPosition.x); - //window.y = clamp(y, minPosition.y, maxPosition.y); - } - - onMinPositionChanged: keepOnScreen(); - onMaxPositionChanged: keepOnScreen(); } + // Default to a standard frame. Can be overriden to provide custom + // frame styles, like a full desktop frame to simulate a modal window + property var frame: DefaultFrame { + z: -1 + anchors.fill: parent + } + + // This mouse area serves to raise the window. To function, it must live + // in the window and have a higher Z-order than the content, but follow + // the position and size of frame decoration + property var activator: MouseArea { + width: frame.decoration.width + height: frame.decoration.height + x: frame.decoration.anchors.margins + y: frame.decoration.anchors.topMargin + propagateComposedEvents: true + hoverEnabled: true + onPressed: { window.raise(); mouse.accepted = false; } + // Debugging visualization + // Rectangle { anchors.fill:parent; color: "#7f00ff00" } + } + + children: [ frame, content, activator ] + signal windowDestroyed(); + Component.onCompleted: { - d.updateParentRect(); + fadeTargetProperty = visible ? 1.0 : 0.0 raise(); } Component.onDestruction: { + content.destroy(); console.log("Destroyed " + window); windowDestroyed(); } - - onParentChanged: { - d.updateParentRect(); - raise(); - } - - onFrameChanged: d.updateParentRect(); - onWidthChanged: d.updateParentRect(); - onHeightChanged: d.updateParentRect(); - onXChanged: d.keepOnScreen(); - onYChanged: d.keepOnScreen(); + onParentChanged: raise(); Connections { target: frame @@ -85,15 +82,15 @@ FocusScope { onClose: window.close(); onPin: window.pin(); onDeltaSize: { - console.log("deltaSize") - content.width = Math.max(content.width + dx, minX) - content.height = Math.max(content.height + dy, minY) + var newSize = Qt.vector2d(content.width + dx, content.height + dy); + newSize = clampVector(newSize, minSize, maxSize); + window.width = newSize.x + window.height = newSize.y } } - function raise() { - if (enabled && parent) { + if (visible && parent) { Desktop.raise(window) if (!focus) { focus = true; @@ -102,48 +99,58 @@ FocusScope { } function pin() { - pinned = ! pinned +// pinned = ! pinned } // our close function performs the same way as the OffscreenUI class: - // don't do anything but manipulate the enabled flag and let the other + // don't do anything but manipulate the targetVisible flag and let the other // mechanisms decide if the window should be destroyed after the close // animation completes function close() { + console.log("Closing " + window) if (destroyOnCloseButton) { destroyOnInvisible = true } - enabled = false; + visible = false; } - onEnabledChanged: { - if (!enabled) { - if (blur) { - blur.visible = true; - } - if (content) { - content.visible = false; - } - } + // + // Enable window visibility transitions + // - opacity = enabled ? 1.0 : 0.0 - // If the dialog is initially invisible, setting opacity doesn't - // trigger making it visible. - if (enabled) { - visible = true; - raise(); + // The target property to animate, usually scale or opacity + property alias fadeTargetProperty: window.opacity + // always start the property at 0 to enable fade in on creation + opacity: 0 + + // Some dialogs should be destroyed when they become + // invisible, so handle that + onVisibleChanged: { + // If someone directly set the visibility to false + // toggle it back on and use the targetVisible flag to transition + // via fading. + if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { + var target = visible; + visible = !visible; + fadeTargetProperty = target ? 1.0 : 0.0; + return; + } + if (!visible && destroyOnInvisible) { + console.log("Destroying " + window); + destroy(); + return; } } // 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, and then when the target animation value is reached, we can - // modify the visibility + // visibility is a boolean flags, it cannot be animated. So when + // targetVisible is changed, we modify a property that can be animated, + // like scale or opacity, and then when the target animation value is reached, + // we can modify the visibility // The actual animator - Behavior on opacity { + Behavior on fadeTargetProperty { NumberAnimation { duration: hifi.effects.fadeInDuration easing.type: Easing.OutCubic @@ -151,31 +158,31 @@ FocusScope { } // Once we're transparent, disable the dialog's visibility - onOpacityChanged: { - visible = (opacity != 0.0); - if (opacity == 1.0) { - content.visible = true; - blur.visible = false; - } + onFadeTargetPropertyChanged: { + visible = (fadeTargetProperty != 0.0); } - // Some dialogs should be destroyed when they become - // invisible, so handle that - onVisibleChanged: { - if (!visible && destroyOnInvisible) { - console.log("Destroying " + window); - destroy(); - } - } Keys.onPressed: { switch(event.key) { case Qt.Key_W: - if (event.modifiers == Qt.ControlModifier) { + if (event.modifiers === Qt.ControlModifier) { event.accepted = true - enabled = false + visible = false } break; } } + + + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + function clampVector(value, min, max) { + return Qt.vector2d( + clamp(value.x, min.x, max.x), + clamp(value.y, min.y, max.y)) + } + } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4c4786c6a2..f9687cf095 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -395,7 +395,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _dependencyManagerIsSetup(setupEssentials(argc, argv)), _window(new MainWindow(desktop())), - _toolWindow(NULL), _undoStackScriptingInterface(&_undoStack), _frameCount(0), _fps(60.0f), @@ -678,10 +677,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _renderEngine->addTask(make_shared()); _renderEngine->registerScene(_main3DScene); - _toolWindow = new ToolWindow(); - _toolWindow->setWindowFlags((_toolWindow->windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowMinimizeButtonHint); - _toolWindow->setWindowTitle("Tools"); - _offscreenContext->makeCurrent(); // Tell our entity edit sender about our known jurisdictions @@ -4751,7 +4746,7 @@ void Application::showFriendsWindow() { auto webWindowClass = _window->findChildren(FRIENDS_WINDOW_OBJECT_NAME); if (webWindowClass.empty()) { auto friendsWindow = new WebWindowClass(FRIENDS_WINDOW_TITLE, FRIENDS_WINDOW_URL, FRIENDS_WINDOW_WIDTH, - FRIENDS_WINDOW_HEIGHT, false); + FRIENDS_WINDOW_HEIGHT); friendsWindow->setParent(_window); friendsWindow->setObjectName(FRIENDS_WINDOW_OBJECT_NAME); connect(friendsWindow, &WebWindowClass::closed, &WebWindowClass::deleteLater); diff --git a/interface/src/Application.h b/interface/src/Application.h index d50fef327b..24e853eabb 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -62,7 +62,6 @@ #include "ui/OverlayConductor.h" #include "ui/overlays/Overlays.h" #include "ui/SnapshotShareDialog.h" -#include "ui/ToolWindow.h" #include "UndoStackScriptingInterface.h" class OffscreenGLCanvas; @@ -164,8 +163,6 @@ public: NodeToOctreeSceneStats* getOcteeSceneStats() { return &_octreeServerSceneStats; } - ToolWindow* getToolWindow() { return _toolWindow ; } - virtual controller::ScriptingInterface* getControllerScriptingInterface() { return _controllerScriptingInterface; } virtual void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine); @@ -400,8 +397,6 @@ private: MainWindow* _window; - ToolWindow* _toolWindow; - QUndoStack _undoStack; UndoStackScriptingInterface _undoStackScriptingInterface; diff --git a/interface/src/scripting/WebWindowClass.cpp b/interface/src/scripting/WebWindowClass.cpp index a5ce817f87..18beee4bbf 100644 --- a/interface/src/scripting/WebWindowClass.cpp +++ b/interface/src/scripting/WebWindowClass.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + #include #include #include @@ -35,48 +36,25 @@ void ScriptEventBridge::emitScriptEvent(const QString& data) { emit scriptEventReceived(data); } -WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow) - : QObject(NULL), - _eventBridge(new ScriptEventBridge(this)), - _isToolWindow(isToolWindow) { +WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height) + : QObject(NULL), _eventBridge(new ScriptEventBridge(this)) { + auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window); + dialogWidget->setWindowTitle(title); + dialogWidget->resize(width, height); + dialogWidget->installEventFilter(this); + connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed); - if (_isToolWindow) { - ToolWindow* toolWindow = qApp->getToolWindow(); + auto layout = new QVBoxLayout(dialogWidget); + layout->setContentsMargins(0, 0, 0, 0); + dialogWidget->setLayout(layout); - auto dockWidget = new QDockWidget(title, toolWindow); - dockWidget->setFeatures(QDockWidget::DockWidgetMovable); - connect(dockWidget, &QDockWidget::visibilityChanged, this, &WebWindowClass::visibilityChanged); + _webView = new QWebView(dialogWidget); - _webView = new QWebView(dockWidget); - addEventBridgeToWindowObject(); + layout->addWidget(_webView); - dockWidget->setWidget(_webView); + addEventBridgeToWindowObject(); - auto titleWidget = new QWidget(dockWidget); - dockWidget->setTitleBarWidget(titleWidget); - - toolWindow->addDockWidget(Qt::TopDockWidgetArea, dockWidget, Qt::Horizontal); - - _windowWidget = dockWidget; - } else { - auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window); - dialogWidget->setWindowTitle(title); - dialogWidget->resize(width, height); - dialogWidget->installEventFilter(this); - connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed); - - auto layout = new QVBoxLayout(dialogWidget); - layout->setContentsMargins(0, 0, 0, 0); - dialogWidget->setLayout(layout); - - _webView = new QWebView(dialogWidget); - - layout->addWidget(_webView); - - addEventBridgeToWindowObject(); - - _windowWidget = dialogWidget; - } + _windowWidget = dialogWidget; auto style = QStyleFactory::create("fusion"); if (style) { @@ -121,13 +99,8 @@ void WebWindowClass::addEventBridgeToWindowObject() { void WebWindowClass::setVisible(bool visible) { if (visible) { - if (_isToolWindow) { - QMetaObject::invokeMethod( - qApp->getToolWindow(), "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); - } else { - QMetaObject::invokeMethod(_windowWidget, "showNormal", Qt::AutoConnection); - QMetaObject::invokeMethod(_windowWidget, "raise", Qt::AutoConnection); - } + QMetaObject::invokeMethod(_windowWidget, "showNormal", Qt::AutoConnection); + QMetaObject::invokeMethod(_windowWidget, "raise", Qt::AutoConnection); } QMetaObject::invokeMethod(_windowWidget, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); } @@ -182,13 +155,18 @@ void WebWindowClass::raise() { QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { WebWindowClass* retVal; QString file = context->argument(0).toString(); + if (context->argument(4).toBool()) { + qWarning() << "ToolWindow views with WebWindow are no longer supported. Use OverlayWebWindow instead"; + return QScriptValue(); + } else { + qWarning() << "WebWindow views are deprecated. Use OverlayWebWindow instead"; + } QMetaObject::invokeMethod(DependencyManager::get().data(), "doCreateWebWindow", Qt::BlockingQueuedConnection, Q_RETURN_ARG(WebWindowClass*, retVal), Q_ARG(const QString&, file), Q_ARG(QString, context->argument(1).toString()), Q_ARG(int, context->argument(2).toInteger()), - Q_ARG(int, context->argument(3).toInteger()), - Q_ARG(bool, context->argument(4).toBool())); + Q_ARG(int, context->argument(3).toInteger())); connect(engine, &QScriptEngine::destroyed, retVal, &WebWindowClass::deleteLater); diff --git a/interface/src/scripting/WebWindowClass.h b/interface/src/scripting/WebWindowClass.h index cc6506b059..8859eb5b37 100644 --- a/interface/src/scripting/WebWindowClass.h +++ b/interface/src/scripting/WebWindowClass.h @@ -39,7 +39,7 @@ class WebWindowClass : public QObject { Q_PROPERTY(QSizeF size READ getSize WRITE setSize); public: - WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow = false); + WebWindowClass(const QString& title, const QString& url, int width, int height); ~WebWindowClass(); static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); @@ -75,7 +75,6 @@ private: QWidget* _windowWidget; QWebView* _webView; ScriptEventBridge* _eventBridge; - bool _isToolWindow; }; #endif diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index fe84f36158..03a4993d92 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -23,6 +23,7 @@ #include "Menu.h" #include "OffscreenUi.h" #include "ui/ModelsBrowser.h" +#include "WebWindowClass.h" #include "WindowScriptingInterface.h" @@ -37,8 +38,8 @@ WindowScriptingInterface::WindowScriptingInterface() : connect(qApp, &Application::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused); } -WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height, bool isToolWindow) { - return new WebWindowClass(title, url, width, height, isToolWindow); +WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height) { + return new WebWindowClass(title, url, width, height); } QScriptValue WindowScriptingInterface::hasFocus() { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 1395639cd0..4b287a24e9 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -19,7 +19,7 @@ #include #include -#include "WebWindowClass.h" +class WebWindowClass; class WindowScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -82,8 +82,8 @@ private slots: void nonBlockingFormAccepted() { _nonBlockingFormActive = false; _formResult = QDialog::Accepted; emit nonBlockingFormClosed(); } void nonBlockingFormRejected() { _nonBlockingFormActive = false; _formResult = QDialog::Rejected; emit nonBlockingFormClosed(); } - WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height, bool isToolWindow); - + WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); + private: QString jsRegExp2QtRegExp(QString string); QDialog* createForm(const QString& title, QScriptValue form); diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 155d41c575..85b8ce0273 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -148,8 +148,7 @@ void DialogsManager::lodTools() { } void DialogsManager::toggleToolWindow() { - QMainWindow* toolWindow = qApp->getToolWindow(); - toolWindow->setVisible(!toolWindow->isVisible()); + DependencyManager::get()->toggleToolWindow(); } void DialogsManager::hmdTools(bool showTools) { diff --git a/interface/src/ui/HMDToolsDialog.cpp b/interface/src/ui/HMDToolsDialog.cpp index f9fc444d4b..a596403948 100644 --- a/interface/src/ui/HMDToolsDialog.cpp +++ b/interface/src/ui/HMDToolsDialog.cpp @@ -79,9 +79,6 @@ HMDToolsDialog::HMDToolsDialog(QWidget* parent) : // what screens we're allowed on watchWindow(windowHandle()); auto dialogsManager = DependencyManager::get(); - if (qApp->getToolWindow()) { - watchWindow(qApp->getToolWindow()->windowHandle()); - } if (dialogsManager->getBandwidthDialog()) { watchWindow(dialogsManager->getBandwidthDialog()->windowHandle()); } diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index b5e24fef1e..5e2322b5c4 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -28,6 +28,7 @@ #include "Snapshot.h" #include "UserActivityLogger.h" #include "UIUtil.h" +#include "scripting/WebWindowClass.h" const int PREFERENCES_HEIGHT_PADDING = 20; @@ -135,7 +136,7 @@ void PreferencesDialog::openFullAvatarModelBrowser() { const auto WIDTH = 900; const auto HEIGHT = 700; if (!_marketplaceWindow) { - _marketplaceWindow = new WebWindowClass("Marketplace", MARKETPLACE_URL, WIDTH, HEIGHT, false); + _marketplaceWindow = new WebWindowClass("Marketplace", MARKETPLACE_URL, WIDTH, HEIGHT); } _marketplaceWindow->setVisible(true); diff --git a/interface/src/ui/PreferencesDialog.h b/interface/src/ui/PreferencesDialog.h index a6c27dee08..77c203b57f 100644 --- a/interface/src/ui/PreferencesDialog.h +++ b/interface/src/ui/PreferencesDialog.h @@ -17,7 +17,7 @@ #include #include -#include "scripting/WebWindowClass.h" +class WebWindowClass; class PreferencesDialog : public QDialog { Q_OBJECT @@ -41,7 +41,7 @@ private: QString _displayNameString; - WebWindowClass* _marketplaceWindow = NULL; + WebWindowClass* _marketplaceWindow { nullptr }; private slots: void accept(); diff --git a/interface/src/ui/ToolWindow.cpp b/interface/src/ui/ToolWindow.cpp deleted file mode 100644 index aab64f9c7e..0000000000 --- a/interface/src/ui/ToolWindow.cpp +++ /dev/null @@ -1,145 +0,0 @@ -// -// ToolWindow.cpp -// interface/src/ui -// -// Created by Ryan Huffman on 11/13/14. -// 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 "Application.h" -#include "MainWindow.h" -#include "ToolWindow.h" -#include "UIUtil.h" - -const int DEFAULT_WIDTH = 300; - -ToolWindow::ToolWindow(QWidget* parent) : - QMainWindow(parent), - _selfHidden(false), - _hasShown(false), - _lastGeometry() { - - setTabPosition(Qt::TopDockWidgetArea, QTabWidget::TabPosition::North); - -# ifndef Q_OS_LINUX - setDockOptions(QMainWindow::ForceTabbedDocks); -# endif - qApp->installEventFilter(this); -} - -bool ToolWindow::event(QEvent* event) { - QEvent::Type type = event->type(); - if (type == QEvent::Show) { - - if (!_hasShown) { - _hasShown = true; - - QMainWindow* mainWindow = qApp->getWindow(); - QRect mainGeometry = mainWindow->geometry(); - - int titleBarHeight = UIUtil::getWindowTitleBarHeight(this); - int topMargin = titleBarHeight; - - _lastGeometry = QRect(mainGeometry.topLeft().x(), mainGeometry.topLeft().y() + topMargin, - DEFAULT_WIDTH, mainGeometry.height() - topMargin); - } - - setGeometry(_lastGeometry); - - return true; - } else if (type == QEvent::Hide) { - _lastGeometry = geometry(); - return true; - } - - return QMainWindow::event(event); -} - -bool ToolWindow::eventFilter(QObject* sender, QEvent* event) { -# ifndef Q_OS_LINUX - switch (event->type()) { - case QEvent::WindowStateChange: - if (qApp->getWindow()->isMinimized()) { - // If we are already visible, we are self-hiding - _selfHidden = isVisible(); - setVisible(false); - } else { - if (_selfHidden) { - setVisible(true); - } - } - break; - case QEvent::ApplicationDeactivate: - _selfHidden = isVisible(); - setVisible(false); - break; - case QEvent::ApplicationActivate: - if (_selfHidden) { - setVisible(true); - } - break; - default: - break; - } -# endif - return false; -} - -void ToolWindow::onChildVisibilityUpdated(bool visible) { - if (!_selfHidden && visible) { - setVisible(true); - } else { - bool hasVisible = false; - QList dockWidgets = findChildren(); - for (int i = 0; i < dockWidgets.count(); i++) { - if (dockWidgets[i]->isVisible()) { - hasVisible = true; - break; - } - } - // If a child was hidden and we don't have any children still visible, hide ourself. - if (!hasVisible) { - setVisible(false); - } - } -} - -void ToolWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget) { - QList dockWidgets = findChildren(); - - QMainWindow::addDockWidget(area, dockWidget); - - // We want to force tabbing, so retabify all of our widgets. - QDockWidget* lastDockWidget = dockWidget; - - foreach (QDockWidget* nextDockWidget, dockWidgets) { - tabifyDockWidget(lastDockWidget, nextDockWidget); - lastDockWidget = nextDockWidget; - } - - connect(dockWidget, &QDockWidget::visibilityChanged, this, &ToolWindow::onChildVisibilityUpdated); -} - -void ToolWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget, Qt::Orientation orientation) { - QList dockWidgets = findChildren(); - - QMainWindow::addDockWidget(area, dockWidget, orientation); - - QDockWidget* lastDockWidget = dockWidget; - - foreach(QDockWidget* nextDockWidget, dockWidgets) { - tabifyDockWidget(lastDockWidget, nextDockWidget); - lastDockWidget = nextDockWidget; - } - - connect(dockWidget, &QDockWidget::visibilityChanged, this, &ToolWindow::onChildVisibilityUpdated); -} - -void ToolWindow::removeDockWidget(QDockWidget* dockWidget) { - QMainWindow::removeDockWidget(dockWidget); - - disconnect(dockWidget, &QDockWidget::visibilityChanged, this, &ToolWindow::onChildVisibilityUpdated); -} diff --git a/interface/src/ui/ToolWindow.h b/interface/src/ui/ToolWindow.h deleted file mode 100644 index 03ae85a418..0000000000 --- a/interface/src/ui/ToolWindow.h +++ /dev/null @@ -1,44 +0,0 @@ -// -// ToolWindow.h -// interface/src/ui -// -// Created by Ryan Huffman on 11/13/14. -// 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 -// - -#ifndef hifi_ToolWindow_h -#define hifi_ToolWindow_h - -#include -#include -#include -#include -#include - -class ToolWindow : public QMainWindow { - Q_OBJECT -public: - ToolWindow(QWidget* parent = NULL); - - virtual bool event(QEvent* event); - virtual void addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget); - virtual void addDockWidget(Qt::DockWidgetArea area, QDockWidget* dockWidget, Qt::Orientation orientation); - virtual void removeDockWidget(QDockWidget* dockWidget); - - virtual bool eventFilter(QObject* sender, QEvent* event); - -public slots: - void onChildVisibilityUpdated(bool visible); - - -private: - // Indicates whether this window was hidden by itself (because the main window lost focus). - bool _selfHidden; - bool _hasShown; - QRect _lastGeometry; -}; - -#endif // hifi_ToolWindow_h diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index d9e8579d58..32cb1131f2 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -340,12 +340,25 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _qmlComponent = new QQmlComponent(_qmlEngine); } -void OffscreenQmlSurface::resize(const QSize& newSize) { +void OffscreenQmlSurface::resize(const QSize& newSize_) { if (!_renderer || !_renderer->_quickWindow) { return; } + const float MAX_OFFSCREEN_DIMENSION = 4096; + QSize newSize = newSize_; + + if (newSize.width() > MAX_OFFSCREEN_DIMENSION || newSize.height() > MAX_OFFSCREEN_DIMENSION) { + float scale = std::min( + ((float)newSize.width() / MAX_OFFSCREEN_DIMENSION), + ((float)newSize.height() / MAX_OFFSCREEN_DIMENSION)); + newSize = QSize( + std::max(static_cast(scale * newSize.width()), 10), + std::max(static_cast(scale * newSize.height()), 10)); + } + + QSize currentSize = _renderer->_quickWindow->geometry().size(); if (newSize == currentSize) { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 489fd87112..7188d49846 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -21,10 +21,6 @@ #include "MessageDialog.h" // Needs to match the constants in resources/qml/Global.js -static const QString OFFSCREEN_ROOT_OBJECT_NAME = "desktopRoot"; -static const QString OFFSCREEN_WINDOW_OBJECT_NAME = "topLevelWindow"; -static QQuickItem* _desktop { nullptr }; - class OffscreenFlags : public QObject { Q_OBJECT Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged) @@ -252,6 +248,48 @@ void OffscreenUi::createDesktop() { _desktop = dynamic_cast(load("Root.qml")); Q_ASSERT(_desktop); getRootContext()->setContextProperty("Desktop", _desktop); + _toolWindow = _desktop->findChild("ToolWindow"); } +void OffscreenUi::toggleToolWindow() { + _toolWindow->setEnabled(!_toolWindow->isEnabled()); +} + +QQuickItem* OffscreenUi::getDesktop() { + return _desktop; +} + +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(); +} + + #include "OffscreenUi.moc" diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 15d2871460..4add90be75 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -12,6 +12,7 @@ #ifndef hifi_OffscreenUi_h #define hifi_OffscreenUi_h +#include #include #include @@ -33,6 +34,12 @@ public: bool navigationFocused(); void setNavigationFocused(bool focused); + QQuickItem* getDesktop(); + QQuickItem* getToolWindow(); + + Q_INVOKABLE void executeOnUiThread(std::function function); + Q_INVOKABLE QVariant returnFromUiThread(std::function function); + // Messagebox replacement functions using ButtonCallback = std::function; static ButtonCallback NO_OP_CALLBACK; @@ -70,6 +77,11 @@ public: QMessageBox::StandardButtons buttons = QMessageBox::Ok); static void error(const QString& text); // Interim dialog in new style + + void toggleToolWindow(); +private: + QQuickItem* _desktop { nullptr }; + QQuickItem* _toolWindow { nullptr }; }; #endif diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 940ba121f3..8bf36b111f 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -31,25 +31,15 @@ static const char* const URL_PROPERTY = "source"; // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine, - [&](QQmlContext* context, QObject* object) { return new QmlWebWindowClass(object); }); + [&](QObject* object) { return new QmlWebWindowClass(object); }); } QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWindow) { - QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); } -void QmlWebWindowClass::handleNavigation(const QString& url) { - bool handled = false; - static auto handler = dynamic_cast(qApp); - if (handler) { - if (handler->canAcceptURL(url)) { - handled = handler->acceptURL(url); - } - } - if (handled) { - QMetaObject::invokeMethod(_qmlWindow, "stop", Qt::AutoConnection); - } +// FIXME remove. +void QmlWebWindowClass::handleNavigation(const QString& url) { } QString QmlWebWindowClass::getURL() const { diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 0d5fc7fbf6..26561a43b6 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -35,6 +35,7 @@ static const char* const TITLE_PROPERTY = "title"; static const char* const WIDTH_PROPERTY = "width"; static const char* const HEIGHT_PROPERTY = "height"; static const char* const VISIBILE_PROPERTY = "visible"; +static const char* const TOOLWINDOW_PROPERTY = "toolWindow"; void QmlScriptEventBridge::emitWebEvent(const QString& data) { QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); @@ -86,13 +87,14 @@ void QmlWindowClass::setupServer() { QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, QScriptContext* context, QScriptEngine* engine, - std::function function) + std::function builder) { const auto argumentCount = context->argumentCount(); QString url; QString title; int width = -1, height = -1; bool visible = true; + bool toolWindow = false; if (argumentCount > 1) { if (!context->argument(0).isUndefined()) { @@ -107,6 +109,9 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, if (context->argument(3).isNumber()) { height = context->argument(3).toInt32(); } + if (context->argument(4).isBool()) { + toolWindow = context->argument(4).toBool(); + } } else { auto argumentObject = context->argument(0); qDebug() << argumentObject.toString(); @@ -125,6 +130,9 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, if (argumentObject.property(VISIBILE_PROPERTY).isBool()) { visible = argumentObject.property(VISIBILE_PROPERTY).toBool(); } + if (argumentObject.property(TOOLWINDOW_PROPERTY).isBool()) { + toolWindow = argumentObject.property(TOOLWINDOW_PROPERTY).toBool(); + } } if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) { @@ -137,17 +145,44 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, } QmlWindowClass* retVal{ nullptr }; - auto offscreenUi = DependencyManager::get(); - qDebug() << "Clearing component cache"; - offscreenUi->getRootContext()->engine()->clearComponentCache(); - // Build the event bridge and wrapper on the main thread - QMetaObject::invokeMethod(offscreenUi.data(), "load", Qt::BlockingQueuedConnection, - Q_ARG(const QString&, qmlSource), - Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { + if (toolWindow) { + auto toolWindow = offscreenUi->getToolWindow(); + QVariantMap properties; + properties.insert(TITLE_PROPERTY, title); + properties.insert(SOURCE_PROPERTY, url); + if (width != -1 && height != -1) { + properties.insert(WIDTH_PROPERTY, width); + properties.insert(HEIGHT_PROPERTY, height); + } + + // Build the event bridge and wrapper on the main thread + QVariant newTabVar; + bool invokeResult = QMetaObject::invokeMethod(toolWindow, "addWebTab", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVariant, newTabVar), + Q_ARG(QVariant, QVariant::fromValue(properties))); + + QQuickItem* newTab = qvariant_cast(newTabVar); + if (!invokeResult || !newTab) { + return QScriptValue(); + } + + offscreenUi->returnFromUiThread([&] { setupServer(); - retVal = function(context, object); + retVal = builder(newTab); + retVal->_toolWindow = true; + offscreenUi->getRootContext()->engine()->setObjectOwnership(retVal->_qmlWindow, QQmlEngine::CppOwnership); + registerObject(url.toLower(), retVal); + return QVariant(); + }); + } else { + // Build the event bridge and wrapper on the main thread + QMetaObject::invokeMethod(offscreenUi.data(), "load", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, qmlSource), + Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { + setupServer(); + retVal = builder(object); context->engine()->setObjectOwnership(retVal->_qmlWindow, QQmlEngine::CppOwnership); registerObject(url.toLower(), retVal); if (!title.isEmpty()) { @@ -158,9 +193,12 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, } object->setProperty(SOURCE_PROPERTY, url); if (visible) { - object->setProperty("enabled", true); + object->setProperty("visible", true); } })); + } + + retVal->_source = url; connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater); return engine->newQObject(retVal); } @@ -168,7 +206,7 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - return internalConstructor("QmlWindow.qml", context, engine, [&](QQmlContext* context, QObject* object){ + return internalConstructor("QmlWindow.qml", context, engine, [&](QObject* object){ return new QmlWindowClass(object); }); } @@ -181,6 +219,21 @@ QmlWindowClass::QmlWindowClass(QObject* qmlWindow) Q_ASSERT(dynamic_cast(_qmlWindow)); } +QmlWindowClass::~QmlWindowClass() { + if (_qmlWindow) { + if (_toolWindow) { + auto offscreenUi = DependencyManager::get(); + auto toolWindow = offscreenUi->getToolWindow(); + auto invokeResult = QMetaObject::invokeMethod(toolWindow, "removeTabForUrl", Qt::QueuedConnection, + Q_ARG(QVariant, _source)); + Q_ASSERT(invokeResult); + } else { + _qmlWindow->deleteLater(); + } + _qmlWindow = nullptr; + } +} + void QmlWindowClass::registerObject(const QString& name, QObject* object) { webChannel.registerObject(name, object); } @@ -189,74 +242,85 @@ void QmlWindowClass::deregisterObject(QObject* object) { webChannel.deregisterObject(object); } -void QmlWindowClass::setVisible(bool visible) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); - return; - } - - auto qmlWindow = asQuickItem(); - if (qmlWindow->isEnabled() != visible) { - qmlWindow->setEnabled(visible); - emit visibilityChanged(visible); - } -} - QQuickItem* QmlWindowClass::asQuickItem() const { + if (_toolWindow) { + return DependencyManager::get()->getToolWindow(); + } return dynamic_cast(_qmlWindow); } -bool QmlWindowClass::isVisible() const { - if (QThread::currentThread() != thread()) { - bool result; - QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); - return result; +void QmlWindowClass::setVisible(bool visible) { + // For tool window tabs we special case visiblility as enable / disable of the tab, not the window + // The tool window itself has special logic based on whether any tabs are enabled + if (_toolWindow) { + auto targetTab = dynamic_cast(_qmlWindow); + DependencyManager::get()->executeOnUiThread([=] { + targetTab->setEnabled(visible); + //emit visibilityChanged(visible); + }); + } else { + QQuickItem* targetWindow = asQuickItem(); + DependencyManager::get()->executeOnUiThread([=] { + targetWindow->setVisible(visible); + //emit visibilityChanged(visible); + }); } +} - return asQuickItem()->isEnabled(); +bool QmlWindowClass::isVisible() const { + // The tool window itself has special logic based on whether any tabs are enabled + if (_toolWindow) { + auto targetTab = dynamic_cast(_qmlWindow); + return DependencyManager::get()->returnFromUiThread([&] { + return QVariant::fromValue(targetTab->isEnabled()); + }).toBool(); + } else { + QQuickItem* targetWindow = asQuickItem(); + return DependencyManager::get()->returnFromUiThread([&] { + return QVariant::fromValue(targetWindow->isVisible()); + }).toBool(); + } } glm::vec2 QmlWindowClass::getPosition() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); + QQuickItem* targetWindow = asQuickItem(); + QVariant result = DependencyManager::get()->returnFromUiThread([&]()->QVariant { + return targetWindow->position(); + }); + return toGlm(result.toPointF()); } void QmlWindowClass::setPosition(const glm::vec2& position) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); - return; - } - - asQuickItem()->setPosition(QPointF(position.x, position.y)); + QQuickItem* targetWindow = asQuickItem(); + DependencyManager::get()->executeOnUiThread([=] { + targetWindow->setPosition(QPointF(position.x, position.y)); + }); } void QmlWindowClass::setPosition(int x, int y) { setPosition(glm::vec2(x, y)); } +// FIXME move to GLM helpers +glm::vec2 toGlm(const QSizeF& size) { + return glm::vec2(size.width(), size.height()); +} + glm::vec2 QmlWindowClass::getSize() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); + QQuickItem* targetWindow = asQuickItem(); + QVariant result = DependencyManager::get()->returnFromUiThread([&]()->QVariant { + return QSizeF(targetWindow->width(), targetWindow->height()); + }); + return toGlm(result.toSizeF()); } void QmlWindowClass::setSize(const glm::vec2& size) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); - } - - asQuickItem()->setSize(QSizeF(size.x, size.y)); + QQuickItem* targetWindow = asQuickItem(); + DependencyManager::get()->executeOnUiThread([=] { + targetWindow->setSize(QSizeF(size.x, size.y)); + }); } void QmlWindowClass::setSize(int width, int height) { @@ -264,27 +328,28 @@ void QmlWindowClass::setSize(int width, int height) { } void QmlWindowClass::setTitle(const QString& title) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); - } - - _qmlWindow->setProperty(TITLE_PROPERTY, title); + QQuickItem* targetWindow = asQuickItem(); + DependencyManager::get()->executeOnUiThread([=] { + targetWindow->setProperty(TITLE_PROPERTY, title); + }); } void QmlWindowClass::close() { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); - } - _qmlWindow->setProperty("destroyOnInvisible", true); - _qmlWindow->setProperty("visible", false); - _qmlWindow->deleteLater(); + DependencyManager::get()->executeOnUiThread([this] { + if (_qmlWindow) { + _qmlWindow->setProperty("destroyOnInvisible", true); + _qmlWindow->setProperty("visible", false); + _qmlWindow->deleteLater(); + _qmlWindow = nullptr; + } + }); } void QmlWindowClass::hasClosed() { } void QmlWindowClass::raise() { - QMetaObject::invokeMethod(_qmlWindow, "raiseWindow", Qt::QueuedConnection); + QMetaObject::invokeMethod(asQuickItem(), "raiseWindow", Qt::QueuedConnection); } #include "QmlWindowClass.moc" diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index 41572b448d..2e848b612d 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -51,6 +51,7 @@ class QmlWindowClass : public QObject { public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); QmlWindowClass(QObject* qmlWindow); + ~QmlWindowClass(); public slots: bool isVisible() const; @@ -84,7 +85,7 @@ protected slots: protected: static QScriptValue internalConstructor(const QString& qmlSource, QScriptContext* context, QScriptEngine* engine, - std::function function); + std::function function); static void setupServer(); static void registerObject(const QString& name, QObject* object); static void deregisterObject(QObject* object); @@ -95,9 +96,10 @@ protected: // FIXME needs to be initialized in the ctor once we have support // for tool window panes in QML - const bool _isToolWindow { false }; + bool _toolWindow { false }; const int _windowId; - QObject* const _qmlWindow; + QObject* _qmlWindow; + QString _source; }; #endif