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 }