/ src / modules / imageresizer / dll / ContextMenuHandler.cpp
ContextMenuHandler.cpp
  1  // ContextMenuHandler.cpp : Implementation of CContextMenuHandler
  2  
  3  #include "pch.h"
  4  #include "ContextMenuHandler.h"
  5  
  6  #include <Settings.h>
  7  #include <trace.h>
  8  
  9  #include <common/themes/icon_helpers.h>
 10  #include <common/utils/process_path.h>
 11  #include <common/utils/resources.h>
 12  #include <common/utils/HDropIterator.h>
 13  #include <common/utils/package.h>
 14  
 15  extern HINSTANCE g_hInst_imageResizer;
 16  
 17  CContextMenuHandler::CContextMenuHandler()
 18  {
 19      m_pidlFolder = NULL;
 20      m_pdtobj = NULL;
 21      context_menu_caption = GET_RESOURCE_STRING_FALLBACK(IDS_IMAGERESIZER_CONTEXT_MENU_ENTRY, L"Resize with Image Resizer");
 22      context_menu_caption_here = GET_RESOURCE_STRING_FALLBACK(IDS_IMAGERESIZER_CONTEXT_MENU_ENTRY_HERE, L"Resize with Image Resizer here");
 23  }
 24  
 25  CContextMenuHandler::~CContextMenuHandler()
 26  {
 27      Uninitialize();
 28  }
 29  
 30  void CContextMenuHandler::Uninitialize()
 31  {
 32      CoTaskMemFree((LPVOID)m_pidlFolder);
 33      m_pidlFolder = NULL;
 34  
 35      if (m_pdtobj)
 36      {
 37          m_pdtobj->Release();
 38          m_pdtobj = NULL;
 39      }
 40  }
 41  
 42  HRESULT CContextMenuHandler::Initialize(_In_opt_ PCIDLIST_ABSOLUTE pidlFolder, _In_opt_ IDataObject* pdtobj, _In_opt_ HKEY /*hkeyProgID*/)
 43  {
 44      Uninitialize();
 45  
 46      if (!CSettingsInstance().GetEnabled())
 47      {
 48          return E_FAIL;
 49      }
 50  
 51      if (pidlFolder)
 52      {
 53          m_pidlFolder = ILClone(pidlFolder);
 54      }
 55  
 56      if (pdtobj)
 57      {
 58          m_pdtobj = pdtobj;
 59          m_pdtobj->AddRef();
 60      }
 61  
 62      return S_OK;
 63  }
 64  
 65  HRESULT CContextMenuHandler::QueryContextMenu(_In_ HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT /*idCmdLast*/, UINT uFlags)
 66  {
 67      if (uFlags & CMF_DEFAULTONLY)
 68          return S_OK;
 69  
 70      if (!CSettingsInstance().GetEnabled())
 71          return E_FAIL;
 72  
 73      // NB: We just check the first item. We could iterate through more if the first one doesn't meet the criteria
 74      HDropIterator i(m_pdtobj);
 75      i.First();
 76      if (i.IsDone())
 77      {
 78          return S_OK;
 79      }
 80  
 81  // Suppressing C26812 warning as the issue is in the shtypes.h library
 82  #pragma warning(suppress : 26812)
 83      PERCEIVED type;
 84      PERCEIVEDFLAG flag;
 85      LPTSTR pszPath = i.CurrentItem();
 86      if (nullptr == pszPath)
 87      {
 88          // Avoid crashes in the following code.
 89          return E_FAIL;
 90      }
 91  
 92      LPTSTR pszExt = PathFindExtension(pszPath);
 93      if (nullptr == pszExt)
 94      {
 95          free(pszPath);
 96          // Avoid crashes in the following code.
 97          return E_FAIL;
 98      }
 99  
100      // TODO: Instead, detect whether there's a WIC codec installed that can handle this file
101      AssocGetPerceivedType(pszExt, &type, &flag, NULL);
102  
103      free(pszPath);
104      bool dragDropFlag = false;
105      // If selected file is an image...
106      if (type == PERCEIVED_TYPE_IMAGE)
107      {
108          HRESULT hr = E_UNEXPECTED;
109          wchar_t strResizePictures[128] = { 0 };
110          // If handling drag-and-drop...
111          if (m_pidlFolder)
112          {
113              dragDropFlag=true;
114              wcscpy_s(strResizePictures, ARRAYSIZE(strResizePictures), context_menu_caption_here.c_str());
115          }
116          else
117          {
118              wcscpy_s(strResizePictures, ARRAYSIZE(strResizePictures), context_menu_caption.c_str());
119          }
120  
121          MENUITEMINFO mii;
122          mii.cbSize = sizeof(MENUITEMINFO);
123          mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_STATE;
124          mii.wID = idCmdFirst + ID_RESIZE_PICTURES;
125          mii.fType = MFT_STRING;
126          mii.dwTypeData = (PWSTR)strResizePictures;
127          mii.fState = MFS_ENABLED;
128          HICON hIcon = static_cast<HICON>(LoadImage(g_hInst_imageResizer, MAKEINTRESOURCE(IDI_RESIZE_PICTURES), IMAGE_ICON, 16, 16, 0));
129          if (hIcon)
130          {
131              mii.fMask |= MIIM_BITMAP;
132              if (m_hbmpIcon == NULL)
133              {
134                  m_hbmpIcon = CreateBitmapFromIcon(hIcon);
135              }
136              mii.hbmpItem = m_hbmpIcon;
137              DestroyIcon(hIcon);
138          }
139  
140          if (dragDropFlag)
141          {
142              // Insert the menu entry at indexMenu+1 since the first entry should be "Copy here"
143              indexMenu++;
144          }
145          else
146          {
147              // indexMenu gets the first possible menu item index based on the location of the shellex registry key.
148              // If the registry entry is under SystemFileAssociations for the image formats, ShellImagePreview (in Windows by default) will be at indexMenu=0
149              // Shell ImagePreview consists of 4 menu items, a separator, Rotate right, Rotate left, and another separator
150              // Check if the entry at indexMenu is a separator, insert the new menu item at indexMenu+1 if true
151              MENUITEMINFO miiExisting;
152              miiExisting.dwTypeData = NULL;
153              miiExisting.fMask = MIIM_TYPE;
154              miiExisting.cbSize = sizeof(MENUITEMINFO);
155              GetMenuItemInfo(hmenu, indexMenu, TRUE, &miiExisting);
156              if (miiExisting.fType == MFT_SEPARATOR)
157              {
158                  indexMenu++;
159              }
160          }
161  
162          if (!InsertMenuItem(hmenu, indexMenu, TRUE, &mii))
163          {
164              m_etwTrace.UpdateState(true);
165  
166              hr = HRESULT_FROM_WIN32(GetLastError());
167              Trace::QueryContextMenuError(hr);
168  
169              m_etwTrace.Flush();
170              m_etwTrace.UpdateState(false);
171          }
172          else
173          {
174              hr = MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
175          }
176          return hr;
177      }
178  
179      return S_OK;
180  }
181  
182  HRESULT CContextMenuHandler::GetCommandString(UINT_PTR idCmd, UINT uType, _In_ UINT* /*pReserved*/, LPSTR pszName, UINT cchMax)
183  {
184      if (idCmd == ID_RESIZE_PICTURES)
185      {
186          if (uType == GCS_VERBW)
187          {
188              wcscpy_s(reinterpret_cast<LPWSTR>(pszName), cchMax, RESIZE_PICTURES_VERBW);
189          }
190      }
191      else
192      {
193          return E_INVALIDARG;
194      }
195  
196      return S_OK;
197  }
198  
199  HRESULT CContextMenuHandler::InvokeCommand(_In_ CMINVOKECOMMANDINFO* pici)
200  {
201      m_etwTrace.UpdateState(true);
202  
203      BOOL fUnicode = FALSE;
204      Trace::Invoked();
205      HRESULT hr = E_FAIL;
206      if (pici->cbSize == sizeof(CMINVOKECOMMANDINFOEX) && pici->fMask & CMIC_MASK_UNICODE)
207      {
208          fUnicode = TRUE;
209      }
210  
211      if (!fUnicode && HIWORD(pici->lpVerb))
212      {
213      }
214      else if (fUnicode && HIWORD(((CMINVOKECOMMANDINFOEX*)pici)->lpVerbW))
215      {
216          if (wcscmp((reinterpret_cast<CMINVOKECOMMANDINFOEX*>(pici))->lpVerbW, RESIZE_PICTURES_VERBW) == 0)
217          {
218              hr = ResizePictures(pici, nullptr);
219          }
220      }
221      else if (LOWORD(pici->lpVerb) == ID_RESIZE_PICTURES)
222      {
223          hr = ResizePictures(pici, nullptr);
224      }
225      Trace::InvokedRet(hr);
226  
227      m_etwTrace.Flush();
228      m_etwTrace.UpdateState(false);
229  
230      return hr;
231  }
232  
233  // This function is used for both MSI and MSIX. If pici is null and psiItemArray is not null then this is called by Invoke(MSIX). If pici is not null and psiItemArray is null then this is called by InvokeCommand(MSI).
234  HRESULT CContextMenuHandler::ResizePictures(CMINVOKECOMMANDINFO* pici, IShellItemArray* psiItemArray)
235  {
236      // Set the application path based on the location of the dll
237      std::wstring path = get_module_folderpath(g_hInst_imageResizer);
238      path = path + L"\\PowerToys.ImageResizer.exe";
239      LPTSTR lpApplicationName = &path[0];
240      // Create an anonymous pipe to stream filenames
241      SECURITY_ATTRIBUTES sa;
242      HANDLE hReadPipe;
243      HANDLE hWritePipe;
244      sa.nLength = sizeof(SECURITY_ATTRIBUTES);
245      sa.lpSecurityDescriptor = NULL;
246      sa.bInheritHandle = TRUE;
247      HRESULT hr = E_FAIL;
248      if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0))
249      {
250          hr = HRESULT_FROM_WIN32(GetLastError());
251          return hr;
252      }
253      if (!SetHandleInformation(hWritePipe, HANDLE_FLAG_INHERIT, 0))
254      {
255          hr = HRESULT_FROM_WIN32(GetLastError());
256          return hr;
257      }
258      CAtlFile writePipe(hWritePipe);
259  
260      CString commandLine;
261      commandLine.Format(_T("\"%s\""), lpApplicationName);
262  
263      // Set the output directory
264      if (m_pidlFolder)
265      {
266          TCHAR szFolder[MAX_PATH];
267          SHGetPathFromIDList(m_pidlFolder, szFolder);
268  
269          commandLine.AppendFormat(_T(" /d \"%s\""), szFolder);
270      }
271  
272      int nSize = commandLine.GetLength() + 1;
273      LPTSTR lpszCommandLine = new TCHAR[nSize];
274      _tcscpy_s(lpszCommandLine, nSize, commandLine);
275  
276      STARTUPINFO startupInfo;
277      ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
278      startupInfo.cb = sizeof(STARTUPINFO);
279      startupInfo.hStdInput = hReadPipe;
280      startupInfo.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
281      if (pici)
282      {
283          startupInfo.wShowWindow = static_cast<WORD>(pici->nShow);
284      }
285      else
286      {
287          startupInfo.wShowWindow = SW_SHOWNORMAL;
288      }
289  
290      PROCESS_INFORMATION processInformation;
291  
292      // Start the resizer
293      CreateProcess(
294          NULL,
295          lpszCommandLine,
296          NULL,
297          NULL,
298          TRUE,
299          0,
300          NULL,
301          NULL,
302          &startupInfo,
303          &processInformation);
304      delete[] lpszCommandLine;
305      if (!CloseHandle(processInformation.hProcess))
306      {
307          hr = HRESULT_FROM_WIN32(GetLastError());
308          return hr;
309      }
310      if (!CloseHandle(processInformation.hThread))
311      {
312          hr = HRESULT_FROM_WIN32(GetLastError());
313          return hr;
314      }
315  
316      // psiItemArray is NULL if called from InvokeCommand. This part is used for the MSI installer. It is not NULL if it is called from Invoke (MSIX).
317      if (!psiItemArray)
318      {
319          // Stream the input files
320          HDropIterator i(m_pdtobj);
321          for (i.First(); !i.IsDone(); i.Next())
322          {
323              CString fileName(i.CurrentItem());
324              fileName.Append(_T("\r\n"));
325  
326              writePipe.Write(fileName, fileName.GetLength() * sizeof(TCHAR));
327          }
328      }
329      else
330      {
331          //m_pdtobj will be NULL when invoked from the MSIX build as Initialize is never called (IShellExtInit functions aren't called in case of MSIX).
332          DWORD fileCount = 0;
333          // Gets the list of files currently selected using the IShellItemArray
334          psiItemArray->GetCount(&fileCount);
335          // Iterate over the list of files
336          for (DWORD i = 0; i < fileCount; i++)
337          {
338              IShellItem* shellItem;
339              psiItemArray->GetItemAt(i, &shellItem);
340              LPWSTR itemName;
341              // Retrieves the entire file system path of the file from its shell item
342              shellItem->GetDisplayName(SIGDN_FILESYSPATH, &itemName);
343              CString fileName(itemName);
344              fileName.Append(_T("\r\n"));
345              // Write the file path into the input stream for image resizer
346              writePipe.Write(fileName, fileName.GetLength() * sizeof(TCHAR));
347          }
348      }
349  
350      writePipe.Close();
351      hr = S_OK;
352      return hr;
353  }
354  
355  HRESULT __stdcall CContextMenuHandler::GetTitle(IShellItemArray* /*psiItemArray*/, LPWSTR* ppszName)
356  {
357      return SHStrDup(context_menu_caption.c_str(), ppszName);
358  }
359  
360  HRESULT __stdcall CContextMenuHandler::GetIcon(IShellItemArray* /*psiItemArray*/, LPWSTR* ppszIcon)
361  {
362      // Since ImageResizer is registered as a COM SurrogateServer the current module filename would be dllhost.exe. To get the icon we need the path of PowerToys.ImageResizerExt.dll, which can be obtained by passing the HINSTANCE of the dll
363      std::wstring iconResourcePath = get_module_filename(g_hInst_imageResizer);
364      iconResourcePath += L",-";
365      iconResourcePath += std::to_wstring(IDI_RESIZE_PICTURES);
366      return SHStrDup(iconResourcePath.c_str(), ppszIcon);
367  }
368  
369  HRESULT __stdcall CContextMenuHandler::GetToolTip(IShellItemArray* /*psiItemArray*/, LPWSTR* ppszInfotip)
370  {
371      *ppszInfotip = nullptr;
372      return E_NOTIMPL;
373  }
374  
375  HRESULT __stdcall CContextMenuHandler::GetCanonicalName(GUID* pguidCommandName)
376  {
377      *pguidCommandName = __uuidof(this);
378      return S_OK;
379  }
380  
381  HRESULT __stdcall CContextMenuHandler::GetState(IShellItemArray* psiItemArray, BOOL /*fOkToBeSlow*/, EXPCMDSTATE* pCmdState)
382  {
383      if (!CSettingsInstance().GetEnabled())
384      {
385          *pCmdState = ECS_HIDDEN;
386          return S_OK;
387      }
388      // Hide if the file is not an image
389      *pCmdState = ECS_HIDDEN;
390      // Suppressing C26812 warning as the issue is in the shtypes.h library
391  #pragma warning(suppress : 26812)
392      PERCEIVED type;
393      PERCEIVEDFLAG flag;
394      IShellItem* shellItem;
395  
396      //Check extension of first item in the list (the item which is right-clicked on)
397      HRESULT getItemAtResult = psiItemArray->GetItemAt(0, &shellItem);
398      if(!SUCCEEDED(getItemAtResult)) {
399          // Avoid crashes in the following code.
400          return E_FAIL;
401      }
402  
403      LPTSTR pszPath;
404      // Retrieves the entire file system path of the file from its shell item
405      HRESULT getDisplayResult = shellItem->GetDisplayName(SIGDN_FILESYSPATH, &pszPath);
406      if (S_OK != getDisplayResult || nullptr == pszPath)
407      {
408          // Avoid crashes in the following code.
409          return E_FAIL;
410      }
411  
412      LPTSTR pszExt = PathFindExtension(pszPath);
413      if (nullptr == pszExt)
414      {
415          CoTaskMemFree(pszPath);
416          // Avoid crashes in the following code.
417          return E_FAIL;
418      }
419  
420      // TODO: Instead, detect whether there's a WIC codec installed that can handle this file
421      AssocGetPerceivedType(pszExt, &type, &flag, NULL);
422  
423      CoTaskMemFree(pszPath);
424      // If selected file is an image...
425      if (type == PERCEIVED_TYPE_IMAGE)
426      {
427          *pCmdState = ECS_ENABLED;
428      }
429      return S_OK;
430  }
431  
432  HRESULT __stdcall CContextMenuHandler::GetFlags(EXPCMDFLAGS* pFlags)
433  {
434      *pFlags = ECF_DEFAULT;
435      return S_OK;
436  }
437  
438  HRESULT __stdcall CContextMenuHandler::EnumSubCommands(IEnumExplorerCommand** ppEnum)
439  {
440      *ppEnum = nullptr;
441      return E_NOTIMPL;
442  }
443  
444  // psiItemArray contains the list of files that have been selected when the context menu entry is invoked
445  HRESULT __stdcall CContextMenuHandler::Invoke(IShellItemArray* psiItemArray, IBindCtx* /*pbc*/)
446  {
447      m_etwTrace.UpdateState(true);
448  
449      Trace::Invoked();
450      HRESULT hr = ResizePictures(nullptr, psiItemArray);
451      Trace::InvokedRet(hr);
452  
453      m_etwTrace.Flush();
454      m_etwTrace.UpdateState(false);
455  
456      return hr;
457  }