2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
7 using System.Runtime.InteropServices;
8 using System.Runtime.InteropServices.ComTypes;
11 namespace Pithos.ShellExtensions.Menus
13 [ClassInterface(ClassInterfaceType.None)]
14 [Guid("B1F1405D-94A1-4692-B72F-FC8CAF8B8700"), ComVisible(true)]
15 public class FileContextMenu : IShellExtInit, IContextMenu
17 private const string MenuHandlername = "CSShellExtContextMenuHandler.FileContextMenuExt";
20 private readonly Dictionary<string, MenuItem> _items;
23 private FileContext _context =new FileContext();
24 public FileContext Context
26 get { return _context; }
27 set { _context = value; }
30 public FileContextMenu()
32 _items = new Dictionary<string, MenuItem>{
33 {"gotoPithos",new MenuItem{
34 MenuText = "&Go to Pithos",
36 VerbCanonicalName = "PITHOSGoTo",
37 VerbHelpText = "Go to Pithos",
39 MenuCommand=OnVerbDisplayFileName,
40 DisplayFlags=DisplayFlags.All
42 {"prevVersions",new MenuItem{
43 MenuText = "&Show Previous Versions",
44 Verb = "prevVersions",
45 VerbCanonicalName = "PITHOSPrevVersions",
46 VerbHelpText = "Go to Pithos and display previous versions",
48 MenuCommand=OnVerbDisplayFileName,
49 DisplayFlags=DisplayFlags.File
57 void OnVerbDisplayFileName(IntPtr hWnd)
59 string message = String.Format("The selected file is {0}\r\n\r\nThe selected Path is {1}",
61 Context.CurrentFolder);
63 System.Windows.Forms.MessageBox.Show(
65 "CSShellExtContextMenuHandler");
66 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_ASSOCCHANGED, HChangeNotifyFlags.SHCNF_IDLIST,
67 IntPtr.Zero, IntPtr.Zero);
71 #region Shell Extension Registration
74 public static void Register(Type t)
78 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, ".cs",
80 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "Directory",
82 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, @"Directory\Background",
84 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "*",
87 //ShellExtReg.MarkApproved(t.GUID, MenuHandlername);
91 Console.WriteLine(ex.Message); // Log the error
92 throw; // Re-throw the exception
96 [ComUnregisterFunction]
97 public static void Unregister(Type t)
101 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, ".cs", MenuHandlername);
102 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "Directory", MenuHandlername);
103 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, @"Directory\Background", MenuHandlername);
104 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "*", MenuHandlername);
106 //ShellExtReg.RemoveApproved(t.GUID, MenuHandlername);
110 Console.WriteLine(ex.Message); // Log the error
111 throw; // Re-throw the exception
118 #region IShellExtInit Members
121 /// Initialize the context menu handler.
123 /// <param name="pidlFolder">
124 /// A pointer to an ITEMIDLIST structure that uniquely identifies a folder.
126 /// <param name="pDataObj">
127 /// A pointer to an IDataObject interface object that can be used to retrieve
128 /// the objects being acted upon.
130 /// <param name="hKeyProgID">
131 /// The registry key for the file object or folder type.
133 public void Initialize(IntPtr pidlFolder, IntPtr pDataObj, IntPtr hKeyProgID)
136 if(pDataObj == IntPtr.Zero && pidlFolder == IntPtr.Zero)
138 throw new ArgumentException("pidlFolder and pDataObj shouldn't be null at the same time");
142 Trace.Write("Initializing");
144 if (pDataObj != IntPtr.Zero)
146 Trace.Write("Got a data object");
148 FORMATETC fe = new FORMATETC();
149 fe.cfFormat = (short)CLIPFORMAT.CF_HDROP;
150 fe.ptd = IntPtr.Zero;
151 fe.dwAspect = DVASPECT.DVASPECT_CONTENT;
153 fe.tymed = TYMED.TYMED_HGLOBAL;
154 STGMEDIUM stm = new STGMEDIUM();
156 // The pDataObj pointer contains the objects being acted upon. In this
157 // example, we get an HDROP handle for enumerating the selected files
159 IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(pDataObj);
160 dataObject.GetData(ref fe, out stm);
164 // Get an HDROP handle.
165 IntPtr hDrop = stm.unionmember;
166 if (hDrop == IntPtr.Zero)
168 throw new ArgumentException();
171 // Determine how many files are involved in this operation.
172 uint nFiles = NativeMethods.DragQueryFile(hDrop, UInt32.MaxValue, null, 0);
174 Trace.Write(String.Format("Got {0} files", nFiles));
175 // This code sample displays the custom context menu item when only
176 // one file is selected.
179 // Get the path of the file.
180 var fileName = new StringBuilder(260);
181 if (0 == NativeMethods.DragQueryFile(hDrop, 0, fileName,
184 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
186 Context.CurrentFile = fileName.ToString();
190 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
195 // Enumerate the selected files and folders.
198 // StringCollection selectedFiles = new StringCollection();
199 // StringBuilder fileName = new StringBuilder(260);
200 // for (uint i = 0; i < nFiles; i++)
202 // // Get the next file name.
203 // if (0 != NativeMethods.DragQueryFile(hDrop, i, fileName,
204 // fileName.Capacity))
206 // // Add the file name to the list.
207 // selectedFiles.Add(fileName.ToString());
211 // // If we did not find any files we can work with, throw
213 // if (selectedFiles.Count == 0)
215 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
220 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
225 NativeMethods.ReleaseStgMedium(ref stm);
229 if (pidlFolder != IntPtr.Zero)
231 Trace.Write("Got a folder");
232 StringBuilder path = new StringBuilder();
233 if (!NativeMethods.SHGetPathFromIDList(pidlFolder, path))
235 int error = Marshal.GetHRForLastWin32Error();
236 Marshal.ThrowExceptionForHR(error);
238 Context.CurrentFolder = path.ToString();
239 Trace.Write(String.Format("Folder is {0}", Context.CurrentFolder));
246 #region IContextMenu Members
249 /// Add commands to a shortcut menu.
251 /// <param name="hMenu">A handle to the shortcut menu.</param>
252 /// <param name="iMenu">
253 /// The zero-based position at which to insert the first new menu item.
255 /// <param name="idCmdFirst">
256 /// The minimum value that the handler can specify for a menu item ID.
258 /// <param name="idCmdLast">
259 /// The maximum value that the handler can specify for a menu item ID.
261 /// <param name="uFlags">
262 /// Optional flags that specify how the shortcut menu can be changed.
265 /// If successful, returns an HRESULT value that has its severity value set
266 /// to SEVERITY_SUCCESS and its code value set to the offset of the largest
267 /// command identifier that was assigned, plus one.
269 public int QueryContextMenu(
276 Trace.Write("Start qcm");
277 // If uFlags include CMF_DEFAULTONLY then we should not do anything.
278 Trace.Write(String.Format("Flags {0}", uFlags));
280 if (((uint)CMF.CMF_DEFAULTONLY & uFlags) != 0)
282 Trace.Write("Default only flag, returning");
283 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
286 if (!Context.IsManaged)
288 Trace.Write("Not a PITHOS folder");
289 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
293 if (!selectedFolder.ToLower().Contains("pithos"))
294 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
297 // Use either InsertMenu or InsertMenuItem to add menu items.
301 DisplayFlags itemType = (Context.IsFolder) ? DisplayFlags.Folder : DisplayFlags.File;
303 Trace.Write(String.Format("Item Flags {0}", itemType));
305 foreach (var menuItem in _items.Values)
307 Trace.Write(String.Format("Menu Flags {0}", menuItem.DisplayFlags));
308 if ((itemType & menuItem.DisplayFlags) != DisplayFlags.None)
310 Trace.Write("Adding Menu");
312 MENUITEMINFO mii = menuItem.CreateInfo(idCmdFirst);
313 if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref mii))
315 return Marshal.GetHRForLastWin32Error();
317 if (largestID < menuItem.MenuDisplayId)
318 largestID = menuItem.MenuDisplayId;
322 Trace.Write("Adding Separator 1");
324 MENUITEMINFO sep = new MENUITEMINFO();
325 sep.cbSize = (uint)Marshal.SizeOf(sep);
326 sep.fMask = MIIM.MIIM_TYPE;
327 sep.fType = MFT.MFT_SEPARATOR;
328 if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref sep))
330 Trace.TraceError("Error adding separator 1\r\n{0}", Marshal.GetLastWin32Error());
331 return Marshal.GetHRForLastWin32Error();
337 Trace.Write("Menus added");
338 // Return an HRESULT value with the severity set to SEVERITY_SUCCESS.
339 // Set the code value to the offset of the largest command identifier
340 // that was assigned, plus one (1).
341 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0,
346 /// Carry out the command associated with a shortcut menu item.
348 /// <param name="pici">
349 /// A pointer to a CMINVOKECOMMANDINFO or CMINVOKECOMMANDINFOEX structure
350 /// containing information about the command.
352 public void InvokeCommand(IntPtr pici)
354 bool isUnicode = false;
356 // Determine which structure is being passed in, CMINVOKECOMMANDINFO or
357 // CMINVOKECOMMANDINFOEX based on the cbSize member of lpcmi. Although
358 // the lpcmi parameter is declared in Shlobj.h as a CMINVOKECOMMANDINFO
359 // structure, in practice it often points to a CMINVOKECOMMANDINFOEX
360 // structure. This struct is an extended version of CMINVOKECOMMANDINFO
361 // and has additional members that allow Unicode strings to be passed.
362 CMINVOKECOMMANDINFO ici = (CMINVOKECOMMANDINFO)Marshal.PtrToStructure(
363 pici, typeof(CMINVOKECOMMANDINFO));
364 CMINVOKECOMMANDINFOEX iciex = new CMINVOKECOMMANDINFOEX();
365 if (ici.cbSize == Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX)))
367 if ((ici.fMask & CMIC.CMIC_MASK_UNICODE) != 0)
370 iciex = (CMINVOKECOMMANDINFOEX)Marshal.PtrToStructure(pici,
371 typeof(CMINVOKECOMMANDINFOEX));
375 // Determines whether the command is identified by its offset or verb.
376 // There are two ways to identify commands:
378 // 1) The command's verb string
379 // 2) The command's identifier offset
381 // If the high-order word of lpcmi->lpVerb (for the ANSI case) or
382 // lpcmi->lpVerbW (for the Unicode case) is nonzero, lpVerb or lpVerbW
383 // holds a verb string. If the high-order word is zero, the command
384 // offset is in the low-order word of lpcmi->lpVerb.
386 // For the ANSI case, if the high-order word is not zero, the command's
387 // verb string is in lpcmi->lpVerb.
388 if (!isUnicode && NativeMethods.HighWord(ici.verb.ToInt32()) != 0)
390 // Is the verb supported by this context menu extension?
391 string verbAnsi = Marshal.PtrToStringAnsi(ici.verb);
392 if (_items.ContainsKey(verbAnsi))
394 _items[verbAnsi].MenuCommand(ici.hwnd);
398 // If the verb is not recognized by the context menu handler, it
399 // must return E_FAIL to allow it to be passed on to the other
400 // context menu handlers that might implement that verb.
401 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
405 // For the Unicode case, if the high-order word is not zero, the
406 // command's verb string is in lpcmi->lpVerbW.
407 else if (isUnicode && NativeMethods.HighWord(iciex.verbW.ToInt32()) != 0)
409 // Is the verb supported by this context menu extension?
410 string verbUTF = Marshal.PtrToStringUni(iciex.verbW);
411 if (_items.ContainsKey(verbUTF))
413 _items[verbUTF].MenuCommand(ici.hwnd);
417 // If the verb is not recognized by the context menu handler, it
418 // must return E_FAIL to allow it to be passed on to the other
419 // context menu handlers that might implement that verb.
420 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
424 // If the command cannot be identified through the verb string, then
425 // check the identifier offset.
428 // Is the command identifier offset supported by this context menu
430 int menuID = NativeMethods.LowWord(ici.verb.ToInt32());
431 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
432 if (menuItem != null)
434 menuItem.MenuCommand(ici.hwnd);
438 // If the verb is not recognized by the context menu handler, it
439 // must return E_FAIL to allow it to be passed on to the other
440 // context menu handlers that might implement that verb.
441 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
447 /// Get information about a shortcut menu command, including the help string
448 /// and the language-independent, or canonical, name for the command.
450 /// <param name="idCmd">Menu command identifier offset.</param>
451 /// <param name="uFlags">
452 /// Flags specifying the information to return. This parameter can have one
453 /// of the following values: GCS_HELPTEXTA, GCS_HELPTEXTW, GCS_VALIDATEA,
454 /// GCS_VALIDATEW, GCS_VERBA, GCS_VERBW.
456 /// <param name="pReserved">Reserved. Must be IntPtr.Zero</param>
457 /// <param name="pszName">
458 /// The address of the buffer to receive the null-terminated string being
461 /// <param name="cchMax">
462 /// Size of the buffer, in characters, to receive the null-terminated string.
464 public void GetCommandString(
468 StringBuilder pszName,
471 uint menuID = idCmd.ToUInt32();
472 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
473 if (menuItem != null)
478 if (menuItem.VerbCanonicalName.Length > cchMax - 1)
480 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
485 pszName.Append(menuItem.VerbCanonicalName);
489 case GCS.GCS_HELPTEXTW:
490 if (menuItem.VerbHelpText.Length > cchMax - 1)
492 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
497 pszName.Append(menuItem.VerbHelpText);