From 00b6b2a2c0d52ee0bfe6d23cae50da8bcbac72ed Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 19 Apr 2017 12:34:59 +1200 Subject: [PATCH] Implement desktop recordings browser dialog --- .../resources/qml/dialogs/AssetDialog.qml | 469 +++++++++++++++++- .../scripting/WindowScriptingInterface.cpp | 6 + libraries/ui/src/OffscreenUi.cpp | 2 +- scripts/system/record.js | 2 +- 4 files changed, 450 insertions(+), 29 deletions(-) diff --git a/interface/resources/qml/dialogs/AssetDialog.qml b/interface/resources/qml/dialogs/AssetDialog.qml index 6b83f6670b..caa688bc81 100644 --- a/interface/resources/qml/dialogs/AssetDialog.qml +++ b/interface/resources/qml/dialogs/AssetDialog.qml @@ -16,11 +16,13 @@ import "../controls-uit" import "../styles-uit" import "../windows" +import "fileDialog" + ModalWindow { id: root resizable: true implicitWidth: 480 - implicitHeight: 360 + (fileDialogItem.keyboardEnabled && fileDialogItem.keyboardRaised ? keyboard.raisedHeight + hifi.dimensions.contentSpacing.y : 0) + implicitHeight: 360 minSize: Qt.vector2d(360, 240) draggable: true @@ -28,7 +30,7 @@ ModalWindow { HifiConstants { id: hifi } Settings { - category: "FileDialog" + category: "AssetDialog" property alias width: root.width property alias height: root.height property alias x: root.x @@ -36,21 +38,30 @@ ModalWindow { } // Set from OffscreenUi::assetDialog() - //property alias caption: root.title; - property string caption - //property alias dir: fileTableModel.folder; - property string dir - //property alias filter: selectionType.filtersString; - property string filter - property int options // Unused + property alias caption: root.title + property alias dir: assetTableModel.folder + property alias filter: selectionType.filtersString // FIXME: Currently only supports simple filters, "*.xxx". + property int options // Not used. - // TODO: Other properties + property bool selectDirectory: false; + property int titleWidth: 0 // For ModalFrame. + + // Not implemented. + //property bool saveDialog: false; + //property bool multiSelect: false; signal selectedAsset(var asset); signal canceled(); Component.onCompleted: { - // TODO: Required? + homeButton.destination = dir; + + if (selectDirectory) { + d.currentSelectionIsFolder = true; + d.currentSelectionPath = assetTableModel.folder; + } + + assetTableView.forceActiveFocus(); } Item { @@ -60,27 +71,432 @@ ModalWindow { height: pane.height anchors.margins: 0 + 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 + } + + 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 + + onDoubleClicked: navigateToRow(row); + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + itemDelegate: Item { + clip: true + + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + + 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) + ? firaSansSemiBold.name : firaSansRegular.name + } + } + + 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 - // TODO - /* - text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" + 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 && fileTableView.currentRow === -1) { - okActionTimer.start(); + || root.selectDirectory && assetTableView.currentRow === -1) { + selectedAsset(d.currentSelectionPath); + root.destroy(); } else { - fileTableView.navigateToCurrentRow(); + assetTableView.navigateToCurrentRow(); } } - */ - text: "Choose" - - onTriggered: { - selectedAsset("/recordings/20170417-233012.hfr"); - root.destroy(); // TODO: root.shown = false? - } } Action { @@ -88,7 +504,7 @@ ModalWindow { text: "Cancel" onTriggered: { canceled(); - root.shown = false; + root.destroy(); } } @@ -115,11 +531,10 @@ ModalWindow { action: cancelAction KeyNavigation.up: selectionType KeyNavigation.left: openButton - KeyNavigation.right: fileTableView.contentItem + KeyNavigation.right: assetTableView.contentItem Keys.onReturnPressed: { canceled(); root.enabled = false } } } - } Keys.onPressed: { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 13ffd2f78d..39cf99f349 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -224,6 +224,12 @@ QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const if (path.isEmpty()) { path = getPreviousBrowseAssetLocation(); } + if (path.left(1) != "/") { + path = "/" + path; + } + if (path.right(1) != "/") { + path = path + "/"; + } QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter); if (!result.isEmpty()) { setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath()); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 3cec37b7aa..3e8bb3f592 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -782,7 +782,7 @@ QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, // FIXME support returning the selected filter... somehow? QVariantMap map; map.insert("caption", caption); - map.insert("dir", QUrl::fromLocalFile(dir)); + map.insert("dir", dir); map.insert("filter", filter); map.insert("options", static_cast(options)); return assetDialog(map); diff --git a/scripts/system/record.js b/scripts/system/record.js index 6790ca7007..8cfdd28202 100644 --- a/scripts/system/record.js +++ b/scripts/system/record.js @@ -489,7 +489,7 @@ break; case LOAD_RECORDING_ACTION: // User wants to select an ATP recording to play. - recording = Window.browseAssets("Select Recording to Play", "recordings", "*.hrf"); + recording = Window.browseAssets("Select Recording to Play", "recordings", "*.hfr"); if (recording) { log("Load recording " + recording); Player.playRecording("atp:" + recording, MyAvatar.position, MyAvatar.orientation);