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(tchar_t* 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 const tchar_t* initialPath /*= nullptr*/, const String& stitle /*=_T("")*/,
52 const String& sfilter /*=_T("")*/, const tchar_t* 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_t 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_DIR)
69 sInitialDir = initialPath;
74 paths::SplitFilename(initialPath, &sInitialDir, &temp, 0);
75 lstrcpy(sSelectedFile, temp.c_str());
79 String filters = sfilter, title = stitle;
81 filters = _("All Files (*.*)|*.*||");
84 title = is_open ? _("Open") : _("Save As");
87 // Convert extension mask from MFC style separators ('|')
88 // to Win32 style separators ('\0')
89 tchar_t* filtersStr = &*filters.begin();
90 ConvertFilter(filtersStr);
92 OPENFILENAME_NT4 ofn = { sizeof OPENFILENAME_NT4 };
93 ofn.hwndOwner = parent;
94 ofn.lpstrFilter = filtersStr;
95 ofn.lpstrCustomFilter = nullptr;
97 ofn.lpstrFile = sSelectedFile;
98 ofn.nMaxFile = MAX_PATH_FULL;
99 ofn.lpstrInitialDir = sInitialDir.empty() ? nullptr : sInitialDir.c_str();
100 ofn.lpstrTitle = title.c_str();
101 ofn.lpstrFileTitle = nullptr;
102 if (defaultExtension)
103 ofn.lpstrDefExt = defaultExtension;
104 ofn.Flags = OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
106 bool bRetVal = false;
108 bRetVal = !!GetOpenFileName((OPENFILENAME *)&ofn);
110 bRetVal = !!GetSaveFileName((OPENFILENAME *)&ofn);
111 // common file dialog populated sSelectedFile variable's buffer
114 path = sSelectedFile;
120 * @brief Helper function for selecting directory
121 * @param [out] path Selected path is returned in this string
122 * @param [in] root_path Initial path shown when dialog is opened
123 * @param [in] titleid Resource string ID for dialog title.
124 * @param [in] hwndOwner Handle to owner window or `nullptr`
125 * @return `true` if valid folder selected (not cancelled)
127 bool SelectFolder(String& path, const tchar_t* root_path /*= nullptr*/,
128 const String& stitle /*=_T("")*/,
129 HWND hwndOwner /*= nullptr*/)
133 tchar_t szPath[MAX_PATH_FULL] = {0};
135 String title = stitle;
136 if (root_path == nullptr)
137 LastSelectedFolder.clear();
139 LastSelectedFolder = root_path;
141 bi.hwndOwner = hwndOwner;
142 bi.pidlRoot = nullptr; // Start from desktop folder
143 bi.pszDisplayName = szPath;
144 bi.lpszTitle = title.c_str();
145 bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI | BIF_VALIDATE;
146 bi.lpfn = BrowseCallbackProc;
147 bi.lParam = (LPARAM)root_path;
149 pidl = SHBrowseForFolder(&bi);
152 if (SHGetPathFromIDList(pidl, szPath))
163 * @brief Callback function for setting selected folder for folder dialog.
165 static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam,
168 // Look for BFFM_INITIALIZED
169 if (uMsg == BFFM_INITIALIZED)
172 SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
174 SendMessage(hwnd, BFFM_SETSELECTION, TRUE, (LPARAM)LastSelectedFolder.c_str());
176 else if (uMsg == BFFM_VALIDATEFAILED)
179 strutils::format_string1(_("%1 does not exist. Do you want to create it?"), (tchar_t *)lParam);
180 int answer = MessageBox(hwnd, strMessage.c_str(), nullptr, MB_YESNO);
183 if (!paths::CreateIfNeeded((tchar_t*)lParam))
185 MessageBox(hwnd, _("Failed to create folder.").c_str(), nullptr, MB_OK | MB_ICONWARNING);
194 * @brief Shows file/folder selection dialog.
196 * We need this custom function so we can select files and folders with the
198 * - If existing filename is selected return it
199 * - If filename in (CFileDialog) editbox and current folder doesn't form
200 * a valid path to file, return current folder.
201 * @param [in] parent Handle to parent window. Can be `nullptr`, but then
202 * CMainFrame is used which can cause modality problems.
203 * @param [out] path Selected folder/filename
204 * @param [in] initialPath Initial file or folder shown/selected.
205 * @return `true` if user choosed a file/folder, `false` if user canceled dialog.
207 bool SelectFileOrFolder(HWND parent, String& path, const tchar_t* initialPath /*= nullptr*/)
209 String title = _("Open");
211 // This will tell common file dialog what to show
212 // and also this will hold its return value
213 tchar_t 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_DIR)
224 sInitialDir = initialPath;
229 paths::SplitFilename(initialPath, &sInitialDir, &temp, 0);
230 lstrcpy(sSelectedFile, temp.c_str());
234 String filters = _("All Files (*.*)|*.*||");
236 // Convert extension mask from MFC style separators ('|')
237 // to Win32 style separators ('\0')
238 tchar_t* filtersStr = &*filters.begin();
239 ConvertFilter(filtersStr);
241 String dirSelTag = _("Folder Selection");
243 // Set initial filename to folder selection tag
244 dirSelTag += _T("."); // Treat it as filename
245 lstrcpy(sSelectedFile, dirSelTag.c_str()); // What is assignment above good for?
247 OPENFILENAME_NT4 ofn = { sizeof OPENFILENAME_NT4 };
248 ofn.hwndOwner = parent;
249 ofn.lpstrFilter = filtersStr;
250 ofn.lpstrCustomFilter = nullptr;
251 ofn.nFilterIndex = 1;
252 ofn.lpstrFile = sSelectedFile;
253 ofn.nMaxFile = MAX_PATH_FULL;
254 ofn.lpstrInitialDir = sInitialDir.empty() ? nullptr : sInitialDir.c_str();
255 ofn.lpstrTitle = title.c_str();
256 ofn.lpstrFileTitle = nullptr;
257 ofn.Flags = OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_NOTESTFILECREATE | OFN_NOCHANGEDIR;
259 bool bRetVal = !!GetOpenFileName((OPENFILENAME *)&ofn);
263 path = sSelectedFile;
264 if (paths::DoesPathExist(path) == paths::DOES_NOT_EXIST)
266 // We have a valid folder name, but propably garbage as a filename.
267 // Return folder name
268 String folder = paths::GetPathOnly(sSelectedFile);
269 path = paths::AddTrailingSlash(folder);
277 * @brief Helper function for converting filter format.
279 * MFC functions separate filter strings with | char which is also
280 * good choice to safe into resource. But WinAPI32 functions we use
281 * needs '\0' as separator. This function replaces '|'s with '\0's.
283 * @param [in,out] filterStr
284 * - in Mask string to convert
285 * - out Converted string
287 static void ConvertFilter(tchar_t* filterStr)
290 while ( (ch = tc::tcschr(filterStr, '|')) != nullptr)