mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
908 lines
30 KiB
C++
908 lines
30 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
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<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 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<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 final : 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 = addComSmartPtrOwner (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> 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<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> openDialogWithPreview (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 (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<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 final : 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
|