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 static BOOL
261 GetPreviousMenuItemInfo(HMENU hMenu, UINT iItem, LPMENUITEMINFOW lpmii)
262 {
263     BOOL bSuccess = FALSE;
264 
265     while (iItem > 0)
266     {
267         bSuccess = GetMenuItemInfoW(hMenu, --iItem, TRUE, lpmii);
268         if (bSuccess || (!bSuccess && GetLastError() != ERROR_MENU_ITEM_NOT_FOUND))
269             break;
270     }
271 
272     return bSuccess;
273 }
274 
275 STDMETHODIMP
276 CCopyToMenu::QueryContextMenu(HMENU hMenu,
277                               UINT indexMenu,
278                               UINT idCmdFirst,
279                               UINT idCmdLast,
280                               UINT uFlags)
281 {
282     MENUITEMINFOW mii;
283     UINT Count = 0;
284 
285     TRACE("CCopyToMenu::QueryContextMenu(%p, %u, %u, %u, %u)\n",
286           hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags);
287 
288     if (uFlags & (CMF_NOVERBS | CMF_VERBSONLY))
289         return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst);
290 
291     m_idCmdFirst = m_idCmdLast = idCmdFirst;
292 
293     // insert separator if necessary
294     ZeroMemory(&mii, sizeof(mii));
295     mii.cbSize = sizeof(mii);
296     mii.fMask = MIIM_TYPE;
297     if (GetPreviousMenuItemInfo(hMenu, indexMenu, &mii) &&
298         mii.fType != MFT_SEPARATOR)
299     {
300         ZeroMemory(&mii, sizeof(mii));
301         mii.cbSize = sizeof(mii);
302         mii.fMask = MIIM_TYPE;
303         mii.fType = MFT_SEPARATOR;
304         if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
305         {
306             ++indexMenu;
307             ++Count;
308         }
309     }
310 
311     // insert "Copy to folder..."
312     CStringW strText(MAKEINTRESOURCEW(IDS_COPYTOMENU));
313     ZeroMemory(&mii, sizeof(mii));
314     mii.cbSize = sizeof(mii);
315     mii.fMask = MIIM_ID | MIIM_TYPE;
316     mii.fType = MFT_STRING;
317     mii.dwTypeData = strText.GetBuffer();
318     mii.cch = wcslen(mii.dwTypeData);
319     mii.wID = m_idCmdLast;
320     if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
321     {
322         m_idCmdAction = m_idCmdLast++;
323         ++indexMenu;
324         ++Count;
325     }
326 
327     return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst + Count);
328 }
329 
330 STDMETHODIMP
331 CMoveToMenu::QueryContextMenu(HMENU hMenu,
332                               UINT indexMenu,
333                               UINT idCmdFirst,
334                               UINT idCmdLast,
335                               UINT uFlags)
336 {
337     MENUITEMINFOW mii;
338     UINT Count = 0;
339 
340     TRACE("CMoveToMenu::QueryContextMenu(%p, %u, %u, %u, %u)\n",
341           hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags);
342 
343     if (uFlags & (CMF_NOVERBS | CMF_VERBSONLY))
344         return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst);
345 
346     m_idCmdFirst = m_idCmdLast = idCmdFirst;
347 
348     // insert separator if necessary
349     CStringW strCopyTo(MAKEINTRESOURCEW(IDS_COPYTOMENU));
350     WCHAR szBuff[128];
351     ZeroMemory(&mii, sizeof(mii));
352     mii.cbSize = sizeof(mii);
353     mii.fMask = MIIM_TYPE;
354     mii.dwTypeData = szBuff;
355     mii.cch = _countof(szBuff);
356     if (GetPreviousMenuItemInfo(hMenu, indexMenu, &mii) &&
357         mii.fType != MFT_SEPARATOR &&
358         !(mii.fType == MFT_STRING && CStringW(szBuff) == strCopyTo))
359     {
360         ZeroMemory(&mii, sizeof(mii));
361         mii.cbSize = sizeof(mii);
362         mii.fMask = MIIM_TYPE;
363         mii.fType = MFT_SEPARATOR;
364         if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
365         {
366             ++indexMenu;
367             ++Count;
368         }
369     }
370 
371     // insert "Move to folder..."
372     CStringW strText(MAKEINTRESOURCEW(IDS_MOVETOMENU));
373     ZeroMemory(&mii, sizeof(mii));
374     mii.cbSize = sizeof(mii);
375     mii.fMask = MIIM_ID | MIIM_TYPE;
376     mii.fType = MFT_STRING;
377     mii.dwTypeData = strText.GetBuffer();
378     mii.cch = wcslen(mii.dwTypeData);
379     mii.wID = m_idCmdLast;
380     if (InsertMenuItemW(hMenu, indexMenu, TRUE, &mii))
381     {
382         m_idCmdAction = m_idCmdLast++;
383         ++indexMenu;
384         ++Count;
385     }
386 
387     return MAKE_HRESULT(SEVERITY_SUCCESS, 0, idCmdFirst + Count);
388 }
389 
390 STDMETHODIMP
391 CCopyMoveToMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpici)
392 {
393     HRESULT hr = E_FAIL;
394     TRACE("CCopyMoveToMenu::InvokeCommand(%p)\n", lpici);
395 
396     if (IS_INTRESOURCE(lpici->lpVerb))
397     {
398         if (m_idCmdFirst + LOWORD(lpici->lpVerb) == m_idCmdAction)
399             hr = DoAction(lpici);
400     }
401     else
402     {
403         if (::lstrcmpiA(lpici->lpVerb, GetVerb()) == 0)
404             hr = DoAction(lpici);
405     }
406 
407     return hr;
408 }
409 
410 STDMETHODIMP
411 CCopyMoveToMenu::GetCommandString(
412     UINT_PTR idCmd,
413     UINT uType,
414     UINT *pwReserved,
415     LPSTR pszName,
416     UINT cchMax)
417 {
418     FIXME("%p %lu %u %p %p %u\n", this,
419           idCmd, uType, pwReserved, pszName, cchMax);
420 
421     return E_NOTIMPL;
422 }
423 
424 STDMETHODIMP
425 CCopyMoveToMenu::HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam)
426 {
427     TRACE("This %p uMsg %x\n", this, uMsg);
428     return E_NOTIMPL;
429 }
430 
431 STDMETHODIMP
432 CCopyMoveToMenu::Initialize(PCIDLIST_ABSOLUTE pidlFolder, IDataObject *pdtobj, HKEY hkeyProgID)
433 {
434     m_pidlFolder.Attach(ILClone(pidlFolder));
435     m_pDataObject = pdtobj;
436     return S_OK;
437 }
438 
439 STDMETHODIMP
440 CCopyMoveToMenu::SetSite(IUnknown *pUnkSite)
441 {
442     m_pSite = pUnkSite;
443     return S_OK;
444 }
445 
446 STDMETHODIMP
447 CCopyMoveToMenu::GetSite(REFIID riid, void **ppvSite)
448 {
449     if (!m_pSite)
450         return E_FAIL;
451 
452     return m_pSite->QueryInterface(riid, ppvSite);
453 }
454