mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
Previously, the top-level modal dismiss broadcaster would attempt to dismiss all modals when the native file chooser was brought to the front or clicked. This would end up calling Component::inputAttemptWhenModal, which would bring the FileChooser's Component to the front, interrupting the mouse click. The fix is to avoid bringing the plugin window to the front when the FileChooser is in a modal state. As the chooser is a native window, the system should take care of enforcing the expected modal behaviour. It's not clear why this issue only affects Cantabile. It seems that in Cantabile, events from the native FileChooser get sent to the plugin editor's parent HWND, whereas this doesn't happen in other hosts. Perhaps Cantabile is hooking/intercepting window messages in some way.
902 lines
30 KiB
C++
902 lines
30 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library.
|
|
Copyright (c) 2022 - Raw Material Software Limited
|
|
|
|
JUCE is an open source library subject to commercial or open-source
|
|
licensing.
|
|
|
|
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
|
Agreement and JUCE Privacy Policy.
|
|
|
|
End User License Agreement: www.juce.com/juce-7-licence
|
|
Privacy Policy: www.juce.com/juce-privacy-policy
|
|
|
|
Or: You may also use this code under the terms of the GPL v3 (see
|
|
www.gnu.org/licenses).
|
|
|
|
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_Messaging_windows.cpp
|
|
namespace detail
|
|
{
|
|
bool dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages);
|
|
} // namespace detail
|
|
|
|
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),
|
|
// 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<size_t> (charsAvailableForResult) + 1);
|
|
|
|
if (startingFile.isDirectory() || startingFile.isRoot())
|
|
{
|
|
initialPath = startingFile.getFullPathName();
|
|
}
|
|
else
|
|
{
|
|
startingFile.getFileName().copyToUTF16 (files,
|
|
static_cast<size_t> (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<URL> 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<Component> owner;
|
|
String title, filtersString;
|
|
std::unique_ptr<CustomComponentHolder> customComponent;
|
|
String initialPath, returnedString;
|
|
|
|
CriticalSection deletingDialog;
|
|
|
|
bool selectsDirectories, isSave, warnAboutOverwrite, selectMultiple;
|
|
|
|
HeapBlock<WCHAR> files;
|
|
HeapBlock<WCHAR> filters;
|
|
|
|
Atomic<HWND> 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<WCHAR, FreeLPWSTR> desktopPath (ptr);
|
|
|
|
if (FAILED (result))
|
|
return false;
|
|
|
|
if (FAILED (SHParseDisplayName (desktopPath.get(), nullptr, &pidl, SFGAO_FOLDER, nullptr)))
|
|
return false;
|
|
}
|
|
|
|
const auto item = [&]
|
|
{
|
|
ComSmartPtr<IShellItem> 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<IFileDialogEvents>
|
|
{
|
|
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<IFileDialog> { d }.getInterface<IOleWindow>())
|
|
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<URL> openDialogVistaAndUp()
|
|
{
|
|
const auto getUrl = [] (IShellItem& item)
|
|
{
|
|
LPWSTR ptr = nullptr;
|
|
|
|
if (item.GetDisplayName (SIGDN_FILESYSPATH, &ptr) != S_OK)
|
|
return URL();
|
|
|
|
const auto path = std::unique_ptr<WCHAR, FreeLPWSTR> { ptr };
|
|
return URL (File (String (path.get())));
|
|
};
|
|
|
|
if (isSave)
|
|
{
|
|
const auto dialog = [&]
|
|
{
|
|
ComSmartPtr<IFileDialog> ptr;
|
|
ptr.CoCreateInstance (CLSID_FileSaveDialog, CLSCTX_INPROC_SERVER);
|
|
return ptr;
|
|
}();
|
|
|
|
if (dialog == nullptr)
|
|
return {};
|
|
|
|
showDialog (*dialog);
|
|
|
|
const auto item = [&]
|
|
{
|
|
ComSmartPtr<IShellItem> 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<IFileOpenDialog> ptr;
|
|
ptr.CoCreateInstance (CLSID_FileOpenDialog, CLSCTX_INPROC_SERVER);
|
|
return ptr;
|
|
}();
|
|
|
|
if (dialog == nullptr)
|
|
return {};
|
|
|
|
showDialog (*dialog);
|
|
|
|
const auto items = [&]
|
|
{
|
|
ComSmartPtr<IShellItemArray> ptr;
|
|
dialog->GetResults (ptr.resetAndGetPointerAddress());
|
|
return ptr;
|
|
}();
|
|
|
|
if (items == nullptr)
|
|
return {};
|
|
|
|
Array<URL> result;
|
|
|
|
DWORD numItems = 0;
|
|
items->GetCount (&numItems);
|
|
|
|
for (DWORD i = 0; i < numItems; ++i)
|
|
{
|
|
ComSmartPtr<IShellItem> scope;
|
|
items->GetItemAt (i, scope.resetAndGetPointerAddress());
|
|
|
|
if (scope != nullptr)
|
|
{
|
|
const auto url = getUrl (*scope);
|
|
|
|
if (! url.isEmpty())
|
|
result.add (url);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Array<URL> openDialogPreVista (bool async)
|
|
{
|
|
Array<URL> 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<URL> openDialog (bool async)
|
|
{
|
|
struct Remover
|
|
{
|
|
explicit Remover (Win32NativeFileChooser& chooser) : item (chooser) {}
|
|
~Remover() { getNativeDialogList().removeValue (&item); }
|
|
|
|
Win32NativeFileChooser& item;
|
|
};
|
|
|
|
const Remover remover (*this);
|
|
|
|
if (SystemStats::getOperatingSystemType() >= SystemStats::WinVista
|
|
&& customComponent == nullptr)
|
|
{
|
|
return openDialogVistaAndUp();
|
|
}
|
|
|
|
return openDialogPreVista (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<HWND, Win32NativeFileChooser*>& getNativeDialogList()
|
|
{
|
|
static HashMap<HWND, Win32NativeFileChooser*> 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<Component> safeCustomComponent (customComponent.get());
|
|
|
|
RECT dialogScreenRect, dialogClientRect;
|
|
GetWindowRect (hdlg, &dialogScreenRect);
|
|
GetClientRect (hdlg, &dialogClientRect);
|
|
|
|
auto screenRectangle = Rectangle<int>::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<int>::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<FilePreviewComponent*> (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<FilePreviewComponent> { 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<Win32NativeFileChooser*> (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<Win32NativeFileChooser*> (((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<LPOFNOTIFY> (lParam);
|
|
|
|
if (ofn->hdr.code == CDN_SELCHANGE)
|
|
if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (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<Native>,
|
|
public Component,
|
|
public FileChooser::Pimpl
|
|
{
|
|
public:
|
|
Native (FileChooser& fileChooser, int flagsIn, FilePreviewComponent* previewComp)
|
|
: owner (fileChooser),
|
|
nativeFileChooser (std::make_unique<Win32NativeFileChooser> (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<Native> 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<FilePreviewComponent>() != nullptr;
|
|
}
|
|
|
|
void inputAttemptWhenModal() override {}
|
|
|
|
private:
|
|
FileChooser& owner;
|
|
std::shared_ptr<Win32NativeFileChooser> nativeFileChooser;
|
|
};
|
|
|
|
//==============================================================================
|
|
bool FileChooser::isPlatformDialogAvailable()
|
|
{
|
|
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
|
|
return false;
|
|
#else
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
|
|
FilePreviewComponent* preview)
|
|
{
|
|
return std::make_shared<FileChooser::Native> (owner, flags, preview);
|
|
}
|
|
|
|
} // namespace juce
|