/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { // Implemented in juce_Messaging_windows.cpp namespace detail { bool dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages); } // namespace detail class Win32NativeFileChooser final : private Thread { public: enum { charsAvailableForResult = 32768 }; Win32NativeFileChooser (Component* parent, int flags, FilePreviewComponent* previewComp, const File& startingFile, const String& titleToUse, const String& filtersToUse) : Thread (SystemStats::getJUCEVersion() + ": Native Win32 FileChooser"), owner (parent), title (titleToUse), filtersString (filtersToUse.replaceCharacter (',', ';')), selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0), // When dealing with directories, it is not possible to 'Save' them. However, one can 'Open' a directory in order to save into it. // If the 'saveMode' and 'canSelectDirectories' flags are both present, create an FileOpenDialog instead of an FileSaveDialog. isSave ((flags & FileBrowserComponent::saveMode) != 0 && ! selectsDirectories), warnAboutOverwrite ((flags & FileBrowserComponent::warnAboutOverwriting) != 0), selectMultiple ((flags & FileBrowserComponent::canSelectMultipleItems) != 0) { auto parentDirectory = startingFile.getParentDirectory(); // Handle nonexistent root directories in the same way as existing ones files.calloc (static_cast (charsAvailableForResult) + 1); if (startingFile.isDirectory() || startingFile.isRoot()) { initialPath = startingFile.getFullPathName(); } else { startingFile.getFileName().copyToUTF16 (files, static_cast (charsAvailableForResult) * sizeof (WCHAR)); initialPath = parentDirectory.getFullPathName(); } if (! selectsDirectories) { if (previewComp != nullptr) customComponent.reset (new CustomComponentHolder (previewComp)); setupFilters(); } } ~Win32NativeFileChooser() override { signalThreadShouldExit(); while (isThreadRunning()) { if (! detail::dispatchNextMessageOnSystemQueue (true)) Thread::sleep (1); } } void open (bool async) { results.clear(); // the thread should not be running nativeDialogRef.set (nullptr); if (async) { jassert (! isThreadRunning()); startThread(); } else { results = openDialog (false); owner->exitModalState (results.size() > 0 ? 1 : 0); } } void cancel() { ScopedLock lock (deletingDialog); customComponent = nullptr; shouldCancel = true; if (auto hwnd = nativeDialogRef.get()) PostMessage (hwnd, WM_CLOSE, 0, 0); } Component* getCustomComponent() { return customComponent.get(); } Array results; private: //============================================================================== class CustomComponentHolder final : public Component { public: CustomComponentHolder (Component* const customComp) { setVisible (true); setOpaque (true); addAndMakeVisible (customComp); setSize (jlimit (20, 800, customComp->getWidth()), customComp->getHeight()); } void paint (Graphics& g) override { g.fillAll (Colours::lightgrey); } void resized() override { if (Component* const c = getChildComponent (0)) c->setBounds (getLocalBounds()); } private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponentHolder) }; //============================================================================== const Component::SafePointer owner; String title, filtersString; std::unique_ptr customComponent; String initialPath, returnedString; CriticalSection deletingDialog; bool selectsDirectories, isSave, warnAboutOverwrite, selectMultiple; HeapBlock files; HeapBlock filters; Atomic nativeDialogRef { nullptr }; bool shouldCancel = false; struct FreeLPWSTR { void operator() (LPWSTR ptr) const noexcept { CoTaskMemFree (ptr); } }; bool showDialog (IFileDialog& dialog) { FILEOPENDIALOGOPTIONS flags = {}; if (FAILED (dialog.GetOptions (&flags))) return false; const auto setBit = [] (FILEOPENDIALOGOPTIONS& field, bool value, FILEOPENDIALOGOPTIONS option) { if (value) field |= option; else field &= ~option; }; setBit (flags, selectsDirectories, FOS_PICKFOLDERS); setBit (flags, warnAboutOverwrite, FOS_OVERWRITEPROMPT); setBit (flags, selectMultiple, FOS_ALLOWMULTISELECT); setBit (flags, customComponent != nullptr, FOS_FORCEPREVIEWPANEON); if (FAILED (dialog.SetOptions (flags)) || FAILED (dialog.SetTitle (title.toUTF16()))) return false; PIDLIST_ABSOLUTE pidl = {}; if (FAILED (SHParseDisplayName (initialPath.toWideCharPointer(), nullptr, &pidl, SFGAO_FOLDER, nullptr))) { LPWSTR ptr = nullptr; auto result = SHGetKnownFolderPath (FOLDERID_Desktop, 0, nullptr, &ptr); std::unique_ptr desktopPath (ptr); if (FAILED (result)) return false; if (FAILED (SHParseDisplayName (desktopPath.get(), nullptr, &pidl, SFGAO_FOLDER, nullptr))) return false; } const auto item = [&] { ComSmartPtr ptr; SHCreateShellItem (nullptr, nullptr, pidl, ptr.resetAndGetPointerAddress()); return ptr; }(); if (item != nullptr) { dialog.SetDefaultFolder (item); if (! initialPath.isEmpty()) dialog.SetFolder (item); } String filename (files.getData()); if (FAILED (dialog.SetFileName (filename.toWideCharPointer()))) return false; auto extension = getDefaultFileExtension (filename); if (extension.isNotEmpty() && FAILED (dialog.SetDefaultExtension (extension.toWideCharPointer()))) return false; const COMDLG_FILTERSPEC spec[] { { filtersString.toWideCharPointer(), filtersString.toWideCharPointer() } }; if (! selectsDirectories && FAILED (dialog.SetFileTypes (numElementsInArray (spec), spec))) return false; struct Events final : public ComBaseClassHelper { explicit Events (Win32NativeFileChooser& o) : owner (o) {} JUCE_COMRESULT OnTypeChange (IFileDialog* d) override { return updateHwnd (d); } JUCE_COMRESULT OnFolderChanging (IFileDialog* d, IShellItem*) override { return updateHwnd (d); } JUCE_COMRESULT OnFileOk (IFileDialog* d) override { return updateHwnd (d); } JUCE_COMRESULT OnFolderChange (IFileDialog* d) override { return updateHwnd (d); } JUCE_COMRESULT OnSelectionChange (IFileDialog* d) override { focusWorkaround(); return updateHwnd (d); } JUCE_COMRESULT OnShareViolation (IFileDialog* d, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) override { return updateHwnd (d); } JUCE_COMRESULT OnOverwrite (IFileDialog* d, IShellItem*, FDE_OVERWRITE_RESPONSE*) override { return updateHwnd (d); } /* Workaround for a bug in Vista+, OpenFileDialog will truncate the initialFile text. Moving the caret along the full length of the text and back will reveal the full string. */ void focusWorkaround() { if (! defaultFileNameTextCaretMoved) { auto makeKeyInput = [] (WORD vk, bool pressed) { INPUT i; ZeroMemory (&i, sizeof (INPUT)); i.type = INPUT_KEYBOARD; i.ki.wVk = vk; i.ki.dwFlags = pressed ? 0 : KEYEVENTF_KEYUP; return i; }; INPUT inputs[] = { makeKeyInput (VK_HOME, true), makeKeyInput (VK_HOME, false), makeKeyInput (VK_END, true), makeKeyInput (VK_END, false), }; SendInput ((UINT) std::size (inputs), inputs, sizeof (INPUT)); defaultFileNameTextCaretMoved = true; } } JUCE_COMRESULT updateHwnd (IFileDialog* d) { HWND hwnd = nullptr; if (auto window = ComSmartPtr (d, IncrementRef::yes).getInterface()) window->GetWindow (&hwnd); ScopedLock lock (owner.deletingDialog); if (owner.shouldCancel) d->Close (S_FALSE); else if (hwnd != nullptr) owner.nativeDialogRef = hwnd; return S_OK; } bool defaultFileNameTextCaretMoved = false; Win32NativeFileChooser& owner; }; { ScopedLock lock (deletingDialog); if (shouldCancel) return false; } const auto result = [&] { struct ScopedAdvise { ScopedAdvise (IFileDialog& d, Events& events) : dialog (d) { dialog.Advise (&events, &cookie); } ~ScopedAdvise() { dialog.Unadvise (cookie); } IFileDialog& dialog; DWORD cookie = 0; }; Events events { *this }; ScopedAdvise scope { dialog, events }; return dialog.Show (GetActiveWindow()) == S_OK; }(); ScopedLock lock (deletingDialog); nativeDialogRef = nullptr; return result; } //============================================================================== Array openDialogWithoutPreview() { const auto getUrl = [] (IShellItem& item) { LPWSTR ptr = nullptr; if (item.GetDisplayName (SIGDN_FILESYSPATH, &ptr) != S_OK) return URL(); const auto path = std::unique_ptr { ptr }; return URL (File (String (path.get()))); }; if (isSave) { const auto dialog = [&] { ComSmartPtr ptr; ptr.CoCreateInstance (CLSID_FileSaveDialog, CLSCTX_INPROC_SERVER); return ptr; }(); if (dialog == nullptr) return {}; showDialog (*dialog); const auto item = [&] { ComSmartPtr ptr; dialog->GetResult (ptr.resetAndGetPointerAddress()); return ptr; }(); if (item == nullptr) return {}; const auto url = getUrl (*item); if (url.isEmpty()) return {}; return { url }; } const auto dialog = [&] { ComSmartPtr ptr; ptr.CoCreateInstance (CLSID_FileOpenDialog, CLSCTX_INPROC_SERVER); return ptr; }(); if (dialog == nullptr) return {}; showDialog (*dialog); const auto items = [&] { ComSmartPtr ptr; dialog->GetResults (ptr.resetAndGetPointerAddress()); return ptr; }(); if (items == nullptr) return {}; Array result; DWORD numItems = 0; items->GetCount (&numItems); for (DWORD i = 0; i < numItems; ++i) { ComSmartPtr scope; items->GetItemAt (i, scope.resetAndGetPointerAddress()); if (scope != nullptr) { const auto url = getUrl (*scope); if (! url.isEmpty()) result.add (url); } } return result; } Array openDialogWithPreview (bool async) { Array selections; if (selectsDirectories) { BROWSEINFO bi = {}; bi.hwndOwner = GetActiveWindow(); bi.pszDisplayName = files; bi.lpszTitle = title.toWideCharPointer(); bi.lParam = (LPARAM) this; bi.lpfn = browseCallbackProc; #ifdef BIF_USENEWUI bi.ulFlags = BIF_USENEWUI | BIF_VALIDATE; #else bi.ulFlags = 0x50; #endif LPITEMIDLIST list = SHBrowseForFolder (&bi); if (! SHGetPathFromIDListW (list, files)) { files[0] = 0; returnedString.clear(); } LPMALLOC al; if (list != nullptr && SUCCEEDED (SHGetMalloc (&al))) al->Free (list); if (files[0] != 0) { File result (String (files.get())); if (returnedString.isNotEmpty()) result = result.getSiblingFile (returnedString); selections.add (URL (result)); } } else { OPENFILENAMEW of = {}; #ifdef OPENFILENAME_SIZE_VERSION_400W of.lStructSize = OPENFILENAME_SIZE_VERSION_400W; #else of.lStructSize = sizeof (of); #endif if (files[0] != 0) { auto startingFile = File (initialPath).getChildFile (String (files.get())); startingFile.getFullPathName().copyToUTF16 (files, charsAvailableForResult * sizeof (WCHAR)); } of.hwndOwner = GetActiveWindow(); of.lpstrFilter = filters.getData(); of.nFilterIndex = 1; of.lpstrFile = files; of.nMaxFile = (DWORD) charsAvailableForResult; of.lpstrInitialDir = initialPath.toWideCharPointer(); of.lpstrTitle = title.toWideCharPointer(); of.Flags = getOpenFilenameFlags (async); of.lCustData = (LPARAM) this; of.lpfnHook = &openCallback; if (isSave) { auto extension = getDefaultFileExtension (files.getData()); if (extension.isNotEmpty()) of.lpstrDefExt = extension.toWideCharPointer(); if (! GetSaveFileName (&of)) return {}; } else { if (! GetOpenFileName (&of)) return {}; } if (selectMultiple && of.nFileOffset > 0 && files[of.nFileOffset - 1] == 0) { const WCHAR* filename = files + of.nFileOffset; while (*filename != 0) { selections.add (URL (File (String (files.get())).getChildFile (String (filename)))); filename += wcslen (filename) + 1; } } else if (files[0] != 0) { selections.add (URL (File (String (files.get())))); } } return selections; } Array openDialog (bool async) { struct Remover { explicit Remover (Win32NativeFileChooser& chooser) : item (chooser) {} ~Remover() { getNativeDialogList().removeValue (&item); } Win32NativeFileChooser& item; }; const Remover remover (*this); if (customComponent == nullptr) return openDialogWithoutPreview(); return openDialogWithPreview (async); } void run() override { results = [&] { struct ScopedCoInitialize { // IUnknown_GetWindow will only succeed when instantiated in a single-thread apartment ScopedCoInitialize() { [[maybe_unused]] const auto result = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); } ~ScopedCoInitialize() { CoUninitialize(); } }; ScopedCoInitialize scope; return openDialog (true); }(); auto safeOwner = owner; auto resultCode = results.size() > 0 ? 1 : 0; MessageManager::callAsync ([resultCode, safeOwner] { if (safeOwner != nullptr) safeOwner->exitModalState (resultCode); }); } static HashMap& getNativeDialogList() { static HashMap dialogs; return dialogs; } static Win32NativeFileChooser* getNativePointerForDialog (HWND hwnd) { return getNativeDialogList()[hwnd]; } //============================================================================== void setupFilters() { const size_t filterSpaceNumChars = 2048; filters.calloc (filterSpaceNumChars); const size_t bytesWritten = filtersString.copyToUTF16 (filters.getData(), filterSpaceNumChars * sizeof (WCHAR)); filtersString.copyToUTF16 (filters + (bytesWritten / sizeof (WCHAR)), ((filterSpaceNumChars - 1) * sizeof (WCHAR) - bytesWritten)); for (size_t i = 0; i < filterSpaceNumChars; ++i) if (filters[i] == '|') filters[i] = 0; } DWORD getOpenFilenameFlags (bool async) { DWORD ofFlags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING; if (warnAboutOverwrite) ofFlags |= OFN_OVERWRITEPROMPT; if (selectMultiple) ofFlags |= OFN_ALLOWMULTISELECT; if (async || customComponent != nullptr) ofFlags |= OFN_ENABLEHOOK; return ofFlags; } String getDefaultFileExtension (const String& filename) const { const auto extension = filename.contains (".") ? filename.fromLastOccurrenceOf (".", false, false) : String(); if (! extension.isEmpty()) return extension; auto tokens = StringArray::fromTokens (filtersString, ";,", "\"'"); tokens.trim(); tokens.removeEmptyStrings(); if (tokens.size() == 1 && tokens[0].removeCharacters ("*.").isNotEmpty()) return tokens[0].fromFirstOccurrenceOf (".", false, false); return {}; } //============================================================================== void initialised (HWND hWnd) { SendMessage (hWnd, BFFM_SETSELECTIONW, TRUE, (LPARAM) initialPath.toWideCharPointer()); initDialog (hWnd); } void validateFailed (const String& path) { returnedString = path; } void initDialog (HWND hdlg) { ScopedLock lock (deletingDialog); getNativeDialogList().set (hdlg, this); if (shouldCancel) { EndDialog (hdlg, 0); } else { nativeDialogRef.set (hdlg); if (customComponent != nullptr) { Component::SafePointer safeCustomComponent (customComponent.get()); RECT dialogScreenRect, dialogClientRect; GetWindowRect (hdlg, &dialogScreenRect); GetClientRect (hdlg, &dialogClientRect); auto screenRectangle = Rectangle::leftTopRightBottom (dialogScreenRect.left, dialogScreenRect.top, dialogScreenRect.right, dialogScreenRect.bottom); auto scale = Desktop::getInstance().getDisplays().getDisplayForRect (screenRectangle, true)->scale; auto physicalComponentWidth = roundToInt (safeCustomComponent->getWidth() * scale); SetWindowPos (hdlg, nullptr, screenRectangle.getX(), screenRectangle.getY(), physicalComponentWidth + jmax (150, screenRectangle.getWidth()), jmax (150, screenRectangle.getHeight()), SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER); auto appendCustomComponent = [safeCustomComponent, dialogClientRect, scale, hdlg]() mutable { if (safeCustomComponent != nullptr) { auto scaledClientRectangle = Rectangle::leftTopRightBottom (dialogClientRect.left, dialogClientRect.top, dialogClientRect.right, dialogClientRect.bottom) / scale; safeCustomComponent->setBounds (scaledClientRectangle.getRight(), scaledClientRectangle.getY(), safeCustomComponent->getWidth(), scaledClientRectangle.getHeight()); safeCustomComponent->addToDesktop (0, hdlg); } }; if (MessageManager::getInstance()->isThisTheMessageThread()) appendCustomComponent(); else MessageManager::callAsync (appendCustomComponent); } } } void destroyDialog (HWND hdlg) { ScopedLock exiting (deletingDialog); getNativeDialogList().remove (hdlg); nativeDialogRef.set (nullptr); if (MessageManager::getInstance()->isThisTheMessageThread()) customComponent = nullptr; else MessageManager::callAsync ([this] { customComponent = nullptr; }); } void selectionChanged (HWND hdlg) { ScopedLock lock (deletingDialog); if (customComponent != nullptr && ! shouldCancel) { if (FilePreviewComponent* comp = dynamic_cast (customComponent->getChildComponent (0))) { WCHAR path [MAX_PATH * 2] = { 0 }; CommDlg_OpenSave_GetFilePath (hdlg, (LPARAM) &path, MAX_PATH); if (MessageManager::getInstance()->isThisTheMessageThread()) { comp->selectedFileChanged (File (path)); } else { MessageManager::callAsync ([safeComp = Component::SafePointer { comp }, selectedFile = File { path }]() mutable { if (safeComp != nullptr) safeComp->selectedFileChanged (selectedFile); }); } } } } //============================================================================== static int CALLBACK browseCallbackProc (HWND hWnd, UINT msg, LPARAM lParam, LPARAM lpData) { auto* self = reinterpret_cast (lpData); switch (msg) { case BFFM_INITIALIZED: self->initialised (hWnd); break; case BFFM_VALIDATEFAILEDW: self->validateFailed (String ((LPCWSTR) lParam)); break; case BFFM_VALIDATEFAILEDA: self->validateFailed (String ((const char*) lParam)); break; default: break; } return 0; } static UINT_PTR CALLBACK openCallback (HWND hwnd, UINT uiMsg, WPARAM /*wParam*/, LPARAM lParam) { auto hdlg = getDialogFromHWND (hwnd); switch (uiMsg) { case WM_INITDIALOG: { if (auto* self = reinterpret_cast (((OPENFILENAMEW*) lParam)->lCustData)) self->initDialog (hdlg); break; } case WM_DESTROY: { if (auto* self = getNativeDialogList()[hdlg]) self->destroyDialog (hdlg); break; } case WM_NOTIFY: { auto ofn = reinterpret_cast (lParam); if (ofn->hdr.code == CDN_SELCHANGE) if (auto* self = reinterpret_cast (ofn->lpOFN->lCustData)) self->selectionChanged (hdlg); break; } default: break; } return 0; } static HWND getDialogFromHWND (HWND hwnd) { if (hwnd == nullptr) return nullptr; HWND dialogH = GetParent (hwnd); if (dialogH == nullptr) dialogH = hwnd; return dialogH; } //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32NativeFileChooser) }; class FileChooser::Native final : public std::enable_shared_from_this, public Component, public FileChooser::Pimpl { public: Native (FileChooser& fileChooser, int flagsIn, FilePreviewComponent* previewComp) : owner (fileChooser), nativeFileChooser (std::make_unique (this, flagsIn, previewComp, fileChooser.startingFile, fileChooser.title, fileChooser.filters)) { auto mainMon = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea; setBounds (mainMon.getX() + mainMon.getWidth() / 4, mainMon.getY() + mainMon.getHeight() / 4, 0, 0); setOpaque (true); setAlwaysOnTop (WindowUtils::areThereAnyAlwaysOnTopWindows()); addToDesktop (0); } ~Native() override { exitModalState (0); nativeFileChooser->cancel(); } void launch() override { std::weak_ptr safeThis = shared_from_this(); enterModalState (true, ModalCallbackFunction::create ([safeThis] (int) { if (auto locked = safeThis.lock()) locked->owner.finished (locked->nativeFileChooser->results); })); nativeFileChooser->open (true); } void runModally() override { #if JUCE_MODAL_LOOPS_PERMITTED enterModalState (true); nativeFileChooser->open (false); exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0); nativeFileChooser->cancel(); owner.finished (nativeFileChooser->results); #else jassertfalse; #endif } bool canModalEventBeSentToComponent (const Component* targetComponent) override { if (targetComponent == nullptr) return false; if (targetComponent == nativeFileChooser->getCustomComponent()) return true; return targetComponent->findParentComponentOfClass() != nullptr; } void inputAttemptWhenModal() override {} private: FileChooser& owner; std::shared_ptr nativeFileChooser; }; //============================================================================== bool FileChooser::isPlatformDialogAvailable() { #if JUCE_DISABLE_NATIVE_FILECHOOSERS return false; #else return true; #endif } std::shared_ptr FileChooser::showPlatformDialog (FileChooser& owner, int flags, FilePreviewComponent* preview) { return std::make_shared (owner, flags, preview); } } // namespace juce