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 static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos.FileContextMenu");
21 private const string MenuHandlername = "Pithos.FileContextMenu";
24 private readonly Dictionary<string, MenuItem> _items;
28 public FileContext Context { get; set; }
30 private IntPtr _gotoBitmap=IntPtr.Zero;
31 private IntPtr _versionBitmap = IntPtr.Zero;
32 private IntPtr _propertiesBitmap = IntPtr.Zero;
34 public FileContextMenu()
36 _gotoBitmap = GetBitmapPtr(Resources.MenuGoToPithos);
37 _versionBitmap = GetBitmapPtr(Resources.MenuHistory);
38 _propertiesBitmap = GetBitmapPtr(Resources.MenuProperties);
43 _items = new Dictionary<string, MenuItem>{
44 {"gotoPithos",new MenuItem{
45 MenuText = "&Go to Pithos",
47 VerbCanonicalName = "PITHOSGoTo",
48 VerbHelpText = "Go to Pithos",
50 MenuCommand=OnGotoPithos,
51 DisplayFlags=DisplayFlags.All,
52 MenuBitmap = _gotoBitmap
54 /*{"showProperties",new MenuItem{
55 MenuText = "&Pithos File Properties",
56 Verb = "showProperties",
57 VerbCanonicalName = "PITHOSProperties",
58 VerbHelpText = "Pithos File Properties",
60 MenuCommand=OnShowProperties,
61 DisplayFlags=DisplayFlags.File,
62 MenuBitmap = _propertiesBitmap
64 {"prevVersions",new MenuItem{
65 MenuText = "&Show Previous Versions",
66 Verb = "prevVersions",
67 VerbCanonicalName = "PITHOSPrevVersions",
68 VerbHelpText = "Go to Pithos and display previous versions",
70 MenuCommand=OnVerbDisplayFileName,
71 DisplayFlags=DisplayFlags.File,
72 MenuBitmap=_versionBitmap
76 IoC.Current.Compose(this);
84 if (_gotoBitmap != IntPtr.Zero)
86 NativeMethods.DeleteObject(_gotoBitmap);
87 _gotoBitmap= IntPtr.Zero;
89 if (_versionBitmap != IntPtr.Zero)
91 NativeMethods.DeleteObject(_versionBitmap);
92 _versionBitmap = IntPtr.Zero;
94 if (_propertiesBitmap != IntPtr.Zero)
96 NativeMethods.DeleteObject(_propertiesBitmap);
97 _propertiesBitmap = IntPtr.Zero;
101 private static IntPtr GetBitmapPtr(Bitmap gotoBitmap)
103 gotoBitmap.MakeTransparent(gotoBitmap.GetPixel(0, 0));
104 var hbitmap = gotoBitmap.GetHbitmap();
108 void OnShowProperties(IntPtr hWnd)
113 void OnVerbDisplayFileName(IntPtr hWnd)
115 string message = String.Format("The selected file is {0}\r\n\r\nThe selected Path is {1}",
117 Context.CurrentFolder);
119 System.Windows.Forms.MessageBox.Show(
121 "Pithos Shell Extensions");
122 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_ASSOCCHANGED, HChangeNotifyFlags.SHCNF_IDLIST,
123 IntPtr.Zero, IntPtr.Zero);
126 void OnGotoPithos(IntPtr hWnd)
128 var settings = Context.Settings;
129 var activeAccount = settings.Accounts.FirstOrDefault(acc => Context.CurrentFile.StartsWith(acc.RootPath,StringComparison.InvariantCultureIgnoreCase));
130 var address = String.Format("{0}/ui/?token={1}&user={2}",
132 activeAccount.ApiKey,
133 Uri.EscapeUriString(activeAccount.AccountName));
136 Process.Start(address);
140 #region Shell Extension Registration
142 [ComRegisterFunction]
143 public static void Register(Type t)
147 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, ".cs",
149 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "Directory",
151 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, @"Directory\Background",
153 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "*",
156 //ShellExtReg.MarkApproved(t.GUID, MenuHandlername);
160 Console.WriteLine(ex.Message); // Log the error
161 throw; // Re-throw the exception
165 [ComUnregisterFunction]
166 public static void Unregister(Type t)
170 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, ".cs", MenuHandlername);
171 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "Directory", MenuHandlername);
172 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, @"Directory\Background", MenuHandlername);
173 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "*", MenuHandlername);
175 //ShellExtReg.RemoveApproved(t.GUID, MenuHandlername);
179 Console.WriteLine(ex.Message); // Log the error
180 throw; // Re-throw the exception
187 #region IShellExtInit Members
190 /// Initialize the context menu handler.
192 /// <param name="pidlFolder">
193 /// A pointer to an ITEMIDLIST structure that uniquely identifies a folder.
195 /// <param name="pDataObj">
196 /// A pointer to an IDataObject interface object that can be used to retrieve
197 /// the objects being acted upon.
199 /// <param name="hKeyProgID">
200 /// The registry key for the file object or folder type.
202 public void Initialize(IntPtr pidlFolder, IntPtr pDataObj, IntPtr hKeyProgID)
205 if(pDataObj == IntPtr.Zero && pidlFolder == IntPtr.Zero)
207 throw new ArgumentException("pidlFolder and pDataObj shouldn't be null at the same time");
211 Debug.WriteLine("Initializing", LogCategories.ShellMenu);
213 if (pDataObj != IntPtr.Zero)
215 Debug.WriteLine("Got a data object", LogCategories.ShellMenu);
217 FORMATETC fe = new FORMATETC();
218 fe.cfFormat = (short)CLIPFORMAT.CF_HDROP;
219 fe.ptd = IntPtr.Zero;
220 fe.dwAspect = DVASPECT.DVASPECT_CONTENT;
222 fe.tymed = TYMED.TYMED_HGLOBAL;
223 STGMEDIUM stm = new STGMEDIUM();
225 // The pDataObj pointer contains the objects being acted upon. In this
226 // example, we get an HDROP handle for enumerating the selected files
228 IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(pDataObj);
229 dataObject.GetData(ref fe, out stm);
233 // Get an HDROP handle.
234 IntPtr hDrop = stm.unionmember;
235 if (hDrop == IntPtr.Zero)
237 throw new ArgumentException();
240 // Determine how many files are involved in this operation.
241 uint nFiles = NativeMethods.DragQueryFile(hDrop, UInt32.MaxValue, null, 0);
243 Debug.WriteLine(String.Format("Got {0} files", nFiles), LogCategories.ShellMenu);
244 // This code sample displays the custom context menu item when only
245 // one file is selected.
248 // Get the path of the file.
249 var fileName = new StringBuilder(260);
250 if (0 == NativeMethods.DragQueryFile(hDrop, 0, fileName,
253 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
255 Context.CurrentFile = fileName.ToString();
259 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
264 // Enumerate the selected files and folders.
267 // StringCollection selectedFiles = new StringCollection();
268 // StringBuilder fileName = new StringBuilder(260);
269 // for (uint i = 0; i < nFiles; i++)
271 // // Get the next file name.
272 // if (0 != NativeMethods.DragQueryFile(hDrop, i, fileName,
273 // fileName.Capacity))
275 // // Add the file name to the list.
276 // selectedFiles.Add(fileName.ToString());
280 // // If we did not find any files we can work with, throw
282 // if (selectedFiles.Count == 0)
284 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
289 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
294 NativeMethods.ReleaseStgMedium(ref stm);
298 if (pidlFolder != IntPtr.Zero)
300 Debug.WriteLine("Got a folder", LogCategories.ShellMenu);
301 StringBuilder path = new StringBuilder();
302 if (!NativeMethods.SHGetPathFromIDList(pidlFolder, path))
304 int error = Marshal.GetHRForLastWin32Error();
305 Marshal.ThrowExceptionForHR(error);
307 Context.CurrentFolder = path.ToString();
308 Debug.WriteLine(String.Format("Folder is {0}", Context.CurrentFolder), LogCategories.ShellMenu);
315 #region IContextMenu Members
318 /// Add commands to a shortcut menu.
320 /// <param name="hMenu">A handle to the shortcut menu.</param>
321 /// <param name="iMenu">
322 /// The zero-based position at which to insert the first new menu item.
324 /// <param name="idCmdFirst">
325 /// The minimum value that the handler can specify for a menu item ID.
327 /// <param name="idCmdLast">
328 /// The maximum value that the handler can specify for a menu item ID.
330 /// <param name="uFlags">
331 /// Optional flags that specify how the shortcut menu can be changed.
334 /// If successful, returns an HRESULT value that has its severity value set
335 /// to SEVERITY_SUCCESS and its code value set to the offset of the largest
336 /// command identifier that was assigned, plus one.
338 public int QueryContextMenu(
345 Debug.WriteLine("Start qcm", LogCategories.ShellMenu);
346 // If uFlags include CMF_DEFAULTONLY then we should not do anything.
347 Debug.WriteLine(String.Format("Flags {0}", uFlags), LogCategories.ShellMenu);
349 if (((uint)CMF.CMF_DEFAULTONLY & uFlags) != 0)
351 Debug.WriteLine("Default only flag, returning", LogCategories.ShellMenu);
352 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
355 if (!Context.IsManaged)
357 Debug.WriteLine("Not a PITHOS folder",LogCategories.ShellMenu);
358 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
362 if (!selectedFolder.ToLower().Contains("pithos"))
363 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
366 // Use either InsertMenu or InsertMenuItem to add menu items.
370 DisplayFlags itemType = (Context.IsFolder) ? DisplayFlags.Folder : DisplayFlags.File;
372 Debug.WriteLine(String.Format("Item Flags {0}", itemType), LogCategories.ShellMenu);
374 if (!NativeMethods.InsertMenu(hMenu, idCmdFirst, MF.MF_SEPARATOR | MF.MF_BYPOSITION, 0, String.Empty))
376 Log.ErrorFormat("Error adding separator 1\r\n{0}", Marshal.GetLastWin32Error());
377 return Marshal.GetHRForLastWin32Error();
380 foreach (var menuItem in _items.Values)
382 Debug.WriteLine(String.Format("Menu Flags {0}", menuItem.DisplayFlags), LogCategories.ShellMenu);
383 if ((itemType & menuItem.DisplayFlags) != DisplayFlags.None)
385 Debug.WriteLine("Adding Menu", LogCategories.ShellMenu);
387 MENUITEMINFO mii = menuItem.CreateInfo(idCmdFirst);
388 if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref mii))
390 var lastError = Marshal.GetLastWin32Error();
391 var lastErrorHR = Marshal.GetHRForLastWin32Error();
394 if (largestID < menuItem.MenuDisplayId)
395 largestID = menuItem.MenuDisplayId;
399 Debug.Write("Adding Separator 1", LogCategories.ShellMenu);
401 /* MENUITEMINFO sep = new MENUITEMINFO();
402 sep.cbSize = (uint)Marshal.SizeOf(sep);
403 sep.fMask = MIIM.MIIM_TYPE;
404 sep.fType = MFT.MFT_SEPARATOR;*/
405 if (!NativeMethods.InsertMenu(hMenu, (uint)_items.Values.Count + idCmdFirst+1,MF.MF_SEPARATOR|MF.MF_BYPOSITION, 0, String.Empty))
407 Log.ErrorFormat("Error adding separator 1\r\n{0}", Marshal.GetLastWin32Error());
408 return Marshal.GetHRForLastWin32Error();
414 Debug.WriteLine("Menus added", LogCategories.ShellOverlays);
415 // Return an HRESULT value with the severity set to SEVERITY_SUCCESS.
416 // Set the code value to the offset of the largest command identifier
417 // that was assigned, plus one (1).
418 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0,
423 /// Carry out the command associated with a shortcut menu item.
425 /// <param name="pici">
426 /// A pointer to a CMINVOKECOMMANDINFO or CMINVOKECOMMANDINFOEX structure
427 /// containing information about the command.
429 public void InvokeCommand(IntPtr pici)
431 bool isUnicode = false;
433 // Determine which structure is being passed in, CMINVOKECOMMANDINFO or
434 // CMINVOKECOMMANDINFOEX based on the cbSize member of lpcmi. Although
435 // the lpcmi parameter is declared in Shlobj.h as a CMINVOKECOMMANDINFO
436 // structure, in practice it often points to a CMINVOKECOMMANDINFOEX
437 // structure. This struct is an extended version of CMINVOKECOMMANDINFO
438 // and has additional members that allow Unicode strings to be passed.
439 CMINVOKECOMMANDINFO ici = (CMINVOKECOMMANDINFO)Marshal.PtrToStructure(
440 pici, typeof(CMINVOKECOMMANDINFO));
441 CMINVOKECOMMANDINFOEX iciex = new CMINVOKECOMMANDINFOEX();
442 if (ici.cbSize == Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX)))
444 if ((ici.fMask & CMIC.CMIC_MASK_UNICODE) != 0)
447 iciex = (CMINVOKECOMMANDINFOEX)Marshal.PtrToStructure(pici,
448 typeof(CMINVOKECOMMANDINFOEX));
452 // Determines whether the command is identified by its offset or verb.
453 // There are two ways to identify commands:
455 // 1) The command's verb string
456 // 2) The command's identifier offset
458 // If the high-order word of lpcmi->lpVerb (for the ANSI case) or
459 // lpcmi->lpVerbW (for the Unicode case) is nonzero, lpVerb or lpVerbW
460 // holds a verb string. If the high-order word is zero, the command
461 // offset is in the low-order word of lpcmi->lpVerb.
463 // For the ANSI case, if the high-order word is not zero, the command's
464 // verb string is in lpcmi->lpVerb.
465 if (!isUnicode && NativeMethods.HighWord(ici.verb.ToInt32()) != 0)
467 // Is the verb supported by this context menu extension?
468 string verbAnsi = Marshal.PtrToStringAnsi(ici.verb);
469 if (_items.ContainsKey(verbAnsi))
471 _items[verbAnsi].MenuCommand(ici.hwnd);
475 // If the verb is not recognized by the context menu handler, it
476 // must return E_FAIL to allow it to be passed on to the other
477 // context menu handlers that might implement that verb.
478 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
482 // For the Unicode case, if the high-order word is not zero, the
483 // command's verb string is in lpcmi->lpVerbW.
484 else if (isUnicode && NativeMethods.HighWord(iciex.verbW.ToInt32()) != 0)
486 // Is the verb supported by this context menu extension?
487 string verbUTF = Marshal.PtrToStringUni(iciex.verbW);
488 if (_items.ContainsKey(verbUTF))
490 _items[verbUTF].MenuCommand(ici.hwnd);
494 // If the verb is not recognized by the context menu handler, it
495 // must return E_FAIL to allow it to be passed on to the other
496 // context menu handlers that might implement that verb.
497 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
501 // If the command cannot be identified through the verb string, then
502 // check the identifier offset.
505 // Is the command identifier offset supported by this context menu
507 int menuID = NativeMethods.LowWord(ici.verb.ToInt32());
508 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
509 if (menuItem != null)
511 menuItem.MenuCommand(ici.hwnd);
515 // If the verb is not recognized by the context menu handler, it
516 // must return E_FAIL to allow it to be passed on to the other
517 // context menu handlers that might implement that verb.
518 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
524 /// Get information about a shortcut menu command, including the help string
525 /// and the language-independent, or canonical, name for the command.
527 /// <param name="idCmd">Menu command identifier offset.</param>
528 /// <param name="uFlags">
529 /// Flags specifying the information to return. This parameter can have one
530 /// of the following values: GCS_HELPTEXTA, GCS_HELPTEXTW, GCS_VALIDATEA,
531 /// GCS_VALIDATEW, GCS_VERBA, GCS_VERBW.
533 /// <param name="pReserved">Reserved. Must be IntPtr.Zero</param>
534 /// <param name="pszName">
535 /// The address of the buffer to receive the null-terminated string being
538 /// <param name="cchMax">
539 /// Size of the buffer, in characters, to receive the null-terminated string.
541 public void GetCommandString(
545 StringBuilder pszName,
548 uint menuID = idCmd.ToUInt32();
549 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
550 if (menuItem != null)
555 if (menuItem.VerbCanonicalName.Length > cchMax - 1)
557 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
562 pszName.Append(menuItem.VerbCanonicalName);
566 case GCS.GCS_HELPTEXTW:
567 if (menuItem.VerbHelpText.Length > cchMax - 1)
569 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
574 pszName.Append(menuItem.VerbHelpText);