1 /*
2  * PROJECT:     ReactOS shell32
3  * LICENSE:     GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
4  * PURPOSE:     CopyTo and MoveTo implementation
5  * COPYRIGHT:   Copyright 2020-2023 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
6  */
7 
8 #include "precomp.h"
9 
10 WINE_DEFAULT_DEBUG_CHANNEL(shell);
11 
12 CCopyMoveToMenu::CCopyMoveToMenu() :
13     m_idCmdFirst(0),
14     m_idCmdLast(0),
15     m_idCmdAction(-1),
16     m_fnOldWndProc(NULL),
17     m_bIgnoreTextBoxChange(FALSE)
18 {
19 }
20 
21 static LRESULT CALLBACK
22 WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
23 {
24     WCHAR szPath[MAX_PATH];
25     CCopyMoveToMenu *this_ =
26         reinterpret_cast<CCopyMoveToMenu *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
27 
28     switch (uMsg)
29     {
30         case WM_COMMAND:
31         {
32             switch (LOWORD(wParam))
33             {
34                 case IDC_BROWSE_FOR_FOLDER_FOLDER_TEXT:
35                 {
36                     if (HIWORD(wParam) == EN_CHANGE)
37                     {
38                         if (!this_->m_bIgnoreTextBoxChange)
39                         {
40                             // get the text
41                             GetDlgItemTextW(hwnd, IDC_BROWSE_FOR_FOLDER_FOLDER_TEXT, szPath, _countof(szPath));
42                             StrTrimW(szPath, L" \t");
43 
44                             // update OK button
45                             BOOL bValid = !PathIsRelative(szPath) && PathIsDirectoryW(szPath);
46                             SendMessageW(hwnd, BFFM_ENABLEOK, 0, bValid);
47 
48                             return 0;
49                         }
50 
51                         // reset flag
52                         this_->m_bIgnoreTextBoxChange = FALSE;
53                     }
54                     break;
55                 }
56             }
57             break;
58         }
59     }
60     return CallWindowProcW(this_->m_fnOldWndProc, hwnd, uMsg, wParam, lParam);
61 }
62 
63 static INT CALLBACK
64 BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
65 {
66     CCopyMoveToMenu *this_ =
67         reinterpret_cast<CCopyMoveToMenu *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
68 
69     switch (uMsg)
70     {
71         case BFFM_INITIALIZED:
72         {
73             SetWindowLongPtr(hwnd, GWLP_USERDATA, lpData);
74             this_ = reinterpret_cast<CCopyMoveToMenu *>(lpData);
75 
76             // Select initial directory
77             SendMessageW(hwnd, BFFM_SETSELECTION, FALSE,
78                 reinterpret_cast<LPARAM>(static_cast<LPCITEMIDLIST>(this_->m_pidlFolder)));
79 
80             // Set caption
81             CString strCaption(MAKEINTRESOURCEW(this_->GetCaptionStringID()));
82             SetWindowTextW(hwnd, strCaption);
83 
84             // Set OK button text
85             CString strCopyOrMove(MAKEINTRESOURCEW(this_->GetButtonStringID()));
86             SetDlgItemText(hwnd, IDOK, strCopyOrMove);
87 
88             // Subclassing
89             this_->m_fnOldWndProc =
90                 reinterpret_cast<WNDPROC>(
91                     SetWindowLongPtr(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc)));
92 
93             // Disable OK
94             PostMessageW(hwnd, BFFM_ENABLEOK, 0, FALSE);
95             break;
96         }
97         case BFFM_SELCHANGED:
98         {
99             if (!this_)
100                 break;
101 
102             WCHAR szPath[MAX_PATH];
103             LPCITEMIDLIST pidl = reinterpret_cast<LPCITEMIDLIST>(lParam);
104 
105             szPath[0] = 0;
106             SHGetPathFromIDListW(pidl, szPath);
107 
108             if (ILIsEqual(pidl, this_->m_pidlFolder))
109                 PostMessageW(hwnd, BFFM_ENABLEOK, 0, FALSE);
110             else if (PathFileExistsW(szPath) || _ILIsDesktop(pidl))
111                 PostMessageW(hwnd, BFFM_ENABLEOK, 0, TRUE);
112             else
113                 PostMessageW(hwnd, BFFM_ENABLEOK, 0, FALSE);
114 
115             // the text box will be updated later soon, ignore it
116             this_->m_bIgnoreTextBoxChange = TRUE;
117             break;
118         }
119     }
120 
121     return FALSE;
122 }
123 
124 HRESULT
125 CCopyMoveToMenu::DoRealFileOp(LPCMINVOKECOMMANDINFO lpici, LPCITEMIDLIST pidl)
126 {
127     CDataObjectHIDA pCIDA(m_pDataObject);
128     if (FAILED_UNEXPECTEDLY(pCIDA.hr()))
129         return pCIDA.hr();
130 
131     PCUIDLIST_ABSOLUTE pidlParent = HIDA_GetPIDLFolder(pCIDA);
132     if (!pidlParent)
133     {
134         ERR("HIDA_GetPIDLFolder failed\n");
135         return E_FAIL;
136     }
137 
138     CStringW strFiles;
139     WCHAR szPath[MAX_PATH];
140     for (UINT n = 0; n < pCIDA->cidl; ++n)
141     {
142         PCUIDLIST_RELATIVE pidlRelative = HIDA_GetPIDLItem(pCIDA, n);
143         if (!pidlRelative)
144             continue;
145 
146         CComHeapPtr<ITEMIDLIST> pidlCombine(ILCombine(pidlParent, pidlRelative));
147         if (!pidl)
148             return E_FAIL;
149 
150         SHGetPathFromIDListW(pidlCombine, szPath);
151 
152         if (n > 0)
153             strFiles += L'|';
154         strFiles += szPath;
155     }
156 
157     strFiles += L'|'; // double null-terminated
158     strFiles.Replace(L'|', L'\0');
159 
160     if (_ILIsDesktop(pidl))
161         SHGetSpecialFolderPathW(NULL, szPath, CSIDL_DESKTOPDIRECTORY, FALSE);
162     else
163         SHGetPathFromIDListW(pidl, szPath);
164     INT cchPath = lstrlenW(szPath);
165     if (cchPath + 1 < MAX_PATH)
166     {
167         szPath[cchPath + 1] = 0; // double null-terminated
168     }
169     else
170     {
171         ERR("Too long path\n");
172         return E_FAIL;
173     }
174 
175     SHFILEOPSTRUCTW op = { lpici->hwnd, GetFileOp(), strFiles, szPath };
176     op.fFlags = FOF_ALLOWUNDO;
177     int res = SHFileOperationW(&op);
178     if (res)
179     {
180         ERR("SHFileOperationW failed with 0x%x\n", res);
181         return E_FAIL;
182     }
183     return S_OK;
184 }
185 
186 static HRESULT
187 DoGetFileTitle(CStringW& strTitle, IDataObject *pDataObject)
188 {
189     CDataObjectHIDA pCIDA(pDataObject);
190     if (FAILED_UNEXPECTEDLY(pCIDA.hr()))
191         return E_FAIL;
192 
193     PCUIDLIST_ABSOLUTE pidlParent = HIDA_GetPIDLFolder(pCIDA);
194     if (!pidlParent)
195     {
196         ERR("HIDA_GetPIDLFolder failed\n");
197         return E_FAIL;
198     }
199 
200     WCHAR szPath[MAX_PATH];
201     PCUIDLIST_RELATIVE pidlRelative = HIDA_GetPIDLItem(pCIDA, 0);
202     if (!pidlRelative)
203     {
204         ERR("HIDA_GetPIDLItem failed\n");
205         return E_FAIL;
206     }
207 
208     CComHeapPtr<ITEMIDLIST> pidlCombine(ILCombine(pidlParent, pidlRelative));
209 
210     if (!SHGetPathFromIDListW(pidlCombine, szPath))
211     {
212         ERR("Cannot get path\n");
213         return E_FAIL;
214     }
215 
216     strTitle = PathFindFileNameW(szPath);
217     if (strTitle.IsEmpty())
218         return E_FAIL;
219 
220     if (pCIDA->cidl > 1)
221         strTitle += L" ...";
222 
223     return S_OK;
224 }
225 
226 HRESULT CCopyMoveToMenu::DoAction(LPCMINVOKECOMMANDINFO lpici)
227 {
228     WCHAR wszPath[MAX_PATH];
229     HRESULT hr = E_FAIL;
230 
231     TRACE("(%p)\n", lpici);
232 
233     if (!SHGetPathFromIDListW(m_pidlFolder, wszPath))
234     {
235         ERR("SHGetPathFromIDListW failed\n");
236         return hr;
237     }
238 
239     CStringW strFileTitle;
240     hr = DoGetFileTitle(strFileTitle, m_pDataObject);
241     if (FAILED(hr))
242         return hr;
243 
244     CStringW strTitle;
245     strTitle.Format(GetActionTitleStringID(), static_cast<LPCWSTR>(strFileTitle));
246 
247     BROWSEINFOW info = { lpici->hwnd };
248     info.pidlRoot = NULL;
249     info.lpszTitle = strTitle;
250     info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI;
251     info.lpfn = BrowseCallbackProc;
252     info.lParam = reinterpret_cast<LPARAM>(this);
253     CComHeapPtr<ITEMIDLIST> pidl(SHBrowseForFolder(&info));
254     if (pidl)
255         hr = DoRealFileOp(lpici, pidl);
256 
257     return hr;
258 }
259 
260 STDMETHODIMP
261 CCopyToMenu::QueryContextMenu(HMENU hMenu,
262                               UINT indexMenu,
263                               UINT idCmdFirst,
264                               UINT idCmdLast,
265                               UINT uFlags)
266 {
267     MENUITEMINFOW mii;
268     UINT Count = 0;
269 
270     TRACE("CCopyToMenu::QueryContextMenu(%p, %u, %u, %u, %u)\n",
271           hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags);
272 
273     if (uFlags & (CMF_NOVERBS | CMF_VERBSONLY))
274         return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst);
275 
276     m_idCmdFirst = m_idCmdLast = idCmdFirst;
277 
278     // insert separator if necessary
279     ZeroMemory(&mii, sizeof(mii));
280     mii.cbSize = sizeof(mii);
281     mii.fMask = MIIM_TYPE;
282     if (GetMenuItemInfoW(hMenu, indexMenu - 1, TRUE, &mii) &&
283         mii.fType != MFT_SEPARATOR)
284     {
285         ZeroMemory(&mii, sizeof(mii));
286         mii.cbSize = sizeof(mii);
287         mii.fMask = MIIM_TYPE;
288         mii.fType = MFT_SEPARATOR;
289         if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
290         {
291             ++indexMenu;
292             ++Count;
293         }
294     }
295 
296     // insert "Copy to folder..."
297     CStringW strText(MAKEINTRESOURCEW(IDS_COPYTOMENU));
298     ZeroMemory(&mii, sizeof(mii));
299     mii.cbSize = sizeof(mii);
300     mii.fMask = MIIM_ID | MIIM_TYPE;
301     mii.fType = MFT_STRING;
302     mii.dwTypeData = strText.GetBuffer();
303     mii.cch = wcslen(mii.dwTypeData);
304     mii.wID = m_idCmdLast;
305     if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
306     {
307         m_idCmdAction = m_idCmdLast++;
308         ++indexMenu;
309         ++Count;
310     }
311 
312     return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst + Count);
313 }
314 
315 STDMETHODIMP
316 CMoveToMenu::QueryContextMenu(HMENU hMenu,
317                               UINT indexMenu,
318                               UINT idCmdFirst,
319                               UINT idCmdLast,
320                               UINT uFlags)
321 {
322     MENUITEMINFOW mii;
323     UINT Count = 0;
324 
325     TRACE("CMoveToMenu::QueryContextMenu(%p, %u, %u, %u, %u)\n",
326           hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags);
327 
328     if (uFlags & (CMF_NOVERBS | CMF_VERBSONLY))
329         return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst);
330 
331     m_idCmdFirst = m_idCmdLast = idCmdFirst;
332 
333     // insert separator if necessary
334     CStringW strCopyTo(MAKEINTRESOURCEW(IDS_COPYTOMENU));
335     WCHAR szBuff[128];
336     ZeroMemory(&mii, sizeof(mii));
337     mii.cbSize = sizeof(mii);
338     mii.fMask = MIIM_TYPE;
339     mii.dwTypeData = szBuff;
340     mii.cch = _countof(szBuff);
341     if (GetMenuItemInfoW(hMenu, indexMenu - 1, TRUE, &mii) &&
342         mii.fType != MFT_SEPARATOR &&
343         !(mii.fType == MFT_STRING && CStringW(szBuff) == strCopyTo))
344     {
345         ZeroMemory(&mii, sizeof(mii));
346         mii.cbSize = sizeof(mii);
347         mii.fMask = MIIM_TYPE;
348         mii.fType = MFT_SEPARATOR;
349         if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
350         {
351             ++indexMenu;
352             ++Count;
353         }
354     }
355 
356     // insert "Move to folder..."
357     CStringW strText(MAKEINTRESOURCEW(IDS_MOVETOMENU));
358     ZeroMemory(&mii, sizeof(mii));
359     mii.cbSize = sizeof(mii);
360     mii.fMask = MIIM_ID | MIIM_TYPE;
361     mii.fType = MFT_STRING;
362     mii.dwTypeData = strText.GetBuffer();
363     mii.cch = wcslen(mii.dwTypeData);
364     mii.wID = m_idCmdLast;
365     if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
366     {
367         m_idCmdAction = m_idCmdLast++;
368         ++indexMenu;
369         ++Count;
370     }
371 
372     return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst + Count);
373 }
374 
375 STDMETHODIMP
376 CCopyMoveToMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpici)
377 {
378     HRESULT hr = E_FAIL;
379     TRACE("CCopyMoveToMenu::InvokeCommand(%p)\n", lpici);
380 
381     if (IS_INTRESOURCE(lpici->lpVerb))
382     {
383         if (m_idCmdFirst + LOWORD(lpici->lpVerb) == m_idCmdAction)
384             hr = DoAction(lpici);
385     }
386     else
387     {
388         if (::lstrcmpiA(lpici->lpVerb, GetVerb()) == 0)
389             hr = DoAction(lpici);
390     }
391 
392     return hr;
393 }
394 
395 STDMETHODIMP
396 CCopyMoveToMenu::GetCommandString(
397     UINT_PTR idCmd,
398     UINT uType,
399     UINT *pwReserved,
400     LPSTR pszName,
401     UINT cchMax)
402 {
403     FIXME("%p %lu %u %p %p %u\n", this,
404           idCmd, uType, pwReserved, pszName, cchMax);
405 
406     return E_NOTIMPL;
407 }
408 
409 STDMETHODIMP
410 CCopyMoveToMenu::HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam)
411 {
412     TRACE("This %p uMsg %x\n", this, uMsg);
413     return E_NOTIMPL;
414 }
415 
416 STDMETHODIMP
417 CCopyMoveToMenu::Initialize(PCIDLIST_ABSOLUTE pidlFolder, IDataObject *pdtobj, HKEY hkeyProgID)
418 {
419     m_pidlFolder.Attach(ILClone(pidlFolder));
420     m_pDataObject = pdtobj;
421     return S_OK;
422 }
423 
424 STDMETHODIMP
425 CCopyMoveToMenu::SetSite(IUnknown *pUnkSite)
426 {
427     m_pSite = pUnkSite;
428     return S_OK;
429 }
430 
431 STDMETHODIMP
432 CCopyMoveToMenu::GetSite(REFIID riid, void **ppvSite)
433 {
434     if (!m_pSite)
435         return E_FAIL;
436 
437     return m_pSite->QueryInterface(riid, ppvSite);
438 }
439