diff --git a/app/DiskArchive.cpp b/app/DiskArchive.cpp index 8e426aa..e68b45e 100644 --- a/app/DiskArchive.cpp +++ b/app/DiskArchive.cpp @@ -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); diff --git a/mdc/ChooseFilesDlg.cpp b/mdc/ChooseFilesDlg.cpp deleted file mode 100644 index f6d58f7..0000000 --- a/mdc/ChooseFilesDlg.cpp +++ /dev/null @@ -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); -} diff --git a/mdc/ChooseFilesDlg.h b/mdc/ChooseFilesDlg.h deleted file mode 100644 index 43f4550..0000000 --- a/mdc/ChooseFilesDlg.h +++ /dev/null @@ -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*/ diff --git a/mdc/Main.cpp b/mdc/Main.cpp index 138238d..4e5088a 100644 --- a/mdc/Main.cpp +++ b/mdc/Main.cpp @@ -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, " %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); } diff --git a/mdc/Main.h b/mdc/Main.h index 369cbc8..5b8f138 100644 --- a/mdc/Main.h +++ b/mdc/Main.h @@ -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); diff --git a/mdc/mdc.cpp b/mdc/mdc.cpp index 61a01d0..43d8702 100644 --- a/mdc/mdc.cpp +++ b/mdc/mdc.cpp @@ -43,7 +43,7 @@ MyApp::MyApp(LPCTSTR lpszAppName) : CWinApp(lpszAppName) */ MyApp::~MyApp(void) { - LOGI("MDC SHUTTING DOWN"); + LOGI("MDC shutting down"); delete gDebugLog; } diff --git a/mdc/mdc.h b/mdc/mdc.h index 4fdc9bc..31239a5 100644 --- a/mdc/mdc.h +++ b/mdc/mdc.h @@ -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 /* diff --git a/mdc/mdc.rc b/mdc/mdc.rc index b514611..2426019 100644 --- a/mdc/mdc.rc +++ b/mdc/mdc.rc @@ -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 diff --git a/mdc/mdc.vcxproj b/mdc/mdc.vcxproj index b7a2e02..27fada7 100644 --- a/mdc/mdc.vcxproj +++ b/mdc/mdc.vcxproj @@ -150,7 +150,6 @@ - @@ -183,7 +182,6 @@ - diff --git a/mdc/mdc.vcxproj.filters b/mdc/mdc.vcxproj.filters index 1424398..ed13339 100644 --- a/mdc/mdc.vcxproj.filters +++ b/mdc/mdc.vcxproj.filters @@ -23,9 +23,6 @@ Header Files - - Header Files - Header Files @@ -51,9 +48,6 @@ Source Files - - Source Files - Source Files diff --git a/util/MyDebug.cpp b/util/MyDebug.cpp index f79684d..92df5fd 100644 --- a/util/MyDebug.cpp +++ b/util/MyDebug.cpp @@ -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]; diff --git a/util/MyDebug.h b/util/MyDebug.h index 20fe2da..20e3af3 100644 --- a/util/MyDebug.h +++ b/util/MyDebug.h @@ -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, ...) \ diff --git a/util/SelectFilesDialog.cpp b/util/SelectFilesDialog.cpp index 51bdc74..7f9a6c1 100644 --- a/util/SelectFilesDialog.cpp +++ b/util/SelectFilesDialog.cpp @@ -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 +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"\\"; diff --git a/util/SelectFilesDialog.h b/util/SelectFilesDialog.h index 809f911..6540516 100644 --- a/util/SelectFilesDialog.h +++ b/util/SelectFilesDialog.h @@ -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*/