🛠️🐜 Antkeeper superbuild with dependencies included https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

619 lines
17 KiB

/*
Native File Dialog
http://www.frogtoss.com/labs
*/
/* only locally define UNICODE in this compilation unit */
#ifndef UNICODE
#define UNICODE
#endif
#include <wchar.h>
#include <stdio.h>
#include <assert.h>
#include <atlbase.h>
#include <windows.h>
#include <ShObjIdl.h>
#include "nfd_common.h"
// allocs the space in outPath -- call free()
static void CopyWCharToNFDChar( const wchar_t *inStr, nfdchar_t **outStr )
{
int inStrCharacterCount = static_cast<int>(wcslen(inStr));
int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0,
inStr, inStrCharacterCount,
NULL, 0, NULL, NULL );
assert( bytesNeeded );
bytesNeeded += 1;
*outStr = (nfdchar_t*)NFDi_Malloc( bytesNeeded );
if ( !*outStr )
return;
int bytesWritten = WideCharToMultiByte( CP_UTF8, 0,
inStr, -1,
*outStr, bytesNeeded,
NULL, NULL );
assert( bytesWritten ); _NFD_UNUSED( bytesWritten );
}
/* includes NULL terminator byte in return */
static size_t GetUTF8ByteCountForWChar( const wchar_t *str )
{
int bytesNeeded = WideCharToMultiByte( CP_UTF8, 0,
str, -1,
NULL, 0, NULL, NULL );
assert( bytesNeeded );
return bytesNeeded+1;
}
// write to outPtr -- no free() necessary. No memory stomp tests are done -- they must be done
// before entering this function.
static int CopyWCharToExistingNFDCharBuffer( const wchar_t *inStr, nfdchar_t *outPtr )
{
int inStrCharacterCount = static_cast<int>(wcslen(inStr));
int bytesNeeded = static_cast<int>(GetUTF8ByteCountForWChar( inStr ));
/* invocation copies null term */
int bytesWritten = WideCharToMultiByte( CP_UTF8, 0,
inStr, -1,
outPtr, bytesNeeded,
NULL, 0 );
assert( bytesWritten );
return bytesWritten;
}
// allocs the space in outStr -- call free()
static void CopyNFDCharToWChar( const nfdchar_t *inStr, wchar_t **outStr )
{
int inStrByteCount = static_cast<int>(strlen(inStr));
int charsNeeded = MultiByteToWideChar(CP_UTF8, 0,
inStr, inStrByteCount,
NULL, 0 );
assert( charsNeeded );
assert( !*outStr );
charsNeeded += 1; // terminator
*outStr = (wchar_t*)NFDi_Malloc( charsNeeded * sizeof(wchar_t) );
if ( !*outStr )
return;
int ret = MultiByteToWideChar(CP_UTF8, 0,
inStr, inStrByteCount,
*outStr, charsNeeded);
(*outStr)[charsNeeded-1] = '\0';
#ifdef _DEBUG
int inStrCharacterCount = static_cast<int>(NFDi_UTF8_Strlen(inStr));
assert( ret == inStrCharacterCount );
#else
_NFD_UNUSED(ret);
#endif
}
/* ext is in format "jpg", no wildcards or separators */
static int AppendExtensionToSpecBuf( const char *ext, char *specBuf, size_t specBufLen )
{
const char SEP[] = ";";
assert( specBufLen > strlen(ext)+3 );
if ( strlen(specBuf) > 0 )
{
strncat( specBuf, SEP, specBufLen - strlen(specBuf) - 1 );
specBufLen += strlen(SEP);
}
char extWildcard[NFD_MAX_STRLEN];
int bytesWritten = sprintf_s( extWildcard, NFD_MAX_STRLEN, "*.%s", ext );
assert( bytesWritten == strlen(ext)+2 );
strncat( specBuf, extWildcard, specBufLen - strlen(specBuf) - 1 );
return NFD_OKAY;
}
static nfdresult_t AddFiltersToDialog( ::IFileDialog *fileOpenDialog, const char *filterList )
{
const wchar_t EMPTY_WSTR[] = L"";
const wchar_t WILDCARD[] = L"*.*";
if ( !filterList || strlen(filterList) == 0 )
return NFD_OKAY;
// Count rows to alloc
UINT filterCount = 1; /* guaranteed to have one filter on a correct, non-empty parse */
const char *p_filterList;
for ( p_filterList = filterList; *p_filterList; ++p_filterList )
{
if ( *p_filterList == ';' )
++filterCount;
}
assert(filterCount);
if ( !filterCount )
{
NFDi_SetError("Error parsing filters.");
return NFD_ERROR;
}
/* filterCount plus 1 because we hardcode the *.* wildcard after the while loop */
COMDLG_FILTERSPEC *specList = (COMDLG_FILTERSPEC*)NFDi_Malloc( sizeof(COMDLG_FILTERSPEC) * (filterCount + 1) );
if ( !specList )
{
return NFD_ERROR;
}
for (size_t i = 0; i < filterCount+1; ++i )
{
specList[i].pszName = NULL;
specList[i].pszSpec = NULL;
}
size_t specIdx = 0;
p_filterList = filterList;
char typebuf[NFD_MAX_STRLEN] = {0}; /* one per comma or semicolon */
char *p_typebuf = typebuf;
char filterName[NFD_MAX_STRLEN] = {0};
char specbuf[NFD_MAX_STRLEN] = {0}; /* one per semicolon */
while ( 1 )
{
if ( NFDi_IsFilterSegmentChar(*p_filterList) )
{
/* append a type to the specbuf (pending filter) */
AppendExtensionToSpecBuf( typebuf, specbuf, NFD_MAX_STRLEN );
p_typebuf = typebuf;
memset( typebuf, 0, sizeof(char)*NFD_MAX_STRLEN );
}
if ( *p_filterList == ';' || *p_filterList == '\0' )
{
/* end of filter -- add it to specList */
// Empty filter name -- Windows describes them by extension.
specList[specIdx].pszName = EMPTY_WSTR;
CopyNFDCharToWChar( specbuf, (wchar_t**)&specList[specIdx].pszSpec );
memset( specbuf, 0, sizeof(char)*NFD_MAX_STRLEN );
++specIdx;
if ( specIdx == filterCount )
break;
}
if ( !NFDi_IsFilterSegmentChar( *p_filterList ))
{
*p_typebuf = *p_filterList;
++p_typebuf;
}
++p_filterList;
}
/* Add wildcard */
specList[specIdx].pszSpec = WILDCARD;
specList[specIdx].pszName = EMPTY_WSTR;
fileOpenDialog->SetFileTypes( filterCount+1, specList );
/* free speclist */
for ( size_t i = 0; i < filterCount; ++i )
{
NFDi_Free( (void*)specList[i].pszSpec );
}
NFDi_Free( specList );
return NFD_OKAY;
}
static nfdresult_t AllocPathSet( IShellItemArray *shellItems, nfdpathset_t *pathSet )
{
const char ERRORMSG[] = "Error allocating pathset.";
assert(shellItems);
assert(pathSet);
// How many items in shellItems?
DWORD numShellItems;
HRESULT result = shellItems->GetCount(&numShellItems);
if ( !SUCCEEDED(result) )
{
NFDi_SetError(ERRORMSG);
return NFD_ERROR;
}
pathSet->count = static_cast<size_t>(numShellItems);
assert( pathSet->count > 0 );
pathSet->indices = (size_t*)NFDi_Malloc( sizeof(size_t)*pathSet->count );
if ( !pathSet->indices )
{
return NFD_ERROR;
}
/* count the total bytes needed for buf */
size_t bufSize = 0;
for ( DWORD i = 0; i < numShellItems; ++i )
{
::IShellItem *shellItem;
result = shellItems->GetItemAt(i, &shellItem);
if ( !SUCCEEDED(result) )
{
NFDi_SetError(ERRORMSG);
return NFD_ERROR;
}
// Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it.
SFGAOF attribs;
result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs );
if ( !SUCCEEDED(result) )
{
NFDi_SetError(ERRORMSG);
return NFD_ERROR;
}
if ( !(attribs & SFGAO_FILESYSTEM) )
continue;
LPWSTR name;
shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name);
// Calculate length of name with UTF-8 encoding
bufSize += GetUTF8ByteCountForWChar( name );
}
assert(bufSize);
pathSet->buf = (nfdchar_t*)NFDi_Malloc( sizeof(nfdchar_t) * bufSize );
memset( pathSet->buf, 0, sizeof(nfdchar_t) * bufSize );
/* fill buf */
nfdchar_t *p_buf = pathSet->buf;
for (DWORD i = 0; i < numShellItems; ++i )
{
::IShellItem *shellItem;
result = shellItems->GetItemAt(i, &shellItem);
if ( !SUCCEEDED(result) )
{
NFDi_SetError(ERRORMSG);
return NFD_ERROR;
}
// Confirm SFGAO_FILESYSTEM is true for this shellitem, or ignore it.
SFGAOF attribs;
result = shellItem->GetAttributes( SFGAO_FILESYSTEM, &attribs );
if ( !SUCCEEDED(result) )
{
NFDi_SetError(ERRORMSG);
return NFD_ERROR;
}
if ( !(attribs & SFGAO_FILESYSTEM) )
continue;
LPWSTR name;
shellItem->GetDisplayName(SIGDN_FILESYSPATH, &name);
int bytesWritten = CopyWCharToExistingNFDCharBuffer(name, p_buf);
ptrdiff_t index = p_buf - pathSet->buf;
assert( index >= 0 );
pathSet->indices[i] = static_cast<size_t>(index);
p_buf += bytesWritten;
}
return NFD_OKAY;
}
static nfdresult_t SetDefaultPath( IFileDialog *dialog, const char *defaultPath )
{
if ( !defaultPath || strlen(defaultPath) == 0 )
return NFD_OKAY;
wchar_t *defaultPathW = {0};
CopyNFDCharToWChar( defaultPath, &defaultPathW );
IShellItem *folder;
HRESULT result = SHCreateItemFromParsingName( defaultPathW, NULL, IID_PPV_ARGS(&folder) );
// Valid non results.
if ( result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || result == HRESULT_FROM_WIN32(ERROR_INVALID_DRIVE) )
{
NFDi_Free( defaultPathW );
return NFD_OKAY;
}
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Error creating ShellItem");
NFDi_Free( defaultPathW );
return NFD_ERROR;
}
// Could also call SetDefaultFolder(), but this guarantees defaultPath -- more consistency across API.
dialog->SetFolder( folder );
NFDi_Free( defaultPathW );
folder->Release();
return NFD_OKAY;
}
/* public */
nfdresult_t NFD_OpenDialog( const char *filterList,
const nfdchar_t *defaultPath,
nfdchar_t **outPath )
{
nfdresult_t nfdResult = NFD_ERROR;
// Init COM library.
HRESULT result = ::CoInitializeEx(NULL,
::COINIT_APARTMENTTHREADED |
::COINIT_DISABLE_OLE1DDE );
if ( !SUCCEEDED(result))
{
NFDi_SetError("Could not initialize COM.");
goto end;
}
::IFileOpenDialog *fileOpenDialog(NULL);
// Create dialog
result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL,
CLSCTX_ALL, ::IID_IFileOpenDialog,
reinterpret_cast<void**>(&fileOpenDialog) );
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not create dialog.");
goto end;
}
// Build the filter list
if ( !AddFiltersToDialog( fileOpenDialog, filterList ) )
{
goto end;
}
// Set the default path
if ( !SetDefaultPath( fileOpenDialog, defaultPath ) )
{
goto end;
}
// Show the dialog.
result = fileOpenDialog->Show(NULL);
if ( SUCCEEDED(result) )
{
// Get the file name
::IShellItem *shellItem(NULL);
result = fileOpenDialog->GetResult(&shellItem);
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not get shell item from dialog.");
goto end;
}
wchar_t *filePath(NULL);
result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath);
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not get file path for selected.");
goto end;
}
CopyWCharToNFDChar( filePath, outPath );
CoTaskMemFree(filePath);
if ( !*outPath )
{
/* error is malloc-based, error message would be redundant */
goto end;
}
nfdResult = NFD_OKAY;
shellItem->Release();
}
else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
{
nfdResult = NFD_CANCEL;
}
else
{
NFDi_SetError("File dialog box show failed.");
nfdResult = NFD_ERROR;
}
end:
::CoUninitialize();
return nfdResult;
}
nfdresult_t NFD_OpenDialogMultiple( const nfdchar_t *filterList,
const nfdchar_t *defaultPath,
nfdpathset_t *outPaths )
{
nfdresult_t nfdResult = NFD_ERROR;
// Init COM library.
HRESULT result = ::CoInitializeEx(NULL,
::COINIT_APARTMENTTHREADED |
::COINIT_DISABLE_OLE1DDE );
if ( !SUCCEEDED(result))
{
NFDi_SetError("Could not initialize COM.");
return NFD_ERROR;
}
::IFileOpenDialog *fileOpenDialog(NULL);
// Create dialog
result = ::CoCreateInstance(::CLSID_FileOpenDialog, NULL,
CLSCTX_ALL, ::IID_IFileOpenDialog,
reinterpret_cast<void**>(&fileOpenDialog) );
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not create dialog.");
goto end;
}
// Build the filter list
if ( !AddFiltersToDialog( fileOpenDialog, filterList ) )
{
goto end;
}
// Set the default path
if ( !SetDefaultPath( fileOpenDialog, defaultPath ) )
{
goto end;
}
// Set a flag for multiple options
DWORD dwFlags;
result = fileOpenDialog->GetOptions(&dwFlags);
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not get options.");
goto end;
}
result = fileOpenDialog->SetOptions(dwFlags | FOS_ALLOWMULTISELECT);
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not set options.");
goto end;
}
// Show the dialog.
result = fileOpenDialog->Show(NULL);
if ( SUCCEEDED(result) )
{
IShellItemArray *shellItems;
result = fileOpenDialog->GetResults( &shellItems );
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not get shell items.");
goto end;
}
if ( AllocPathSet( shellItems, outPaths ) == NFD_ERROR )
{
goto end;
}
shellItems->Release();
nfdResult = NFD_OKAY;
}
else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
{
nfdResult = NFD_CANCEL;
}
else
{
NFDi_SetError("File dialog box show failed.");
nfdResult = NFD_ERROR;
}
end:
::CoUninitialize();
return nfdResult;
}
nfdresult_t NFD_SaveDialog( const nfdchar_t *filterList,
const nfdchar_t *defaultPath,
nfdchar_t **outPath )
{
nfdresult_t nfdResult = NFD_ERROR;
// Init COM library.
HRESULT result = ::CoInitializeEx(NULL,
::COINIT_APARTMENTTHREADED |
::COINIT_DISABLE_OLE1DDE );
if ( !SUCCEEDED(result))
{
NFDi_SetError("Could not initialize COM.");
return NFD_ERROR;
}
::IFileSaveDialog *fileSaveDialog(NULL);
// Create dialog
result = ::CoCreateInstance(::CLSID_FileSaveDialog, NULL,
CLSCTX_ALL, ::IID_IFileSaveDialog,
reinterpret_cast<void**>(&fileSaveDialog) );
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not create dialog.");
goto end;
}
// Build the filter list
if ( !AddFiltersToDialog( fileSaveDialog, filterList ) )
{
goto end;
}
// Set the default path
if ( !SetDefaultPath( fileSaveDialog, defaultPath ) )
{
goto end;
}
// Show the dialog.
result = fileSaveDialog->Show(NULL);
if ( SUCCEEDED(result) )
{
// Get the file name
::IShellItem *shellItem;
result = fileSaveDialog->GetResult(&shellItem);
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not get shell item from dialog.");
goto end;
}
wchar_t *filePath(NULL);
result = shellItem->GetDisplayName(::SIGDN_FILESYSPATH, &filePath);
if ( !SUCCEEDED(result) )
{
NFDi_SetError("Could not get file path for selected.");
goto end;
}
CopyWCharToNFDChar( filePath, outPath );
CoTaskMemFree(filePath);
if ( !*outPath )
{
/* error is malloc-based, error message would be redundant */
goto end;
}
nfdResult = NFD_OKAY;
shellItem->Release();
}
else if (result == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
{
nfdResult = NFD_CANCEL;
}
else
{
NFDi_SetError("File dialog box show failed.");
nfdResult = NFD_ERROR;
}
end:
::CoUninitialize();
return nfdResult;
}