OSDN Git Service

refactor
[winmerge-jp/winmerge-jp.git] / Src / FileOrFolderSelect.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 //    WinMerge:  an interactive diff/merge utility
3 //    Copyright (C) 1997-2000  Thingamahoochie Software
4 //    Author: Dean Grimm
5 //    SPDX-License-Identifier: GPL-2.0-or-later
6 /////////////////////////////////////////////////////////////////////////////
7 /** 
8  * @file  FileOrFolderSelect.cpp
9  *
10  * @brief Implementation of the file and folder selection routines.
11  */
12
13 #include "pch.h"
14 #include <windows.h>
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.
18 #include <shlobj.h>
19 #pragma warning (pop)
20 #include <sys/stat.h>
21 #include "Environment.h"
22 #include "paths.h"
23 #include "MergeApp.h"
24
25 static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam,
26                 LPARAM lpData);
27 static void ConvertFilter(tchar_t* filterStr);
28
29 /** @brief Last selected folder for folder selection dialog. */
30 static String LastSelectedFolder;
31
32 /**
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
49  */
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*/)
53 {
54         path.clear(); // Clear output param
55
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};
59         String sInitialDir;
60
61         // check if specified path is a file
62         if (initialPath && initialPath[0])
63         {
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)
68                 {
69                         sInitialDir = initialPath;
70                 }
71                 else
72                 {
73                         String temp;
74                         paths::SplitFilename(initialPath, &sInitialDir, &temp, 0);
75                         lstrcpy(sSelectedFile, temp.c_str());
76                 }
77         }
78
79         String filters = sfilter, title = stitle;
80         if (sfilter.empty())
81                 filters = _("All Files (*.*)|*.*||");
82         if (stitle.empty())
83         {
84                 title = is_open ? _("Open") : _("Save As");
85         }
86
87         // Convert extension mask from MFC style separators ('|')
88         //  to Win32 style separators ('\0')
89         tchar_t* filtersStr = &*filters.begin();
90         ConvertFilter(filtersStr);
91
92         OPENFILENAME_NT4 ofn = { sizeof OPENFILENAME_NT4 };
93         ofn.hwndOwner = parent;
94         ofn.lpstrFilter = filtersStr;
95         ofn.lpstrCustomFilter = nullptr;
96         ofn.nFilterIndex = 1;
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;
105
106         bool bRetVal = false;
107         if (is_open)
108                 bRetVal = !!GetOpenFileName((OPENFILENAME *)&ofn);
109         else
110                 bRetVal = !!GetSaveFileName((OPENFILENAME *)&ofn);
111         // common file dialog populated sSelectedFile variable's buffer
112
113         if (bRetVal)
114                 path = sSelectedFile;
115         
116         return bRetVal;
117 }
118
119 /** 
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)
126  */
127 bool SelectFolder(String& path, const tchar_t* root_path /*= nullptr*/, 
128                         const String& stitle /*=_T("")*/, 
129                         HWND hwndOwner /*= nullptr*/) 
130 {
131         BROWSEINFO bi;
132         LPITEMIDLIST pidl;
133         tchar_t szPath[MAX_PATH_FULL] = {0};
134         bool bRet = false;
135         String title = stitle;
136         if (root_path == nullptr)
137                 LastSelectedFolder.clear();
138         else
139                 LastSelectedFolder = root_path;
140
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;
148
149         pidl = SHBrowseForFolder(&bi);
150         if (pidl != nullptr)
151         {
152                 if (SHGetPathFromIDList(pidl, szPath))
153                 {
154                         path = szPath;
155                         bRet = true;
156                 }
157                 CoTaskMemFree(pidl);
158         }
159         return bRet;
160 }
161
162 /**
163  * @brief Callback function for setting selected folder for folder dialog.
164  */
165 static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam,
166                 LPARAM lpData)
167 {
168         // Look for BFFM_INITIALIZED
169         if (uMsg == BFFM_INITIALIZED)
170         {
171                 if (lpData != NULL)
172                         SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
173                 else
174                         SendMessage(hwnd, BFFM_SETSELECTION, TRUE, (LPARAM)LastSelectedFolder.c_str());
175         }
176         else if (uMsg == BFFM_VALIDATEFAILED)
177         {
178                 String strMessage = 
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);
181                 if (answer == IDYES)
182                 {
183                         if (!paths::CreateIfNeeded((tchar_t*)lParam))
184                         {
185                                 MessageBox(hwnd, _("Failed to create folder.").c_str(), nullptr, MB_OK | MB_ICONWARNING);
186                         }
187                 }
188                 return 1;
189         }
190         return 0;
191 }
192
193 /** 
194  * @brief Shows file/folder selection dialog.
195  *
196  * We need this custom function so we can select files and folders with the
197  * same dialog.
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.
206  */
207 bool SelectFileOrFolder(HWND parent, String& path, const tchar_t* initialPath /*= nullptr*/)
208 {
209         String title = _("Open");
210
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];
214         String sInitialDir;
215
216         // check if specified path is a file
217         if (initialPath!=nullptr && initialPath[0] != '\0')
218         {
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)
223                 {
224                         sInitialDir = initialPath;
225                 }
226                 else
227                 {
228                         String temp;
229                         paths::SplitFilename(initialPath, &sInitialDir, &temp, 0);
230                         lstrcpy(sSelectedFile, temp.c_str());
231                 }
232         }
233
234         String filters = _("All Files (*.*)|*.*||");
235
236         // Convert extension mask from MFC style separators ('|')
237         //  to Win32 style separators ('\0')
238         tchar_t* filtersStr = &*filters.begin();
239         ConvertFilter(filtersStr);
240
241         String dirSelTag = _("Folder Selection");
242
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?
246
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;
258
259         bool bRetVal = !!GetOpenFileName((OPENFILENAME *)&ofn);
260
261         if (bRetVal)
262         {
263                 path = sSelectedFile;
264                 if (paths::DoesPathExist(path) == paths::DOES_NOT_EXIST)
265                 {
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);
270                 }
271         }
272         return bRetVal;
273 }
274
275
276 /** 
277  * @brief Helper function for converting filter format.
278  *
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.
282  *
283  * @param [in,out] filterStr
284  * - in Mask string to convert
285  * - out Converted string
286  */
287 static void ConvertFilter(tchar_t* filterStr)
288 {
289         tchar_t *ch;
290         while ( (ch = tc::tcschr(filterStr, '|')) != nullptr)
291         {
292                 filterStr = ch + 1;
293                 *ch = '\0';
294         }
295 }