2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
8 using System.Runtime.InteropServices;
9 using System.Runtime.InteropServices.ComTypes;
11 using Pithos.ShellExtensions.Properties;
13 namespace Pithos.ShellExtensions.Menus
15 [ClassInterface(ClassInterfaceType.None)]
16 [Guid("B1F1405D-94A1-4692-B72F-FC8CAF8B8700"), ComVisible(true)]
17 public class FileContextMenu : IShellExtInit, IContextMenu
19 private const string MenuHandlername = "CSShellExtContextMenuHandler.FileContextMenuExt";
22 private readonly Dictionary<string, MenuItem> _items;
26 public FileContext Context { get; set; }
28 private IntPtr _gotoBitmap=IntPtr.Zero;
29 private IntPtr _versionBitmap = IntPtr.Zero;
31 public FileContextMenu()
33 _gotoBitmap = GetBitmapPtr(Resources.MenuGoToPithos);
34 _versionBitmap = GetBitmapPtr(Resources.MenuHistory);
39 _items = new Dictionary<string, MenuItem>{
40 {"gotoPithos",new MenuItem{
41 MenuText = "&Go to Pithos",
43 VerbCanonicalName = "PITHOSGoTo",
44 VerbHelpText = "Go to Pithos",
46 MenuCommand=OnGotoPithos,
47 DisplayFlags=DisplayFlags.All,
48 MenuBitmap = _gotoBitmap
50 {"prevVersions",new MenuItem{
51 MenuText = "&Show Previous Versions",
52 Verb = "prevVersions",
53 VerbCanonicalName = "PITHOSPrevVersions",
54 VerbHelpText = "Go to Pithos and display previous versions",
56 MenuCommand=OnVerbDisplayFileName,
57 DisplayFlags=DisplayFlags.File,
58 MenuBitmap=_versionBitmap
62 IoC.Current.Compose(this);
70 if (_gotoBitmap != IntPtr.Zero)
72 NativeMethods.DeleteObject(_gotoBitmap);
73 _gotoBitmap= IntPtr.Zero;
75 if (_versionBitmap != IntPtr.Zero)
77 NativeMethods.DeleteObject(_versionBitmap);
78 _versionBitmap = IntPtr.Zero;
82 private static IntPtr GetBitmapPtr(Bitmap gotoBitmap)
84 gotoBitmap.MakeTransparent(gotoBitmap.GetPixel(0, 0));
85 var hbitmap = gotoBitmap.GetHbitmap();
90 void OnVerbDisplayFileName(IntPtr hWnd)
92 string message = String.Format("The selected file is {0}\r\n\r\nThe selected Path is {1}",
94 Context.CurrentFolder);
96 System.Windows.Forms.MessageBox.Show(
98 "CSShellExtContextMenuHandler");
99 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_ASSOCCHANGED, HChangeNotifyFlags.SHCNF_IDLIST,
100 IntPtr.Zero, IntPtr.Zero);
103 void OnGotoPithos(IntPtr hWnd)
105 Context.Settings.Reload();
106 Process.Start(Context.Settings.PithosSite);
110 #region Shell Extension Registration
112 [ComRegisterFunction]
113 public static void Register(Type t)
117 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, ".cs",
119 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "Directory",
121 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, @"Directory\Background",
123 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "*",
126 //ShellExtReg.MarkApproved(t.GUID, MenuHandlername);
130 Console.WriteLine(ex.Message); // Log the error
131 throw; // Re-throw the exception
135 [ComUnregisterFunction]
136 public static void Unregister(Type t)
140 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, ".cs", MenuHandlername);
141 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "Directory", MenuHandlername);
142 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, @"Directory\Background", MenuHandlername);
143 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "*", MenuHandlername);
145 //ShellExtReg.RemoveApproved(t.GUID, MenuHandlername);
149 Console.WriteLine(ex.Message); // Log the error
150 throw; // Re-throw the exception
157 #region IShellExtInit Members
160 /// Initialize the context menu handler.
162 /// <param name="pidlFolder">
163 /// A pointer to an ITEMIDLIST structure that uniquely identifies a folder.
165 /// <param name="pDataObj">
166 /// A pointer to an IDataObject interface object that can be used to retrieve
167 /// the objects being acted upon.
169 /// <param name="hKeyProgID">
170 /// The registry key for the file object or folder type.
172 public void Initialize(IntPtr pidlFolder, IntPtr pDataObj, IntPtr hKeyProgID)
175 if(pDataObj == IntPtr.Zero && pidlFolder == IntPtr.Zero)
177 throw new ArgumentException("pidlFolder and pDataObj shouldn't be null at the same time");
181 Debug.WriteLine("Initializing", LogCategories.ShellMenu);
183 if (pDataObj != IntPtr.Zero)
185 Debug.WriteLine("Got a data object", LogCategories.ShellMenu);
187 FORMATETC fe = new FORMATETC();
188 fe.cfFormat = (short)CLIPFORMAT.CF_HDROP;
189 fe.ptd = IntPtr.Zero;
190 fe.dwAspect = DVASPECT.DVASPECT_CONTENT;
192 fe.tymed = TYMED.TYMED_HGLOBAL;
193 STGMEDIUM stm = new STGMEDIUM();
195 // The pDataObj pointer contains the objects being acted upon. In this
196 // example, we get an HDROP handle for enumerating the selected files
198 IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(pDataObj);
199 dataObject.GetData(ref fe, out stm);
203 // Get an HDROP handle.
204 IntPtr hDrop = stm.unionmember;
205 if (hDrop == IntPtr.Zero)
207 throw new ArgumentException();
210 // Determine how many files are involved in this operation.
211 uint nFiles = NativeMethods.DragQueryFile(hDrop, UInt32.MaxValue, null, 0);
213 Debug.WriteLine(String.Format("Got {0} files", nFiles), LogCategories.ShellMenu);
214 // This code sample displays the custom context menu item when only
215 // one file is selected.
218 // Get the path of the file.
219 var fileName = new StringBuilder(260);
220 if (0 == NativeMethods.DragQueryFile(hDrop, 0, fileName,
223 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
225 Context.CurrentFile = fileName.ToString();
229 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
234 // Enumerate the selected files and folders.
237 // StringCollection selectedFiles = new StringCollection();
238 // StringBuilder fileName = new StringBuilder(260);
239 // for (uint i = 0; i < nFiles; i++)
241 // // Get the next file name.
242 // if (0 != NativeMethods.DragQueryFile(hDrop, i, fileName,
243 // fileName.Capacity))
245 // // Add the file name to the list.
246 // selectedFiles.Add(fileName.ToString());
250 // // If we did not find any files we can work with, throw
252 // if (selectedFiles.Count == 0)
254 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
259 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
264 NativeMethods.ReleaseStgMedium(ref stm);
268 if (pidlFolder != IntPtr.Zero)
270 Debug.WriteLine("Got a folder", LogCategories.ShellMenu);
271 StringBuilder path = new StringBuilder();
272 if (!NativeMethods.SHGetPathFromIDList(pidlFolder, path))
274 int error = Marshal.GetHRForLastWin32Error();
275 Marshal.ThrowExceptionForHR(error);
277 Context.CurrentFolder = path.ToString();
278 Debug.WriteLine(String.Format("Folder is {0}", Context.CurrentFolder), LogCategories.ShellMenu);
285 #region IContextMenu Members
288 /// Add commands to a shortcut menu.
290 /// <param name="hMenu">A handle to the shortcut menu.</param>
291 /// <param name="iMenu">
292 /// The zero-based position at which to insert the first new menu item.
294 /// <param name="idCmdFirst">
295 /// The minimum value that the handler can specify for a menu item ID.
297 /// <param name="idCmdLast">
298 /// The maximum value that the handler can specify for a menu item ID.
300 /// <param name="uFlags">
301 /// Optional flags that specify how the shortcut menu can be changed.
304 /// If successful, returns an HRESULT value that has its severity value set
305 /// to SEVERITY_SUCCESS and its code value set to the offset of the largest
306 /// command identifier that was assigned, plus one.
308 public int QueryContextMenu(
315 Debug.WriteLine("Start qcm", LogCategories.ShellMenu);
316 // If uFlags include CMF_DEFAULTONLY then we should not do anything.
317 Debug.WriteLine(String.Format("Flags {0}", uFlags), LogCategories.ShellMenu);
319 if (((uint)CMF.CMF_DEFAULTONLY & uFlags) != 0)
321 Debug.WriteLine("Default only flag, returning", LogCategories.ShellMenu);
322 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
325 if (!Context.IsManaged)
327 Debug.WriteLine("Not a PITHOS folder",LogCategories.ShellMenu);
328 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
332 if (!selectedFolder.ToLower().Contains("pithos"))
333 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
336 // Use either InsertMenu or InsertMenuItem to add menu items.
340 DisplayFlags itemType = (Context.IsFolder) ? DisplayFlags.Folder : DisplayFlags.File;
342 Debug.WriteLine(String.Format("Item Flags {0}", itemType), LogCategories.ShellMenu);
344 foreach (var menuItem in _items.Values)
346 Debug.WriteLine(String.Format("Menu Flags {0}", menuItem.DisplayFlags), LogCategories.ShellMenu);
347 if ((itemType & menuItem.DisplayFlags) != DisplayFlags.None)
349 Debug.WriteLine("Adding Menu", LogCategories.ShellMenu);
351 MENUITEMINFO mii = menuItem.CreateInfo(idCmdFirst);
352 if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref mii))
354 var lastError = Marshal.GetLastWin32Error();
355 var lastErrorHR = Marshal.GetHRForLastWin32Error();
358 if (largestID < menuItem.MenuDisplayId)
359 largestID = menuItem.MenuDisplayId;
363 Debug.Write("Adding Separator 1", LogCategories.ShellMenu);
365 MENUITEMINFO sep = new MENUITEMINFO();
366 sep.cbSize = (uint)Marshal.SizeOf(sep);
367 sep.fMask = MIIM.MIIM_TYPE;
368 sep.fType = MFT.MFT_SEPARATOR;
369 if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref sep))
371 Trace.TraceError("Error adding separator 1\r\n{0}", Marshal.GetLastWin32Error());
372 return Marshal.GetHRForLastWin32Error();
378 Debug.WriteLine("Menus added", LogCategories.ShellOverlays);
379 // Return an HRESULT value with the severity set to SEVERITY_SUCCESS.
380 // Set the code value to the offset of the largest command identifier
381 // that was assigned, plus one (1).
382 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0,
387 /// Carry out the command associated with a shortcut menu item.
389 /// <param name="pici">
390 /// A pointer to a CMINVOKECOMMANDINFO or CMINVOKECOMMANDINFOEX structure
391 /// containing information about the command.
393 public void InvokeCommand(IntPtr pici)
395 bool isUnicode = false;
397 // Determine which structure is being passed in, CMINVOKECOMMANDINFO or
398 // CMINVOKECOMMANDINFOEX based on the cbSize member of lpcmi. Although
399 // the lpcmi parameter is declared in Shlobj.h as a CMINVOKECOMMANDINFO
400 // structure, in practice it often points to a CMINVOKECOMMANDINFOEX
401 // structure. This struct is an extended version of CMINVOKECOMMANDINFO
402 // and has additional members that allow Unicode strings to be passed.
403 CMINVOKECOMMANDINFO ici = (CMINVOKECOMMANDINFO)Marshal.PtrToStructure(
404 pici, typeof(CMINVOKECOMMANDINFO));
405 CMINVOKECOMMANDINFOEX iciex = new CMINVOKECOMMANDINFOEX();
406 if (ici.cbSize == Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX)))
408 if ((ici.fMask & CMIC.CMIC_MASK_UNICODE) != 0)
411 iciex = (CMINVOKECOMMANDINFOEX)Marshal.PtrToStructure(pici,
412 typeof(CMINVOKECOMMANDINFOEX));
416 // Determines whether the command is identified by its offset or verb.
417 // There are two ways to identify commands:
419 // 1) The command's verb string
420 // 2) The command's identifier offset
422 // If the high-order word of lpcmi->lpVerb (for the ANSI case) or
423 // lpcmi->lpVerbW (for the Unicode case) is nonzero, lpVerb or lpVerbW
424 // holds a verb string. If the high-order word is zero, the command
425 // offset is in the low-order word of lpcmi->lpVerb.
427 // For the ANSI case, if the high-order word is not zero, the command's
428 // verb string is in lpcmi->lpVerb.
429 if (!isUnicode && NativeMethods.HighWord(ici.verb.ToInt32()) != 0)
431 // Is the verb supported by this context menu extension?
432 string verbAnsi = Marshal.PtrToStringAnsi(ici.verb);
433 if (_items.ContainsKey(verbAnsi))
435 _items[verbAnsi].MenuCommand(ici.hwnd);
439 // If the verb is not recognized by the context menu handler, it
440 // must return E_FAIL to allow it to be passed on to the other
441 // context menu handlers that might implement that verb.
442 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
446 // For the Unicode case, if the high-order word is not zero, the
447 // command's verb string is in lpcmi->lpVerbW.
448 else if (isUnicode && NativeMethods.HighWord(iciex.verbW.ToInt32()) != 0)
450 // Is the verb supported by this context menu extension?
451 string verbUTF = Marshal.PtrToStringUni(iciex.verbW);
452 if (_items.ContainsKey(verbUTF))
454 _items[verbUTF].MenuCommand(ici.hwnd);
458 // If the verb is not recognized by the context menu handler, it
459 // must return E_FAIL to allow it to be passed on to the other
460 // context menu handlers that might implement that verb.
461 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
465 // If the command cannot be identified through the verb string, then
466 // check the identifier offset.
469 // Is the command identifier offset supported by this context menu
471 int menuID = NativeMethods.LowWord(ici.verb.ToInt32());
472 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
473 if (menuItem != null)
475 menuItem.MenuCommand(ici.hwnd);
479 // If the verb is not recognized by the context menu handler, it
480 // must return E_FAIL to allow it to be passed on to the other
481 // context menu handlers that might implement that verb.
482 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
488 /// Get information about a shortcut menu command, including the help string
489 /// and the language-independent, or canonical, name for the command.
491 /// <param name="idCmd">Menu command identifier offset.</param>
492 /// <param name="uFlags">
493 /// Flags specifying the information to return. This parameter can have one
494 /// of the following values: GCS_HELPTEXTA, GCS_HELPTEXTW, GCS_VALIDATEA,
495 /// GCS_VALIDATEW, GCS_VERBA, GCS_VERBW.
497 /// <param name="pReserved">Reserved. Must be IntPtr.Zero</param>
498 /// <param name="pszName">
499 /// The address of the buffer to receive the null-terminated string being
502 /// <param name="cchMax">
503 /// Size of the buffer, in characters, to receive the null-terminated string.
505 public void GetCommandString(
509 StringBuilder pszName,
512 uint menuID = idCmd.ToUInt32();
513 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
514 if (menuItem != null)
519 if (menuItem.VerbCanonicalName.Length > cchMax - 1)
521 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
526 pszName.Append(menuItem.VerbCanonicalName);
530 case GCS.GCS_HELPTEXTW:
531 if (menuItem.VerbHelpText.Length > cchMax - 1)
533 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
538 pszName.Append(menuItem.VerbHelpText);