2 /* -----------------------------------------------------------------------
3 * <copyright file="FileContextMenu.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
15 * 2. Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials
18 * provided with the distribution.
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
34 * The views and conclusions contained in the software and
35 * documentation are those of the authors and should not be
36 * interpreted as representing official policies, either expressed
37 * or implied, of GRNET S.A.
39 * -----------------------------------------------------------------------
43 using System.Collections.Generic;
44 using System.ComponentModel.Composition;
45 using System.Diagnostics;
46 using System.Diagnostics.Contracts;
49 using System.Runtime.InteropServices;
50 using System.Runtime.InteropServices.ComTypes;
52 using System.Threading.Tasks;
53 using Pithos.ShellExtensions.Properties;
55 namespace Pithos.ShellExtensions.Menus
57 [ClassInterface(ClassInterfaceType.None)]
58 [Guid("B1F1405D-94A1-4692-B72F-FC8CAF8B8700"), ComVisible(true)]
59 public class FileContextMenu : IShellExtInit, IContextMenu
61 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger("Pithos.FileContextMenu");
63 private const string MenuHandlername = "Pithos.FileContextMenu";
66 private readonly Dictionary<string, MenuItem> _items;
70 public FileContext Context { get; set; }
72 private IntPtr _gotoBitmap=IntPtr.Zero;
73 private IntPtr _versionBitmap = IntPtr.Zero;
74 private IntPtr _propertiesBitmap = IntPtr.Zero;
76 public FileContextMenu()
78 _gotoBitmap = GetBitmapPtr(Resources.MenuGoToPithos);
79 _versionBitmap = GetBitmapPtr(Resources.MenuHistory);
80 _propertiesBitmap = GetBitmapPtr(Resources.MenuProperties);
85 _items = new Dictionary<string, MenuItem>{
86 {"gotoPithos",new MenuItem{
87 MenuText = "&Go to Pithos",
89 VerbCanonicalName = "PITHOSGoTo",
90 VerbHelpText = "Go to Pithos",
92 MenuCommand=OnGotoPithos,
93 DisplayFlags=DisplayFlags.All,
94 MenuBitmap = _gotoBitmap
96 {"showProperties",new MenuItem{
97 MenuText = "&Pithos Properties",
98 Verb = "showProperties",
99 VerbCanonicalName = "PITHOSProperties",
100 VerbHelpText = "Pithos Properties",
102 MenuCommand=OnShowProperties,
103 DisplayFlags=DisplayFlags.All,
104 MenuBitmap = _propertiesBitmap
106 {"prevVersions",new MenuItem{
107 MenuText = "&Show Previous Versions",
108 Verb = "prevVersions",
109 VerbCanonicalName = "PITHOSPrevVersions",
110 VerbHelpText = "Go to Pithos and display previous versions",
112 MenuCommand=OnVerbDisplayFileName,
113 DisplayFlags=DisplayFlags.File,
114 MenuBitmap=_versionBitmap
118 IoC.Current.Compose(this);
126 if (_gotoBitmap != IntPtr.Zero)
128 NativeMethods.DeleteObject(_gotoBitmap);
129 _gotoBitmap= IntPtr.Zero;
131 if (_versionBitmap != IntPtr.Zero)
133 NativeMethods.DeleteObject(_versionBitmap);
134 _versionBitmap = IntPtr.Zero;
136 if (_propertiesBitmap != IntPtr.Zero)
138 NativeMethods.DeleteObject(_propertiesBitmap);
139 _propertiesBitmap = IntPtr.Zero;
143 private static IntPtr GetBitmapPtr(Bitmap gotoBitmap)
145 gotoBitmap.MakeTransparent(gotoBitmap.GetPixel(0, 0));
146 var hbitmap = gotoBitmap.GetHbitmap();
150 void OnShowProperties(IntPtr hWnd)
152 var filePath = Context.CurrentFile ?? Context.CurrentFolder;
153 if (String.IsNullOrWhiteSpace(filePath))
155 Debug.WriteLine("No current file or folder");
160 Debug.WriteLine("Will display properties for {0}",filePath);
163 var client = PithosHost.GetCommandsClient();
164 client.BeginShowProperties(Context.CurrentFile,c=>
168 c.AsyncWaitHandle.WaitOne();
171 catch (Exception exc)
173 Trace.WriteLine(exc.ToString());
179 void OnVerbDisplayFileName(IntPtr hWnd)
181 string message = String.Format("The selected file is {0}\r\n\r\nThe selected Path is {1}",
183 Context.CurrentFolder);
185 System.Windows.Forms.MessageBox.Show(
187 "Pithos Shell Extensions");
188 NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_ASSOCCHANGED, HChangeNotifyFlags.SHCNF_IDLIST,
189 IntPtr.Zero, IntPtr.Zero);
192 async void OnGotoPithos(IntPtr hWnd)
196 using (var client = PithosHost.GetCommandsClient())
198 await TaskEx.Run(() => client.GotoSite(Context.CurrentFile));
201 catch (Exception exc)
203 Trace.WriteLine(exc.ToString());
209 #region Shell Extension Registration
211 [ComRegisterFunction]
212 public static void Register(Type t)
216 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, ".cs",
218 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "Directory",
220 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, @"Directory\Background",
222 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "*",
225 //ShellExtReg.MarkApproved(t.GUID, MenuHandlername);
229 Console.WriteLine(ex.Message); // Log the error
230 throw; // Re-throw the exception
234 [ComUnregisterFunction]
235 public static void Unregister(Type t)
239 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, ".cs", MenuHandlername);
240 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "Directory", MenuHandlername);
241 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, @"Directory\Background", MenuHandlername);
242 ShellExtReg.UnregisterShellExtContextMenuHandler(t.GUID, "*", MenuHandlername);
244 //ShellExtReg.RemoveApproved(t.GUID, MenuHandlername);
248 Console.WriteLine(ex.Message); // Log the error
249 throw; // Re-throw the exception
256 #region IShellExtInit Members
259 /// Initialize the context menu handler.
261 /// <param name="pidlFolder">
262 /// A pointer to an ITEMIDLIST structure that uniquely identifies a folder.
264 /// <param name="pDataObj">
265 /// A pointer to an IDataObject interface object that can be used to retrieve
266 /// the objects being acted upon.
268 /// <param name="hKeyProgID">
269 /// The registry key for the file object or folder type.
271 public void Initialize(IntPtr pidlFolder, IntPtr pDataObj, IntPtr hKeyProgID)
274 if(pDataObj == IntPtr.Zero && pidlFolder == IntPtr.Zero)
276 throw new ArgumentException("pidlFolder and pDataObj shouldn't be null at the same time");
280 Debug.WriteLine("Initializing", LogCategories.ShellMenu);
282 if (pDataObj != IntPtr.Zero)
284 Debug.WriteLine("Got a data object", LogCategories.ShellMenu);
286 FORMATETC fe = new FORMATETC();
287 fe.cfFormat = (short)CLIPFORMAT.CF_HDROP;
288 fe.ptd = IntPtr.Zero;
289 fe.dwAspect = DVASPECT.DVASPECT_CONTENT;
291 fe.tymed = TYMED.TYMED_HGLOBAL;
292 STGMEDIUM stm = new STGMEDIUM();
294 // The pDataObj pointer contains the objects being acted upon. In this
295 // example, we get an HDROP handle for enumerating the selected files
297 IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(pDataObj);
298 dataObject.GetData(ref fe, out stm);
302 // Get an HDROP handle.
303 IntPtr hDrop = stm.unionmember;
304 if (hDrop == IntPtr.Zero)
306 throw new ArgumentException();
309 // Determine how many files are involved in this operation.
310 uint nFiles = NativeMethods.DragQueryFile(hDrop, UInt32.MaxValue, null, 0);
312 Debug.WriteLine(String.Format("Got {0} files", nFiles), LogCategories.ShellMenu);
313 // This code sample displays the custom context menu item when only
314 // one file is selected.
317 // Get the path of the file.
318 var fileName = new StringBuilder(260);
319 if (0 == NativeMethods.DragQueryFile(hDrop, 0, fileName,
322 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
324 Context.CurrentFile = fileName.ToString();
328 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
333 // Enumerate the selected files and folders.
336 // StringCollection selectedFiles = new StringCollection();
337 // StringBuilder fileName = new StringBuilder(260);
338 // for (uint i = 0; i < nFiles; i++)
340 // // Get the next file name.
341 // if (0 != NativeMethods.DragQueryFile(hDrop, i, fileName,
342 // fileName.Capacity))
344 // // Add the file name to the list.
345 // selectedFiles.Add(fileName.ToString());
349 // // If we did not find any files we can work with, throw
351 // if (selectedFiles.Count == 0)
353 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
358 // Marshal.ThrowExceptionForHR(WinError.E_FAIL);
363 NativeMethods.ReleaseStgMedium(ref stm);
367 if (pidlFolder != IntPtr.Zero)
369 Debug.WriteLine("Got a folder", LogCategories.ShellMenu);
370 StringBuilder path = new StringBuilder();
371 if (!NativeMethods.SHGetPathFromIDList(pidlFolder, path))
373 int error = Marshal.GetHRForLastWin32Error();
374 Marshal.ThrowExceptionForHR(error);
376 Context.CurrentFolder = path.ToString();
377 Debug.WriteLine(String.Format("Folder is {0}", Context.CurrentFolder), LogCategories.ShellMenu);
384 #region IContextMenu Members
387 /// Add commands to a shortcut menu.
389 /// <param name="hMenu">A handle to the shortcut menu.</param>
390 /// <param name="iMenu">
391 /// The zero-based position at which to insert the first new menu item.
393 /// <param name="idCmdFirst">
394 /// The minimum value that the handler can specify for a menu item ID.
396 /// <param name="idCmdLast">
397 /// The maximum value that the handler can specify for a menu item ID.
399 /// <param name="uFlags">
400 /// Optional flags that specify how the shortcut menu can be changed.
403 /// If successful, returns an HRESULT value that has its severity value set
404 /// to SEVERITY_SUCCESS and its code value set to the offset of the largest
405 /// command identifier that was assigned, plus one.
407 public int QueryContextMenu(
414 Debug.WriteLine("Start qcm", LogCategories.ShellMenu);
415 // If uFlags include CMF_DEFAULTONLY then we should not do anything.
416 Debug.WriteLine(String.Format("Flags {0}", uFlags), LogCategories.ShellMenu);
418 if (((uint)CMF.CMF_DEFAULTONLY & uFlags) != 0)
420 Debug.WriteLine("Default only flag, returning", LogCategories.ShellMenu);
421 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
424 if (!Context.IsManaged)
426 Debug.WriteLine("Not a PITHOS folder",LogCategories.ShellMenu);
427 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
431 if (!selectedFolder.ToLower().Contains("pithos"))
432 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
435 // Use either InsertMenu or InsertMenuItem to add menu items.
439 DisplayFlags itemType = (Context.IsFolder) ? DisplayFlags.Folder : DisplayFlags.File;
441 Debug.WriteLine(String.Format("Item Flags {0}", itemType), LogCategories.ShellMenu);
443 if (!NativeMethods.InsertMenu(hMenu, idCmdFirst, MF.MF_SEPARATOR | MF.MF_BYPOSITION, 0, String.Empty))
445 Log.ErrorFormat("Error adding separator 1\r\n{0}", Marshal.GetLastWin32Error());
446 return Marshal.GetHRForLastWin32Error();
449 foreach (var menuItem in _items.Values)
451 Debug.WriteLine(String.Format("Menu Flags {0}", menuItem.DisplayFlags), LogCategories.ShellMenu);
452 if ((itemType & menuItem.DisplayFlags) != DisplayFlags.None)
454 Debug.WriteLine("Adding Menu", LogCategories.ShellMenu);
456 MENUITEMINFO mii = menuItem.CreateInfo(idCmdFirst);
457 if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref mii))
459 var lastError = Marshal.GetLastWin32Error();
460 var lastErrorHR = Marshal.GetHRForLastWin32Error();
463 if (largestID < menuItem.MenuDisplayId)
464 largestID = menuItem.MenuDisplayId;
468 Debug.Write("Adding Separator 1", LogCategories.ShellMenu);
470 /* MENUITEMINFO sep = new MENUITEMINFO();
471 sep.cbSize = (uint)Marshal.SizeOf(sep);
472 sep.fMask = MIIM.MIIM_TYPE;
473 sep.fType = MFT.MFT_SEPARATOR;*/
474 if (!NativeMethods.InsertMenu(hMenu, (uint)_items.Values.Count + idCmdFirst+1,MF.MF_SEPARATOR|MF.MF_BYPOSITION, 0, String.Empty))
476 Log.ErrorFormat("Error adding separator 1\r\n{0}", Marshal.GetLastWin32Error());
477 return Marshal.GetHRForLastWin32Error();
483 Debug.WriteLine("Menus added", LogCategories.ShellOverlays);
484 // Return an HRESULT value with the severity set to SEVERITY_SUCCESS.
485 // Set the code value to the offset of the largest command identifier
486 // that was assigned, plus one (1).
487 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0,
492 /// Carry out the command associated with a shortcut menu item.
494 /// <param name="pici">
495 /// A pointer to a CMINVOKECOMMANDINFO or CMINVOKECOMMANDINFOEX structure
496 /// containing information about the command.
498 public void InvokeCommand(IntPtr pici)
500 bool isUnicode = false;
502 // Determine which structure is being passed in, CMINVOKECOMMANDINFO or
503 // CMINVOKECOMMANDINFOEX based on the cbSize member of lpcmi. Although
504 // the lpcmi parameter is declared in Shlobj.h as a CMINVOKECOMMANDINFO
505 // structure, in practice it often points to a CMINVOKECOMMANDINFOEX
506 // structure. This struct is an extended version of CMINVOKECOMMANDINFO
507 // and has additional members that allow Unicode strings to be passed.
508 CMINVOKECOMMANDINFO ici = (CMINVOKECOMMANDINFO)Marshal.PtrToStructure(
509 pici, typeof(CMINVOKECOMMANDINFO));
510 CMINVOKECOMMANDINFOEX iciex = new CMINVOKECOMMANDINFOEX();
511 if (ici.cbSize == Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX)))
513 if ((ici.fMask & CMIC.CMIC_MASK_UNICODE) != 0)
516 iciex = (CMINVOKECOMMANDINFOEX)Marshal.PtrToStructure(pici,
517 typeof(CMINVOKECOMMANDINFOEX));
521 // Determines whether the command is identified by its offset or verb.
522 // There are two ways to identify commands:
524 // 1) The command's verb string
525 // 2) The command's identifier offset
527 // If the high-order word of lpcmi->lpVerb (for the ANSI case) or
528 // lpcmi->lpVerbW (for the Unicode case) is nonzero, lpVerb or lpVerbW
529 // holds a verb string. If the high-order word is zero, the command
530 // offset is in the low-order word of lpcmi->lpVerb.
532 // For the ANSI case, if the high-order word is not zero, the command's
533 // verb string is in lpcmi->lpVerb.
534 if (!isUnicode && NativeMethods.HighWord(ici.verb.ToInt32()) != 0)
536 // Is the verb supported by this context menu extension?
537 string verbAnsi = Marshal.PtrToStringAnsi(ici.verb);
538 if (_items.ContainsKey(verbAnsi))
540 _items[verbAnsi].MenuCommand(ici.hwnd);
544 // If the verb is not recognized by the context menu handler, it
545 // must return E_FAIL to allow it to be passed on to the other
546 // context menu handlers that might implement that verb.
547 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
551 // For the Unicode case, if the high-order word is not zero, the
552 // command's verb string is in lpcmi->lpVerbW.
553 else if (isUnicode && NativeMethods.HighWord(iciex.verbW.ToInt32()) != 0)
555 // Is the verb supported by this context menu extension?
556 string verbUTF = Marshal.PtrToStringUni(iciex.verbW);
557 if (_items.ContainsKey(verbUTF))
559 _items[verbUTF].MenuCommand(ici.hwnd);
563 // If the verb is not recognized by the context menu handler, it
564 // must return E_FAIL to allow it to be passed on to the other
565 // context menu handlers that might implement that verb.
566 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
570 // If the command cannot be identified through the verb string, then
571 // check the identifier offset.
574 // Is the command identifier offset supported by this context menu
576 int menuID = NativeMethods.LowWord(ici.verb.ToInt32());
577 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
578 if (menuItem != null)
580 menuItem.MenuCommand(ici.hwnd);
584 // If the verb is not recognized by the context menu handler, it
585 // must return E_FAIL to allow it to be passed on to the other
586 // context menu handlers that might implement that verb.
587 Marshal.ThrowExceptionForHR(WinError.E_FAIL);
593 /// Get information about a shortcut menu command, including the help string
594 /// and the language-independent, or canonical, name for the command.
596 /// <param name="idCmd">Menu command identifier offset.</param>
597 /// <param name="uFlags">
598 /// Flags specifying the information to return. This parameter can have one
599 /// of the following values: GCS_HELPTEXTA, GCS_HELPTEXTW, GCS_VALIDATEA,
600 /// GCS_VALIDATEW, GCS_VERBA, GCS_VERBW.
602 /// <param name="pReserved">Reserved. Must be IntPtr.Zero</param>
603 /// <param name="pszName">
604 /// The address of the buffer to receive the null-terminated string being
607 /// <param name="cchMax">
608 /// Size of the buffer, in characters, to receive the null-terminated string.
610 public void GetCommandString(
614 StringBuilder pszName,
617 uint menuID = idCmd.ToUInt32();
618 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
619 if (menuItem != null)
624 if (menuItem.VerbCanonicalName.Length > cchMax - 1)
626 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
631 pszName.Append(menuItem.VerbCanonicalName);
635 case GCS.GCS_HELPTEXTW:
636 if (menuItem.VerbHelpText.Length > cchMax - 1)
638 Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
643 pszName.Append(menuItem.VerbHelpText);