File selection dialog update, part 1

This adds a replacement for the SelectFilesDialog class.  It has
been updated to use Explorer-style dialogs, which are a bit nicer
than the old-style dialogs.  Hopefully this will eliminate some of the
brain damage, like the disappearing Accept button.

This change only updates MDC.  A second change will update the main
app and remove the old code.

Also, updated the MDC version to 3.0.0, and changed the web site
linked in the Help menu from faddensoft.com to a2ciderpress.com.
This commit is contained in:
Andy McFadden 2014-12-01 22:07:39 -08:00
parent c78017b1d2
commit b2cd857031
14 changed files with 606 additions and 197 deletions

View File

@ -1161,9 +1161,9 @@ bool DiskArchive::BulkAdd(ActionProgressDialog* pActionProgress,
WCHAR curDir[MAX_PATH] = L"";
bool retVal = false;
LOGI("Opts: '%ls' typePres=%d", (LPCWSTR) pAddOpts->fStoragePrefix,
LOGD("Opts: '%ls' typePres=%d", (LPCWSTR) pAddOpts->fStoragePrefix,
pAddOpts->fTypePreservation);
LOGI(" sub=%d strip=%d ovwr=%d",
LOGD(" sub=%d strip=%d ovwr=%d",
pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames,
pAddOpts->fOverwriteExisting);

View File

@ -1,22 +0,0 @@
/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Support for the "choose files" dialog.
*/
#include "stdafx.h"
#include "ChooseFilesDlg.h"
void ChooseFilesDlg::ShiftControls(int deltaX, int deltaY)
{
/*
* These only need to be here so that the initial move puts them
* where they belong. Once the dialog has been created, the
* CFileDialog will move things where they need to go.
*/
MoveControl(this, IDC_CHOOSEFILES_STATIC1, 0, deltaY, false);
SelectFilesDialog::ShiftControls(deltaX, deltaY);
}

View File

@ -1,38 +0,0 @@
/*
* CiderPress
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Choose files and directories.
*/
#ifndef MDC_CHOOSEFILESDLG_H
#define MDC_CHOOSEFILESDLG_H
#include "../util/UtilLib.h"
#include "resource.h"
class ChooseFilesDlg : public SelectFilesDialog {
public:
ChooseFilesDlg(CWnd* pParentWnd = NULL) :
SelectFilesDialog(L"IDD_CHOOSE_FILES", pParentWnd)
{
SetWindowTitle(L"Choose Files...");
fAcceptButtonID = IDC_SELECT_ACCEPT;
}
virtual ~ChooseFilesDlg(void) {}
private:
/*
* Override base class version so we can move our stuff around.
*
* It's important that the base class be called last, because it calls
* Invalidate to redraw the dialog.
*/
virtual void ShiftControls(int deltaX, int deltaY) override;
//DECLARE_MESSAGE_MAP()
};
#endif /*MDC_CHOOSEFILESDLG_H*/

View File

@ -10,13 +10,12 @@
#include "Main.h"
#include "mdc.h"
#include "AboutDlg.h"
#include "ChooseFilesDlg.h"
#include "ProgressDlg.h"
#include "resource.h"
#include "../diskimg/DiskImg.h"
#include "../zlib/zlib.h"
const WCHAR* kWebSiteURL = L"http://www.faddensoft.com/";
const WCHAR* kWebSiteURL = L"http://www.a2ciderpress.com/";
BEGIN_MESSAGE_MAP(MainWindow, CFrameWnd)
@ -59,10 +58,6 @@ MainWindow::MainWindow()
MainWindow::~MainWindow()
{
// int cc;
// cc = ::WinHelp(m_hWnd, ::AfxGetApp()->m_pszHelpFilePath, HELP_QUIT, 0);
// LOGI("Turning off WinHelp returned %d", cc);
DiskImgLib::Global::AppCleanup();
}
@ -74,7 +69,7 @@ void MainWindow::OnFileExit(void)
void MainWindow::OnHelpWebSite(void)
{
// Go to the faddenSoft web site.
// Go to the CiderPress web site.
int err;
err = (int) ::ShellExecute(m_hWnd, L"open", kWebSiteURL, NULL, NULL,
@ -96,7 +91,6 @@ void MainWindow::OnHelpAbout(void)
LOGI("HelpAbout returned %d", result);
}
void MainWindow::OnFileScan(void)
{
if (0) {
@ -132,8 +126,8 @@ BOOL MainWindow::PeekAndPump(void)
* ==========================================================================
*/
/*static*/ void
MainWindow::DebugMsgHandler(const char* file, int line, const char* msg)
/*static*/ void MainWindow::DebugMsgHandler(const char* file, int line,
const char* msg)
{
ASSERT(file != NULL);
ASSERT(msg != NULL);
@ -141,7 +135,6 @@ MainWindow::DebugMsgHandler(const char* file, int line, const char* msg)
LOG_BASE(DebugLog::LOG_INFO, file, line, "<diskimg> %hs", msg);
}
/*static*/ NuResult MainWindow::NufxErrorMsgHandler(NuArchive* /*pArchive*/,
void* vErrorMessage)
{
@ -154,38 +147,37 @@ MainWindow::DebugMsgHandler(const char* file, int line, const char* msg)
return kNuOK;
}
const int kLocalFssep = '\\';
typedef struct ScanOpts {
struct ScanOpts {
FILE* outfp;
ProgressDlg* pProgress;
} ScanOpts;
};
void MainWindow::ScanFiles(void)
{
ChooseFilesDlg chooseFiles;
ScanOpts scanOpts;
WCHAR curDir[MAX_PATH] = L"";
CString errMsg;
CString outPath;
CString errMsg, newDir;
bool doResetDir = false;
ScanOpts scanOpts;
memset(&scanOpts, 0, sizeof(scanOpts));
/* choose input files */
chooseFiles.DoModal();
if (chooseFiles.GetExitStatus() != IDOK) {
// choose input files
SelectFilesDialog2 chooseFiles(L"IDD_CHOOSE_FILES", this);
chooseFiles.SetWindowTitle(L"Choose Files...");
INT_PTR retval = chooseFiles.DoModal();
if (retval != IDOK) {
return;
}
const WCHAR* buf = chooseFiles.GetFileNames();
LOGI("Selected path = '%ls' (offset=%d)", buf,
chooseFiles.GetFileNameOffset());
/* choose output file */
// choose output file; use an Explorer-style dialog for consistency
CString outPath;
CFileDialog dlg(FALSE, L"txt", NULL,
OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN,
L"Text Files (*.txt)|*.txt|All Files (*.*)|*.*||", this);
L"Text Files (*.txt)|*.txt|All Files (*.*)|*.*||", this,
0, FALSE /*disable Vista style*/);
dlg.m_ofn.lpstrTitle = L"Save Output As...";
wcscpy(dlg.m_ofn.lpstrFile, L"mdc-out.txt");
@ -209,9 +201,9 @@ void MainWindow::ScanFiles(void)
kAppMajorVersion, kAppMinorVersion, kAppBugVersion,
major, minor, bug);
fprintf(scanOpts.outfp,
"Copyright (C) 2006 by faddenSoft, LLC. All rights reserved.\n");
"Copyright (C) 2014 by faddenSoft, LLC. All rights reserved.\n");
fprintf(scanOpts.outfp,
"MDC is part of CiderPress, available from http://www.faddensoft.com/.\n");
"MDC is part of CiderPress, available from http://www.a2ciderpress.com/.\n");
NuGetVersion(&major, &minor, &bug, NULL, NULL);
fprintf(scanOpts.outfp,
"Linked against NufxLib v%ld.%ld.%ld and zlib v%hs\n",
@ -224,8 +216,10 @@ void MainWindow::ScanFiles(void)
ShowFailureMsg(this, errMsg, IDS_FAILED);
goto bail;
}
if (SetCurrentDirectory(buf) == false) {
errMsg.Format(L"Unable to set current directory to '%ls'.", buf);
newDir = chooseFiles.GetDirectory();
if (SetCurrentDirectory(newDir) == false) {
errMsg.Format(L"Unable to change current directory to '%ls'.",
(LPCWSTR) newDir);
ShowFailureMsg(this, errMsg, IDS_FAILED);
goto bail;
}
@ -234,7 +228,7 @@ void MainWindow::ScanFiles(void)
time_t now;
now = time(NULL);
fprintf(scanOpts.outfp,
"Run started at %.24hs in '%ls'\n\n", ctime(&now), buf);
"Run started at %.24hs in '%ls'\n\n", ctime(&now), newDir);
/* obstruct input to the main window */
EnableWindow(FALSE);
@ -257,19 +251,18 @@ void MainWindow::ScanFiles(void)
start = time(NULL);
/* start cranking */
buf += chooseFiles.GetFileNameOffset();
while (*buf != '\0') {
if (Process(buf, &scanOpts, &errMsg) != 0) {
LOGI("Skipping '%ls': %ls.", buf, (LPCWSTR) errMsg);
const CStringArray& arr = chooseFiles.GetFileNames();
for (int i = 0; i < arr.GetCount(); i++) {
const CString& name = arr.GetAt(i);
if (Process(name, &scanOpts, &errMsg) != 0) {
LOGI("Skipping '%ls': %ls.", (LPCWSTR) name, (LPCWSTR) errMsg);
}
if (fCancelFlag) {
LOGI("CANCELLED by user");
MessageBox(L"Cancelled!", L"MDC", MB_OK);
LOGI("Canceled by user");
MessageBox(L"Canceled!", L"MDC", MB_OK);
goto bail;
}
buf += wcslen(buf)+1;
}
end = time(NULL);
fprintf(scanOpts.outfp, "\nScan completed in %ld seconds.\n",
@ -278,11 +271,10 @@ void MainWindow::ScanFiles(void)
{
SetWindowText(L"MDC Done!");
CString doneMsg;
CString doneMsg = L"Processing completed.";
CString appName;
appName.LoadString(IDS_APP_TITLE);
doneMsg.Format(L"Processing completed.");
scanOpts.pProgress->MessageBox(doneMsg, appName, MB_OK|MB_ICONINFORMATION);
}

View File

@ -72,7 +72,8 @@ private:
void* vErrorMessage);
/*
* Scan a set of files.
* Prompts the user to select the input set and output file, then starts
* the scan.
*/
void ScanFiles(void);

View File

@ -43,7 +43,7 @@ MyApp::MyApp(LPCTSTR lpszAppName) : CWinApp(lpszAppName)
*/
MyApp::~MyApp(void)
{
LOGI("MDC SHUTTING DOWN");
LOGI("MDC shutting down");
delete gDebugLog;
}

View File

@ -13,8 +13,8 @@
#include "resource.h"
/* MDC version numbers */
#define kAppMajorVersion 2
#define kAppMinorVersion 2
#define kAppMajorVersion 3
#define kAppMinorVersion 0
#define kAppBugVersion 0
/*

View File

@ -45,7 +45,7 @@ BEGIN
END
POPUP "&Help"
BEGIN
MENUITEM "Visit faddenSoft &web site", IDM_HELP_WEBSITE
MENUITEM "Visit CiderPress &web site", IDM_HELP_WEBSITE
MENUITEM "&About ...", IDM_HELP_ABOUT
END
END
@ -81,14 +81,12 @@ BEGIN
LTEXT "Multi-Disk Catalog",IDC_STATIC,33,15,55,8
END
IDD_CHOOSE_FILES DIALOGEX 0, 0, 272, 131
STYLE DS_SETFONT | DS_3DLOOK | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS
IDD_CHOOSE_FILES DIALOGEX 0, 0, 270, 112
STYLE DS_SETFONT | DS_3DLOOK | DS_CONTROL | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN
FONT 8, "MS Sans Serif", 0, 0, 0x1
BEGIN
LTEXT "",1119,0,0,272,87,NOT WS_GROUP,WS_EX_STATICEDGE
PUSHBUTTON "&Accept",IDC_SELECT_ACCEPT,220,88,50,14
PUSHBUTTON "Cancel",IDCANCEL,220,107,50,14
LTEXT "Select the disk images to scan. If you select a folder, all files in that folder will be processed.",IDC_CHOOSEFILES_STATIC1,7,104,199,17
LTEXT "",1119,0,0,270,87,NOT WS_GROUP
LTEXT "Select the disk images to scan. If you select a folder, all files in that folder will be processed. Use Ctrl-click and Shift-click to select multiple items.",IDC_CHOOSEFILES_STATIC1,7,87,263,17
END
IDD_PROGRESS DIALOG 0, 0, 250, 66
@ -151,7 +149,8 @@ BEGIN
"IDD_CHOOSE_FILES", DIALOG
BEGIN
VERTGUIDE, 7
BOTTOMMARGIN, 121
BOTTOMMARGIN, 104
HORZGUIDE, 87
END
IDD_PROGRESS, DIALOG

View File

@ -150,7 +150,6 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="AboutDlg.h" />
<ClInclude Include="ChooseFilesDlg.h" />
<ClInclude Include="Main.h" />
<ClInclude Include="mdc.h" />
<ClInclude Include="ProgressDlg.h" />
@ -183,7 +182,6 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="AboutDlg.cpp" />
<ClCompile Include="ChooseFilesDlg.cpp" />
<ClCompile Include="Main.cpp" />
<ClCompile Include="mdc.cpp" />
<ClCompile Include="ProgressDlg.cpp" />

View File

@ -23,9 +23,6 @@
<ClInclude Include="AboutDlg.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ChooseFilesDlg.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Main.h">
<Filter>Header Files</Filter>
</ClInclude>
@ -51,9 +48,6 @@
<ClCompile Include="AboutDlg.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ChooseFilesDlg.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Main.cpp">
<Filter>Source Files</Filter>
</ClCompile>

View File

@ -65,6 +65,13 @@ void DebugLog::Log(LogSeverity severity, const char* file, int line,
if (severity < 0 || severity > sizeof(kSeverityChars) - 1) {
severity = LOG_UNKNOWN;
}
if (severity == LOG_VERBOSE) {
// Globally disable. They still get compiled, which helps to
// prevent bit-rot. TODO: be fancier and have LOGV map to
// a do-nothing inline function that the compiler will effectively
// eliminate.
return;
}
va_list argptr;
char textBuf[4096];

View File

@ -64,15 +64,17 @@ extern DebugLog* gDebugLog; // declare and allocate in app
* Log macros, with priority specifier. The output will be written to the
* log file, if one is open, and to the debugger output window, if available.
*
* The verbose-level debugging should be enabled on a file-by-file basis,
* but that doesn't seem to work (pre-compiled header interference, maybe?).
* The verbose-level debugging should be enabled on a file-by-file basis
* with a compile-time define, but that doesn't seem to work (pre-compiled
* header interference, maybe?).
*/
#ifdef SHOW_LOGV
//#ifdef SHOW_LOGV
# define LOGV(format, ...) \
LOG_BASE(DebugLog::LOG_VERBOSE, __FILE__, __LINE__, format, __VA_ARGS__)
#else
# define LOGV(format, ...) ((void)0)
#endif
//#else
//# define LOGV(format, ...) ((void)0)
//#endif
#define LOGD(format, ...) \
LOG_BASE(DebugLog::LOG_DEBUG, __FILE__, __LINE__, format, __VA_ARGS__)
#define LOGI(format, ...) \

View File

@ -3,49 +3,6 @@
* Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved.
* See the file LICENSE for distribution terms.
*/
/*
* Support for file selection dialog.
*
* What we want to do is get the full set of selected files out of the dialog.
* However, Windows does not provide a way to do that. There are various
* slightly awkward approaches:
*
* - Get the IShellView pointer via the IShellBrowser pointer via the
* undocumented WM_GETISHELLBROWSER message. You can then play with the
* shellview window, select items with SelectItem(), and get an object
* representing the selected items with another call. What form the
* object takes is frustratingly unspecified.
* http://www.codeproject.com/dialog/select_all_button.asp
* - Get the CListView so you can play with its members. This is done by
* GetParent --> GetItem(lst2) --> GetItem(1), which is probably
* somewhat fragile, but seems to work on 98 through XP. Standard LVN
* stuff applies.
* http://www.codeguru.com/mfc/comments/10216.shtml
* - Using a window hook (not an OFN hook), get the class name and strcmp()
* it for the class we're looking for ("syslistview32"). Once we have
* it, proceed as above.
* http://www.codeproject.com/dialog/customize_dialog.asp
*
* Care must be taken with the ListView because it doesn't contain file names,
* but rather display names (potentially without filename extensions). The
* next big assumptive leap is that the ItemData pointer is a shell PIDL that
* can be used directly.
*
* The PIDL stored is, of course, relative to the IShellFolder currently being
* displayed. There's no easy way to get that, but if we just go ahead and
* call SHGetPathFromIDList anyway we get the right file name with the wrong
* path (it appears to be the desktop folder). We can strip the path off and
* prepend the value from the undocumented GetFolderPath() call (which just
* issues a CDM_GETFOLDERPATH message).
*
* To make matters even more interesting, it is necessary to provide a "hook"
* function to prevent double-clicking from closing the dialog while side-
* stepping our additions. Of course, adding an OFN hook renders all of the
* existing message map and initialization stuff inoperable. You can stuff
* things through various "user data" pointers and end up with access to your
* object, and if you cram the hook procedure's hDlg into the object's m_hWnd
* you can treat "this" like a window instead of passing HWNDs around.
*/
#include "stdafx.h"
#include "SelectFilesDialog.h"
#include "PathName.h"
@ -53,6 +10,374 @@
#include <dlgs.h>
void SelectFilesDialog2::OnInitDone()
{
// Tweak the controls
SetControlText(IDOK, L"Accept");
HideControl(stc2); // "Files of type"
HideControl(cmb1); // (file type combo)
// Configure a window proc to intercept events. We need to do it this
// way, rather than using m_ofn.lpfnHook, because the CFileDialog hook
// does not receive messages intended for the standard controls of the
// default dialog box. Since our goal is to intercept the "OK" button,
// we need to set a proc for the whole window.
CWnd* pParent = GetParent();
fPrevWndProc = (WNDPROC) ::SetWindowLongPtr(pParent->m_hWnd, GWL_WNDPROC,
(LONG_PTR) MyWindowProc);
// Stuff a pointer to this object into the userdata field.
::SetWindowLongPtr(pParent->m_hWnd, GWL_USERDATA, (LONG_PTR) this);
}
void SelectFilesDialog2::OnFolderChange()
{
// We get one of these shortly after OnInitDone. We can't do this in
// OnInitDone because the dialog isn't ready yet.
//
// Ideally we'd just call GetFolderPath(), but for some reason that
// returns an empty string for places like Desktop or My Documents
// (which, unlike Computer or Libraries, do have filesystem paths).
// You actually get the path in the string returned from the multi-select
// file dialog, but apparently you have to use a semi-hidden method
// to get at it from OnFolderChange.
//
// In other words, par for the course in Windows file dialogs.
fCurrentDirectory = L"";
CWnd* pParent = GetParent();
LRESULT len = pParent->SendMessage(CDM_GETFOLDERIDLIST, 0, NULL);
if (len <= 0) {
LOGW("Can't get folder ID list, len is %d", len);
} else {
LPCITEMIDLIST pidlFolder = (LPCITEMIDLIST) CoTaskMemAlloc(len);
len = pParent->SendMessage(CDM_GETFOLDERIDLIST, len,
(LPARAM) pidlFolder);
ASSERT(len > 0); // should fail earlier, or not at all
WCHAR buf[MAX_PATH];
BOOL result = SHGetPathFromIDList(pidlFolder, buf);
if (result) {
fCurrentDirectory = buf;
} else {
fCurrentDirectory = L"";
}
CoTaskMemFree((LPVOID) pidlFolder);
}
LOGD("OnFolderChange: '%ls'", (LPCWSTR) fCurrentDirectory);
}
BOOL SelectFilesDialog2::OnFileNameOK()
{
// This function provides "custom validation of filenames that are
// entered into a common file dialog box". We don't need to validate
// filenames here, but we do require it for another reason: if the user
// double-clicks a file, the dialog will accept the name and close
// without our WindowProc getting a WM_COMMAND.
//
// This function doesn't get called if we select a file or files and
// hit the OK button or enter key, because we intercept that before
// the dialog can get to it. It also doesn't get called if you
// double-click directory, as directory traversal is handled internally
// by the dialog.
//
// It's possible to click then shift-double-click to select a range,
// so we can have content in both the list and the text box. This is
// really just another way to hit "OK".
LOGD("OnFileNameOK");
CFileDialog* pDialog = (CFileDialog*) GetParent();
FPResult fpr = OKButtonClicked(pDialog);
switch (fpr) {
case kFPDone:
return 0; // success, let the dialog exit
case kFPPassThru:
LOGW("WEIRD: got OK_PASSTHRU from double click");
return 1;
case kFPNoFiles:
LOGW("WEIRD: got OK_NOFILES from double click");
return 1;
case kFPError:
return 1;
default:
assert(false);
return 1;
}
// not reached
}
/*static*/ LRESULT CALLBACK SelectFilesDialog2::MyWindowProc(HWND hwnd,
UINT uMsg, WPARAM wParam, LPARAM lParam)
{
SelectFilesDialog2* pSFD =
(SelectFilesDialog2*) ::GetWindowLong(hwnd, GWL_USERDATA);
if (uMsg == WM_COMMAND) {
// React to a click on the OK button (also triggered by hitting return
// in the filename text box).
if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDOK) {
// Obtain a CFileDialog pointer from the window handle. The
// SelectFilesDialog pointer only gets us to a child window
// (the one used to implement the templated custom stuff).
CFileDialog* pDialog = (CFileDialog*) CWnd::FromHandle(hwnd);
FPResult fpr = pSFD->OKButtonClicked(pDialog);
switch (fpr) {
case kFPDone:
// Success; close the dialog with IDOK as the return.
LOGD("Calling EndDialog");
pDialog->EndDialog(IDOK);
return 0;
case kFPPassThru:
LOGD("passing through");
// User typed a single name that didn't resolve to a
// simple file. Let the CFileDialog have it so it can
// do a directory change. (We don't just want to return
// nonzero -- fall through to call prior window proc.)
break;
case kFPNoFiles:
// No files have been selected. We popped up a message box.
// Discontinue processing.
return 0;
case kFPError:
// Something failed internally. Don't let the parent dialog
// continue on.
return 0;
default:
assert(false);
return 0;
}
}
}
return ::CallWindowProc(pSFD->fPrevWndProc, hwnd, uMsg, wParam, lParam);
}
SelectFilesDialog2::FPResult SelectFilesDialog2::OKButtonClicked(CFileDialog* pDialog)
{
// There are two not-quite-independent sources of filenames. As
// ordinary (non-directory) files are selected, they are added to the
// edit control. The user is free to edit the text in the box. This is
// then what would be returned to the user of the file dialog. With
// OFN_FILEMUSTEXIST, the dialog would even confirm that the files exist.
//
// It is possible to select a bunch of files, delete the text, type
// some more names, and hit OK. In that case you would get a completely
// disjoint set of names in the list control and edit control.
//
// Complicating matters somewhat is the "hide extensions for known file
// types" feature, which strips the filename extensions from the entries
// in the list control. As the files are selected, the names -- with
// extensions -- are added to the edit box. So we'd really like to get
// the names from the edit control.
//
// (Win7: Control Panel, Appearance and Personalization, Folder Options,
// View tab, scroll down, "Hide extensions for known file types" checkbox.)
//
// So we need to get the directory names out of the list control, because
// that's the only place we can find them, and we need to get the file
// names out of the edit control, because that's the only way to get the
// full file name (not to mention any names the user typed).
//
// We have a special case: we want to be able to type a path into
// the filename field to go directly there (e.g. "C:\src\test", or a
// network path like "\\webby\fadden"). If the text edit field contains
// a single name, and the name isn't a simple file, we want to let the
// selection dialog exercise its default behavior.
LOGV("OKButtonClicked");
// reset array in case we had a previous partially-successful attempt
fFileNameArray.RemoveAll();
// add a slash to the directory name, unless it's e.g. "C:\"
CString curDirSlash = fCurrentDirectory;
if (curDirSlash.GetAt(curDirSlash.GetLength() - 1) != '\\') {
curDirSlash += '\\';
}
// Get the contents of the text edit control.
CString editStr;
LRESULT editLen = pDialog->SendMessage(CDM_GETSPEC, 0, NULL);
if (editLen > 0) {
LPTSTR buf = editStr.GetBuffer(editLen);
pDialog->SendMessage(CDM_GETSPEC, editLen, (LPARAM) buf);
editStr.ReleaseBuffer();
}
LOGV("textedit has '%ls'", (LPCWSTR) editStr);
// Parse the contents into fFileNameArray.
int fileCount = ParseFileNames(editStr);
if (fileCount < 0) {
::MessageBeep(MB_ICONERROR);
return kFPError;
}
// find the ShellView control
CWnd* pListWrapper = pDialog->GetDlgItem(lst2);
if (pListWrapper == NULL) {
LOGE("Unable to find ShellView (lst2=%d) in file dialog", lst2);
return kFPError;
}
// get the list control, with voodoo
CListCtrl* pList = (CListCtrl*) pListWrapper->GetDlgItem(1);
if (pList == NULL) {
LOGE("Unable to find list control");
return kFPError;
}
// Check to see if nothing is selected, or exactly one directory has
// been found in the text box.
int listCount = pList->GetSelectedCount();
LOGD("Found %d selected items", listCount);
if (listCount + fileCount == 0) {
MessageBox(L"Please select one or more files and directories.",
m_ofn.lpstrTitle, MB_OK | MB_ICONWARNING);
return kFPNoFiles;
} else if (fileCount == 1 && listCount == 0) {
CString file(fFileNameArray[0]);
CString path;
if (file.Find('\\') == -1) {
// just the filename, prepend the current dir
path = curDirSlash + file;
} else {
// full (?) path, don't alter it
path = file;
}
DWORD attr = GetFileAttributes(path);
LOGV("Checking to see if '%ls' is a directory: 0x%08lx",
(LPCWSTR) path, attr);
if (attr != INVALID_FILE_ATTRIBUTES &&
(attr & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
return kFPPassThru;
}
}
// Run through the list, looking for directories. We have to prepend the
// current directory to each entry and check the filesystem.
int index = pList->GetNextItem(-1, LVNI_SELECTED);
int dirCount = 0;
while (listCount--) {
CString itemText(pList->GetItemText(index, 0));
CString path(curDirSlash + itemText);
DWORD attr = GetFileAttributes(path);
LOGV(" %d: 0x%08lx '%ls'", index, attr, (LPCWSTR) itemText);
if (attr != INVALID_FILE_ATTRIBUTES &&
(attr & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
fFileNameArray.Add(itemText);
dirCount++;
}
index = pList->GetNextItem(index, LVNI_SELECTED);
}
LOGV(" added %d directories", dirCount);
return kFPDone;
}
int SelectFilesDialog2::ParseFileNames(const CString& str)
{
// The filename string can come in two forms. If only one filename was
// selected, the entire string will be the filename (spaces and all). If
// more than one file was selected, the individual filenames will be
// surrounded by double quotes. All of this assumes that the names were
// put there by the dialog -- if the user just types a bunch of
// space-separated filenames without quotes, we can't tell that from a
// single long name with spaces.
//
// Double quotes aren't legal in Windows filenames, so we don't have
// to worry about embedded quotes.
//
// So: if the string contains any double quotes, we assume that a series
// of quoted names follows. Anything outside quotes is accepted as a
// single space-separated name. If the string does not have any '"',
// we just grab the whole thing as a single entry.
//
// It's possible to see a full path here -- we allow a special case
// where the user can type a path into the text box to change directories.
// We only expect to see one of those, though, so it must not be quoted.
//
// The dialog seems to leave an extra space at the end of multi-file
// strings.
LOGD("ParseFileNames '%ls'", (LPCWSTR) str);
if (str.GetLength() == 0) {
// empty string
return 0;
} else if (str.Find('\"') == -1) {
// no quotes, single filename, possibly with spaces
fFileNameArray.Add(str);
return 1;
} else if (str.Find('\\') != -1) {
// should not be multiple full/partial paths, string is invalid
LOGW("Found multiple paths in '%ls'", (LPCWSTR) str);
return -1;
}
const WCHAR* cp = str;
const WCHAR* start;
while (*cp != '\0') {
// consume whitespace, which should only be spaces
while (*cp == ' ') {
cp++;
}
if (*cp == '\0') {
// reached end of string
break;
} else if (*cp == '\"') {
// grab quoted item
cp++;
start = cp;
while (*cp != '\"' && *cp != '\0') {
cp++;
}
if (cp == start) {
// empty quoted string; ignore
LOGV(" found empty string in '%ls'", (LPCWSTR) str);
} else {
CString name(start, cp - start);
LOGV(" got quoted '%ls'", (LPCWSTR) name);
fFileNameArray.Add(name);
}
if (*cp != '\0') {
cp++; // advance past closing quote
} else {
LOGV(" (missing closing quote)");
}
} else {
// found unquoted characters
start = cp;
cp++;
while (*cp != ' ' && *cp != '\"' && *cp != '\0') {
cp++;
}
CString name(start, cp - start);
LOGV(" got unquoted '%ls'", (LPCWSTR) name);
fFileNameArray.Add(name);
}
}
return 0;
}
/*
* Our CFileDialog "hook" function.
*
@ -85,7 +410,7 @@
switch (uiMsg) {
case WM_INITDIALOG:
LOGI("WM_INITDIALOG, OFN=0x%08lx", lParam);
LOGD("WM_INITDIALOG, OFN=0x%08lx", lParam);
SetWindowLong(hDlg, GWL_USERDATA, lParam);
break;
case WM_NOTIFY: // 0x4e
@ -101,9 +426,9 @@
ASSERT(pSFD != NULL);
return pSFD->HandleHelp(hDlg, (LPHELPINFO) lParam);
default:
//LOGI("OFNHookProc: hDlg=0x%08lx uiMsg=0x%08lx "
// "wParam=0x%08lx lParam=0x%08lx",
// hDlg, uiMsg, wParam, lParam);
LOGV("OFNHookProc: hDlg=0x%08lx uiMsg=0x%08lx "
"wParam=0x%08lx lParam=0x%08lx",
hDlg, uiMsg, wParam, lParam);
break;
}
@ -173,7 +498,7 @@ UINT SelectFilesDialog::HandleNotify(HWND hDlg, LPOFNOTIFY pofn)
*/
UINT SelectFilesDialog::HandleCommand(HWND hDlg, WPARAM wParam, LPARAM lParam)
{
LOGI(" HandleCommand wParam=%d lParam=0x%08lx", wParam, lParam);
LOGD(" HandleCommand wParam=%d lParam=0x%08lx", wParam, lParam);
if ((int) wParam == fAcceptButtonID) {
MyOnAccept();
@ -191,9 +516,9 @@ UINT SelectFilesDialog::HandleCommand(HWND hDlg, WPARAM wParam, LPARAM lParam)
*/
UINT SelectFilesDialog::HandleSize(HWND hDlg, UINT nType, int cx, int cy)
{
//LOGI("Dialog: old size %d,%d (ready=%d)",
// fLastWinSize.Width(), fLastWinSize.Height(), fReady);
//LOGI("Dialog: new size %d,%d", cx, cy);
LOGD("Dialog: old size %d,%d (ready=%d)",
fLastWinSize.Width(), fLastWinSize.Height(), fReady);
LOGD("Dialog: new size %d,%d", cx, cy);
// we get called once before we have a chance to initialize
if (!fReady)
@ -202,10 +527,11 @@ UINT SelectFilesDialog::HandleSize(HWND hDlg, UINT nType, int cx, int cy)
int deltaX, deltaY;
deltaX = cx - fLastWinSize.Width();
deltaY = cy - fLastWinSize.Height();
//LOGI("Delta is %d,%d", deltaX, deltaY);
LOGD("Delta is %d,%d", deltaX, deltaY);
ShiftControls(deltaX, 0 /*deltaY*/);
// TODO: this is wrong
GetParent()->GetWindowRect(&fLastWinSize);
return 1;
@ -229,7 +555,6 @@ UINT SelectFilesDialog::HandleHelp(HWND hDlg, LPHELPINFO lpHelpInfo)
return TRUE; // yes, we handled it
}
/*
* When the CFileDialog finishes doing its thing, we "fix" stuff a bit.
* We can't really do this earlier, because we'd be destroying windows that
@ -259,7 +584,7 @@ void SelectFilesDialog::MyOnInitDone(void)
pWnd->GetWindowRect(&cancelRect);
vertDiff = acceptRect.top - okRect.top;
LOGI("vertDiff = %d (horizDiff=%d)", vertDiff,
LOGD("vertDiff = %d (horizDiff=%d)", vertDiff,
acceptRect.left - okRect.left);
ShiftControls(0, -vertDiff);
@ -303,7 +628,6 @@ void SelectFilesDialog::ShiftControls(int deltaX, int deltaY)
Invalidate(true);
}
/*
* Get the list view control out of the common file dialog.
*
@ -357,7 +681,7 @@ void SelectFilesDialog::MyOnFileNameChange(void)
*/
void SelectFilesDialog::MyOnAccept(void)
{
//LOGI("OnAccept!");
LOGD("OnAccept!");
PrepEndDialog();
}
@ -375,7 +699,7 @@ bool SelectFilesDialog::PrepEndDialog(void)
// let sub-classes copy data out
if (!MyDataExchange(true)) {
LOGI("MyDataExchange failed!");
LOGW("MyDataExchange failed!");
return false;
}
@ -395,14 +719,15 @@ bool SelectFilesDialog::PrepEndDialog(void)
* Fortunately I believe the world is divided into "typers" and
* "clickers", and so long as their paths don't cross we're fine.
*/
LOGI("PrepEndDialog: got max=%d off=%d", m_ofn.nMaxFile, m_ofn.nFileOffset);
LOGD("PrepEndDialog: got max=%d off=%d str='%ls'",
m_ofn.nMaxFile, m_ofn.nFileOffset, m_ofn.lpstrFile);
if (m_ofn.nFileOffset != 0) {
WCHAR* buf = m_ofn.lpstrFile;
buf += m_ofn.nFileOffset;
while (*buf != '\0') {
if (buf > m_ofn.lpstrFile)
*(buf-1) = '\\';
LOGI(" File '%ls'", buf);
LOGD(" File '%ls'", buf);
buf += wcslen(buf) +1;
}
//Sleep(1000);
@ -413,7 +738,7 @@ bool SelectFilesDialog::PrepEndDialog(void)
/* stick a '\' on the very end, so we get double-null action later */
*(m_ofn.lpstrFile + nextSpot) = '\\';
}
LOGI("Last offset was %d", nextSpot);
LOGD("Last offset was %d", nextSpot);
#if 0
/* make it clear that they're only getting one */
@ -435,7 +760,7 @@ bool SelectFilesDialog::PrepEndDialog(void)
*/
pList = (CListCtrl*) GetListCtrl();
if (pList == NULL) {
LOGI("GLITCH: could not get list control");
LOGW("GLITCH: could not get list control");
return false;
}
ASSERT(pList != NULL);
@ -443,6 +768,7 @@ bool SelectFilesDialog::PrepEndDialog(void)
CString fileNames;
int count = pList->GetSelectedCount();
LOGD("List control has %d items; nextSpot=%d", count, nextSpot);
if (count == 0) {
if (nextSpot == 0) {
MessageBox(L"Please select one or more files and directories.",
@ -453,12 +779,14 @@ bool SelectFilesDialog::PrepEndDialog(void)
}
/* nothing but typed-in names */
LOGD("Using typed-in names");
fileNames = m_ofn.lpstrFile;
fFileNameOffset = m_ofn.nFileOffset;
} else {
bool compare;
if (nextSpot == 0) {
fileNames = GetFolderPath();
LOGD("set filenames to folder path (%ls)", (LPCWSTR) fileNames);
/* add a trailing '\', which gets stomped to '\0' later on */
if (fileNames.Right(1) != L"\\")
fileNames += L"\\";

View File

@ -6,6 +6,35 @@
/*
* File selection dialog, a sub-class of "Open" that allows multiple selection
* of both files and directories.
*
* This is something of a nightmare to work through. The standard Windows
* dialog will return multiple selected files, but omits the directories,
* leaving the developer to find alternative means of acquiring the complete
* list of files. The most popular approach is to dig into the CListView
* object (lst2) and peruse the set of selected files from the control itself.
*
* Complicating this is the existence of three very different dialog
* implementations, known as "old style", "explorer" and "vista". Since
* we are currently targeting WinXP as a minimum OS level, and I would
* prefer not to have multiple implementations, this code targets the
* explorer-style dialogs.
*
* The API follows the standard file dialog multi-select pattern, which
* returned a directory name followed by a series of files in that directory.
* We simplify things a bit by returning the pathname separately and the
* filenames in a string array.
*
* The current implementation owes a debt to Hojjat Bohlooli's
* SelectDialog sample (http://www.codeproject.com/Articles/28015/).
*
* Other relevant links:
* http://www.codeproject.com/dialog/select_all_button.asp
* http://www.codeproject.com/dialog/select_all_button.asp
* http://www.codeproject.com/dialog/customize_dialog.asp
* http://stackoverflow.com/questions/31059/
*
* I wish I could say all this nonsense was fixed in the "vista" dialogs,
* but it isn't (e.g. http://stackoverflow.com/questions/8269696/ ).
*/
#ifndef UTIL_SELECTFILESDIALOG_H
#define UTIL_SELECTFILESDIALOG_H
@ -23,18 +52,20 @@ class SelectFilesDialog : public CFileDialog {
public:
enum { kFileNameBufSize = 32768 };
SelectFilesDialog(const WCHAR* rctmpl, CWnd* pParentWnd = NULL) :
CFileDialog(true, NULL, NULL, OFN_HIDEREADONLY, NULL, pParentWnd)
CFileDialog(true, NULL, NULL, OFN_HIDEREADONLY, NULL, pParentWnd,
0, FALSE /*disable Vista style*/)
{
m_ofn.Flags |= OFN_ENABLETEMPLATE | OFN_ALLOWMULTISELECT |
OFN_HIDEREADONLY | OFN_FILEMUSTEXIST;
OFN_HIDEREADONLY | OFN_EXPLORER | OFN_ENABLESIZING;
m_ofn.lpTemplateName = rctmpl;
m_ofn.hInstance = AfxGetInstanceHandle();
m_ofn.lpstrFile = new WCHAR[kFileNameBufSize];
m_ofn.lpstrFile[0] = m_ofn.lpstrFile[1] = '\0';
m_ofn.nMaxFile = kFileNameBufSize;
m_ofn.nFileOffset = -1;
m_ofn.Flags |= OFN_ENABLEHOOK;
m_ofn.lpfnHook = OFNHookProc;
m_ofn.lCustData = (long)this;
m_ofn.lCustData = (LPARAM) this;
fExitStatus = IDABORT;
fFileNames = new WCHAR[2];
@ -64,9 +95,10 @@ public:
ASSERT(len > wcslen(fileNames));
ASSERT(fileNames[len] == '\0');
ASSERT(fileNames[len-1] == '\0');
LOGI("SetFileNames '%ls' %d %d", fileNames, len, fileNameOffset);
LOGD("SetFileNames '%ls' %d %d", fileNames, len, fileNameOffset);
delete[] fFileNames;
fFileNames = wcsdup(fileNames);
fFileNames = new WCHAR[len];
memcpy(fFileNames, fileNames, len * sizeof(WCHAR));
fFileNameOffset = fileNameOffset;
}
@ -93,7 +125,7 @@ protected:
virtual void DestroyItem(CWnd* pDlg, int id) {
CWnd* pWnd = pDlg->GetDlgItem(id);
if (pWnd == NULL) {
LOGI("Could not find item %d", id);
LOGW("Could not find item %p %d", pDlg, id);
return;
}
pWnd->DestroyWindow();
@ -121,8 +153,124 @@ private:
WCHAR* fFileNames;
CRect fLastWinSize;
//DECLARE_MESSAGE_MAP()
};
/*
* File selection, based on an "open" common file dialog.
*/
class SelectFilesDialog2 : public CFileDialog {
public:
SelectFilesDialog2(const WCHAR* rctmpl, CWnd* pParentWnd = NULL) :
CFileDialog(true, NULL, NULL, OFN_HIDEREADONLY, NULL, pParentWnd,
0, FALSE /*disable Vista style*/)
{
// Set flags. We specify ALLOWMULTISELECT but no filename buffer;
// we want the multi-select behavior but we don't want to return
// the filenames in the filename buf.
m_ofn.Flags |= OFN_ENABLETEMPLATE | OFN_ALLOWMULTISELECT |
OFN_HIDEREADONLY | OFN_EXPLORER | OFN_ENABLESIZING;
// Configure use of a template. The dialog template must have
// WS_CHILD and WS_CLIPSIBLINGS set, and should have DS_3DLOOK and
// DS_CONTROL set as well.
m_ofn.lpTemplateName = rctmpl;
m_ofn.hInstance = AfxGetInstanceHandle();
}
virtual ~SelectFilesDialog2(void) {}
/*
* Gets the directory name where the files live. This is a full path.
*/
const CString& GetDirectory(void) const { return fCurrentDirectory; }
/*
* Gets the file name array. This is only valid if the dialog exited
* successfully (DoModal returned IDOK).
*/
const CStringArray& GetFileNames(void) const { return fFileNameArray; }
/*
* Sets the window title; must be called before DoModal.
*/
void SetWindowTitle(const WCHAR* title) {
fCustomTitle = title;
m_ofn.lpstrTitle = (LPCWSTR) fCustomTitle;
}
protected:
/*
* Finish configuring the file dialog.
*/
virtual void OnInitDone() override;
/*
* Track changes to the current directory.
*
* Updates fCurrentDirectory with the path currently being used by the
* dialog. For items with no path (e.g. Computer or Libraries), this
* will set an empty string.
*/
virtual void OnFolderChange() override;
private:
/*
* Custom filename validation; in our case, it's a double-click trap.
*
* Returns 0 if the name looks good, 1 otherwise. If we return 1, the
* dialog will not close.
*/
virtual BOOL OnFileNameOK() override;
/*
* Our WindowProc callback function. Watches for the OK button.
*/
static LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
LPARAM lParam);
// filename parse result
enum FPResult { kFPDone, kFPPassThru, kFPNoFiles, kFPError };
/*
* The "OK" (actually "Open" or "Accept") button was clicked. Extract
* the filenames from the list control.
*
* Possible return values:
* OK_DONE
* We have successfully obtained the list of files and folders.
* OK_PASSTHRU
* Let the parent dialog process the event. This is done when the
* edit box contains a directory name -- we want the dialog to
* change to that directory.
* OK_NOFILES
* No files were selected. Keep the dialog open.
* OK_ERROR
* Something went wrong.
*/
FPResult OKButtonClicked(CFileDialog* pDialog);
/*
* Parses the file name string returned by the dialog. Adds them to
* fPathNameArray. Returns the number of names found, or -1 if the
* string appears to be invalid.
*/
int ParseFileNames(const CString& str);
/*
* Previous WindowProc. Most messages will be forwarded to this.
*/
WNDPROC fPrevWndProc;
CString fCustomTitle;
// Directory the dialog is currently accessing. Prepend this to the
// entries in fFileNameArray to get the full path.
CString fCurrentDirectory;
// File names of selected files.
CStringArray fFileNameArray;
};
#endif /*UTIL_SELECTFILESDIALOG_H*/