diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 7fb5cd3127..0286c45ac3 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -262,11 +262,10 @@ FocusScope { } Component { id: fileDialogBuilder; FileDialog { } } - function fileOpenDialog(properties) { + function fileDialog(properties) { return fileDialogBuilder.createObject(desktop, properties); } - MenuMouseHandler { id: menuPopperUpper } function popupMenu(point) { menuPopperUpper.popup(desktop, rootMenu.items, point); diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 3a26c3ca27..142ed198c0 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 1.4 import Qt.labs.folderlistmodel 2.1 import Qt.labs.settings 1.0 import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs import ".." import "../windows" @@ -39,7 +40,6 @@ ModalWindow { property bool showHidden: false; // FIXME implement property bool multiSelect: false; - // FIXME implement property bool saveDialog: false; property var helper: fileDialogHelper property alias model: fileTableView.model @@ -124,6 +124,8 @@ ModalWindow { currentSelectionIsFolder = fileTableView.model.isFolder(row); if (root.selectDirectory || !currentSelectionIsFolder) { currentSelection.text = helper.urlToPath(currentSelectionUrl); + } else { + currentSelection.text = "" } } @@ -175,8 +177,7 @@ ModalWindow { if (isFolder) { fileTableView.model.folder = file } else { - root.selectedFile(file); - root.destroy(); + okAction.trigger(); } } } @@ -185,8 +186,10 @@ ModalWindow { id: currentSelection style: TextFieldStyle { renderType: Text.QtRendering } anchors { right: root.selectDirectory ? parent.right : selectionType.left; rightMargin: 8; left: parent.left; leftMargin: 8; top: selectionType.top } - readOnly: true - activeFocusOnTab: false + readOnly: !root.saveDialog + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); } FileTypeSelection { @@ -206,25 +209,82 @@ ModalWindow { spacing: 8 Button { id: openButton - text: root.selectDirectory ? "Choose" : "Open" - enabled: currentSelection.text ? true : false - onClicked: { selectedFile(d.currentSelectionUrl); root.visible = false; } - Keys.onReturnPressed: { selectedFile(d.currentSelectionUrl); root.visible = false; } - + action: okAction + Keys.onReturnPressed: okAction.trigger() KeyNavigation.up: selectionType KeyNavigation.left: selectionType KeyNavigation.right: cancelButton } Button { id: cancelButton - text: "Cancel" + action: cancelAction KeyNavigation.up: selectionType KeyNavigation.left: openButton KeyNavigation.right: fileTableView.contentItem Keys.onReturnPressed: { canceled(); root.enabled = false } - onClicked: { canceled(); root.visible = false; } } } + + Action { + id: okAction + text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open") + enabled: currentSelection.text ? true : false + onTriggered: { + if (root.saveDialog) { + // 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, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + 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; + } + } + + selectedFile(d.currentSelectionUrl); + root.destroy() + } else { + selectedFile(d.currentSelectionUrl); + root.destroy() + } + + } + + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { canceled(); root.visible = false; } + } } Keys.onPressed: { diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index ca00da1aa3..3b7cc2c9a8 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -25,6 +25,10 @@ ModalWindow { destroy(); } + function exec() { + return OffscreenUi.waitForMessageBoxResult(root); + } + property alias detailedText: detailedText.text property alias text: mainTextContainer.text property alias informativeText: informativeTextContainer.text diff --git a/libraries/ui/src/FileDialogHelper.cpp b/libraries/ui/src/FileDialogHelper.cpp index f8a0929702..f1cbb22f56 100644 --- a/libraries/ui/src/FileDialogHelper.cpp +++ b/libraries/ui/src/FileDialogHelper.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include QUrl FileDialogHelper::home() { @@ -26,7 +28,11 @@ QString FileDialogHelper::urlToPath(const QUrl& url) { return url.toLocalFile(); } -bool FileDialogHelper::validPath(const QString& path) { +bool FileDialogHelper::fileExists(const QString& path) { + return QFile(path).exists(); +} + +bool FileDialogHelper::validPath(const QString& path) { return QFile(path).exists(); } @@ -38,3 +44,47 @@ QUrl FileDialogHelper::pathToUrl(const QString& path) { return QUrl::fromLocalFile(path); } + +QUrl FileDialogHelper::saveHelper(const QString& saveText, const QUrl& currentFolder, const QStringList& selectionFilters) { + qDebug() << "Calling save helper with " << saveText << " " << currentFolder << " " << selectionFilters; + + QFileInfo fileInfo(saveText); + + // Check if it's a relative path and if it is resolve to the absolute path + { + if (fileInfo.isRelative()) { + fileInfo = QFileInfo(currentFolder.toLocalFile() + "/" + fileInfo.filePath()); + } + } + + // Check if we need to append an extension, but only if the current resolved path isn't a directory + if (!fileInfo.isDir()) { + QString fileName = fileInfo.fileName(); + if (!fileName.contains(".") && selectionFilters.size() == 1) { + const QRegularExpression extensionRe{ ".*(\\.[a-zA-Z0-9]+)$" }; + QString filter = selectionFilters[0]; + auto match = extensionRe.match(filter); + if (match.hasMatch()) { + fileInfo = QFileInfo(fileInfo.filePath() + match.captured(1)); + } + } + } + + return QUrl::fromLocalFile(fileInfo.absoluteFilePath()); +} + +bool FileDialogHelper::urlIsDir(const QUrl& url) { + return QFileInfo(url.toLocalFile()).isDir(); +} + +bool FileDialogHelper::urlIsFile(const QUrl& url) { + return QFileInfo(url.toLocalFile()).isFile(); +} + +bool FileDialogHelper::urlExists(const QUrl& url) { + return QFileInfo(url.toLocalFile()).exists(); +} + +bool FileDialogHelper::urlIsWritable(const QUrl& url) { + return QFileInfo(url.toLocalFile()).isWritable(); +} diff --git a/libraries/ui/src/FileDialogHelper.h b/libraries/ui/src/FileDialogHelper.h index edb702eeda..0142473533 100644 --- a/libraries/ui/src/FileDialogHelper.h +++ b/libraries/ui/src/FileDialogHelper.h @@ -48,9 +48,15 @@ public: Q_INVOKABLE QUrl home(); Q_INVOKABLE QStringList standardPath(StandardLocation location); Q_INVOKABLE QString urlToPath(const QUrl& url); + Q_INVOKABLE bool urlIsDir(const QUrl& url); + Q_INVOKABLE bool urlIsFile(const QUrl& url); + Q_INVOKABLE bool urlExists(const QUrl& url); + Q_INVOKABLE bool urlIsWritable(const QUrl& url); + Q_INVOKABLE bool fileExists(const QString& path); Q_INVOKABLE bool validPath(const QString& path); Q_INVOKABLE bool validFolder(const QString& path); Q_INVOKABLE QUrl pathToUrl(const QString& path); + Q_INVOKABLE QUrl saveHelper(const QString& saveText, const QUrl& currentFolder, const QStringList& selectionFilters); }; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 3ac12d014f..fa40fedb9b 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -105,6 +105,7 @@ void OffscreenUi::create(QOpenGLContext* context) { OffscreenQmlSurface::create(context); auto rootContext = getRootContext(); + rootContext->setContextProperty("OffscreenUi", this); rootContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); rootContext->setContextProperty("urlHandler", new UrlHandler()); rootContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); @@ -219,7 +220,7 @@ QQuickItem* OffscreenUi::createMessageBox(QMessageBox::Icon icon, const QString& return qvariant_cast(result); } -QMessageBox::StandardButton OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) { +int OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) { if (!messageBox) { return QMessageBox::NoButton; } @@ -241,7 +242,7 @@ QMessageBox::StandardButton OffscreenUi::messageBox(QMessageBox::Icon icon, cons return result; } - return waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton)); + return static_cast(waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton))); } QMessageBox::StandardButton OffscreenUi::critical(const QString& title, const QString& text, @@ -478,6 +479,26 @@ private slots: } }; + +QString OffscreenUi::fileDialog(const QVariantMap& properties) { + QVariant buildDialogResult; + bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + + if (!invokeResult) { + qWarning() << "Failed to create file open dialog"; + return QString(); + } + + QVariant result = FileDialogListener(qvariant_cast(buildDialogResult)).waitForResult(); + if (!result.isValid()) { + return QString(); + } + qDebug() << result.toString(); + return result.toUrl().toLocalFile(); +} + QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { if (QThread::currentThread() != thread()) { QString result; @@ -497,23 +518,31 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, map.insert("dir", QUrl::fromLocalFile(dir)); map.insert("filter", filter); map.insert("options", static_cast(options)); + return fileDialog(map); +} - QVariant buildDialogResult; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileOpenDialog", - Q_RETURN_ARG(QVariant, buildDialogResult), - Q_ARG(QVariant, QVariant::fromValue(map))); - - if (!invokeResult) { - qWarning() << "Failed to create file open dialog"; - return QString(); +QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) { + if (QThread::currentThread() != thread()) { + QString result; + QMetaObject::invokeMethod(this, "fileSaveDialog", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QString, result), + Q_ARG(QString, caption), + Q_ARG(QString, dir), + Q_ARG(QString, filter), + Q_ARG(QString*, selectedFilter), + Q_ARG(QFileDialog::Options, options)); + return result; } - QVariant result = FileDialogListener(qvariant_cast(buildDialogResult)).waitForResult(); - if (!result.isValid()) { - return QString(); - } - qDebug() << result.toString(); - return result.toUrl().toLocalFile(); + // FIXME support returning the selected filter... somehow? + QVariantMap map; + map.insert("caption", caption); + map.insert("dir", QUrl::fromLocalFile(dir)); + map.insert("filter", filter); + map.insert("options", static_cast(options)); + map.insert("saveDialog", true); + + return fileDialog(map); } QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { @@ -521,7 +550,7 @@ QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, cons } QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) { - return QFileDialog::getSaveFileName((QWidget*)ignored, caption, dir, filter, selectedFilter, options); + return DependencyManager::get()->fileSaveDialog(caption, dir, filter, selectedFilter, options); } diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 73a9ca7c47..de479853f3 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -47,7 +47,7 @@ public: // Must be called from the main thread QQuickItem* createMessageBox(QMessageBox::Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); // Must be called from the main thread - QMessageBox::StandardButton waitForMessageBoxResult(QQuickItem* messageBox); + Q_INVOKABLE int waitForMessageBoxResult(QQuickItem* messageBox); /// Same design as QMessageBox::critical(), will block, returns result static QMessageBox::StandardButton critical(void* ignored, const QString& title, const QString& text, @@ -87,10 +87,12 @@ public: QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); - // file dialog compatibility Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + // Compatibility with QFileDialog::getOpenFileName static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); + // Compatibility with QFileDialog::getSaveFileName static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0); @@ -105,6 +107,8 @@ public: static QString getItem(void *ignored, const QString & title, const QString & label, const QStringList & items, int current = 0, bool editable = true, bool * ok = 0, Qt::WindowFlags flags = 0, Qt::InputMethodHints inputMethodHints = Qt::ImhNone); private: + QString fileDialog(const QVariantMap& properties); + QQuickItem* _desktop { nullptr }; QQuickItem* _toolWindow { nullptr }; };