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