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