From 8005547d03d9c495d5f516191312c6b2460e3ebb Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 13 Apr 2018 13:52:19 -0700 Subject: [PATCH] rollback some qml changes on android --- .../qml/controls-uit/+android/Button.qml | 125 +++ .../qml/controls-uit/+android/Table.qml | 165 ++++ .../qml/desktop/+android/Desktop.qml | 575 ++++++++++++ .../dialogs/+android/CustomQueryDialog.qml | 338 +++++++ .../qml/dialogs/+android/FileDialog.qml | 840 ++++++++++++++++++ .../qml/dialogs/+android/QueryDialog.qml | 231 +++++ .../+android/AssetDialogContent.qml | 533 +++++++++++ 7 files changed, 2807 insertions(+) create mode 100644 interface/resources/qml/controls-uit/+android/Button.qml create mode 100644 interface/resources/qml/controls-uit/+android/Table.qml create mode 100644 interface/resources/qml/desktop/+android/Desktop.qml create mode 100644 interface/resources/qml/dialogs/+android/CustomQueryDialog.qml create mode 100644 interface/resources/qml/dialogs/+android/FileDialog.qml create mode 100644 interface/resources/qml/dialogs/+android/QueryDialog.qml create mode 100644 interface/resources/qml/dialogs/assetDialog/+android/AssetDialogContent.qml diff --git a/interface/resources/qml/controls-uit/+android/Button.qml b/interface/resources/qml/controls-uit/+android/Button.qml new file mode 100644 index 0000000000..2f05b35685 --- /dev/null +++ b/interface/resources/qml/controls-uit/+android/Button.qml @@ -0,0 +1,125 @@ +// +// Button.qml +// +// Created by David Rowe on 16 Feb 2016 +// Copyright 2016 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 as Original +import QtQuick.Controls.Styles 1.4 +import TabletScriptingInterface 1.0 + +import "../styles-uit" + +Original.Button { + id: root; + + property int color: 0 + property int colorScheme: hifi.colorSchemes.light + property string buttonGlyph: ""; + + width: hifi.dimensions.buttonWidth + height: hifi.dimensions.controlLineHeight + + HifiConstants { id: hifi } + + onHoveredChanged: { + if (hovered) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + onFocusChanged: { + if (focus) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + } + + style: ButtonStyle { + + background: Rectangle { + radius: hifi.buttons.radius + + border.width: (control.color === hifi.buttons.none || + (control.color === hifi.buttons.noneBorderless && control.hovered) || + (control.color === hifi.buttons.noneBorderlessWhite && control.hovered) || + (control.color === hifi.buttons.noneBorderlessGray && control.hovered)) ? 1 : 0; + border.color: control.color === hifi.buttons.noneBorderless ? hifi.colors.blueHighlight : + (control.color === hifi.buttons.noneBorderlessGray ? hifi.colors.baseGray : hifi.colors.white); + + gradient: Gradient { + GradientStop { + position: 0.2 + color: { + if (!control.enabled) { + hifi.buttons.disabledColorStart[control.colorScheme] + } else if (control.pressed) { + hifi.buttons.pressedColor[control.color] + } else if (control.hovered) { + hifi.buttons.hoveredColor[control.color] + } else if (!control.hovered && control.focus) { + hifi.buttons.focusedColor[control.color] + } else { + hifi.buttons.colorStart[control.color] + } + } + } + GradientStop { + position: 1.0 + color: { + if (!control.enabled) { + hifi.buttons.disabledColorFinish[control.colorScheme] + } else if (control.pressed) { + hifi.buttons.pressedColor[control.color] + } else if (control.hovered) { + hifi.buttons.hoveredColor[control.color] + } else if (!control.hovered && control.focus) { + hifi.buttons.focusedColor[control.color] + } else { + hifi.buttons.colorFinish[control.color] + } + } + } + } + } + + label: Item { + HiFiGlyphs { + id: buttonGlyph; + visible: root.buttonGlyph !== ""; + text: root.buttonGlyph === "" ? hifi.glyphs.question : root.buttonGlyph; + // Size + size: 34; + // Anchors + anchors.right: buttonText.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + // Style + color: enabled ? hifi.buttons.textColor[control.color] + : hifi.buttons.disabledTextColor[control.colorScheme]; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + RalewayBold { + id: buttonText; + anchors.centerIn: parent; + font.capitalization: Font.AllUppercase + color: enabled ? hifi.buttons.textColor[control.color] + : hifi.buttons.disabledTextColor[control.colorScheme] + size: hifi.fontSizes.buttonLabel + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: control.text + } + } + } +} diff --git a/interface/resources/qml/controls-uit/+android/Table.qml b/interface/resources/qml/controls-uit/+android/Table.qml new file mode 100644 index 0000000000..3c1d0fcd3c --- /dev/null +++ b/interface/resources/qml/controls-uit/+android/Table.qml @@ -0,0 +1,165 @@ +// +// Table.qml +// +// Created by David Rowe on 18 Feb 2016 +// Copyright 2016 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Controls 2.2 as QQC2 + +import "../styles-uit" + +TableView { + id: tableView + + property int colorScheme: hifi.colorSchemes.light + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light + property bool expandSelectedRow: false + property bool centerHeaderText: false + readonly property real headerSpacing: 3 //spacing between sort indicator and table header title + property var titlePaintedPos: [] // storing extra data position behind painted + // title text and sort indicatorin table's header + signal titlePaintedPosSignal(int column) //signal that extradata position gets changed + + model: ListModel { } + + Component.onCompleted: { + if (flickableItem !== null && flickableItem !== undefined) { + tableView.flickableItem.QQC2.ScrollBar.vertical = scrollbar + } + } + + QQC2.ScrollBar { + id: scrollbar + parent: tableView.flickableItem + policy: QQC2.ScrollBar.AsNeeded + orientation: Qt.Vertical + visible: size < 1.0 + topPadding: tableView.headerVisible ? hifi.dimensions.tableHeaderHeight + 1 : 1 + anchors.top: tableView.top + anchors.left: tableView.right + anchors.bottom: tableView.bottom + + background: Item { + implicitWidth: hifi.dimensions.scrollbarBackgroundWidth + Rectangle { + anchors { + fill: parent; + topMargin: tableView.headerVisible ? hifi.dimensions.tableHeaderHeight : 0 + } + color: isLightColorScheme ? hifi.colors.tableScrollBackgroundLight + : hifi.colors.tableScrollBackgroundDark + } + } + + contentItem: Item { + implicitWidth: hifi.dimensions.scrollbarHandleWidth + Rectangle { + anchors.fill: parent + radius: (width - 4)/2 + color: isLightColorScheme ? hifi.colors.tableScrollHandleLight : hifi.colors.tableScrollHandleDark + } + } + } + + headerVisible: false + headerDelegate: Rectangle { + height: hifi.dimensions.tableHeaderHeight + color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + + + RalewayRegular { + id: titleText + x: centerHeaderText ? (parent.width - paintedWidth - + ((sortIndicatorVisible && + sortIndicatorColumn === styleData.column) ? + (titleSort.paintedWidth / 5 + tableView.headerSpacing) : 0)) / 2 : + hifi.dimensions.tablePadding + text: styleData.value + size: hifi.fontSizes.tableHeading + font.capitalization: Font.AllUppercase + color: hifi.colors.baseGrayHighlight + horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft) + anchors.verticalCenter: parent.verticalCenter + } + + //actual image of sort indicator in glyph font only 20% of real font size + //i.e. if the charachter size set to 60 pixels, actual image is 12 pixels + HiFiGlyphs { + id: titleSort + text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn + color: hifi.colors.darkGray + opacity: 0.6; + size: hifi.fontSizes.tableHeadingIcon + anchors.verticalCenter: titleText.verticalCenter + anchors.left: titleText.right + anchors.leftMargin: -(hifi.fontSizes.tableHeadingIcon / 2.5) + tableView.headerSpacing + visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column + onXChanged: { + titlePaintedPos[styleData.column] = titleText.x + titleText.paintedWidth + + paintedWidth / 5 + tableView.headerSpacing*2 + titlePaintedPosSignal(styleData.column) + } + } + + Rectangle { + width: 1 + anchors { + left: parent.left + top: parent.top + topMargin: 1 + bottom: parent.bottom + bottomMargin: 2 + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + visible: styleData.column > 0 + } + + Rectangle { + height: 1 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + } + } + + // Use rectangle to draw border with rounded corners. + frameVisible: false + Rectangle { + color: "#00000000" + anchors { fill: parent; margins: -2 } + border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight + border.width: 2 + } + anchors.margins: 2 // Shrink TableView to lie within border. + + backgroundVisible: true + + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + style: TableViewStyle { + // Needed in order for rows to keep displaying rows after end of table entries. + backgroundColor: tableView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark + alternateBackgroundColor: tableView.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd + padding.top: headerVisible ? hifi.dimensions.tableHeaderHeight: 0 + } + + rowDelegate: Rectangle { + height: (styleData.selected && expandSelectedRow ? 1.8 : 1) * hifi.dimensions.tableRowHeight + color: styleData.selected + ? hifi.colors.primaryHighlight + : tableView.isLightColorScheme + ? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) + : (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd) + } +} diff --git a/interface/resources/qml/desktop/+android/Desktop.qml b/interface/resources/qml/desktop/+android/Desktop.qml new file mode 100644 index 0000000000..6a68f63d0a --- /dev/null +++ b/interface/resources/qml/desktop/+android/Desktop.qml @@ -0,0 +1,575 @@ +// +// Desktop.qml +// +// Created by Bradley Austin Davis on 15 Apr 2015 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 1.4 + +import "../dialogs" +import "../js/Utils.js" as Utils + +// This is our primary 'desktop' object to which all VR dialogs and windows are childed. +FocusScope { + id: desktop + objectName: "desktop" + anchors.fill: parent + + readonly property int invalid_position: -9999; + property rect recommendedRect: Qt.rect(0,0,0,0); + property var expectedChildren; + property bool repositionLocked: true + property bool hmdHandMouseActive: false + + onRepositionLockedChanged: { + if (!repositionLocked) { + d.handleSizeChanged(); + } + + } + + onHeightChanged: d.handleSizeChanged(); + + onWidthChanged: d.handleSizeChanged(); + + // Controls and windows can trigger this signal to ensure the desktop becomes visible + // when they're opened. + signal showDesktop(); + + // This is for JS/QML communication, which is unused in the Desktop, + // but not having this here results in spurious warnings about a + // missing signal + signal sendToScript(var message); + + // 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 { + id: rootMenuId + objectName: "rootMenu" + + property var exclusionGroups: ({}); + property Component exclusiveGroupMaker: Component { + ExclusiveGroup { + } + } + + function addExclusionGroup(qmlAction, exclusionGroup) { + + var exclusionGroupId = exclusionGroup.toString(); + if(!exclusionGroups[exclusionGroupId]) { + exclusionGroups[exclusionGroupId] = exclusiveGroupMaker.createObject(rootMenuId); + } + + qmlAction.exclusiveGroup = exclusionGroups[exclusionGroupId] + } + } + + // FIXME: Alpha gradients display as fuschia under QtQuick 2.5 on OSX/AMD + // because shaders are 4.2, and do not include #version declarations. + property bool gradientsSupported: Qt.platform.os != "osx" && !~GL.vendor.indexOf("ATI") + + readonly property alias zLevels: zLevels + QtObject { + id: zLevels; + readonly property real normal: 1 // make windows always appear higher than QML overlays and other non-window controls. + readonly property real top: 2000 + readonly property real modal: 4000 + readonly property real menu: 8000 + } + + QtObject { + id: d + + function handleSizeChanged() { + if (desktop.repositionLocked) { + return; + } + var oldRecommendedRect = recommendedRect; + var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedHUDRect(); + var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, + newRecommendedRectJS.width, + newRecommendedRectJS.height); + + var oldChildren = expectedChildren; + var newChildren = d.getRepositionChildren(); + if (oldRecommendedRect != Qt.rect(0,0,0,0) && oldRecommendedRect != Qt.rect(0,0,1,1) + && (oldRecommendedRect != newRecommendedRect + || oldChildren != newChildren) + ) { + expectedChildren = newChildren; + d.repositionAll(); + } + recommendedRect = newRecommendedRect; + } + + 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 findParentMatching(item, predicate) { + while (item) { + if (predicate(item)) { + break; + } + item = item.parent; + } + return item; + } + + function findMatchingChildren(item, predicate) { + var results = []; + for (var i in item.children) { + var child = item.children[i]; + if (predicate(child)) { + results.push(child); + } + } + return results; + } + + function isTopLevelWindow(item) { + return item.topLevelWindow; + } + + function isAlwaysOnTopWindow(window) { + return window.alwaysOnTop; + } + + function isModalWindow(window) { + return window.modality !== Qt.NonModal; + } + + function getTopLevelWindows(predicate) { + return findMatchingChildren(desktop, function(child) { + return (isTopLevelWindow(child) && (!predicate || predicate(child))); + }); + } + + function getDesktopWindow(item) { + return findParentMatching(item, isTopLevelWindow) + } + + function fixupZOrder(windows, basis, topWindow) { + windows.sort(function(a, b){ return a.z - b.z; }); + + if ((topWindow.z >= basis) && (windows[windows.length - 1] === topWindow)) { + return; + } + + var lastZ = -1; + var lastTargetZ = basis - 1; + for (var i = 0; i < windows.length; ++i) { + var window = windows[i]; + if (!window.visible) { + continue + } + + if (topWindow && (topWindow === window)) { + continue + } + + if (window.z > lastZ) { + lastZ = window.z; + ++lastTargetZ; + } + if (DebugQML) { + console.log("Assigning z order " + lastTargetZ + " to " + window) + } + + window.z = lastTargetZ; + } + if (topWindow) { + ++lastTargetZ; + if (DebugQML) { + console.log("Assigning z order " + lastTargetZ + " to " + topWindow) + } + topWindow.z = lastTargetZ; + } + + return lastTargetZ; + } + + function raiseWindow(targetWindow) { + var predicate; + var zBasis; + if (isModalWindow(targetWindow)) { + predicate = isModalWindow; + zBasis = zLevels.modal + } else if (isAlwaysOnTopWindow(targetWindow)) { + predicate = function(window) { + return (isAlwaysOnTopWindow(window) && !isModalWindow(window)); + } + zBasis = zLevels.top + } else { + predicate = function(window) { + return (!isAlwaysOnTopWindow(window) && !isModalWindow(window)); + } + zBasis = zLevels.normal + } + + var windows = getTopLevelWindows(predicate); + fixupZOrder(windows, zBasis, targetWindow); + } + + Component.onCompleted: { + //offscreenWindow.activeFocusItemChanged.connect(onWindowFocusChanged); + focusHack.start(); + } + + function onWindowFocusChanged() { + //console.log("Focus item is " + offscreenWindow.activeFocusItem); + + // FIXME this needs more testing before it can go into production + // and I already cant produce any way to have a modal dialog lose focus + // to a non-modal one. + /* + var focusedWindow = getDesktopWindow(offscreenWindow.activeFocusItem); + + if (isModalWindow(focusedWindow)) { + return; + } + + // new focused window is not modal... check if there are any modal windows + var windows = getTopLevelWindows(isModalWindow); + if (0 === windows.length) { + return; + } + + // There are modal windows present, force focus back to the top-most modal window + windows.sort(function(a, b){ return a.z - b.z; }); + windows[windows.length - 1].focus = true; + */ + +// var focusedItem = offscreenWindow.activeFocusItem ; +// if (DebugQML && focusedItem) { +// var rect = desktop.mapFromItem(focusedItem, 0, 0, focusedItem.width, focusedItem.height); +// focusDebugger.x = rect.x; +// focusDebugger.y = rect.y; +// focusDebugger.width = rect.width +// focusDebugger.height = rect.height +// } + } + + function getRepositionChildren(predicate) { + return findMatchingChildren(desktop, function(child) { + return (child.shouldReposition === true && (!predicate || predicate(child))); + }); + } + + function repositionAll() { + if (desktop.repositionLocked) { + return; + } + + var oldRecommendedRect = recommendedRect; + var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; + var newRecommendedRect = Controller.getRecommendedHUDRect(); + var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; + var windows = d.getTopLevelWindows(); + for (var i = 0; i < windows.length; ++i) { + var targetWindow = windows[i]; + if (targetWindow.visible) { + repositionWindow(targetWindow, true, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); + } + } + + // also reposition the other children that aren't top level windows but want to be repositioned + var otherChildren = d.getRepositionChildren(); + for (var i = 0; i < otherChildren.length; ++i) { + var child = otherChildren[i]; + repositionWindow(child, true, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); + } + + } + } + + property bool pinned: false + property var hiddenChildren: [] + + function togglePinned() { + pinned = !pinned + } + + function isPointOnWindow(point) { + for (var i = 0; i < desktop.visibleChildren.length; i++) { + var child = desktop.visibleChildren[i]; + if (child.hasOwnProperty("modality")) { + var mappedPoint = mapToItem(child, point.x, point.y); + if (child.hasOwnProperty("frame")) { + var outLine = child.frame.children[2]; + var framePoint = outLine.mapFromGlobal(point.x, point.y); + if (outLine.contains(framePoint)) { + return true; + } + } + + if (child.contains(mappedPoint)) { + return true; + } + } + } + return false; + } + + function setPinned(newPinned) { + pinned = newPinned + } + + property real unpinnedAlpha: 1.0; + + Behavior on unpinnedAlpha { + NumberAnimation { + easing.type: Easing.Linear; + duration: 300 + } + } + + state: "NORMAL" + states: [ + State { + name: "NORMAL" + PropertyChanges { target: desktop; unpinnedAlpha: 1.0 } + }, + State { + name: "PINNED" + PropertyChanges { target: desktop; unpinnedAlpha: 0.0 } + } + ] + + transitions: [ + Transition { + NumberAnimation { properties: "unpinnedAlpha"; duration: 300 } + } + ] + + onPinnedChanged: { + if (pinned) { + d.raiseWindow(desktop); + desktop.focus = true; + desktop.forceActiveFocus(); + + // recalculate our non-pinned children + hiddenChildren = d.findMatchingChildren(desktop, function(child){ + return !d.isTopLevelWindow(child) && child.visible && !child.pinned; + }); + + hiddenChildren.forEach(function(child){ + child.opacity = Qt.binding(function(){ return desktop.unpinnedAlpha }); + }); + } + state = pinned ? "PINNED" : "NORMAL" + } + + onShowDesktop: pinned = false + + function raise(item) { + var targetWindow = d.getDesktopWindow(item); + if (!targetWindow) { + console.warn("Could not find top level window for " + item); + return; + } + + // Fix up the Z-order (takes into account if this is a modal window) + d.raiseWindow(targetWindow); + var setFocus = true; + if (!d.isModalWindow(targetWindow)) { + var modalWindows = d.getTopLevelWindows(d.isModalWindow); + if (modalWindows.length) { + setFocus = false; + } + } + + if (setFocus) { + targetWindow.focus = true; + } + + showDesktop(); + } + + function ensureTitleBarVisible(targetWindow) { + // Reposition window to ensure that title bar is vertically inside window. + if (targetWindow.frame && targetWindow.frame.decoration) { + var topMargin = -targetWindow.frame.decoration.anchors.topMargin; // Frame's topMargin is a negative value. + targetWindow.y = Math.max(targetWindow.y, topMargin); + } + } + + function centerOnVisible(item) { + var targetWindow = d.getDesktopWindow(item); + if (!targetWindow) { + console.warn("Could not find top level window for " + item); + return; + } + + if (typeof Controller === "undefined") { + console.warn("Controller not yet available... can't center"); + return; + } + + var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedHUDRect(); + var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, + newRecommendedRectJS.width, + newRecommendedRectJS.height); + var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; + var newX = newRecommendedRect.x + ((newRecommendedRect.width - targetWindow.width) / 2); + var newY = newRecommendedRect.y + ((newRecommendedRect.height - targetWindow.height) / 2); + targetWindow.x = newX; + targetWindow.y = newY; + + ensureTitleBarVisible(targetWindow); + + // If we've noticed that our recommended desktop rect has changed, record that change here. + if (recommendedRect != newRecommendedRect) { + recommendedRect = newRecommendedRect; + } + } + + function repositionOnVisible(item) { + var targetWindow = d.getDesktopWindow(item); + if (!targetWindow) { + console.warn("Could not find top level window for " + item); + return; + } + + if (typeof Controller === "undefined") { + console.warn("Controller not yet available... can't reposition targetWindow:" + targetWindow); + return; + } + + var oldRecommendedRect = recommendedRect; + var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; + var newRecommendedRect = Controller.getRecommendedHUDRect(); + var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; + repositionWindow(targetWindow, false, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); + } + + function repositionWindow(targetWindow, forceReposition, + oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions) { + + if (desktop.width === 0 || desktop.height === 0) { + return; + } + + if (!targetWindow) { + console.warn("Could not find top level window for " + item); + return; + } + + var recommended = Controller.getRecommendedHUDRect(); + var maxX = recommended.x + recommended.width; + var maxY = recommended.y + recommended.height; + var newPosition = Qt.vector2d(targetWindow.x, targetWindow.y); + + // if we asked to force reposition, or if the window is completely outside of the recommended rectangle, reposition it + if (forceReposition || (targetWindow.x > maxX || (targetWindow.x + targetWindow.width) < recommended.x) || + (targetWindow.y > maxY || (targetWindow.y + targetWindow.height) < recommended.y)) { + newPosition.x = -1 + newPosition.y = -1 + } + + if (newPosition.x === -1 && newPosition.y === -1) { + var originRelativeX = (targetWindow.x - oldRecommendedRect.x); + var originRelativeY = (targetWindow.y - oldRecommendedRect.y); + if (isNaN(originRelativeX)) { + originRelativeX = 0; + } + if (isNaN(originRelativeY)) { + originRelativeY = 0; + } + var fractionX = Utils.clamp(originRelativeX / oldRecommendedDimmensions.x, 0, 1); + var fractionY = Utils.clamp(originRelativeY / oldRecommendedDimmensions.y, 0, 1); + var newX = (fractionX * newRecommendedDimmensions.x) + newRecommendedRect.x; + var newY = (fractionY * newRecommendedDimmensions.y) + newRecommendedRect.y; + newPosition = Qt.vector2d(newX, newY); + } + targetWindow.x = newPosition.x; + targetWindow.y = newPosition.y; + + ensureTitleBarVisible(targetWindow); + } + + Component { id: messageDialogBuilder; MessageDialog { } } + function messageBox(properties) { + return messageDialogBuilder.createObject(desktop, properties); + } + + Component { id: inputDialogBuilder; QueryDialog { } } + function inputDialog(properties) { + return inputDialogBuilder.createObject(desktop, properties); + } + + Component { id: customInputDialogBuilder; CustomQueryDialog { } } + function customInputDialog(properties) { + return customInputDialogBuilder.createObject(desktop, properties); + } + + Component { id: fileDialogBuilder; FileDialog { } } + function fileDialog(properties) { + return fileDialogBuilder.createObject(desktop, properties); + } + + Component { id: assetDialogBuilder; AssetDialog { } } + function assetDialog(properties) { + return assetDialogBuilder.createObject(desktop, properties); + } + + function unfocusWindows() { + // First find the active focus item, and unfocus it, all the way + // up the parent chain to the window + var currentFocus = offscreenWindow.activeFocusItem; + var targetWindow = d.getDesktopWindow(currentFocus); + while (currentFocus) { + if (currentFocus === targetWindow) { + break; + } + currentFocus.focus = false; + currentFocus = currentFocus.parent; + } + + // Unfocus all windows + var windows = d.getTopLevelWindows(); + for (var i = 0; i < windows.length; ++i) { + windows[i].focus = false; + } + + // For the desktop to have active focus + desktop.focus = true; + desktop.forceActiveFocus(); + } + + function openBrowserWindow(request, profile) { + var component = Qt.createComponent("../Browser.qml"); + var newWindow = component.createObject(desktop); + newWindow.webView.profile = profile; + request.openIn(newWindow.webView); + } + + FocusHack { id: focusHack; } + + Rectangle { + id: focusDebugger; + objectName: "focusDebugger" + z: 9999; visible: false; color: "red" + ColorAnimation on color { from: "#7fffff00"; to: "#7f0000ff"; duration: 1000; loops: 9999 } + } + + Action { + text: "Toggle Focus Debugger" + shortcut: "Ctrl+Shift+F" + enabled: DebugQML + onTriggered: focusDebugger.visible = !focusDebugger.visible + } + +} diff --git a/interface/resources/qml/dialogs/+android/CustomQueryDialog.qml b/interface/resources/qml/dialogs/+android/CustomQueryDialog.qml new file mode 100644 index 0000000000..b1b6de4644 --- /dev/null +++ b/interface/resources/qml/dialogs/+android/CustomQueryDialog.qml @@ -0,0 +1,338 @@ +// +// CustomQueryDialog.qml +// +// Created by Zander Otavka on 7/14/16 +// Copyright 2016 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 +// + +import QtQuick 2.7; +import QtQuick.Dialogs 1.2 as OriginalDialogs; +import QtQuick.Controls 1.4; + +import "../controls-uit"; +import "../styles-uit"; +import "../windows"; + +ModalWindow { + id: root; + HifiConstants { id: hifi; } + implicitWidth: 640; + implicitHeight: 320; + visible: true; + keyboardOverride: true // Disable ModalWindow's keyboard. + + signal selected(var result); + signal canceled(); + + property int icon: hifi.icons.none; + property string iconText: ""; + property int iconSize: 35; + onIconChanged: updateIcon(); + + property var textInput; + property var comboBox; + property var checkBox; + onTextInputChanged: { + if (textInput && textInput.text !== undefined) { + textField.text = textInput.text; + } + } + onComboBoxChanged: { + if (comboBox && comboBox.index !== undefined) { + comboBoxField.currentIndex = comboBox.index; + } + } + onCheckBoxChanged: { + if (checkBox && checkBox.checked !== undefined) { + checkBoxField.checked = checkBox.checked; + } + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + onKeyboardRaisedChanged: d.resize(); + + property var warning: ""; + property var result; + + property var implicitCheckState: null; + + property int titleWidth: 0; + onTitleWidthChanged: d.resize(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + function updateCheckbox() { + if (checkBox.disableForItems) { + var currentItemInDisableList = false; + for (var i in checkBox.disableForItems) { + if (comboBoxField.currentIndex === checkBox.disableForItems[i]) { + currentItemInDisableList = true; + break; + } + } + + if (currentItemInDisableList) { + checkBoxField.enabled = false; + if (checkBox.checkStateOnDisable !== null && checkBox.checkStateOnDisable !== undefined) { + root.implicitCheckState = checkBoxField.checked; + checkBoxField.checked = checkBox.checkStateOnDisable; + } + root.warning = checkBox.warningOnDisable; + } else { + checkBoxField.enabled = true; + if (root.implicitCheckState !== null) { + checkBoxField.checked = root.implicitCheckState; + root.implicitCheckState = null; + } + root.warning = ""; + } + } + } + + Item { + clip: true; + width: pane.width; + height: pane.height; + anchors.margins: 0; + + QtObject { + id: d; + readonly property int minWidth: 480 + readonly property int maxWdith: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, pane.width); + var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) + + (extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) + + (buttons.height + 3 * hifi.dimensions.contentSpacing.y) + + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + hifi.dimensions.contentSpacing.y) : 0); + + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + root.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? + d.maxHeight : targetHeight); + } + } + + Item { + anchors { + top: parent.top; + bottom: extraInputs.visible ? extraInputs.top : buttons.top; + left: parent.left; + right: parent.right; + margins: 0; + } + + // FIXME make a text field type that can be bound to a history for autocompletion + TextField { + id: textField; + label: root.textInput.label; + focus: root.textInput ? true : false; + visible: root.textInput ? true : false; + anchors { + left: parent.left; + right: parent.right; + bottom: keyboard.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + } + + Keyboard { + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: raised ? hifi.dimensions.contentSpacing.y : 0 + } + } + } + + Item { + id: extraInputs; + visible: Boolean(root.checkBox || root.comboBox); + anchors { + left: parent.left; + right: parent.right; + bottom: buttons.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + height: comboBoxField.controlHeight; + onHeightChanged: d.resize(); + onWidthChanged: d.resize(); + + CheckBox { + id: checkBoxField; + text: root.checkBox.label; + focus: Boolean(root.checkBox); + visible: Boolean(root.checkBox); + anchors { + left: parent.left; + bottom: parent.bottom; + leftMargin: 6; // Magic number to align with warning icon + bottomMargin: 6; + } + } + + ComboBox { + id: comboBoxField; + label: root.comboBox.label; + focus: Boolean(root.comboBox); + visible: Boolean(root.comboBox); + Binding on x { + when: comboBoxField.visible + value: !checkBoxField.visible ? buttons.x : acceptButton.x + } + + Binding on width { + when: comboBoxField.visible + value: !checkBoxField.visible ? buttons.width : buttons.width - acceptButton.x + } + anchors { + right: parent.right; + bottom: parent.bottom; + } + model: root.comboBox ? root.comboBox.items : []; + onAccepted: { + updateCheckbox(); + focus = true; + } + } + } + + Row { + id: buttons; + focus: true; + spacing: hifi.dimensions.contentSpacing.x; + layoutDirection: Qt.RightToLeft; + onHeightChanged: d.resize(); + onWidthChanged: { + d.resize(); + resizeWarningText(); + } + + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + + function resizeWarningText() { + var rowWidth = buttons.width; + var buttonsWidth = acceptButton.width + cancelButton.width + hifi.dimensions.contentSpacing.x * 2; + var warningIconWidth = warningIcon.width + hifi.dimensions.contentSpacing.x; + warningText.width = rowWidth - buttonsWidth - warningIconWidth; + } + + Button { + id: cancelButton; + action: cancelAction; + } + + Button { + id: acceptButton; + action: acceptAction; + } + + Text { + id: warningText; + visible: Boolean(root.warning); + text: root.warning; + wrapMode: Text.WordWrap; + font.italic: true; + maximumLineCount: 2; + } + + HiFiGlyphs { + id: warningIcon; + visible: Boolean(root.warning); + text: hifi.glyphs.alert; + size: hifi.dimensions.controlLineHeight; + width: 20 // Line up with checkbox. + } + } + + Action { + id: cancelAction; + text: qsTr("Cancel"); + shortcut: "Esc"; + onTriggered: { + root.result = null; + root.canceled(); + // FIXME we are leaking memory to avoid a crash + // root.destroy(); + + root.disableFade = true + visible = false; + } + } + + Action { + id: acceptAction; + text: qsTr("Add"); + shortcut: "Return" + onTriggered: { + var result = {}; + if (textInput) { + result.textInput = textField.text; + } + if (comboBox) { + result.comboBox = comboBoxField.currentIndex; + result.comboBoxText = comboBoxField.currentText; + } + if (checkBox) { + result.checkBox = checkBoxField.enabled ? checkBoxField.checked : null; + } + root.result = JSON.stringify(result); + root.selected(root.result); + // FIXME we are leaking memory to avoid a crash + // root.destroy(); + + root.disableFade = true + visible = false; + } + } + } + + Keys.onPressed: { + if (!visible) { + return; + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger(); + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + acceptAction.trigger(); + event.accepted = true; + break; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + updateIcon(); + updateCheckbox(); + d.resize(); + textField.forceActiveFocus(); + } +} diff --git a/interface/resources/qml/dialogs/+android/FileDialog.qml b/interface/resources/qml/dialogs/+android/FileDialog.qml new file mode 100644 index 0000000000..548ab453a7 --- /dev/null +++ b/interface/resources/qml/dialogs/+android/FileDialog.qml @@ -0,0 +1,840 @@ +// +// FileDialog.qml +// +// Created by Bradley Austin Davis on 14 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import Qt.labs.folderlistmodel 2.1 +import Qt.labs.settings 1.0 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Controls 1.4 + +import ".." +import "../controls-uit" +import "../styles-uit" +import "../windows" + +import "fileDialog" + +//FIXME implement shortcuts for favorite location +ModalWindow { + id: root + resizable: true + implicitWidth: 480 + implicitHeight: 360 + (fileDialogItem.keyboardEnabled && fileDialogItem.keyboardRaised ? keyboard.raisedHeight + hifi.dimensions.contentSpacing.y : 0) + + minSize: Qt.vector2d(360, 240) + draggable: true + + HifiConstants { id: hifi } + + property var filesModel: ListModel { } + + Settings { + category: "FileDialog" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + + // Set from OffscreenUi::getOpenFile() + property alias caption: root.title; + // Set from OffscreenUi::getOpenFile() + property alias dir: fileTableModel.folder; + // Set from OffscreenUi::getOpenFile() + property alias filter: selectionType.filtersString; + // Set from OffscreenUi::getOpenFile() + property int options; // <-- FIXME unused + + property string iconText: root.title !== "" ? hifi.glyphs.scriptUpload : "" + property int iconSize: 40 + + property bool selectDirectory: false; + property bool showHidden: false; + // FIXME implement + property bool multiSelect: false; + property bool saveDialog: false; + property var helper: fileDialogHelper + property alias model: fileTableView.model + property var drives: helper.drives() + + property int titleWidth: 0 + + signal selectedFile(var file); + signal canceled(); + signal selected(int button); + function click(button) { + clickedButton = button; + selected(button); + destroy(); + } + + property int clickedButton: OriginalDialogs.StandardButton.NoButton; + + Component.onCompleted: { + console.log("Helper " + helper + " drives " + drives); + + fileDialogItem.keyboardEnabled = HMD.active; + + // HACK: The following lines force the model to initialize properly such that the go-up button + // works properly from the initial screen. + var initialFolder = folderListModel.folder; + fileTableModel.folder = helper.pathToUrl(drives[0]); + fileTableModel.folder = initialFolder; + + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; + + // Clear selection when click on external frame. + frameClicked.connect(function() { d.clearSelection(); }); + + if (selectDirectory) { + currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); + d.currentSelectionIsFolder = true; + d.currentSelectionUrl = initialFolder; + } + + helper.contentsChanged.connect(function() { + if (folderListModel) { + // Make folderListModel refresh. + var save = folderListModel.folder; + folderListModel.folder = ""; + folderListModel.folder = save; + } + }); + + focusTimer.start(); + } + + Timer { + id: focusTimer + interval: 10 + running: false + repeat: false + onTriggered: { + fileTableView.contentItem.forceActiveFocus(); + } + } + + Item { + id: fileDialogItem + clip: true + width: pane.width + height: pane.height + anchors.margins: 0 + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + drag.target: root + onClicked: { + d.clearSelection(); + // Defocus text field so that the keyboard gets hidden. + // Clicking also breaks keyboard navigation apart from backtabbing to cancel + frame.forceActiveFocus(); + } + } + + Row { + id: navControls + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: parent.left + } + spacing: hifi.dimensions.contentSpacing.x + + GlyphButton { + id: upButton + glyph: hifi.glyphs.levelUp + width: height + size: 30 + enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" + onClicked: d.navigateUp(); + Keys.onReturnPressed: { d.navigateUp(); } + KeyNavigation.tab: homeButton + KeyNavigation.backtab: upButton + KeyNavigation.left: upButton + KeyNavigation.right: homeButton + } + + GlyphButton { + id: homeButton + property var destination: helper.home(); + glyph: hifi.glyphs.home + size: 28 + width: height + enabled: d.homeDestination ? true : false + onClicked: d.navigateHome(); + Keys.onReturnPressed: { d.navigateHome(); } + KeyNavigation.tab: fileTableView.contentItem + KeyNavigation.backtab: upButton + KeyNavigation.left: upButton + } + } + + ComboBox { + id: pathSelector + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } + + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + choices.push(folders[0]); + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + + if (folders[0] === "") { + // Special handling for OSX root dir. + choices[0] = "/"; + } + + choices.reverse(); + + if (drives && drives.length > 1) { + choices.push("This PC"); + } + + if (choices.length > 0) { + pathSelector.model = choices; + } + } + + onLastValidFolderChanged: { + var folder = d.capitalizeDrive(lastValidFolder); + calculatePathChoices(folder); + } + + onCurrentTextChanged: { + var folder = currentText; + + if (/^[a-zA-z]:$/.test(folder)) { + folder = "file:///" + folder + "/"; + } else if (folder === "This PC") { + folder = "file:///"; + } else { + folder = helper.pathToUrl(folder); + } + + if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + if (root.selectDirectory) { + currentSelection.text = currentText !== "This PC" ? currentText : ""; + d.currentSelectionUrl = helper.pathToUrl(currentText); + } + fileTableModel.folder = folder; + } + } + + KeyNavigation.up: fileTableView.contentItem + KeyNavigation.down: fileTableView.contentItem + KeyNavigation.tab: fileTableView.contentItem + KeyNavigation.backtab: fileTableView.contentItem + KeyNavigation.left: fileTableView.contentItem + KeyNavigation.right: fileTableView.contentItem + } + + QtObject { + id: d + property var currentSelectionUrl; + readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); + property bool currentSelectionIsFolder; + property var backStack: [] + property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } + property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } + property var homeDestination: helper.home(); + + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + + function update() { + var row = fileTableView.currentRow; + + if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } + return; + } + + currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); + currentSelectionIsFolder = fileTableView.model !== filesModel ? + fileTableView.model.isFolder(row) : + fileTableModel.isFolder(row); + if (root.selectDirectory || !currentSelectionIsFolder) { + currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); + } else { + currentSelection.text = ""; + } + } + + function navigateUp() { + if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { + fileTableModel.folder = fileTableModel.parentFolder; + return true; + } + } + + function navigateHome() { + fileTableModel.folder = homeDestination; + return true; + } + + function clearSelection() { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + update(); + } + } + + FolderListModel { + id: folderListModel + nameFilters: selectionType.currentFilter + showDirsFirst: true + showDotAndDotDot: false + showFiles: !root.selectDirectory + Component.onCompleted: { + showFiles = !root.selectDirectory + } + + onFolderChanged: { + fileTableModel.update(); // Update once the data from the folder change is available. + } + + function getItem(index, field) { + return get(index, field); + } + } + + ListModel { + // Emulates FolderListModel but contains drive data. + id: driveListModel + + property int count: 1 + + Component.onCompleted: initialize(); + + function initialize() { + var drive, + i; + + count = drives.length; + + for (i = 0; i < count; i++) { + drive = drives[i].slice(0, -1); // Remove trailing "/". + append({ + fileName: drive, + fileModified: new Date(0), + fileSize: 0, + filePath: drive + "/", + fileIsDir: true, + fileNameSort: drive.toLowerCase() + }); + } + } + + function getItem(index, field) { + return get(index)[field]; + } + } + + Component { + id: filesModelBuilder + ListModel { } + } + + QtObject { + id: fileTableModel + + // FolderListModel has a couple of problems: + // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 + // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 + // + // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with + // drive information when viewing at the computer level. + + property var folder + property int sortOrder: Qt.AscendingOrder + property int sortColumn: 0 + property var model: folderListModel + property string parentFolder: calculateParentFolder(); + + readonly property string rootFolder: "file:///" + + function calculateParentFolder() { + if (model === folderListModel) { + if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { + return rootFolder; + } else { + return folderListModel.parentFolder; + } + } else { + return ""; + } + } + + onFolderChanged: { + if (folder === rootFolder) { + model = driveListModel; + helper.monitorDirectory(""); + update(); + } else { + var needsUpdate = model === driveListModel && folder === folderListModel.folder; + + model = folderListModel; + folderListModel.folder = folder; + helper.monitorDirectory(helper.urlToPath(folder)); + + if (needsUpdate) { + update(); + } + } + } + + function isFolder(row) { + if (row === -1) { + return false; + } + return filesModel.get(row).fileIsDir; + } + + function get(row) { + return filesModel.get(row) + } + + function update() { + var dataFields = ["fileName", "fileModified", "fileSize"], + sortFields = ["fileNameSort", "fileModified", "fileSize"], + dataField = dataFields[sortColumn], + sortField = sortFields[sortColumn], + sortValue, + fileName, + fileIsDir, + comparisonFunction, + lower, + middle, + upper, + rows = 0, + i; + + var newFilesModel = filesModelBuilder.createObject(root); + + comparisonFunction = sortOrder === Qt.AscendingOrder + ? function(a, b) { return a < b; } + : function(a, b) { return a > b; } + + for (i = 0; i < model.count; i++) { + fileName = model.getItem(i, "fileName"); + fileIsDir = model.getItem(i, "fileIsDir"); + + sortValue = model.getItem(i, dataField); + if (dataField === "fileName") { + // Directories first by prefixing a "*". + // Case-insensitive. + sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); + } + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (comparisonFunction(sortValue, newFilesModel.get(middle)[sortField])) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + newFilesModel.insert(lower, { + fileName: fileName, + fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), + fileSize: model.getItem(i, "fileSize"), + filePath: model.getItem(i, "filePath"), + fileIsDir: fileIsDir, + fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() + }); + + rows++; + } + filesModel = newFilesModel; + + d.clearSelection(); + } + } + + Table { + id: fileTableView + colorScheme: hifi.colorSchemes.light + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height + } + headerVisible: !selectDirectory + onDoubleClicked: navigateToRow(row); + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + sortIndicatorColumn: 0 + sortIndicatorOrder: Qt.AscendingOrder + sortIndicatorVisible: true + + model: filesModel + + function updateSort() { + fileTableModel.sortOrder = sortIndicatorOrder; + fileTableModel.sortColumn = sortIndicatorColumn; + fileTableModel.update(); + } + + onSortIndicatorColumnChanged: { updateSort(); } + + onSortIndicatorOrderChanged: { updateSort(); } + + itemDelegate: Item { + clip: true + + FiraSansSemiBold { + text: getText(); + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) + ? "Fira Sans SemiBold" : "Fira Sans" + + function getText() { + if (styleData.row === -1) { + return styleData.value; + } + + switch (styleData.column) { + case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; + case 2: return fileTableView.model.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 { + id: fileNameColumn + role: "fileName" + title: "Name" + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false + resizable: true + } + TableViewColumn { + id: fileModifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + movable: false + resizable: true + visible: !selectDirectory + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileModifiedColumn.width + movable: false + resizable: true + visible: !selectDirectory + } + + function navigateToRow(row) { + currentRow = row; + navigateToCurrentRow(); + } + + function navigateToCurrentRow() { + var currentModel = fileTableView.model !== filesModel ? fileTableView.model : fileTableModel + var row = fileTableView.currentRow + var isFolder = currentModel.isFolder(row); + var file = currentModel.get(row).filePath; + if (isFolder) { + currentModel.folder = helper.pathToUrl(file); + } else { + okAction.trigger(); + } + } + + property string prefix: "" + + function addToPrefix(event) { + if (!event.text || event.text === "") { + return false; + } + var newPrefix = prefix + event.text.toLowerCase(); + var matchedIndex = -1; + for (var i = 0; i < model.count; ++i) { + var name = model !== filesModel ? model.get(i).fileName.toLowerCase() : + filesModel.get(i).fileName.toLowerCase(); + if (0 === name.indexOf(newPrefix)) { + matchedIndex = i; + break; + } + } + + if (matchedIndex !== -1) { + fileTableView.selection.clear(); + fileTableView.selection.select(matchedIndex); + fileTableView.currentRow = matchedIndex; + fileTableView.prefix = newPrefix; + } + prefixClearTimer.restart(); + return true; + } + + Timer { + id: prefixClearTimer + interval: 1000 + repeat: false + running: false + onTriggered: fileTableView.prefix = ""; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + case Qt.Key_Tab: + case Qt.Key_Backtab: + event.accepted = false; + break; + case Qt.Key_Escape: + event.accepted = true; + root.click(OriginalDialogs.StandardButton.Cancel); + break; + default: + if (addToPrefix(event)) { + event.accepted = true + } else { + event.accepted = false; + } + break; + } + } + + KeyNavigation.tab: root.saveDialog ? currentSelection : openButton + } + + TextField { + id: currentSelection + label: selectDirectory ? "Directory:" : "File name:" + anchors { + left: parent.left + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 + bottom: keyboard.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + readOnly: !root.saveDialog + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); + KeyNavigation.up: fileTableView.contentItem + KeyNavigation.down: openButton + KeyNavigation.tab: openButton + KeyNavigation.backtab: fileTableView.contentItem + } + + FileTypeSelection { + id: selectionType + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } + visible: !selectDirectory && filtersCount > 1 + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: buttonRow + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + + Button { + id: openButton + color: hifi.buttons.blue + action: okAction + Keys.onReturnPressed: okAction.trigger() + KeyNavigation.right: cancelButton + KeyNavigation.up: root.saveDialog ? currentSelection : fileTableView.contentItem + KeyNavigation.tab: cancelButton + } + + Button { + id: cancelButton + action: cancelAction + Keys.onReturnPressed: { cancelAction.trigger() } + KeyNavigation.left: openButton + KeyNavigation.up: root.saveDialog ? currentSelection : fileTableView.contentItem + KeyNavigation.backtab: openButton + } + } + + Action { + id: okAction + text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && fileTableView.currentRow === -1) { + okActionTimer.start(); + } else { + fileTableView.navigateToCurrentRow(); + } + } + } + + Timer { + id: okActionTimer + interval: 50 + running: false + repeat: false + onTriggered: { + if (!root.saveDialog) { + selectedFile(d.currentSelectionUrl); + root.destroy() + return; + } + + // Handle the ambiguity between different cases + // * typed name (with or without extension) + // * full path vs relative vs filename only + var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); + + if (!selection) { + desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) + return; + } + + if (helper.urlIsDir(selection)) { + root.dir = selection; + currentSelection.text = ""; + return; + } + + // Check if the file is a valid target + if (!helper.urlIsWritable(selection)) { + desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Warning, + text: "Unable to write to location " + selection + }) + return; + } + + if (helper.urlExists(selection)) { + var messageBox = desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + text: "Do you wish to overwrite " + selection + "?", + }); + var result = messageBox.exec(); + if (OriginalDialogs.StandardButton.Yes !== result) { + return; + } + } + + console.log("Selecting " + selection) + selectedFile(selection); + root.destroy(); + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { canceled(); root.shown = false; } + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + event.accepted = d.navigateUp(); + break; + + case Qt.Key_Home: + event.accepted = d.navigateHome(); + break; + + case Qt.Key_Escape: + event.accepted = true; + root.click(OriginalDialogs.StandardButton.Cancel); + break; + } + } +} diff --git a/interface/resources/qml/dialogs/+android/QueryDialog.qml b/interface/resources/qml/dialogs/+android/QueryDialog.qml new file mode 100644 index 0000000000..aec6d8a286 --- /dev/null +++ b/interface/resources/qml/dialogs/+android/QueryDialog.qml @@ -0,0 +1,231 @@ +// +// QueryDialog.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 1.4 + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +ModalWindow { + id: root + HifiConstants { id: hifi } + implicitWidth: 640 + implicitHeight: 320 + visible: true + keyboardOverride: true // Disable ModalWindow's keyboard. + + signal selected(var result); + signal canceled(); + + property int icon: hifi.icons.none + property string iconText: "" + property int iconSize: 35 + onIconChanged: updateIcon(); + + property var items; + property string label + property var result; + property alias current: textResult.text + + // For text boxes + property alias placeholderText: textResult.placeholderText + + // For combo boxes + property bool editable: true; + + property int titleWidth: 0 + onTitleWidthChanged: d.resize(); + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + Item { + id: modalWindowItem + clip: true + width: pane.width + height: pane.height + anchors.margins: 0 + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWdith: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, pane.width) + var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + root.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0) + } + } + + Item { + anchors { + top: parent.top + bottom: keyboard.top; + left: parent.left; + right: parent.right; + margins: 0 + bottomMargin: 2 * hifi.dimensions.contentSpacing.y + } + + // FIXME make a text field type that can be bound to a history for autocompletion + TextField { + id: textResult + label: root.label + visible: items ? false : true + anchors { + left: parent.left; + right: parent.right; + bottom: parent.bottom + } + KeyNavigation.down: acceptButton + KeyNavigation.tab: acceptButton + } + + ComboBox { + id: comboBox + label: root.label + visible: items ? true : false + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + model: items ? items : [] + KeyNavigation.down: acceptButton + KeyNavigation.tab: acceptButton + } + } + + property alias keyboardOverride: root.keyboardOverride + property alias keyboardRaised: root.keyboardRaised + property alias punctuationMode: root.punctuationMode + Keyboard { + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Flow { + id: buttons + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + layoutDirection: Qt.RightToLeft + anchors { + bottom: parent.bottom + right: parent.right + margins: 0 + bottomMargin: hifi.dimensions.contentSpacing.y + } + Button { + id: cancelButton + action: cancelAction + KeyNavigation.left: acceptButton + KeyNavigation.up: items ? comboBox : textResult + KeyNavigation.backtab: acceptButton + } + Button { + id: acceptButton + action: acceptAction + KeyNavigation.right: cancelButton + KeyNavigation.up: items ? comboBox : textResult + KeyNavigation.tab: cancelButton + KeyNavigation.backtab: items ? comboBox : textResult + } + } + + Action { + id: cancelAction + text: qsTr("Cancel"); + shortcut: "Esc" + onTriggered: { + root.canceled(); + // FIXME we are leaking memory to avoid a crash + // root.destroy(); + + root.disableFade = true + visible = false; + } + } + + Action { + id: acceptAction + text: qsTr("OK"); + shortcut: "Return" + onTriggered: { + root.result = items ? comboBox.currentText : textResult.text + root.selected(root.result); + // FIXME we are leaking memory to avoid a crash + // root.destroy(); + + root.disableFade = true + visible = false; + } + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger() + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + if (acceptButton.focus) { + acceptAction.trigger() + } else if (cancelButton.focus) { + cancelAction.trigger() + } else if (comboBox.focus || comboBox.popup.focus) { + comboBox.showList() + } + event.accepted = true; + break; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + updateIcon(); + d.resize(); + if (items) { + comboBox.forceActiveFocus() + } else { + textResult.forceActiveFocus() + } + } +} diff --git a/interface/resources/qml/dialogs/assetDialog/+android/AssetDialogContent.qml b/interface/resources/qml/dialogs/assetDialog/+android/AssetDialogContent.qml new file mode 100644 index 0000000000..54bdb0a888 --- /dev/null +++ b/interface/resources/qml/dialogs/assetDialog/+android/AssetDialogContent.qml @@ -0,0 +1,533 @@ +// +// AssetDialogContent.qml +// +// Created by David Rowe on 19 Apr 2017 +// Copyright 2017 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 +// + +import QtQuick 2.7 +import QtQuick.Controls 1.5 + +import "../../controls-uit" +import "../../styles-uit" + +import "../fileDialog" + +Item { + // Set from OffscreenUi::assetDialog() + property alias dir: assetTableModel.folder + property alias filter: selectionType.filtersString // FIXME: Currently only supports simple filters, "*.xxx". + property int options // Not used. + + property bool selectDirectory: false + + // Not implemented. + //property bool saveDialog: false; + //property bool multiSelect: false; + + property bool singleClickNavigate: false + + HifiConstants { id: hifi } + + Component.onCompleted: { + homeButton.destination = dir; + + if (selectDirectory) { + d.currentSelectionIsFolder = true; + d.currentSelectionPath = assetTableModel.folder; + } + + assetTableView.forceActiveFocus(); + } + + Item { + id: assetDialogItem + anchors.fill: parent + clip: true + + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + drag.target: root + onClicked: { + d.clearSelection(); + frame.forceActiveFocus(); + assetTableView.forceActiveFocus(); + } + } + + Row { + id: navControls + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: parent.left + } + spacing: hifi.dimensions.contentSpacing.x + + GlyphButton { + id: upButton + glyph: hifi.glyphs.levelUp + width: height + size: 30 + enabled: assetTableModel.parentFolder !== "" + onClicked: d.navigateUp(); + } + + GlyphButton { + id: homeButton + property string destination: "" + glyph: hifi.glyphs.home + size: 28 + width: height + enabled: destination !== "" + //onClicked: d.navigateHome(); + onClicked: assetTableModel.folder = destination; + } + } + + ComboBox { + id: pathSelector + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } + z: 10 + + property string lastValidFolder: assetTableModel.folder + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + choices.push(folders[0]); + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + + if (folders[0] === "") { + choices[0] = "/"; + } + + choices.reverse(); + + if (choices.length > 0) { + pathSelector.model = choices; + } + } + + onLastValidFolderChanged: { + var folder = lastValidFolder; + calculatePathChoices(folder); + } + + onCurrentTextChanged: { + var folder = currentText; + + if (folder !== "/") { + folder += "/"; + } + + if (folder !== assetTableModel.folder) { + if (root.selectDirectory) { + currentSelection.text = currentText; + d.currentSelectionPath = currentText; + } + assetTableModel.folder = folder; + assetTableView.forceActiveFocus(); + } + } + } + + QtObject { + id: d + + property string currentSelectionPath + property bool currentSelectionIsFolder + property var tableViewConnection: Connections { target: assetTableView; onCurrentRowChanged: d.update(); } + + function update() { + var row = assetTableView.currentRow; + + if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } + return; + } + + var rowInfo = assetTableModel.get(row); + currentSelectionPath = rowInfo.filePath; + currentSelectionIsFolder = rowInfo.fileIsDir; + if (root.selectDirectory || !currentSelectionIsFolder) { + currentSelection.text = currentSelectionPath; + } else { + currentSelection.text = ""; + } + } + + function navigateUp() { + if (assetTableModel.parentFolder !== "") { + assetTableModel.folder = assetTableModel.parentFolder; + return true; + } + return false; + } + + function navigateHome() { + assetTableModel.folder = homeButton.destination; + return true; + } + + function clearSelection() { + assetTableView.selection.clear(); + assetTableView.currentRow = -1; + update(); + } + } + + ListModel { + id: assetTableModel + + property string folder + property string parentFolder: "" + readonly property string rootFolder: "/" + + onFolderChanged: { + parentFolder = calculateParentFolder(); + update(); + } + + function calculateParentFolder() { + if (folder !== "/") { + return folder.slice(0, folder.slice(0, -1).lastIndexOf("/") + 1); + } + return ""; + } + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function onGetAllMappings(error, map) { + var mappings, + fileTypeFilter, + index, + path, + fileName, + fileType, + fileIsDir, + isValid, + subDirectory, + subDirectories = [], + fileNameSort, + rows = 0, + lower, + middle, + upper, + i, + length; + + clear(); + + if (error === "") { + mappings = Object.keys(map); + fileTypeFilter = filter.replace("*", "").toLowerCase(); + + for (i = 0, length = mappings.length; i < length; i++) { + index = mappings[i].lastIndexOf("/"); + + path = mappings[i].slice(0, mappings[i].lastIndexOf("/") + 1); + fileName = mappings[i].slice(path.length); + fileType = fileName.slice(fileName.lastIndexOf(".")); + fileIsDir = false; + isValid = false; + + if (fileType.toLowerCase() === fileTypeFilter) { + if (path === folder) { + isValid = !selectDirectory; + } else if (path.length > folder.length) { + subDirectory = path.slice(folder.length); + index = subDirectory.indexOf("/"); + if (index === subDirectory.lastIndexOf("/")) { + fileName = subDirectory.slice(0, index); + if (subDirectories.indexOf(fileName) === -1) { + fileIsDir = true; + isValid = true; + subDirectories.push(fileName); + } + } + } + } + + if (isValid) { + fileNameSort = (fileIsDir ? "*" : "") + fileName.toLowerCase(); + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (fileNameSort < get(middle)["fileNameSort"]) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + filePath: path + (fileIsDir ? "" : fileName), + fileIsDir: fileIsDir, + fileNameSort: fileNameSort + }); + + rows++; + } + } + + } else { + console.log("Error getting mappings from Asset Server"); + } + } + + function update() { + d.clearSelection(); + clear(); + Assets.getAllMappings(onGetAllMappings); + } + } + + Table { + id: assetTableView + colorScheme: hifi.colorSchemes.light + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height + } + + model: assetTableModel + + focus: true + + onClicked: { + if (singleClickNavigate) { + navigateToRow(row); + } + } + + onDoubleClicked: navigateToRow(row); + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + itemDelegate: Item { + clip: true + + FiraSansSemiBold { + text: styleData.value + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + font.family: (styleData.row !== -1 && assetTableView.model.get(styleData.row).fileIsDir) + ? "Fira Sans SemiBold" : "Fira Sans" + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: assetTableView.width + movable: false + resizable: false + } + + function navigateToRow(row) { + currentRow = row; + navigateToCurrentRow(); + } + + function navigateToCurrentRow() { + if (model.isFolder(currentRow)) { + model.folder = model.get(currentRow).filePath; + } else { + okAction.trigger(); + } + } + + Timer { + id: prefixClearTimer + interval: 1000 + repeat: false + running: false + onTriggered: assetTableView.prefix = ""; + } + + property string prefix: "" + + function addToPrefix(event) { + if (!event.text || event.text === "") { + return false; + } + var newPrefix = prefix + event.text.toLowerCase(); + var matchedIndex = -1; + for (var i = 0; i < model.count; ++i) { + var name = model.get(i).fileName.toLowerCase(); + if (0 === name.indexOf(newPrefix)) { + matchedIndex = i; + break; + } + } + + if (matchedIndex !== -1) { + assetTableView.selection.clear(); + assetTableView.selection.select(matchedIndex); + assetTableView.currentRow = matchedIndex; + assetTableView.prefix = newPrefix; + } + prefixClearTimer.restart(); + return true; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + case Qt.Key_Tab: + case Qt.Key_Backtab: + event.accepted = false; + break; + + default: + if (addToPrefix(event)) { + event.accepted = true + } else { + event.accepted = false; + } + break; + } + } + } + + TextField { + id: currentSelection + label: selectDirectory ? "Directory:" : "File name:" + anchors { + left: parent.left + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 + bottom: buttonRow.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + readOnly: true + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); + } + + FileTypeSelection { + id: selectionType + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } + visible: !selectDirectory && filtersCount > 1 + KeyNavigation.left: assetTableView + KeyNavigation.right: openButton + } + + Action { + id: okAction + text: currentSelection.text && root.selectDirectory && assetTableView.currentRow === -1 ? "Choose" : "Open" + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && assetTableView.currentRow === -1) { + selectedAsset(d.currentSelectionPath); + root.destroy(); + } else { + assetTableView.navigateToCurrentRow(); + } + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { + canceled(); + root.destroy(); + } + } + + Row { + id: buttonRow + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + + Button { + id: openButton + color: hifi.buttons.blue + action: okAction + Keys.onReturnPressed: okAction.trigger() + KeyNavigation.up: selectionType + KeyNavigation.left: selectionType + KeyNavigation.right: cancelButton + } + + Button { + id: cancelButton + action: cancelAction + KeyNavigation.up: selectionType + KeyNavigation.left: openButton + KeyNavigation.right: assetTableView.contentItem + Keys.onReturnPressed: { canceled(); root.enabled = false } + } + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + event.accepted = d.navigateUp(); + break; + + case Qt.Key_Home: + event.accepted = d.navigateHome(); + break; + + } + } +}