1 /////////////////////////////////////////////////////////////////////////////
2 // WinMerge: an interactive diff/merge utility
3 // Copyright (C) 1997-2000 Thingamahoochie Software
5 // SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
8 * @file FileOrFolderSelect.cpp
10 * @brief Implementation of the file and folder selection routines.
15 #include "FileOrFolderSelect.h"
16 #pragma warning (push) // prevent "warning C4091: 'typedef ': ignored on left of 'tagGPFIDL_FLAGS' when no variable is declared"
17 #pragma warning (disable:4091) // VC bug when using XP enabled toolsets.
21 #include "Environment.h"
25 static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam,
27 static void ConvertFilter(LPTSTR filterStr);
29 /** @brief Last selected folder for folder selection dialog. */
30 static String LastSelectedFolder;
33 * @brief Helper function for selecting folder or file.
34 * This function shows standard Windows file selection dialog for selecting
35 * file or folder to open or file to save. The last parameter @p is_open selects
36 * between open or save modes. Biggest difference is that in save-mode Windows
37 * asks if user wants to override existing file.
38 * @param [in] parent Handle to parent window. Can be `nullptr`, but then
39 * CMainFrame is used which can cause modality problems.
40 * @param [out] path Selected path is returned in this string
41 * @param [in] initialPath Initial path (and file) shown when dialog is opened
42 * @param [in] titleid Resource string ID for dialog title.
43 * @param [in] filterid 0 or STRING ID for filter string
44 * - 0 means "All files (*.*)". Note the string formatting!
45 * @param [in] is_open Selects Open/Save -dialog (mode).
46 * @note Be careful when setting @p parent to `nullptr` as there are potential
47 * modality problems with this. Dialog can be lost behind other windows!
48 * @param [in] defaultExtension Extension to append if user doesn't provide one
50 bool SelectFile(HWND parent, String& path, bool is_open /*= true*/,
51 LPCTSTR initialPath /*= nullptr*/, const String& stitle /*=_T("")*/,
52 const String& sfilter /*=_T("")*/, LPCTSTR defaultExtension /*= nullptr*/)
54 path.clear(); // Clear output param
56 // This will tell common file dialog what to show
57 // and also this will hold its return value
58 TCHAR sSelectedFile[MAX_PATH_FULL] = {0};
61 // check if specified path is a file
62 if (initialPath && initialPath[0])
64 // If initial path info includes a file
65 // we put the bare filename into sSelectedFile
66 // so the common file dialog will start up with that file selected
67 if (paths::DoesPathExist(initialPath) == paths::IS_EXISTING_FILE)
70 paths::SplitFilename(initialPath, 0, &temp, 0);
71 lstrcpy(sSelectedFile, temp.c_str());
72 sInitialDir = paths::GetParentPath(initialPath);
76 sInitialDir = initialPath;
80 String filters = sfilter, title = stitle;
82 filters = _("All Files (*.*)|*.*||");
85 title = is_open ? _("Open") : _("Save As");
88 // Convert extension mask from MFC style separators ('|')
89 // to Win32 style separators ('\0')
90 LPTSTR filtersStr = &*filters.begin();
91 ConvertFilter(filtersStr);
93 OPENFILENAME_NT4 ofn = { sizeof OPENFILENAME_NT4 };
94 ofn.hwndOwner = parent;
95 ofn.lpstrFilter = filtersStr;
96 ofn.lpstrCustomFilter = nullptr;
98 ofn.lpstrFile = sSelectedFile;
99 ofn.nMaxFile = MAX_PATH_FULL;
100 ofn.lpstrInitialDir = sInitialDir.empty() ? nullptr : sInitialDir.c_str();
101 ofn.lpstrTitle = title.c_str();
102 ofn.lpstrFileTitle = nullptr;
103 if (defaultExtension)
104 ofn.lpstrDefExt = defaultExtension;
105 ofn.Flags = OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
107 bool bRetVal = false;
109 bRetVal = !!GetOpenFileName((OPENFILENAME *)&ofn);
111 bRetVal = !!GetSaveFileName((OPENFILENAME *)&ofn);
112 // common file dialog populated sSelectedFile variable's buffer
115 path = sSelectedFile;
121 * @brief Helper function for selecting directory
122 * @param [out] path Selected path is returned in this string
123 * @param [in] root_path Initial path shown when dialog is opened
124 * @param [in] titleid Resource string ID for dialog title.
125 * @param [in] hwndOwner Handle to owner window or `nullptr`
126 * @return `true` if valid folder selected (not cancelled)
128 bool SelectFolder(String& path, LPCTSTR root_path /*= nullptr*/,
129 const String& stitle /*=_T("")*/,
130 HWND hwndOwner /*= nullptr*/)
134 TCHAR szPath[MAX_PATH_FULL] = {0};
136 String title = stitle;
137 if (root_path == nullptr)
138 LastSelectedFolder.clear();
140 LastSelectedFolder = root_path;
142 bi.hwndOwner = hwndOwner;
143 bi.pidlRoot = nullptr; // Start from desktop folder
144 bi.pszDisplayName = szPath;
145 bi.lpszTitle = title.c_str();
146 bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI | BIF_VALIDATE;
147 bi.lpfn = BrowseCallbackProc;
148 bi.lParam = (LPARAM)root_path;
150 pidl = SHBrowseForFolder(&bi);
153 if (SHGetPathFromIDList(pidl, szPath))
164 * @brief Callback function for setting selected folder for folder dialog.
166 static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam,
169 // Look for BFFM_INITIALIZED
170 if (uMsg == BFFM_INITIALIZED)
173 SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
175 SendMessage(hwnd, BFFM_SETSELECTION, TRUE, (LPARAM)LastSelectedFolder.c_str());
177 else if (uMsg == BFFM_VALIDATEFAILED)
180 strutils::format_string1(_("%1 does not exist. Do you want to create it?"), (TCHAR *)lParam);
181 int answer = MessageBox(hwnd, strMessage.c_str(), nullptr, MB_YESNO);
184 if (!paths::CreateIfNeeded((TCHAR*)lParam))
186 MessageBox(hwnd, _("Failed to create folder.").c_str(), nullptr, MB_OK | MB_ICONWARNING);
195 * @brief Shows file/folder selection dialog.
197 * We need this custom function so we can select files and folders with the
199 * - If existing filename is selected return it
200 * - If filename in (CFileDialog) editbox and current folder doesn't form
201 * a valid path to file, return current folder.
202 * @param [in] parent Handle to parent window. Can be `nullptr`, but then
203 * CMainFrame is used which can cause modality problems.
204 * @param [out] path Selected folder/filename
205 * @param [in] initialPath Initial file or folder shown/selected.
206 * @return `true` if user choosed a file/folder, `false` if user canceled dialog.
208 bool SelectFileOrFolder(HWND parent, String& path, LPCTSTR initialPath /*= nullptr*/)
210 String title = _("Open");
212 // This will tell common file dialog what to show
213 // and also this will hold its return value
214 TCHAR sSelectedFile[MAX_PATH_FULL];
216 // check if specified path is a file
217 if (initialPath!=nullptr && initialPath[0] != '\0')
219 // If initial path info includes a file
220 // we put the bare filename into sSelectedFile
221 // so the common file dialog will start up with that file selected
222 if (paths::DoesPathExist(initialPath) == paths::IS_EXISTING_FILE)
225 paths::SplitFilename(initialPath, 0, &temp, 0);
226 lstrcpy(sSelectedFile, temp.c_str());
230 String filters = _("All Files (*.*)|*.*||");
232 // Convert extension mask from MFC style separators ('|')
233 // to Win32 style separators ('\0')
234 LPTSTR filtersStr = &*filters.begin();
235 ConvertFilter(filtersStr);
237 String dirSelTag = _("Folder Selection");
239 // Set initial filename to folder selection tag
240 dirSelTag += _T("."); // Treat it as filename
241 lstrcpy(sSelectedFile, dirSelTag.c_str()); // What is assignment above good for?
243 OPENFILENAME_NT4 ofn = { sizeof OPENFILENAME_NT4 };
244 ofn.hwndOwner = parent;
245 ofn.lpstrFilter = filtersStr;
246 ofn.lpstrCustomFilter = nullptr;
247 ofn.nFilterIndex = 1;
248 ofn.lpstrFile = sSelectedFile;
249 ofn.nMaxFile = MAX_PATH_FULL;
250 ofn.lpstrInitialDir = initialPath;
251 ofn.lpstrTitle = title.c_str();
252 ofn.lpstrFileTitle = nullptr;
253 ofn.Flags = OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_NOTESTFILECREATE | OFN_NOCHANGEDIR;
255 bool bRetVal = !!GetOpenFileName((OPENFILENAME *)&ofn);
259 path = sSelectedFile;
260 if (paths::DoesPathExist(path) == paths::DOES_NOT_EXIST)
262 // We have a valid folder name, but propably garbage as a filename.
263 // Return folder name
264 String folder = paths::GetPathOnly(sSelectedFile);
265 path = paths::AddTrailingSlash(folder);
273 * @brief Helper function for converting filter format.
275 * MFC functions separate filter strings with | char which is also
276 * good choice to safe into resource. But WinAPI32 functions we use
277 * needs '\0' as separator. This function replaces '|'s with '\0's.
279 * @param [in,out] filterStr
280 * - in Mask string to convert
281 * - out Converted string
283 static void ConvertFilter(LPTSTR filterStr)
286 while ( (ch = _tcschr(filterStr, '|')) != nullptr)