/**************************************************************************** ** ** 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$ ** ****************************************************************************/ #define QT_NO_URL_CAST_FROM_STRING 1 #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0601 #endif #include "qwindowscombase.h" #include "qwindowsdialoghelpers.h" #include "qwindowscontext.h" #include "qwindowswindow.h" #include "qwindowsintegration.h" #include "qwindowstheme.h" // Color conversion helpers #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // #define USE_NATIVE_COLOR_DIALOG /* Testing purposes only */ QT_BEGIN_NAMESPACE #ifndef QT_NO_DEBUG_STREAM /* Output UID (IID, CLSID) as C++ constants. * The constants are contained in the Windows SDK libs, but not for MinGW. */ static inline QString guidToString(const GUID &g) { QString rc; QTextStream str(&rc); str.setIntegerBase(16); str.setNumberFlags(str.numberFlags() | QTextStream::ShowBase); str << '{' << g.Data1 << ", " << g.Data2 << ", " << g.Data3; str.setFieldWidth(2); str.setFieldAlignment(QTextStream::AlignRight); str.setPadChar(QLatin1Char('0')); str << ",{" << g.Data4[0] << ", " << g.Data4[1] << ", " << g.Data4[2] << ", " << g.Data4[3] << ", " << g.Data4[4] << ", " << g.Data4[5] << ", " << g.Data4[6] << ", " << g.Data4[7] << "}};"; return rc; } inline QDebug operator<<(QDebug d, const GUID &g) { QDebugStateSaver saver(d); d.nospace(); d << guidToString(g); return d; } #endif // !QT_NO_DEBUG_STREAM // Return an allocated wchar_t array from a QString, reserve more memory if desired. static wchar_t *qStringToWCharArray(const QString &s, size_t reserveSize = 0) { const size_t stringSize = s.size(); wchar_t *result = new wchar_t[qMax(stringSize + 1, reserveSize)]; s.toWCharArray(result); result[stringSize] = 0; return result; } namespace QWindowsDialogs { /*! \fn eatMouseMove() After closing a windows dialog with a double click (i.e. open a file) the message queue still contains a dubious WM_MOUSEMOVE message where the left button is reported to be down (wParam != 0). remove all those messages (usually 1) and post the last one with a reset button state. \ingroup qt-lighthouse-win */ void eatMouseMove() { MSG msg = {0, 0, 0, 0, 0, {0, 0} }; while (PeekMessage(&msg, 0, WM_MOUSEMOVE, WM_MOUSEMOVE, PM_REMOVE)) ; if (msg.message == WM_MOUSEMOVE) PostMessage(msg.hwnd, msg.message, 0, msg.lParam); qCDebug(lcQpaDialogs) << __FUNCTION__ << "triggered=" << (msg.message == WM_MOUSEMOVE); } } // namespace QWindowsDialogs /*! \class QWindowsNativeDialogBase \brief Base class for Windows native dialogs. Base classes for native dialogs (using the CLSID-based dialog interfaces "IFileDialog", etc. available from Windows Vista on) that mimick the behaviour of their QDialog counterparts as close as possible. Instances of derived classes are controlled by QWindowsDialogHelperBase-derived classes. A major difference is that there is only an exec(), which is a modal, blocking call; there is no non-blocking show(). There 2 types of native dialogs: \list \li Dialogs provided by the Comdlg32 library (ChooseColor, ChooseFont). They only provide a modal, blocking function call (with idle processing). \li File dialogs are classes derived from IFileDialog. They inherit IModalWindow and their exec() method (calling IModalWindow::Show()) is similarly blocking, but methods like close() can be called on them from event handlers. \endlist \sa QWindowsDialogHelperBase \internal \ingroup qt-lighthouse-win */ class QWindowsNativeDialogBase : public QObject { Q_OBJECT public: virtual void setWindowTitle(const QString &title) = 0; bool executed() const { return m_executed; } void exec(HWND owner = 0) { doExec(owner); m_executed = true; } signals: void accepted(); void rejected(); public slots: virtual void close() = 0; protected: QWindowsNativeDialogBase() : m_executed(false) {} private: virtual void doExec(HWND owner = 0) = 0; bool m_executed; }; /*! \class QWindowsDialogHelperBase \brief Helper for native Windows dialogs. Provides basic functionality and introduces new virtuals. The native dialog is created in setVisible_sys() since then modality and the state of DontUseNativeDialog is known. Modal dialogs are then run by exec(). Non-modal dialogs are shown using a separate thread started in show() should they support it. \sa QWindowsDialogThread, QWindowsNativeDialogBase \internal \ingroup qt-lighthouse-win */ template void QWindowsDialogHelperBase::cleanupThread() { if (m_thread) { // Thread may be running if the dialog failed to close. if (m_thread->isRunning()) m_thread->wait(500); if (m_thread->isRunning()) { m_thread->terminate(); m_thread->wait(300); if (m_thread->isRunning()) qCCritical(lcQpaDialogs) <<__FUNCTION__ << "Failed to terminate thread."; else qCWarning(lcQpaDialogs) << __FUNCTION__ << "Thread terminated."; } delete m_thread; m_thread = 0; } } template QWindowsNativeDialogBase *QWindowsDialogHelperBase::nativeDialog() const { if (m_nativeDialog.isNull()) { qWarning("%s invoked with no native dialog present.", __FUNCTION__); return 0; } return m_nativeDialog.data(); } template void QWindowsDialogHelperBase::timerEvent(QTimerEvent *) { startDialogThread(); } template QWindowsNativeDialogBase *QWindowsDialogHelperBase::ensureNativeDialog() { // Create dialog and apply common settings. Check "executed" flag as well // since for example IFileDialog::Show() works only once. if (m_nativeDialog.isNull() || m_nativeDialog->executed()) m_nativeDialog = QWindowsNativeDialogBasePtr(createNativeDialog()); return m_nativeDialog.data(); } /*! \class QWindowsDialogThread \brief Run a non-modal native dialog in a separate thread. \sa QWindowsDialogHelperBase \internal \ingroup qt-lighthouse-win */ class QWindowsDialogThread : public QThread { public: typedef QSharedPointer QWindowsNativeDialogBasePtr; explicit QWindowsDialogThread(const QWindowsNativeDialogBasePtr &d, HWND owner) : m_dialog(d), m_owner(owner) {} void run(); private: const QWindowsNativeDialogBasePtr m_dialog; const HWND m_owner; }; void QWindowsDialogThread::run() { qCDebug(lcQpaDialogs) << '>' << __FUNCTION__; m_dialog->exec(m_owner); qCDebug(lcQpaDialogs) << '<' << __FUNCTION__; } template bool QWindowsDialogHelperBase::show(Qt::WindowFlags, Qt::WindowModality windowModality, QWindow *parent) { const bool modal = (windowModality != Qt::NonModal); if (!parent) parent = QGuiApplication::focusWindow(); // Need a parent window, else the application loses activation when closed. if (parent) { m_ownerWindow = QWindowsWindow::handleOf(parent); } else { m_ownerWindow = 0; } qCDebug(lcQpaDialogs) << __FUNCTION__ << "modal=" << modal << " modal supported? " << supportsNonModalDialog(parent) << "native=" << m_nativeDialog.data() << "owner" << m_ownerWindow; if (!modal && !supportsNonModalDialog(parent)) return false; // Was it changed in-between? if (!ensureNativeDialog()) return false; // Start a background thread to show the dialog. For modal dialogs, // a subsequent call to exec() may follow. So, start an idle timer // which will start the dialog thread. If exec() is then called, the // timer is stopped and dialog->exec() is called directly. cleanupThread(); if (modal) { m_timerId = this->startTimer(0); } else { startDialogThread(); } return true; } template void QWindowsDialogHelperBase::startDialogThread() { Q_ASSERT(!m_nativeDialog.isNull()); Q_ASSERT(!m_thread); m_thread = new QWindowsDialogThread(m_nativeDialog, m_ownerWindow); m_thread->start(); stopTimer(); } template void QWindowsDialogHelperBase::stopTimer() { if (m_timerId) { this->killTimer(m_timerId); m_timerId = 0; } } // Find a file dialog window created by IFileDialog by process id, window // title and class, which starts with a hash '#'. struct FindDialogContext { explicit FindDialogContext(const QString &titleIn) : title(qStringToWCharArray(titleIn)), processId(GetCurrentProcessId()), hwnd(0) {} const QScopedArrayPointer title; const DWORD processId; HWND hwnd; // contains the HWND of the window found. }; static BOOL QT_WIN_CALLBACK findDialogEnumWindowsProc(HWND hwnd, LPARAM lParam) { FindDialogContext *context = reinterpret_cast(lParam); DWORD winPid = 0; GetWindowThreadProcessId(hwnd, &winPid); if (winPid != context->processId) return TRUE; wchar_t buf[256]; if (!RealGetWindowClass(hwnd, buf, sizeof(buf)/sizeof(wchar_t)) || buf[0] != L'#') return TRUE; if (!GetWindowTextW(hwnd, buf, sizeof(buf)/sizeof(wchar_t)) || wcscmp(buf, context->title.data()) != 0) return TRUE; context->hwnd = hwnd; return FALSE; } static inline HWND findDialogWindow(const QString &title) { FindDialogContext context(title); EnumWindows(findDialogEnumWindowsProc, reinterpret_cast(&context)); return context.hwnd; } template void QWindowsDialogHelperBase::hide() { if (m_nativeDialog) m_nativeDialog->close(); m_ownerWindow = 0; } template void QWindowsDialogHelperBase::exec() { qCDebug(lcQpaDialogs) << __FUNCTION__; stopTimer(); if (QWindowsNativeDialogBase *nd = nativeDialog()) { nd->exec(m_ownerWindow); m_nativeDialog.clear(); } } /*! \class QWindowsFileDialogSharedData \brief Explicitly shared file dialog parameters that are not in QFileDialogOptions. Contain Parameters that need to be cached while the native dialog does not exist yet. In addition, the data are updated by the change notifications of the IFileDialogEvent, as querying them after the dialog has closed does not reliably work. Provides thread-safe setters (for the non-modal case). \internal \ingroup qt-lighthouse-win \sa QFileDialogOptions */ class QWindowsFileDialogSharedData { public: QWindowsFileDialogSharedData() : m_data(new Data) {} void fromOptions(const QSharedPointer &o); QUrl directory() const; void setDirectory(const QUrl &); QString selectedNameFilter() const; void setSelectedNameFilter(const QString &); QList selectedFiles() const; void setSelectedFiles(const QList &); QString selectedFile() const; private: class Data : public QSharedData { public: QUrl directory; QString selectedNameFilter; QList selectedFiles; QMutex mutex; }; QExplicitlySharedDataPointer m_data; }; inline QUrl QWindowsFileDialogSharedData::directory() const { m_data->mutex.lock(); const QUrl result = m_data->directory; m_data->mutex.unlock(); return result; } inline void QWindowsFileDialogSharedData::setDirectory(const QUrl &d) { QMutexLocker locker(&m_data->mutex); m_data->directory = d; } inline QString QWindowsFileDialogSharedData::selectedNameFilter() const { m_data->mutex.lock(); const QString result = m_data->selectedNameFilter; m_data->mutex.unlock(); return result; } inline void QWindowsFileDialogSharedData::setSelectedNameFilter(const QString &f) { QMutexLocker locker(&m_data->mutex); m_data->selectedNameFilter = f; } inline QList QWindowsFileDialogSharedData::selectedFiles() const { m_data->mutex.lock(); const QList result = m_data->selectedFiles; m_data->mutex.unlock(); return result; } inline QString QWindowsFileDialogSharedData::selectedFile() const { const QList files = selectedFiles(); return files.isEmpty() ? QString() : files.front().toLocalFile(); } inline void QWindowsFileDialogSharedData::setSelectedFiles(const QList &urls) { QMutexLocker locker(&m_data->mutex); m_data->selectedFiles = urls; } inline void QWindowsFileDialogSharedData::fromOptions(const QSharedPointer &o) { QMutexLocker locker(&m_data->mutex); m_data->directory = o->initialDirectory(); m_data->selectedFiles = o->initiallySelectedFiles(); m_data->selectedNameFilter = o->initiallySelectedNameFilter(); } /*! \class QWindowsNativeFileDialogEventHandler \brief Listens to IFileDialog events and forwards them to QWindowsNativeFileDialogBase Events like 'folder change' that have an equivalent signal in QFileDialog are forwarded. \sa QWindowsNativeFileDialogBase, QWindowsFileDialogHelper \internal \ingroup qt-lighthouse-win */ class QWindowsNativeFileDialogBase; class QWindowsNativeFileDialogEventHandler : public QWindowsComBase { Q_DISABLE_COPY(QWindowsNativeFileDialogEventHandler) public: static IFileDialogEvents *create(QWindowsNativeFileDialogBase *nativeFileDialog); // IFileDialogEvents methods IFACEMETHODIMP OnFileOk(IFileDialog *); IFACEMETHODIMP OnFolderChange(IFileDialog *) { return S_OK; } IFACEMETHODIMP OnFolderChanging(IFileDialog *, IShellItem *); IFACEMETHODIMP OnHelp(IFileDialog *) { return S_OK; } IFACEMETHODIMP OnSelectionChange(IFileDialog *); IFACEMETHODIMP OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; } IFACEMETHODIMP OnTypeChange(IFileDialog *); IFACEMETHODIMP OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; } QWindowsNativeFileDialogEventHandler(QWindowsNativeFileDialogBase *nativeFileDialog) : m_nativeFileDialog(nativeFileDialog) {} private: QWindowsNativeFileDialogBase *m_nativeFileDialog; }; IFileDialogEvents *QWindowsNativeFileDialogEventHandler::create(QWindowsNativeFileDialogBase *nativeFileDialog) { IFileDialogEvents *result; QWindowsNativeFileDialogEventHandler *eventHandler = new QWindowsNativeFileDialogEventHandler(nativeFileDialog); if (FAILED(eventHandler->QueryInterface(IID_IFileDialogEvents, reinterpret_cast(&result)))) { qErrnoWarning("Unable to obtain IFileDialogEvents"); return 0; } eventHandler->Release(); return result; } /*! \class QWindowsShellItem \brief Wrapper for IShellItem \sa QWindowsNativeFileDialogBase \internal \ingroup qt-lighthouse-win */ class QWindowsShellItem { public: typedef std::vector IShellItems; explicit QWindowsShellItem(IShellItem *item); SFGAOF attributes() const { return m_attributes; } QString normalDisplay() const // base name, usually { return displayName(m_item, SIGDN_NORMALDISPLAY); } QString desktopAbsoluteParsing() const { return displayName(m_item, SIGDN_DESKTOPABSOLUTEPARSING); } QString path() const; // Only set for 'FileSystem' (SFGAO_FILESYSTEM) items QUrl url() const; bool isFileSystem() const { return (m_attributes & SFGAO_FILESYSTEM) != 0; } bool isDir() const { return (m_attributes & SFGAO_FOLDER) != 0; } // Copy using IFileOperation bool canCopy() const { return (m_attributes & SFGAO_CANCOPY) != 0; } // Supports IStream bool canStream() const { return (m_attributes & SFGAO_STREAM) != 0; } bool copyData(QIODevice *out); static IShellItems itemsFromItemArray(IShellItemArray *items); #ifndef QT_NO_DEBUG_STREAM void format(QDebug &d) const; #endif private: static QString displayName(IShellItem *item, SIGDN mode); static QString libraryItemDefaultSaveFolder(IShellItem *item); QUrl urlValue() const; IShellItem *m_item; SFGAOF m_attributes; }; QWindowsShellItem::QWindowsShellItem(IShellItem *item) : m_item(item) , m_attributes(0) { if (FAILED(item->GetAttributes(SFGAO_CAPABILITYMASK | SFGAO_DISPLAYATTRMASK | SFGAO_CONTENTSMASK | SFGAO_STORAGECAPMASK, &m_attributes))) m_attributes = 0; } QString QWindowsShellItem::path() const { if (isFileSystem()) return QDir::cleanPath(QWindowsShellItem::displayName(m_item, SIGDN_FILESYSPATH)); // Check for a "Library" item if (isDir()) return QWindowsShellItem::libraryItemDefaultSaveFolder(m_item); return QString(); } QUrl QWindowsShellItem::urlValue() const // plain URL as returned by SIGDN_URL, not set for all items { QUrl result; const QString urlString = displayName(m_item, SIGDN_URL); if (!urlString.isEmpty()) { const QUrl parsed = QUrl(urlString); if (parsed.isValid()) { result = parsed; } else { qWarning("%s: Unable to decode URL \"%s\": %s", __FUNCTION__, qPrintable(urlString), qPrintable(parsed.errorString())); } } return result; } QUrl QWindowsShellItem::url() const { // Prefer file if existent to avoid any misunderstandings about UNC shares const QString fsPath = path(); if (!fsPath.isEmpty()) return QUrl::fromLocalFile(fsPath); const QUrl urlV = urlValue(); if (urlV.isValid()) return urlV; // Last resort: encode the absolute desktop parsing id as data URL const QString data = QStringLiteral("data:text/plain;base64,") + QLatin1String(desktopAbsoluteParsing().toLatin1().toBase64()); return QUrl(data); } QString QWindowsShellItem::displayName(IShellItem *item, SIGDN mode) { LPWSTR name = nullptr; QString result; if (SUCCEEDED(item->GetDisplayName(mode, &name))) { result = QString::fromWCharArray(name); CoTaskMemFree(name); } return result; } QWindowsShellItem::IShellItems QWindowsShellItem::itemsFromItemArray(IShellItemArray *items) { IShellItems result; DWORD itemCount = 0; if (FAILED(items->GetCount(&itemCount)) || itemCount == 0) return result; result.reserve(itemCount); for (DWORD i = 0; i < itemCount; ++i) { IShellItem *item = nullptr; if (SUCCEEDED(items->GetItemAt(i, &item))) result.push_back(item); } return result; } bool QWindowsShellItem::copyData(QIODevice *out) { if (!canCopy() || !canStream()) return false; IStream *istream = nullptr; HRESULT hr = m_item->BindToHandler(NULL, BHID_Stream, IID_PPV_ARGS(&istream)); if (FAILED(hr)) return false; enum : ULONG { bufSize = 102400 }; char buffer[bufSize]; ULONG bytesRead; forever { bytesRead = 0; hr = istream->Read(buffer, bufSize, &bytesRead); // S_FALSE: EOF reached if ((hr == S_OK || hr == S_FALSE) && bytesRead) out->write(buffer, bytesRead); else break; } istream->Release(); return hr == S_OK || hr == S_FALSE; } // Helper for "Libraries": collections of folders appearing from Windows 7 // on, visible in the file dialogs. // Load a library from a IShellItem (sanitized copy of the inline function // SHLoadLibraryFromItem from ShObjIdl.h, which does not exist for MinGW). static IShellLibrary *sHLoadLibraryFromItem(IShellItem *libraryItem, DWORD mode) { // ID symbols present from Windows 7 on: static const CLSID classId_ShellLibrary = {0xd9b3211d, 0xe57f, 0x4426, {0xaa, 0xef, 0x30, 0xa8, 0x6, 0xad, 0xd3, 0x97}}; static const IID iId_IShellLibrary = {0x11a66efa, 0x382e, 0x451a, {0x92, 0x34, 0x1e, 0xe, 0x12, 0xef, 0x30, 0x85}}; IShellLibrary *helper = nullptr; IShellLibrary *result = nullptr; if (SUCCEEDED(CoCreateInstance(classId_ShellLibrary, NULL, CLSCTX_INPROC_SERVER, iId_IShellLibrary, reinterpret_cast(&helper)))) if (SUCCEEDED(helper->LoadLibraryFromItem(libraryItem, mode))) helper->QueryInterface(iId_IShellLibrary, reinterpret_cast(&result)); if (helper) helper->Release(); return result; } // Return default save folders of a library-type item. QString QWindowsShellItem::libraryItemDefaultSaveFolder(IShellItem *item) { QString result; if (IShellLibrary *library = sHLoadLibraryFromItem(item, STGM_READ | STGM_SHARE_DENY_WRITE)) { IShellItem *item = nullptr; if (SUCCEEDED(library->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem, reinterpret_cast(&item)))) { result = QDir::cleanPath(QWindowsShellItem::displayName(item, SIGDN_FILESYSPATH)); item->Release(); } library->Release(); } return result; } #ifndef QT_NO_DEBUG_STREAM void QWindowsShellItem::format(QDebug &d) const { d << "attributes=0x" << hex << attributes() << dec; if (isFileSystem()) d << " [filesys]"; if (isDir()) d << " [dir]"; if (canStream()) d << " [stream]"; if (canCopy()) d << " [copyable]"; d << ", normalDisplay=\"" << normalDisplay() << "\", desktopAbsoluteParsing=\"" << desktopAbsoluteParsing() << '"'; const QString pathS = path(); if (!pathS.isEmpty()) d << ", path=\"" << pathS << '"'; const QUrl urlV = urlValue(); if (urlV.isValid()) d << "\", url=" << urlV; } QDebug operator<<(QDebug d, const QWindowsShellItem &i) { QDebugStateSaver saver(d); d.nospace(); d.noquote(); d << "QShellItem("; i.format(d); d << ')'; return d; } QDebug operator<<(QDebug d, IShellItem *i) { QDebugStateSaver saver(d); d.nospace(); d.noquote(); d << "IShellItem(" << static_cast(i); if (i) { d << ", "; QWindowsShellItem(i).format(d); } d << ')'; return d; } #endif // !QT_NO_DEBUG_STREAM /*! \class QWindowsNativeFileDialogBase \brief Windows native file dialog wrapper around IFileOpenDialog, IFileSaveDialog. Provides convenience methods. Note that only IFileOpenDialog has multi-file functionality. \sa QWindowsNativeFileDialogEventHandler, QWindowsFileDialogHelper \internal \ingroup qt-lighthouse-win */ class QWindowsNativeFileDialogBase : public QWindowsNativeDialogBase { Q_OBJECT Q_PROPERTY(bool hideFiltersDetails READ hideFiltersDetails WRITE setHideFiltersDetails) public: ~QWindowsNativeFileDialogBase() override; inline static QWindowsNativeFileDialogBase *create(QFileDialogOptions::AcceptMode am, const QWindowsFileDialogSharedData &data); void setWindowTitle(const QString &title) override; inline void setMode(QFileDialogOptions::FileMode mode, QFileDialogOptions::AcceptMode acceptMode, QFileDialogOptions::FileDialogOptions options); inline void setDirectory(const QUrl &directory); inline void updateDirectory() { setDirectory(m_data.directory()); } inline QString directory() const; void doExec(HWND owner = 0) override; virtual void setNameFilters(const QStringList &f); inline void selectNameFilter(const QString &filter); inline void updateSelectedNameFilter() { selectNameFilter(m_data.selectedNameFilter()); } inline QString selectedNameFilter() const; void selectFile(const QString &fileName) const; bool hideFiltersDetails() const { return m_hideFiltersDetails; } void setHideFiltersDetails(bool h) { m_hideFiltersDetails = h; } void setDefaultSuffix(const QString &s); inline bool hasDefaultSuffix() const { return m_hasDefaultSuffix; } inline void setLabelText(QFileDialogOptions::DialogLabel l, const QString &text); // Return the selected files for tracking in OnSelectionChanged(). virtual QList selectedFiles() const = 0; // Return the result for tracking in OnFileOk(). Differs from selection for // example by appended default suffixes, etc. virtual QList dialogResult() const = 0; inline void onFolderChange(IShellItem *); inline void onSelectionChange(); inline void onTypeChange(); inline bool onFileOk(); signals: void directoryEntered(const QUrl &directory); void currentChanged(const QUrl &file); void filterSelected(const QString & filter); public slots: void close() override; protected: explicit QWindowsNativeFileDialogBase(const QWindowsFileDialogSharedData &data); bool init(const CLSID &clsId, const IID &iid); void setDefaultSuffixSys(const QString &s); inline IFileDialog * fileDialog() const { return m_fileDialog; } static IShellItem *shellItem(const QUrl &url); const QWindowsFileDialogSharedData &data() const { return m_data; } QWindowsFileDialogSharedData &data() { return m_data; } private: IFileDialog *m_fileDialog = nullptr; IFileDialogEvents *m_dialogEvents = nullptr; DWORD m_cookie = 0; QStringList m_nameFilters; bool m_hideFiltersDetails = false; bool m_hasDefaultSuffix = false; QWindowsFileDialogSharedData m_data; QString m_title; }; QWindowsNativeFileDialogBase::QWindowsNativeFileDialogBase(const QWindowsFileDialogSharedData &data) : m_data(data) { } QWindowsNativeFileDialogBase::~QWindowsNativeFileDialogBase() { if (m_dialogEvents && m_fileDialog) m_fileDialog->Unadvise(m_cookie); if (m_dialogEvents) m_dialogEvents->Release(); if (m_fileDialog) m_fileDialog->Release(); } bool QWindowsNativeFileDialogBase::init(const CLSID &clsId, const IID &iid) { HRESULT hr = CoCreateInstance(clsId, NULL, CLSCTX_INPROC_SERVER, iid, reinterpret_cast(&m_fileDialog)); if (FAILED(hr)) { qErrnoWarning("CoCreateInstance failed"); return false; } m_dialogEvents = QWindowsNativeFileDialogEventHandler::create(this); if (!m_dialogEvents) return false; // Register event handler hr = m_fileDialog->Advise(m_dialogEvents, &m_cookie); if (FAILED(hr)) { qErrnoWarning("IFileDialog::Advise failed"); return false; } qCDebug(lcQpaDialogs) << __FUNCTION__ << m_fileDialog << m_dialogEvents << m_cookie; return true; } void QWindowsNativeFileDialogBase::setWindowTitle(const QString &title) { m_title = title; m_fileDialog->SetTitle(reinterpret_cast(title.utf16())); } IShellItem *QWindowsNativeFileDialogBase::shellItem(const QUrl &url) { if (url.isLocalFile()) { IShellItem *result = nullptr; const QString native = QDir::toNativeSeparators(url.toLocalFile()); const HRESULT hr = SHCreateItemFromParsingName(reinterpret_cast(native.utf16()), NULL, IID_IShellItem, reinterpret_cast(&result)); if (FAILED(hr)) { qErrnoWarning("%s: SHCreateItemFromParsingName(%s)) failed", __FUNCTION__, qPrintable(url.toString())); return nullptr; } return result; } else if (url.scheme() == QLatin1String("clsid")) { // Support for virtual folders via GUID // (see https://msdn.microsoft.com/en-us/library/windows/desktop/dd378457(v=vs.85).aspx) // specified as "clsid:" (without '{', '}'). IShellItem *result = nullptr; const auto uuid = QUuid::fromString(url.path()); if (uuid.isNull()) { qWarning() << __FUNCTION__ << ": Invalid CLSID: " << url.path(); return nullptr; } PIDLIST_ABSOLUTE idList; HRESULT hr = SHGetKnownFolderIDList(uuid, 0, 0, &idList); if (FAILED(hr)) { qErrnoWarning("%s: SHGetKnownFolderIDList(%s)) failed", __FUNCTION__, qPrintable(url.toString())); return nullptr; } hr = SHCreateItemFromIDList(idList, IID_IShellItem, reinterpret_cast(&result)); CoTaskMemFree(idList); if (FAILED(hr)) { qErrnoWarning("%s: SHCreateItemFromIDList(%s)) failed", __FUNCTION__, qPrintable(url.toString())); return nullptr; } return result; } else { qWarning() << __FUNCTION__ << ": Unhandled scheme: " << url.scheme(); } return 0; } void QWindowsNativeFileDialogBase::setDirectory(const QUrl &directory) { if (!directory.isEmpty()) { if (IShellItem *psi = QWindowsNativeFileDialogBase::shellItem(directory)) { m_fileDialog->SetFolder(psi); psi->Release(); } } } QString QWindowsNativeFileDialogBase::directory() const { QString result; IShellItem *item = 0; if (m_fileDialog && SUCCEEDED(m_fileDialog->GetFolder(&item)) && item) { result = QWindowsShellItem(item).path(); item->Release(); } return result; } void QWindowsNativeFileDialogBase::doExec(HWND owner) { qCDebug(lcQpaDialogs) << '>' << __FUNCTION__; // Show() blocks until the user closes the dialog, the dialog window // gets a WM_CLOSE or the parent window is destroyed. const HRESULT hr = m_fileDialog->Show(owner); QWindowsDialogs::eatMouseMove(); qCDebug(lcQpaDialogs) << '<' << __FUNCTION__ << " returns " << hex << hr; if (hr == S_OK) { emit accepted(); } else { emit rejected(); } } void QWindowsNativeFileDialogBase::setMode(QFileDialogOptions::FileMode mode, QFileDialogOptions::AcceptMode acceptMode, QFileDialogOptions::FileDialogOptions options) { DWORD flags = FOS_PATHMUSTEXIST; if (QWindowsContext::readAdvancedExplorerSettings(L"Hidden", 1) == 1) // 1:show, 2:hidden flags |= FOS_FORCESHOWHIDDEN; if (options & QFileDialogOptions::DontResolveSymlinks) flags |= FOS_NODEREFERENCELINKS; switch (mode) { case QFileDialogOptions::AnyFile: if (acceptMode == QFileDialogOptions::AcceptSave) flags |= FOS_NOREADONLYRETURN; if (!(options & QFileDialogOptions::DontConfirmOverwrite)) flags |= FOS_OVERWRITEPROMPT; break; case QFileDialogOptions::ExistingFile: flags |= FOS_FILEMUSTEXIST; break; case QFileDialogOptions::Directory: case QFileDialogOptions::DirectoryOnly: // QTBUG-63645: Restrict to file system items, as Qt cannot deal with // places like 'Network', etc. flags |= FOS_PICKFOLDERS | FOS_FILEMUSTEXIST | FOS_FORCEFILESYSTEM; break; case QFileDialogOptions::ExistingFiles: flags |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT; break; } qCDebug(lcQpaDialogs) << __FUNCTION__ << "mode=" << mode << "acceptMode=" << acceptMode << "options=" << options << "results in" << showbase << hex << flags; if (FAILED(m_fileDialog->SetOptions(flags))) qErrnoWarning("%s: SetOptions() failed", __FUNCTION__); } // Split a list of name filters into description and actual filters struct FilterSpec { QString description; QString filter; }; static QList filterSpecs(const QStringList &filters, bool hideFilterDetails, int *totalStringLength) { QList result; result.reserve(filters.size()); *totalStringLength = 0; const QRegularExpression filterSeparatorRE(QStringLiteral("[;\\s]+")); const QString separator = QStringLiteral(";"); Q_ASSERT(filterSeparatorRE.isValid()); // Split filter specification as 'Texts (*.txt[;] *.doc)', '*.txt[;] *.doc' // into description and filters specification as '*.txt;*.doc' for (const QString &filterString : filters) { const int openingParenPos = filterString.lastIndexOf(QLatin1Char('(')); const int closingParenPos = openingParenPos != -1 ? filterString.indexOf(QLatin1Char(')'), openingParenPos + 1) : -1; FilterSpec filterSpec; filterSpec.filter = closingParenPos == -1 ? filterString : filterString.mid(openingParenPos + 1, closingParenPos - openingParenPos - 1).trimmed(); if (filterSpec.filter.isEmpty()) filterSpec.filter += QLatin1Char('*'); filterSpec.filter.replace(filterSeparatorRE, separator); filterSpec.description = filterString; if (hideFilterDetails && openingParenPos != -1) { // Do not show pattern in description filterSpec.description.truncate(openingParenPos); while (filterSpec.description.endsWith(QLatin1Char(' '))) filterSpec.description.truncate(filterSpec.description.size() - 1); } *totalStringLength += filterSpec.filter.size() + filterSpec.description.size(); result.push_back(filterSpec); } return result; } void QWindowsNativeFileDialogBase::setNameFilters(const QStringList &filters) { /* Populates an array of COMDLG_FILTERSPEC from list of filters, * store the strings in a flat, contiguous buffer. */ m_nameFilters = filters; int totalStringLength = 0; const QList specs = filterSpecs(filters, m_hideFiltersDetails, &totalStringLength); const int size = specs.size(); QScopedArrayPointer buffer(new WCHAR[totalStringLength + 2 * size]); QScopedArrayPointer comFilterSpec(new COMDLG_FILTERSPEC[size]); WCHAR *ptr = buffer.data(); // Split filter specification as 'Texts (*.txt[;] *.doc)' // into description and filters specification as '*.txt;*.doc' for (int i = 0; i < size; ++i) { // Display glitch (CLSID only): Any filter not filtering on suffix (such as // '*', 'a.*') will be duplicated in combo: 'All files (*) (*)', // 'AAA files (a.*) (a.*)' QString description = specs[i].description; const QString &filter = specs[i].filter; if (!m_hideFiltersDetails && !filter.startsWith(QLatin1String("*."))) { const int pos = description.lastIndexOf(QLatin1Char('(')); if (pos > 0) description.truncate(pos); } // Add to buffer. comFilterSpec[i].pszName = ptr; ptr += description.toWCharArray(ptr); *ptr++ = 0; comFilterSpec[i].pszSpec = ptr; ptr += specs[i].filter.toWCharArray(ptr); *ptr++ = 0; } m_fileDialog->SetFileTypes(size, comFilterSpec.data()); } void QWindowsNativeFileDialogBase::setDefaultSuffix(const QString &s) { setDefaultSuffixSys(s); m_hasDefaultSuffix = !s.isEmpty(); } void QWindowsNativeFileDialogBase::setDefaultSuffixSys(const QString &s) { // If this parameter is non-empty, it will be appended by the dialog for the 'Any files' // filter ('*'). If this parameter is non-empty and the current filter has a suffix, // the dialog will append the filter's suffix. wchar_t *wSuffix = const_cast(reinterpret_cast(s.utf16())); m_fileDialog->SetDefaultExtension(wSuffix); } static inline IFileDialog2 *getFileDialog2(IFileDialog *fileDialog) { IFileDialog2 *result; return SUCCEEDED(fileDialog->QueryInterface(IID_IFileDialog2, reinterpret_cast(&result))) ? result : nullptr; } void QWindowsNativeFileDialogBase::setLabelText(QFileDialogOptions::DialogLabel l, const QString &text) { wchar_t *wText = const_cast(reinterpret_cast(text.utf16())); switch (l) { case QFileDialogOptions::FileName: m_fileDialog->SetFileNameLabel(wText); break; case QFileDialogOptions::Accept: m_fileDialog->SetOkButtonLabel(wText); break; case QFileDialogOptions::Reject: if (IFileDialog2 *dialog2 = getFileDialog2(m_fileDialog)) { dialog2->SetCancelButtonLabel(wText); dialog2->Release(); } break; case QFileDialogOptions::LookIn: case QFileDialogOptions::FileType: case QFileDialogOptions::DialogLabelCount: break; } } static bool isHexRange(const QString& s, int start, int end) { for (;start < end; ++start) { QChar ch = s.at(start); if (!(ch.isDigit() || (ch >= QLatin1Char('a') && ch <= QLatin1Char('f')) || (ch >= QLatin1Char('A') && ch <= QLatin1Char('F')))) return false; } return true; } static inline bool isClsid(const QString &s) { // detect "374DE290-123F-4565-9164-39C4925E467B". const QChar dash(QLatin1Char('-')); return s.size() == 36 && isHexRange(s, 0, 8) && s.at(8) == dash && isHexRange(s, 9, 13) && s.at(13) == dash && isHexRange(s, 14, 18) && s.at(18) == dash && isHexRange(s, 19, 23) && s.at(23) == dash && isHexRange(s, 24, 36); } void QWindowsNativeFileDialogBase::selectFile(const QString &fileName) const { // Hack to prevent CLSIDs from being set as file name due to // QFileDialogPrivate::initialSelection() being QString-based. if (!isClsid(fileName)) m_fileDialog->SetFileName((wchar_t*)fileName.utf16()); } // Return the index of the selected filter, accounting for QFileDialog // sometimes stripping the filter specification depending on the // hideFilterDetails setting. static int indexOfNameFilter(const QStringList &filters, const QString &needle) { const int index = filters.indexOf(needle); if (index >= 0) return index; for (int i = 0; i < filters.size(); ++i) if (filters.at(i).startsWith(needle)) return i; return -1; } void QWindowsNativeFileDialogBase::selectNameFilter(const QString &filter) { if (filter.isEmpty()) return; const int index = indexOfNameFilter(m_nameFilters, filter); if (index < 0) { qWarning("%s: Invalid parameter '%s' not found in '%s'.", __FUNCTION__, qPrintable(filter), qPrintable(m_nameFilters.join(QLatin1String(", ")))); return; } m_fileDialog->SetFileTypeIndex(index + 1); // one-based. } QString QWindowsNativeFileDialogBase::selectedNameFilter() const { UINT uIndex = 0; if (SUCCEEDED(m_fileDialog->GetFileTypeIndex(&uIndex))) { const int index = uIndex - 1; // one-based if (index < m_nameFilters.size()) return m_nameFilters.at(index); } return QString(); } void QWindowsNativeFileDialogBase::onFolderChange(IShellItem *item) { if (item) { const QUrl directory = QWindowsShellItem(item).url(); m_data.setDirectory(directory); emit directoryEntered(directory); } } void QWindowsNativeFileDialogBase::onSelectionChange() { const QList current = selectedFiles(); m_data.setSelectedFiles(current); qCDebug(lcQpaDialogs) << __FUNCTION__ << current << current.size(); if (current.size() == 1) emit currentChanged(current.front()); } void QWindowsNativeFileDialogBase::onTypeChange() { const QString filter = selectedNameFilter(); m_data.setSelectedNameFilter(filter); emit filterSelected(filter); } bool QWindowsNativeFileDialogBase::onFileOk() { // Store selected files as GetResults() returns invalid data after the dialog closes. m_data.setSelectedFiles(dialogResult()); return true; } void QWindowsNativeFileDialogBase::close() { m_fileDialog->Close(S_OK); // IFileDialog::Close() does not work unless invoked from a callback. // Try to find the window and send it a WM_CLOSE in addition. const HWND hwnd = findDialogWindow(m_title); qCDebug(lcQpaDialogs) << __FUNCTION__ << "closing" << hwnd; if (hwnd && IsWindowVisible(hwnd)) PostMessageW(hwnd, WM_CLOSE, 0, 0); } HRESULT QWindowsNativeFileDialogEventHandler::OnFolderChanging(IFileDialog *, IShellItem *item) { m_nativeFileDialog->onFolderChange(item); return S_OK; } HRESULT QWindowsNativeFileDialogEventHandler::OnSelectionChange(IFileDialog *) { m_nativeFileDialog->onSelectionChange(); return S_OK; } HRESULT QWindowsNativeFileDialogEventHandler::OnTypeChange(IFileDialog *) { m_nativeFileDialog->onTypeChange(); return S_OK; } HRESULT QWindowsNativeFileDialogEventHandler::OnFileOk(IFileDialog *) { return m_nativeFileDialog->onFileOk() ? S_OK : S_FALSE; } /*! \class QWindowsNativeSaveFileDialog \brief Windows native file save dialog wrapper around IFileSaveDialog. Implements single-selection methods. \internal \ingroup qt-lighthouse-win */ class QWindowsNativeSaveFileDialog : public QWindowsNativeFileDialogBase { Q_OBJECT public: explicit QWindowsNativeSaveFileDialog(const QWindowsFileDialogSharedData &data) : QWindowsNativeFileDialogBase(data) {} void setNameFilters(const QStringList &f) override; QList selectedFiles() const override; QList dialogResult() const override; }; // Return the first suffix from the name filter "Foo files (*.foo;*.bar)" -> "foo". // Also handles the simple name filter case "*.txt" -> "txt" static inline QString suffixFromFilter(const QString &filter) { int suffixPos = filter.indexOf(QLatin1String("*.")); if (suffixPos < 0) return QString(); suffixPos += 2; int endPos = filter.indexOf(QLatin1Char(' '), suffixPos + 1); if (endPos < 0) endPos = filter.indexOf(QLatin1Char(';'), suffixPos + 1); if (endPos < 0) endPos = filter.indexOf(QLatin1Char(')'), suffixPos + 1); if (endPos < 0) endPos = filter.size(); return filter.mid(suffixPos, endPos - suffixPos); } void QWindowsNativeSaveFileDialog::setNameFilters(const QStringList &f) { QWindowsNativeFileDialogBase::setNameFilters(f); // QTBUG-31381, QTBUG-30748: IFileDialog will update the suffix of the selected name // filter only if a default suffix is set (see docs). Set the first available // suffix unless we have a defaultSuffix. if (!hasDefaultSuffix()) { for (const QString &filter : f) { const QString suffix = suffixFromFilter(filter); if (!suffix.isEmpty()) { setDefaultSuffixSys(suffix); break; } } } // m_hasDefaultSuffix } QList QWindowsNativeSaveFileDialog::dialogResult() const { QList result; IShellItem *item = 0; if (SUCCEEDED(fileDialog()->GetResult(&item)) && item) result.append(QWindowsShellItem(item).url()); return result; } QList QWindowsNativeSaveFileDialog::selectedFiles() const { QList result; IShellItem *item = 0; const HRESULT hr = fileDialog()->GetCurrentSelection(&item); if (SUCCEEDED(hr) && item) { result.append(QWindowsShellItem(item).url()); item->Release(); } return result; } /*! \class QWindowsNativeOpenFileDialog \brief Windows native file save dialog wrapper around IFileOpenDialog. Implements multi-selection methods. \internal \ingroup qt-lighthouse-win */ class QWindowsNativeOpenFileDialog : public QWindowsNativeFileDialogBase { public: explicit QWindowsNativeOpenFileDialog(const QWindowsFileDialogSharedData &data) : QWindowsNativeFileDialogBase(data) {} QList selectedFiles() const override; QList dialogResult() const override; private: inline IFileOpenDialog *openFileDialog() const { return static_cast(fileDialog()); } }; // Helpers for managing a list of temporary copies of items with no // file system representation (SFGAO_FILESYSTEM unset, for example devices // using MTP) returned by IFileOpenDialog. This emulates the behavior // of the Win32 API GetOpenFileName() used in Qt 4 (QTBUG-57070). Q_GLOBAL_STATIC(QStringList, temporaryItemCopies) static void cleanupTemporaryItemCopies() { for (const QString &file : qAsConst(*temporaryItemCopies())) QFile::remove(file); } static QString createTemporaryItemCopy(QWindowsShellItem &qItem) { if (!qItem.canCopy() || !qItem.canStream()) return QString(); QString pattern = qItem.normalDisplay(); const int lastDot = pattern.lastIndexOf(QLatin1Char('.')); const QString placeHolder = QStringLiteral("_XXXXXX"); if (lastDot >= 0) pattern.insert(lastDot, placeHolder); else pattern.append(placeHolder); QTemporaryFile targetFile(QDir::tempPath() + QLatin1Char('/') + pattern); targetFile.setAutoRemove(false); if (!targetFile.open() || !qItem.copyData(&targetFile)) return QString(); const QString result = targetFile.fileName(); if (temporaryItemCopies()->isEmpty()) qAddPostRoutine(cleanupTemporaryItemCopies); temporaryItemCopies()->append(result); return result; } QList QWindowsNativeOpenFileDialog::dialogResult() const { QList result; IShellItemArray *items = 0; if (SUCCEEDED(openFileDialog()->GetResults(&items)) && items) { for (IShellItem *item : QWindowsShellItem::itemsFromItemArray(items)) { QWindowsShellItem qItem(item); const QString path = qItem.path(); if (path.isEmpty() && !qItem.isDir()) { const QString temporaryCopy = createTemporaryItemCopy(qItem); if (temporaryCopy.isEmpty()) qWarning() << "Unable to create a local copy of" << qItem; else result.append(QUrl::fromLocalFile(temporaryCopy)); } else { result.append(qItem.url()); } } } return result; } QList QWindowsNativeOpenFileDialog::selectedFiles() const { QList result; IShellItemArray *items = 0; const HRESULT hr = openFileDialog()->GetSelectedItems(&items); if (SUCCEEDED(hr) && items) { for (IShellItem *item : QWindowsShellItem::itemsFromItemArray(items)) { const QWindowsShellItem qItem(item); const QUrl url = qItem.url(); if (url.isValid()) result.append(url); else qWarning().nospace() << __FUNCTION__<< ": Unable to obtain URL of " << qItem; } } return result; } /*! \brief Factory method for QWindowsNativeFileDialogBase returning QWindowsNativeOpenFileDialog or QWindowsNativeSaveFileDialog depending on QFileDialog::AcceptMode. */ QWindowsNativeFileDialogBase *QWindowsNativeFileDialogBase::create(QFileDialogOptions::AcceptMode am, const QWindowsFileDialogSharedData &data) { QWindowsNativeFileDialogBase *result = 0; if (am == QFileDialogOptions::AcceptOpen) { result = new QWindowsNativeOpenFileDialog(data); if (!result->init(CLSID_FileOpenDialog, IID_IFileOpenDialog)) { delete result; return 0; } } else { result = new QWindowsNativeSaveFileDialog(data); if (!result->init(CLSID_FileSaveDialog, IID_IFileSaveDialog)) { delete result; return 0; } } return result; } /*! \class QWindowsFileDialogHelper \brief Helper for native Windows file dialogs For Qt 4 compatibility, do not create native non-modal dialogs on widgets, but only on QQuickWindows, which do not have a fallback. \internal \ingroup qt-lighthouse-win */ class QWindowsFileDialogHelper : public QWindowsDialogHelperBase { public: QWindowsFileDialogHelper() {} bool supportsNonModalDialog(const QWindow * /* parent */ = 0) const override { return false; } bool defaultNameFilterDisables() const override { return false; } void setDirectory(const QUrl &directory) override; QUrl directory() const override; void selectFile(const QUrl &filename) override; QList selectedFiles() const override; void setFilter() override; void selectNameFilter(const QString &filter) override; QString selectedNameFilter() const override; private: QWindowsNativeDialogBase *createNativeDialog() override; inline QWindowsNativeFileDialogBase *nativeFileDialog() const { return static_cast(nativeDialog()); } // Cache for the case no native dialog is created. QWindowsFileDialogSharedData m_data; }; QWindowsNativeDialogBase *QWindowsFileDialogHelper::createNativeDialog() { QWindowsNativeFileDialogBase *result = QWindowsNativeFileDialogBase::create(options()->acceptMode(), m_data); if (!result) return 0; QObject::connect(result, &QWindowsNativeDialogBase::accepted, this, &QPlatformDialogHelper::accept); QObject::connect(result, &QWindowsNativeDialogBase::rejected, this, &QPlatformDialogHelper::reject); QObject::connect(result, &QWindowsNativeFileDialogBase::directoryEntered, this, &QPlatformFileDialogHelper::directoryEntered); QObject::connect(result, &QWindowsNativeFileDialogBase::currentChanged, this, &QPlatformFileDialogHelper::currentChanged); QObject::connect(result, &QWindowsNativeFileDialogBase::filterSelected, this, &QPlatformFileDialogHelper::filterSelected); // Apply settings. const QSharedPointer &opts = options(); m_data.fromOptions(opts); const QFileDialogOptions::FileMode mode = opts->fileMode(); result->setWindowTitle(opts->windowTitle()); result->setMode(mode, opts->acceptMode(), opts->options()); result->setHideFiltersDetails(opts->testOption(QFileDialogOptions::HideNameFilterDetails)); const QStringList nameFilters = opts->nameFilters(); if (!nameFilters.isEmpty()) result->setNameFilters(nameFilters); if (opts->isLabelExplicitlySet(QFileDialogOptions::FileName)) result->setLabelText(QFileDialogOptions::FileName, opts->labelText(QFileDialogOptions::FileName)); if (opts->isLabelExplicitlySet(QFileDialogOptions::Accept)) result->setLabelText(QFileDialogOptions::Accept, opts->labelText(QFileDialogOptions::Accept)); if (opts->isLabelExplicitlySet(QFileDialogOptions::Reject)) result->setLabelText(QFileDialogOptions::Reject, opts->labelText(QFileDialogOptions::Reject)); result->updateDirectory(); result->updateSelectedNameFilter(); const QList initialSelection = opts->initiallySelectedFiles(); if (!initialSelection.empty()) { const QUrl &url = initialSelection.constFirst(); if (url.isLocalFile()) { QFileInfo info(url.toLocalFile()); if (!info.isDir()) result->selectFile(info.fileName()); } else { result->selectFile(url.path()); // TODO url.fileName() once it exists } } // No need to select initialNameFilter if mode is Dir if (mode != QFileDialogOptions::Directory && mode != QFileDialogOptions::DirectoryOnly) { const QString initialNameFilter = opts->initiallySelectedNameFilter(); if (!initialNameFilter.isEmpty()) result->selectNameFilter(initialNameFilter); } const QString defaultSuffix = opts->defaultSuffix(); if (!defaultSuffix.isEmpty()) result->setDefaultSuffix(defaultSuffix); return result; } void QWindowsFileDialogHelper::setDirectory(const QUrl &directory) { qCDebug(lcQpaDialogs) << __FUNCTION__ << directory.toString(); m_data.setDirectory(directory); if (hasNativeDialog()) nativeFileDialog()->updateDirectory(); } QUrl QWindowsFileDialogHelper::directory() const { return m_data.directory(); } void QWindowsFileDialogHelper::selectFile(const QUrl &fileName) { qCDebug(lcQpaDialogs) << __FUNCTION__ << fileName.toString(); if (hasNativeDialog()) // Might be invoked from the QFileDialog constructor. nativeFileDialog()->selectFile(fileName.toLocalFile()); // ## should use QUrl::fileName() once it exists } QList QWindowsFileDialogHelper::selectedFiles() const { return m_data.selectedFiles(); } void QWindowsFileDialogHelper::setFilter() { qCDebug(lcQpaDialogs) << __FUNCTION__; } void QWindowsFileDialogHelper::selectNameFilter(const QString &filter) { m_data.setSelectedNameFilter(filter); if (hasNativeDialog()) nativeFileDialog()->updateSelectedNameFilter(); } QString QWindowsFileDialogHelper::selectedNameFilter() const { return m_data.selectedNameFilter(); } /*! \class QWindowsXpNativeFileDialog \brief Native Windows directory dialog for Windows XP using SHlib-functions. Uses the synchronous GetOpenFileNameW(), GetSaveFileNameW() from ComDlg32 or SHBrowseForFolder() for directories. \internal \sa QWindowsXpFileDialogHelper \ingroup qt-lighthouse-win */ class QWindowsXpNativeFileDialog : public QWindowsNativeDialogBase { Q_OBJECT public: typedef QSharedPointer OptionsPtr; static QWindowsXpNativeFileDialog *create(const OptionsPtr &options, const QWindowsFileDialogSharedData &data); void setWindowTitle(const QString &t) override { m_title = t; } void doExec(HWND owner = 0) override; int existingDirCallback(HWND hwnd, UINT uMsg, LPARAM lParam); public slots: void close() override {} private: typedef BOOL (APIENTRY *PtrGetOpenFileNameW)(LPOPENFILENAMEW); typedef BOOL (APIENTRY *PtrGetSaveFileNameW)(LPOPENFILENAMEW); explicit QWindowsXpNativeFileDialog(const OptionsPtr &options, const QWindowsFileDialogSharedData &data); void populateOpenFileName(OPENFILENAME *ofn, HWND owner) const; QList execExistingDir(HWND owner); QList execFileNames(HWND owner, int *selectedFilterIndex) const; const OptionsPtr m_options; QString m_title; QPlatformDialogHelper::DialogCode m_result; QWindowsFileDialogSharedData m_data; static PtrGetOpenFileNameW m_getOpenFileNameW; static PtrGetSaveFileNameW m_getSaveFileNameW; }; QWindowsXpNativeFileDialog::PtrGetOpenFileNameW QWindowsXpNativeFileDialog::m_getOpenFileNameW = 0; QWindowsXpNativeFileDialog::PtrGetSaveFileNameW QWindowsXpNativeFileDialog::m_getSaveFileNameW = 0; QWindowsXpNativeFileDialog *QWindowsXpNativeFileDialog::create(const OptionsPtr &options, const QWindowsFileDialogSharedData &data) { // GetOpenFileNameW() GetSaveFileName() are resolved // dynamically as not to create a dependency on Comdlg32, which // is used on XP only. if (!m_getOpenFileNameW) { QSystemLibrary library(QStringLiteral("Comdlg32")); m_getOpenFileNameW = (PtrGetOpenFileNameW)(library.resolve("GetOpenFileNameW")); m_getSaveFileNameW = (PtrGetSaveFileNameW)(library.resolve("GetSaveFileNameW")); } if (m_getOpenFileNameW && m_getSaveFileNameW) return new QWindowsXpNativeFileDialog(options, data); return 0; } QWindowsXpNativeFileDialog::QWindowsXpNativeFileDialog(const OptionsPtr &options, const QWindowsFileDialogSharedData &data) : m_options(options), m_result(QPlatformDialogHelper::Rejected), m_data(data) { setWindowTitle(m_options->windowTitle()); } void QWindowsXpNativeFileDialog::doExec(HWND owner) { int selectedFilterIndex = -1; const QList selectedFiles = m_options->fileMode() == QFileDialogOptions::DirectoryOnly ? execExistingDir(owner) : execFileNames(owner, &selectedFilterIndex); m_data.setSelectedFiles(selectedFiles); QWindowsDialogs::eatMouseMove(); if (selectedFiles.isEmpty()) { m_result = QPlatformDialogHelper::Rejected; emit rejected(); } else { const QStringList nameFilters = m_options->nameFilters(); if (selectedFilterIndex >= 0 && selectedFilterIndex < nameFilters.size()) m_data.setSelectedNameFilter(nameFilters.at(selectedFilterIndex)); const QUrl &firstFile = selectedFiles.constFirst(); m_data.setDirectory(firstFile.adjusted(QUrl::RemoveFilename)); m_result = QPlatformDialogHelper::Accepted; emit accepted(); } } // Callback for QWindowsNativeXpFileDialog directory dialog. // MFC Directory Dialog. Contrib: Steve Williams (minor parts from Scott Powers) static int QT_WIN_CALLBACK xpFileDialogGetExistingDirCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) { QWindowsXpNativeFileDialog *dialog = reinterpret_cast(lpData); return dialog->existingDirCallback(hwnd, uMsg, lParam); } /* The correct declaration of the SHGetPathFromIDList symbol is * being used in mingw-w64 as of r6215, which is a v3 snapshot. */ #if defined(Q_CC_MINGW) && (!defined(__MINGW64_VERSION_MAJOR) || __MINGW64_VERSION_MAJOR < 3) typedef ITEMIDLIST *qt_LpItemIdList; #else typedef PIDLIST_ABSOLUTE qt_LpItemIdList; #endif int QWindowsXpNativeFileDialog::existingDirCallback(HWND hwnd, UINT uMsg, LPARAM lParam) { switch (uMsg) { case BFFM_INITIALIZED: { if (!m_title.isEmpty()) SetWindowText(hwnd, reinterpret_cast(m_title.utf16())); const QString initialFile = QDir::toNativeSeparators(m_data.directory().toLocalFile()); if (!initialFile.isEmpty()) SendMessage(hwnd, BFFM_SETSELECTION, TRUE, LPARAM(initialFile.utf16())); } break; case BFFM_SELCHANGED: { wchar_t path[MAX_PATH]; const bool ok = SHGetPathFromIDList(reinterpret_cast(lParam), path) && path[0]; SendMessage(hwnd, BFFM_ENABLEOK, ok ? 1 : 0, 1); } break; } return 0; } QList QWindowsXpNativeFileDialog::execExistingDir(HWND owner) { BROWSEINFO bi; wchar_t initPath[MAX_PATH]; initPath[0] = 0; bi.hwndOwner = owner; bi.pidlRoot = NULL; bi.lpszTitle = 0; bi.pszDisplayName = initPath; bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT | BIF_NEWDIALOGSTYLE; bi.lpfn = xpFileDialogGetExistingDirCallbackProc; bi.lParam = LPARAM(this); QList selectedFiles; if (qt_LpItemIdList pItemIDList = SHBrowseForFolder(&bi)) { wchar_t path[MAX_PATH]; path[0] = 0; if (SHGetPathFromIDList(pItemIDList, path) && path[0]) selectedFiles.push_back(QUrl::fromLocalFile(QDir::cleanPath(QString::fromWCharArray(path)))); IMalloc *pMalloc; if (SHGetMalloc(&pMalloc) == NOERROR) { pMalloc->Free(pItemIDList); pMalloc->Release(); } } return selectedFiles; } // Open/Save files void QWindowsXpNativeFileDialog::populateOpenFileName(OPENFILENAME *ofn, HWND owner) const { ZeroMemory(ofn, sizeof(OPENFILENAME)); ofn->lStructSize = sizeof(OPENFILENAME); ofn->hwndOwner = owner; // Create a buffer with the filter strings. int totalStringLength = 0; const QList specs = filterSpecs(m_options->nameFilters(), m_options->options() & QFileDialogOptions::HideNameFilterDetails, &totalStringLength); const int size = specs.size(); wchar_t *ptr = new wchar_t[totalStringLength + 2 * size + 1]; ofn->lpstrFilter = ptr; for (const FilterSpec &spec : specs) { ptr += spec.description.toWCharArray(ptr); *ptr++ = 0; ptr += spec.filter.toWCharArray(ptr); *ptr++ = 0; } *ptr = 0; const int nameFilterIndex = indexOfNameFilter(m_options->nameFilters(), m_data.selectedNameFilter()); if (nameFilterIndex >= 0) ofn->nFilterIndex = nameFilterIndex + 1; // 1..n based. // lpstrFile receives the initial selection and is the buffer // for the target. If it contains any invalid character, the dialog // will not show. ofn->nMaxFile = 65535; const QString initiallySelectedFile = QDir::toNativeSeparators(m_data.selectedFile()).remove(QLatin1Char('<')). remove(QLatin1Char('>')).remove(QLatin1Char('"')).remove(QLatin1Char('|')); ofn->lpstrFile = qStringToWCharArray(initiallySelectedFile, ofn->nMaxFile); ofn->lpstrInitialDir = qStringToWCharArray(QDir::toNativeSeparators(m_data.directory().toLocalFile())); ofn->lpstrTitle = (wchar_t*)m_title.utf16(); // Determine lpstrDefExt. Note that the current MSDN docs document this // member wrong. It should rather be documented as "the default extension // if no extension was given and if the current filter does not have an // extension (e.g (*)). If the current filter has an extension, use // the extension of the current filter". if (m_options->acceptMode() == QFileDialogOptions::AcceptSave) { QString defaultSuffix = m_options->defaultSuffix(); if (defaultSuffix.startsWith(QLatin1Char('.'))) defaultSuffix.remove(0, 1); // QTBUG-33156, also create empty strings to trigger the appending mechanism. ofn->lpstrDefExt = qStringToWCharArray(defaultSuffix); } // Flags. ofn->Flags = (OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_EXPLORER | OFN_PATHMUSTEXIST); if (m_options->fileMode() == QFileDialogOptions::ExistingFile || m_options->fileMode() == QFileDialogOptions::ExistingFiles) ofn->Flags |= (OFN_FILEMUSTEXIST); if (m_options->fileMode() == QFileDialogOptions::ExistingFiles) ofn->Flags |= (OFN_ALLOWMULTISELECT); if (!(m_options->options() & QFileDialogOptions::DontConfirmOverwrite)) ofn->Flags |= OFN_OVERWRITEPROMPT; } QList QWindowsXpNativeFileDialog::execFileNames(HWND owner, int *selectedFilterIndex) const { *selectedFilterIndex = -1; OPENFILENAME ofn; populateOpenFileName(&ofn, owner); QList result; const bool isSave = m_options->acceptMode() == QFileDialogOptions::AcceptSave; if (isSave ? m_getSaveFileNameW(&ofn) : m_getOpenFileNameW(&ofn)) { *selectedFilterIndex = ofn.nFilterIndex - 1; const QString dir = QDir::cleanPath(QString::fromWCharArray(ofn.lpstrFile)); result.push_back(QUrl::fromLocalFile(dir)); // For multiselection, the first item is the path followed // by "\0\0\0\0". if (ofn.Flags & (OFN_ALLOWMULTISELECT)) { wchar_t *ptr = ofn.lpstrFile + dir.size() + 1; if (*ptr) { result.pop_front(); const QString path = dir + QLatin1Char('/'); while (*ptr) { const QString fileName = QString::fromWCharArray(ptr); result.push_back(QUrl::fromLocalFile(path + fileName)); ptr += fileName.size() + 1; } // extract multiple files } // has multiple files } // multiple flag set } delete [] ofn.lpstrFile; delete [] ofn.lpstrInitialDir; delete [] ofn.lpstrFilter; delete [] ofn.lpstrDefExt; return result; } /*! \class QWindowsXpFileDialogHelper \brief Dialog helper using QWindowsXpNativeFileDialog \sa QWindowsXpNativeFileDialog \internal \ingroup qt-lighthouse-win */ class QWindowsXpFileDialogHelper : public QWindowsDialogHelperBase { public: QWindowsXpFileDialogHelper() = default; bool supportsNonModalDialog(const QWindow * /* parent */ = 0) const override { return false; } bool defaultNameFilterDisables() const override { return true; } void setDirectory(const QUrl &directory) override; QUrl directory() const override; void selectFile(const QUrl &url) override; QList selectedFiles() const override; void setFilter() override {} void selectNameFilter(const QString &) override; QString selectedNameFilter() const override; private: QWindowsNativeDialogBase *createNativeDialog() override; inline QWindowsXpNativeFileDialog *nativeFileDialog() const { return static_cast(nativeDialog()); } QWindowsFileDialogSharedData m_data; }; QWindowsNativeDialogBase *QWindowsXpFileDialogHelper::createNativeDialog() { m_data.fromOptions(options()); if (QWindowsXpNativeFileDialog *result = QWindowsXpNativeFileDialog::create(options(), m_data)) { QObject::connect(result, &QWindowsNativeDialogBase::accepted, this, &QPlatformDialogHelper::accept); QObject::connect(result, &QWindowsNativeDialogBase::rejected, this, &QPlatformDialogHelper::reject); return result; } return 0; } void QWindowsXpFileDialogHelper::setDirectory(const QUrl &directory) { m_data.setDirectory(directory); // Dialog cannot be updated at run-time. } QUrl QWindowsXpFileDialogHelper::directory() const { return m_data.directory(); } void QWindowsXpFileDialogHelper::selectFile(const QUrl &url) { m_data.setSelectedFiles(QList() << url); // Dialog cannot be updated at run-time. } QList QWindowsXpFileDialogHelper::selectedFiles() const { return m_data.selectedFiles(); } void QWindowsXpFileDialogHelper::selectNameFilter(const QString &f) { m_data.setSelectedNameFilter(f); // Dialog cannot be updated at run-time. } QString QWindowsXpFileDialogHelper::selectedNameFilter() const { return m_data.selectedNameFilter(); } /*! \class QWindowsNativeColorDialog \brief Native Windows color dialog. Wrapper around Comdlg32's ChooseColor() function. Not currently in use as QColorDialog is equivalent. \sa QWindowsColorDialogHelper \sa #define USE_NATIVE_COLOR_DIALOG \internal \ingroup qt-lighthouse-win */ typedef QSharedPointer SharedPointerColor; #ifdef USE_NATIVE_COLOR_DIALOG class QWindowsNativeColorDialog : public QWindowsNativeDialogBase { Q_OBJECT public: enum { CustomColorCount = 16 }; explicit QWindowsNativeColorDialog(const SharedPointerColor &color); void setWindowTitle(const QString &) override {} public slots: void close() override {} private: void doExec(HWND owner = 0) override; COLORREF m_customColors[CustomColorCount]; QPlatformDialogHelper::DialogCode m_code; SharedPointerColor m_color; }; QWindowsNativeColorDialog::QWindowsNativeColorDialog(const SharedPointerColor &color) : m_code(QPlatformDialogHelper::Rejected), m_color(color) { std::fill(m_customColors, m_customColors + 16, COLORREF(0)); } void QWindowsNativeColorDialog::doExec(HWND owner) { typedef BOOL (WINAPI *ChooseColorWType)(LPCHOOSECOLORW); CHOOSECOLOR chooseColor; ZeroMemory(&chooseColor, sizeof(chooseColor)); chooseColor.lStructSize = sizeof(chooseColor); chooseColor.hwndOwner = owner; chooseColor.lpCustColors = m_customColors; QRgb *qCustomColors = QColorDialogOptions::customColors(); const int customColorCount = qMin(QColorDialogOptions::customColorCount(), int(CustomColorCount)); for (int c= 0; c < customColorCount; ++c) m_customColors[c] = qColorToCOLORREF(QColor(qCustomColors[c])); chooseColor.rgbResult = qColorToCOLORREF(*m_color); chooseColor.Flags = CC_FULLOPEN | CC_RGBINIT; static ChooseColorWType chooseColorW = 0; if (!chooseColorW) { QSystemLibrary library(QStringLiteral("Comdlg32")); chooseColorW = (ChooseColorWType)library.resolve("ChooseColorW"); } if (chooseColorW) { m_code = chooseColorW(&chooseColor) ? QPlatformDialogHelper::Accepted : QPlatformDialogHelper::Rejected; QWindowsDialogs::eatMouseMove(); } else { m_code = QPlatformDialogHelper::Rejected; } if (m_code == QPlatformDialogHelper::Accepted) { *m_color = COLORREFToQColor(chooseColor.rgbResult); for (int c= 0; c < customColorCount; ++c) qCustomColors[c] = COLORREFToQColor(m_customColors[c]).rgb(); emit accepted(); } else { emit rejected(); } } /*! \class QWindowsColorDialogHelper \brief Helper for native Windows color dialogs Not currently in use as QColorDialog is equivalent. \sa #define USE_NATIVE_COLOR_DIALOG \sa QWindowsNativeColorDialog \internal \ingroup qt-lighthouse-win */ class QWindowsColorDialogHelper : public QWindowsDialogHelperBase { public: QWindowsColorDialogHelper() : m_currentColor(new QColor) {} virtual bool supportsNonModalDialog() { return false; } virtual QColor currentColor() const { return *m_currentColor; } virtual void setCurrentColor(const QColor &c) { *m_currentColor = c; } private: inline QWindowsNativeColorDialog *nativeFileDialog() const { return static_cast(nativeDialog()); } virtual QWindowsNativeDialogBase *createNativeDialog(); SharedPointerColor m_currentColor; }; QWindowsNativeDialogBase *QWindowsColorDialogHelper::createNativeDialog() { QWindowsNativeColorDialog *nativeDialog = new QWindowsNativeColorDialog(m_currentColor); nativeDialog->setWindowTitle(options()->windowTitle()); connect(nativeDialog, &QWindowsNativeDialogBase::accepted, this, &QPlatformDialogHelper::accept); connect(nativeDialog, &QWindowsNativeDialogBase::rejected, this, &QPlatformDialogHelper::reject); return nativeDialog; } #endif // USE_NATIVE_COLOR_DIALOG namespace QWindowsDialogs { // QWindowsDialogHelperBase creation functions bool useHelper(QPlatformTheme::DialogType type) { if (QWindowsIntegration::instance()->options() & QWindowsIntegration::NoNativeDialogs) return false; switch (type) { case QPlatformTheme::FileDialog: return true; case QPlatformTheme::ColorDialog: #ifdef USE_NATIVE_COLOR_DIALOG return true; #else break; #endif case QPlatformTheme::FontDialog: case QPlatformTheme::MessageDialog: break; default: break; } return false; } QPlatformDialogHelper *createHelper(QPlatformTheme::DialogType type) { if (QWindowsIntegration::instance()->options() & QWindowsIntegration::NoNativeDialogs) return 0; switch (type) { case QPlatformTheme::FileDialog: if (QWindowsIntegration::instance()->options() & QWindowsIntegration::XpNativeDialogs) return new QWindowsXpFileDialogHelper(); return new QWindowsFileDialogHelper; case QPlatformTheme::ColorDialog: #ifdef USE_NATIVE_COLOR_DIALOG return new QWindowsColorDialogHelper(); #else break; #endif case QPlatformTheme::FontDialog: case QPlatformTheme::MessageDialog: break; default: break; } return 0; } } // namespace QWindowsDialogs QT_END_NAMESPACE #include "qwindowsdialoghelpers.moc"