Support save file dialog

This commit is contained in:
Bradley Austin Davis 2016-02-03 14:49:50 -08:00
parent 8f85abfec8
commit 60d97c45af
7 changed files with 186 additions and 34 deletions

View file

@ -262,11 +262,10 @@ FocusScope {
} }
Component { id: fileDialogBuilder; FileDialog { } } Component { id: fileDialogBuilder; FileDialog { } }
function fileOpenDialog(properties) { function fileDialog(properties) {
return fileDialogBuilder.createObject(desktop, properties); return fileDialogBuilder.createObject(desktop, properties);
} }
MenuMouseHandler { id: menuPopperUpper } MenuMouseHandler { id: menuPopperUpper }
function popupMenu(point) { function popupMenu(point) {
menuPopperUpper.popup(desktop, rootMenu.items, point); menuPopperUpper.popup(desktop, rootMenu.items, point);

View file

@ -3,6 +3,7 @@ import QtQuick.Controls 1.4
import Qt.labs.folderlistmodel 2.1 import Qt.labs.folderlistmodel 2.1
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import QtQuick.Dialogs 1.2 as OriginalDialogs
import ".." import ".."
import "../windows" import "../windows"
@ -39,7 +40,6 @@ ModalWindow {
property bool showHidden: false; property bool showHidden: false;
// FIXME implement // FIXME implement
property bool multiSelect: false; property bool multiSelect: false;
// FIXME implement
property bool saveDialog: false; property bool saveDialog: false;
property var helper: fileDialogHelper property var helper: fileDialogHelper
property alias model: fileTableView.model property alias model: fileTableView.model
@ -124,6 +124,8 @@ ModalWindow {
currentSelectionIsFolder = fileTableView.model.isFolder(row); currentSelectionIsFolder = fileTableView.model.isFolder(row);
if (root.selectDirectory || !currentSelectionIsFolder) { if (root.selectDirectory || !currentSelectionIsFolder) {
currentSelection.text = helper.urlToPath(currentSelectionUrl); currentSelection.text = helper.urlToPath(currentSelectionUrl);
} else {
currentSelection.text = ""
} }
} }
@ -175,8 +177,7 @@ ModalWindow {
if (isFolder) { if (isFolder) {
fileTableView.model.folder = file fileTableView.model.folder = file
} else { } else {
root.selectedFile(file); okAction.trigger();
root.destroy();
} }
} }
} }
@ -185,8 +186,10 @@ ModalWindow {
id: currentSelection id: currentSelection
style: TextFieldStyle { renderType: Text.QtRendering } style: TextFieldStyle { renderType: Text.QtRendering }
anchors { right: root.selectDirectory ? parent.right : selectionType.left; rightMargin: 8; left: parent.left; leftMargin: 8; top: selectionType.top } anchors { right: root.selectDirectory ? parent.right : selectionType.left; rightMargin: 8; left: parent.left; leftMargin: 8; top: selectionType.top }
readOnly: true readOnly: !root.saveDialog
activeFocusOnTab: false activeFocusOnTab: !readOnly
onActiveFocusChanged: if (activeFocus) { selectAll(); }
onAccepted: okAction.trigger();
} }
FileTypeSelection { FileTypeSelection {
@ -206,25 +209,82 @@ ModalWindow {
spacing: 8 spacing: 8
Button { Button {
id: openButton id: openButton
text: root.selectDirectory ? "Choose" : "Open" action: okAction
enabled: currentSelection.text ? true : false Keys.onReturnPressed: okAction.trigger()
onClicked: { selectedFile(d.currentSelectionUrl); root.visible = false; }
Keys.onReturnPressed: { selectedFile(d.currentSelectionUrl); root.visible = false; }
KeyNavigation.up: selectionType KeyNavigation.up: selectionType
KeyNavigation.left: selectionType KeyNavigation.left: selectionType
KeyNavigation.right: cancelButton KeyNavigation.right: cancelButton
} }
Button { Button {
id: cancelButton id: cancelButton
text: "Cancel" action: cancelAction
KeyNavigation.up: selectionType KeyNavigation.up: selectionType
KeyNavigation.left: openButton KeyNavigation.left: openButton
KeyNavigation.right: fileTableView.contentItem KeyNavigation.right: fileTableView.contentItem
Keys.onReturnPressed: { canceled(); root.enabled = false } 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: { Keys.onPressed: {

View file

@ -25,6 +25,10 @@ ModalWindow {
destroy(); destroy();
} }
function exec() {
return OffscreenUi.waitForMessageBoxResult(root);
}
property alias detailedText: detailedText.text property alias detailedText: detailedText.text
property alias text: mainTextContainer.text property alias text: mainTextContainer.text
property alias informativeText: informativeTextContainer.text property alias informativeText: informativeTextContainer.text

View file

@ -12,6 +12,8 @@
#include <QtCore/QDir> #include <QtCore/QDir>
#include <QtCore/QFile> #include <QtCore/QFile>
#include <QtCore/QDebug>
#include <QtCore/QRegularExpression>
QUrl FileDialogHelper::home() { QUrl FileDialogHelper::home() {
@ -26,7 +28,11 @@ QString FileDialogHelper::urlToPath(const QUrl& url) {
return url.toLocalFile(); 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(); return QFile(path).exists();
} }
@ -38,3 +44,47 @@ QUrl FileDialogHelper::pathToUrl(const QString& path) {
return QUrl::fromLocalFile(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();
}

View file

@ -48,9 +48,15 @@ public:
Q_INVOKABLE QUrl home(); Q_INVOKABLE QUrl home();
Q_INVOKABLE QStringList standardPath(StandardLocation location); Q_INVOKABLE QStringList standardPath(StandardLocation location);
Q_INVOKABLE QString urlToPath(const QUrl& url); 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 validPath(const QString& path);
Q_INVOKABLE bool validFolder(const QString& path); Q_INVOKABLE bool validFolder(const QString& path);
Q_INVOKABLE QUrl pathToUrl(const QString& path); Q_INVOKABLE QUrl pathToUrl(const QString& path);
Q_INVOKABLE QUrl saveHelper(const QString& saveText, const QUrl& currentFolder, const QStringList& selectionFilters);
}; };

View file

@ -105,6 +105,7 @@ void OffscreenUi::create(QOpenGLContext* context) {
OffscreenQmlSurface::create(context); OffscreenQmlSurface::create(context);
auto rootContext = getRootContext(); auto rootContext = getRootContext();
rootContext->setContextProperty("OffscreenUi", this);
rootContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags()); rootContext->setContextProperty("offscreenFlags", offscreenFlags = new OffscreenFlags());
rootContext->setContextProperty("urlHandler", new UrlHandler()); rootContext->setContextProperty("urlHandler", new UrlHandler());
rootContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); rootContext->setContextProperty("fileDialogHelper", new FileDialogHelper());
@ -219,7 +220,7 @@ QQuickItem* OffscreenUi::createMessageBox(QMessageBox::Icon icon, const QString&
return qvariant_cast<QQuickItem*>(result); return qvariant_cast<QQuickItem*>(result);
} }
QMessageBox::StandardButton OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) { int OffscreenUi::waitForMessageBoxResult(QQuickItem* messageBox) {
if (!messageBox) { if (!messageBox) {
return QMessageBox::NoButton; return QMessageBox::NoButton;
} }
@ -241,7 +242,7 @@ QMessageBox::StandardButton OffscreenUi::messageBox(QMessageBox::Icon icon, cons
return result; return result;
} }
return waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton)); return static_cast<QMessageBox::StandardButton>(waitForMessageBoxResult(createMessageBox(icon, title, text, buttons, defaultButton)));
} }
QMessageBox::StandardButton OffscreenUi::critical(const QString& title, const QString& text, 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<QQuickItem*>(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) { QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
if (QThread::currentThread() != thread()) { if (QThread::currentThread() != thread()) {
QString result; QString result;
@ -497,23 +518,31 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir,
map.insert("dir", QUrl::fromLocalFile(dir)); map.insert("dir", QUrl::fromLocalFile(dir));
map.insert("filter", filter); map.insert("filter", filter);
map.insert("options", static_cast<int>(options)); map.insert("options", static_cast<int>(options));
return fileDialog(map);
}
QVariant buildDialogResult; QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileOpenDialog", if (QThread::currentThread() != thread()) {
Q_RETURN_ARG(QVariant, buildDialogResult), QString result;
Q_ARG(QVariant, QVariant::fromValue(map))); QMetaObject::invokeMethod(this, "fileSaveDialog", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QString, result),
if (!invokeResult) { Q_ARG(QString, caption),
qWarning() << "Failed to create file open dialog"; Q_ARG(QString, dir),
return QString(); Q_ARG(QString, filter),
Q_ARG(QString*, selectedFilter),
Q_ARG(QFileDialog::Options, options));
return result;
} }
QVariant result = FileDialogListener(qvariant_cast<QQuickItem*>(buildDialogResult)).waitForResult(); // FIXME support returning the selected filter... somehow?
if (!result.isValid()) { QVariantMap map;
return QString(); map.insert("caption", caption);
} map.insert("dir", QUrl::fromLocalFile(dir));
qDebug() << result.toString(); map.insert("filter", filter);
return result.toUrl().toLocalFile(); map.insert("options", static_cast<int>(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) { 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) { 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<OffscreenUi>()->fileSaveDialog(caption, dir, filter, selectedFilter, options);
} }

View file

@ -47,7 +47,7 @@ public:
// Must be called from the main thread // Must be called from the main thread
QQuickItem* createMessageBox(QMessageBox::Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); QQuickItem* createMessageBox(QMessageBox::Icon icon, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton);
// Must be called from the main thread // 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 /// Same design as QMessageBox::critical(), will block, returns result
static QMessageBox::StandardButton critical(void* ignored, const QString& title, const QString& text, static QMessageBox::StandardButton critical(void* ignored, const QString& title, const QString& text,
@ -87,10 +87,12 @@ public:
QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButtons buttons = QMessageBox::Ok,
QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); 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 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 // 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); 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); 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); 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: private:
QString fileDialog(const QVariantMap& properties);
QQuickItem* _desktop { nullptr }; QQuickItem* _desktop { nullptr };
QQuickItem* _toolWindow { nullptr }; QQuickItem* _toolWindow { nullptr };
}; };