blob: fee8063f13da3baf1b4d91e629e9d196c8589c57 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qwinrtfiledialoghelper.h"
#include "qwinrtfileengine.h"
#include <QtCore/qcoreapplication.h>
#include <QtCore/QEventLoop>
#include <QtCore/QMap>
#include <QtCore/QVector>
#include <QtCore/qfunctions_winrt.h>
#include <private/qeventdispatcher_winrt_p.h>
#include <functional>
#include <wrl.h>
#include <windows.foundation.h>
#include <windows.storage.pickers.h>
#include <Windows.Applicationmodel.Activation.h>
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows::ApplicationModel::Activation;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Storage;
using namespace ABI::Windows::Storage::Pickers;
typedef IAsyncOperationCompletedHandler<StorageFile *> SingleFileHandler;
typedef IAsyncOperationCompletedHandler<IVectorView<StorageFile *> *> MultipleFileHandler;
typedef IAsyncOperationCompletedHandler<StorageFolder *> SingleFolderHandler;
QT_BEGIN_NAMESPACE
// Required for save file picker
class WindowsStringVector : public RuntimeClass<IVector<HSTRING>>
{
public:
HRESULT __stdcall GetAt(quint32 index, HSTRING *item)
{
*item = impl.at(int(index));
return S_OK;
}
HRESULT __stdcall get_Size(quint32 *size)
{
*size = quint32(impl.size());
return S_OK;
}
HRESULT __stdcall GetView(IVectorView<HSTRING> **view)
{
*view = nullptr;
return E_NOTIMPL;
}
HRESULT __stdcall IndexOf(HSTRING value, quint32 *index, boolean *found)
{
*found = false;
for (int i = 0; i < impl.size(); ++i) {
qint32 result;
HRESULT hr = WindowsCompareStringOrdinal(impl.at(i), value, &result);
if (FAILED(hr))
return hr;
if (result == 0) {
*index = quint32(i);
*found = true;
break;
}
}
return S_OK;
}
HRESULT __stdcall SetAt(quint32 index, HSTRING item)
{
HSTRING newItem;
HRESULT hr = WindowsDuplicateString(item, &newItem);
if (FAILED(hr))
return hr;
impl[int(index)] = newItem;
return S_OK;
}
HRESULT __stdcall InsertAt(quint32 index, HSTRING item)
{
HSTRING newItem;
HRESULT hr = WindowsDuplicateString(item, &newItem);
if (FAILED(hr))
return hr;
impl.insert(int(index), newItem);
return S_OK;
}
HRESULT __stdcall RemoveAt(quint32 index)
{
WindowsDeleteString(impl.takeAt(int(index)));
return S_OK;
}
HRESULT __stdcall Append(HSTRING item)
{
HSTRING newItem;
HRESULT hr = WindowsDuplicateString(item, &newItem);
if (FAILED(hr))
return hr;
impl.append(newItem);
return S_OK;
}
HRESULT __stdcall RemoveAtEnd()
{
WindowsDeleteString(impl.takeLast());
return S_OK;
}
HRESULT __stdcall Clear()
{
foreach (const HSTRING &item, impl)
WindowsDeleteString(item);
impl.clear();
return S_OK;
}
private:
QVector<HSTRING> impl;
};
template<typename T>
static bool initializePicker(HSTRING runtimeId, T **picker, const QSharedPointer<QFileDialogOptions> &options)
{
HRESULT hr;
ComPtr<IInspectable> basePicker;
hr = RoActivateInstance(runtimeId, &basePicker);
RETURN_FALSE_IF_FAILED("Failed to instantiate file picker");
hr = basePicker.Get()->QueryInterface(IID_PPV_ARGS(picker));
RETURN_FALSE_IF_FAILED("Failed to cast file picker");
if (options->isLabelExplicitlySet(QFileDialogOptions::Accept)) {
const QString labelText = options->labelText(QFileDialogOptions::Accept);
HStringReference labelTextRef(reinterpret_cast<const wchar_t *>(labelText.utf16()),
uint(labelText.length()));
hr = (*picker)->put_CommitButtonText(labelTextRef.Get());
RETURN_FALSE_IF_FAILED("Failed to set commit button text");
}
return true;
}
template<typename T>
static bool initializeOpenPickerOptions(T *picker, const QSharedPointer<QFileDialogOptions> &options)
{
HRESULT hr;
hr = picker->put_ViewMode(options->viewMode() == QFileDialogOptions::Detail
? PickerViewMode_Thumbnail : PickerViewMode_List);
RETURN_FALSE_IF_FAILED("Failed to set picker view mode");
ComPtr<IVector<HSTRING>> filters;
hr = picker->get_FileTypeFilter(&filters);
RETURN_FALSE_IF_FAILED("Failed to get file type filters list");
for (const QString &namedFilter : options->nameFilters()) {
for (const QString &filter : QPlatformFileDialogHelper::cleanFilterList(namedFilter)) {
// Remove leading star
const int offset = (filter.length() > 1 && filter.startsWith(QLatin1Char('*'))) ? 1 : 0;
HStringReference filterRef(reinterpret_cast<const wchar_t *>(filter.utf16() + offset),
uint(filter.length() - offset));
hr = filters->Append(filterRef.Get());
if (FAILED(hr)) {
qWarning("Failed to add named file filter \"%s\": %s",
qPrintable(filter), qPrintable(qt_error_string(hr)));
}
}
}
// The file dialog won't open with an empty list - add a default wildcard
quint32 size;
hr = filters->get_Size(&size);
RETURN_FALSE_IF_FAILED("Failed to get file type filters list size");
if (!size) {
hr = filters->Append(HString::MakeReference(L"*").Get());
RETURN_FALSE_IF_FAILED("Failed to add default wildcard to file type filters list");
}
return true;
}
static bool pickFiles(IFileOpenPicker *picker, QWinRTFileDialogHelper *helper, bool singleFile)
{
Q_ASSERT(picker);
Q_ASSERT(helper);
HRESULT hr;
hr = QEventDispatcherWinRT::runOnXamlThread([picker, helper, singleFile]() {
HRESULT hr;
if (singleFile) {
ComPtr<IAsyncOperation<StorageFile *>> op;
hr = picker->PickSingleFileAsync(&op);
RETURN_HR_IF_FAILED("Failed to open single file picker");
hr = op->put_Completed(Callback<SingleFileHandler>(helper, &QWinRTFileDialogHelper::onSingleFilePicked).Get());
RETURN_HR_IF_FAILED("Failed to attach file picker callback");
} else {
ComPtr<IAsyncOperation<IVectorView<StorageFile *> *>> op;
hr = picker->PickMultipleFilesAsync(&op);
RETURN_HR_IF_FAILED("Failed to open multi file picker");
hr = op->put_Completed(Callback<MultipleFileHandler>(helper, &QWinRTFileDialogHelper::onMultipleFilesPicked).Get());
RETURN_HR_IF_FAILED("Failed to attach multi file callback");
}
return S_OK;
});
return SUCCEEDED(hr);
}
static bool pickFolder(IFolderPicker *picker, QWinRTFileDialogHelper *helper)
{
Q_ASSERT(picker);
Q_ASSERT(helper);
HRESULT hr;
hr = QEventDispatcherWinRT::runOnXamlThread([picker, helper]() {
HRESULT hr;
ComPtr<IAsyncOperation<StorageFolder *>> op;
hr = picker->PickSingleFolderAsync(&op);
RETURN_HR_IF_FAILED("Failed to open folder picker");
hr = op->put_Completed(Callback<SingleFolderHandler>(helper, &QWinRTFileDialogHelper::onSingleFolderPicked).Get());
RETURN_HR_IF_FAILED("Failed to attach folder picker callback");
return S_OK;
});
return SUCCEEDED(hr);
}
static bool pickSaveFile(IFileSavePicker *picker, QWinRTFileDialogHelper *helper)
{
Q_ASSERT(picker);
Q_ASSERT(helper);
HRESULT hr;
hr = QEventDispatcherWinRT::runOnXamlThread([picker, helper]() {
HRESULT hr;
ComPtr<IAsyncOperation<StorageFile *>> op;
hr = picker->PickSaveFileAsync(&op);
RETURN_HR_IF_FAILED("Failed to open save file picker");
hr = op->put_Completed(Callback<SingleFileHandler>(helper, &QWinRTFileDialogHelper::onSingleFilePicked).Get());
RETURN_HR_IF_FAILED("Failed to attach save file picker callback");
return S_OK;
});
return SUCCEEDED(hr);
}
class QWinRTFileDialogHelperPrivate
{
public:
bool shown;
QEventLoop loop;
// Input
QUrl directory;
QUrl saveFileName;
QString selectedNameFilter;
// Output
QList<QUrl> selectedFiles;
};
QWinRTFileDialogHelper::QWinRTFileDialogHelper()
: QPlatformFileDialogHelper(), d_ptr(new QWinRTFileDialogHelperPrivate)
{
Q_D(QWinRTFileDialogHelper);
d->shown = false;
}
void QWinRTFileDialogHelper::exec()
{
Q_D(QWinRTFileDialogHelper);
if (!d->shown)
show(Qt::Dialog, Qt::ApplicationModal, nullptr);
d->loop.exec();
}
bool QWinRTFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
{
Q_UNUSED(windowFlags)
Q_UNUSED(windowModality)
Q_UNUSED(parent)
Q_D(QWinRTFileDialogHelper);
HRESULT hr;
const QSharedPointer<QFileDialogOptions> dialogOptions = options();
switch (dialogOptions->acceptMode()) {
default:
case QFileDialogOptions::AcceptOpen: {
switch (dialogOptions->fileMode()) {
case QFileDialogOptions::AnyFile:
case QFileDialogOptions::ExistingFile:
case QFileDialogOptions::ExistingFiles: {
ComPtr<IFileOpenPicker> picker;
if (!initializePicker(HString::MakeReference(RuntimeClass_Windows_Storage_Pickers_FileOpenPicker).Get(),
picker.GetAddressOf(), dialogOptions)) {
return false;
}
if (!initializeOpenPickerOptions(picker.Get(), dialogOptions))
return false;
if (!pickFiles(picker.Get(), this, dialogOptions->fileMode() == QFileDialogOptions::ExistingFile))
return false;
break;
}
case QFileDialogOptions::Directory:
case QFileDialogOptions::DirectoryOnly: {
ComPtr<IFolderPicker> picker;
if (!initializePicker(HString::MakeReference(RuntimeClass_Windows_Storage_Pickers_FolderPicker).Get(),
picker.GetAddressOf(), dialogOptions)) {
return false;
}
if (!initializeOpenPickerOptions(picker.Get(), dialogOptions))
return false;
if (!pickFolder(picker.Get(), this))
return false;
break;
}
}
break;
}
case QFileDialogOptions::AcceptSave: {
ComPtr<IFileSavePicker> picker;
if (!initializePicker(HString::MakeReference(RuntimeClass_Windows_Storage_Pickers_FileSavePicker).Get(),
picker.GetAddressOf(), dialogOptions)) {
return false;
}
if (!dialogOptions->nameFilters().isEmpty()) {
ComPtr<IMap<HSTRING, IVector<HSTRING> *>> choices;
hr = picker->get_FileTypeChoices(&choices);
RETURN_FALSE_IF_FAILED("Failed to get file extension choices");
const QStringList nameFilters = dialogOptions->nameFilters();
for (const QString &namedFilter : nameFilters) {
ComPtr<IVector<HSTRING>> entry = Make<WindowsStringVector>();
const QStringList cleanFilter = QPlatformFileDialogHelper::cleanFilterList(namedFilter);
for (const QString &filter : cleanFilter) {
// Remove leading star
const int starOffset = (filter.length() > 1 && filter.startsWith(QLatin1Char('*'))) ? 1 : 0;
HStringReference filterRef(reinterpret_cast<const wchar_t *>(filter.utf16() + starOffset),
uint(filter.length() - starOffset));
hr = entry->Append(filterRef.Get());
if (FAILED(hr)) {
qWarning("Failed to add named file filter \"%s\": %s",
qPrintable(filter), qPrintable(qt_error_string(hr)));
}
}
const int offset = namedFilter.indexOf(QLatin1String(" ("));
const QString filterTitle = namedFilter.mid(0, offset);
HStringReference namedFilterRef(reinterpret_cast<const wchar_t *>(filterTitle.utf16()),
uint(filterTitle.length()));
boolean replaced;
hr = choices->Insert(namedFilterRef.Get(), entry.Get(), &replaced);
// Only print a warning as * or *.* is not a valid choice on Windows 10
// but used on a regular basis on all other platforms
if (FAILED(hr)) {
qWarning("Failed to insert file extension choice entry: %s: %s",
qPrintable(filterTitle), qPrintable(qt_error_string(hr)));
}
}
}
QString suffix = dialogOptions->defaultSuffix();
if (!suffix.isEmpty()) {
if (!suffix.startsWith(QLatin1Char('.')))
suffix.prepend(QLatin1Char('.'));
HStringReference nativeSuffix(reinterpret_cast<const wchar_t *>(suffix.utf16()),
uint(suffix.length()));
hr = picker->put_DefaultFileExtension(nativeSuffix.Get());
RETURN_FALSE_IF_FAILED_WITH_ARGS("Failed to set default file extension \"%s\"", qPrintable(suffix));
}
QString suggestedName = QFileInfo(d->saveFileName.toLocalFile()).fileName();
if (suggestedName.isEmpty() && dialogOptions->initiallySelectedFiles().size() > 0)
suggestedName = QFileInfo(dialogOptions->initiallySelectedFiles().first().toLocalFile())
.fileName();
if (suggestedName.isEmpty()) {
const auto fileInfo = QFileInfo(dialogOptions->initialDirectory().toLocalFile());
if (!fileInfo.isDir())
suggestedName = fileInfo.fileName();
}
if (!suggestedName.isEmpty()) {
HStringReference nativeSuggestedName(reinterpret_cast<const wchar_t *>(suggestedName.utf16()),
uint(suggestedName.length()));
hr = picker->put_SuggestedFileName(nativeSuggestedName.Get());
RETURN_FALSE_IF_FAILED("Failed to set suggested file name");
}
if (!pickSaveFile(picker.Get(), this))
return false;
break;
}
}
d->shown = true;
return true;
}
void QWinRTFileDialogHelper::hide()
{
Q_D(QWinRTFileDialogHelper);
if (!d->shown)
return;
d->shown = false;
}
void QWinRTFileDialogHelper::setDirectory(const QUrl &directory)
{
Q_D(QWinRTFileDialogHelper);
d->directory = directory;
}
QUrl QWinRTFileDialogHelper::directory() const
{
Q_D(const QWinRTFileDialogHelper);
return d->directory;
}
void QWinRTFileDialogHelper::selectFile(const QUrl &saveFileName)
{
Q_D(QWinRTFileDialogHelper);
d->saveFileName = saveFileName;
}
QList<QUrl> QWinRTFileDialogHelper::selectedFiles() const
{
Q_D(const QWinRTFileDialogHelper);
return d->selectedFiles;
}
void QWinRTFileDialogHelper::selectNameFilter(const QString &selectedNameFilter)
{
Q_D(QWinRTFileDialogHelper);
d->selectedNameFilter = selectedNameFilter;
}
QString QWinRTFileDialogHelper::selectedNameFilter() const
{
Q_D(const QWinRTFileDialogHelper);
return d->selectedNameFilter;
}
HRESULT QWinRTFileDialogHelper::onSingleFilePicked(IAsyncOperation<StorageFile *> *args, AsyncStatus status)
{
Q_D(QWinRTFileDialogHelper);
QEventLoopLocker locker(&d->loop);
d->shown = false;
d->selectedFiles.clear();
if (status == Canceled || status == Error) {
emit reject();
return S_OK;
}
HRESULT hr;
ComPtr<IStorageFile> file;
hr = args->GetResults(&file);
Q_ASSERT_SUCCEEDED(hr);
return onFilePicked(file.Get());
}
HRESULT QWinRTFileDialogHelper::onMultipleFilesPicked(IAsyncOperation<IVectorView<StorageFile *> *> *args, AsyncStatus status)
{
Q_D(QWinRTFileDialogHelper);
QEventLoopLocker locker(&d->loop);
d->shown = false;
d->selectedFiles.clear();
if (status == Canceled || status == Error) {
emit reject();
return S_OK;
}
HRESULT hr;
ComPtr<IVectorView<StorageFile *>> fileList;
hr = args->GetResults(&fileList);
RETURN_HR_IF_FAILED("Failed to get file list");
return onFilesPicked(fileList.Get());
}
HRESULT QWinRTFileDialogHelper::onSingleFolderPicked(IAsyncOperation<StorageFolder *> *args, AsyncStatus status)
{
Q_D(QWinRTFileDialogHelper);
QEventLoopLocker locker(&d->loop);
d->shown = false;
d->selectedFiles.clear();
if (status == Canceled || status == Error) {
emit reject();
return S_OK;
}
HRESULT hr;
ComPtr<IStorageFolder> folder;
hr = args->GetResults(&folder);
Q_ASSERT_SUCCEEDED(hr);
return onFolderPicked(folder.Get());
}
HRESULT QWinRTFileDialogHelper::onFilesPicked(IVectorView<StorageFile *> *files)
{
HRESULT hr;
quint32 size;
hr = files->get_Size(&size);
Q_ASSERT_SUCCEEDED(hr);
if (!size) {
emit reject();
return S_OK;
}
for (quint32 i = 0; i < size; ++i) {
ComPtr<IStorageFile> file;
hr = files->GetAt(i, &file);
Q_ASSERT_SUCCEEDED(hr);
appendFile(file.Get());
}
emit accept();
return S_OK;
}
HRESULT QWinRTFileDialogHelper::onFolderPicked(IStorageFolder *folder)
{
if (!folder) {
emit reject();
return S_OK;
}
appendFile(folder);
emit accept();
return S_OK;
}
HRESULT QWinRTFileDialogHelper::onFilePicked(IStorageFile *file)
{
if (!file) {
emit reject();
return S_OK;
}
appendFile(file);
emit accept();
return S_OK;
}
void QWinRTFileDialogHelper::appendFile(IInspectable *file)
{
Q_D(QWinRTFileDialogHelper);
HRESULT hr;
ComPtr<IStorageItem> item;
hr = file->QueryInterface(IID_PPV_ARGS(&item));
Q_ASSERT_SUCCEEDED(hr);
HString path;
hr = item->get_Path(path.GetAddressOf());
Q_ASSERT_SUCCEEDED(hr);
quint32 pathLen;
const wchar_t *pathStr = path.GetRawBuffer(&pathLen);
const QString filePath = QString::fromWCharArray(pathStr, pathLen);
QWinRTFileEngineHandler::registerFile(filePath, item.Get());
d->selectedFiles.append(QUrl::fromLocalFile(filePath));
}
QT_END_NAMESPACE