/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { // Implemented in juce_win32_Messaging.cpp bool dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages); class Win32NativeFileChooser : private Thread { public: enum { charsAvailableForResult = 32768 }; Win32NativeFileChooser (Component* parent, int flags, FilePreviewComponent* previewComp, const File& startingFile, const String& titleToUse, const String& filtersToUse) : Thread ("Native Win32 FileChooser"), owner (parent), title (titleToUse), filtersString (filtersToUse.replaceCharacter (',', ';')), selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0), isSave ((flags & FileBrowserComponent::saveMode) != 0), 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 (! 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 : 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); } }; #if JUCE_MSVC bool showDialog (IFileDialog& dialog, bool async) { 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 : 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 { 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); } JUCE_COMRESULT updateHwnd (IFileDialog* d) { HWND hwnd = nullptr; if (auto window = ComSmartPtr { d }.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; } 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 (async ? nullptr : static_cast (owner->getWindowHandle())) == S_OK; }(); ScopedLock lock (deletingDialog); nativeDialogRef = nullptr; return result; } //============================================================================== Array openDialogVistaAndUp (bool async) { 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, async); 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, async); 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; } #endif Array openDialogPreVista (bool async) { Array selections; if (selectsDirectories) { BROWSEINFO bi = {}; bi.hwndOwner = (HWND) (async ? nullptr : owner->getWindowHandle()); 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 = (HWND) (async ? nullptr : owner->getWindowHandle()); 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 JUCE_MSVC if (SystemStats::getOperatingSystemType() >= SystemStats::WinVista && customComponent == nullptr) { return openDialogVistaAndUp (async); } #endif return openDialogPreVista (async); } void run() override { results = [&] { struct ScopedCoInitialize { // IUnknown_GetWindow will only succeed when instantiated in a single-thread apartment ScopedCoInitialize() { ignoreUnused (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 { auto extension = filename.fromLastOccurrenceOf (".", false, false); if (extension.isEmpty()) { auto tokens = StringArray::fromTokens (filtersString, ";,", "\"'"); tokens.trim(); tokens.removeEmptyStrings(); if (tokens.size() == 1 && tokens[0].removeCharacters ("*.").isNotEmpty()) extension = tokens[0].fromFirstOccurrenceOf (".", false, false); } return extension; } //============================================================================== 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 : 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 (juce_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; } 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