All files
[pithos-ms-client] / trunk / Pithos.ShellExtensions / Menus / FileContextMenu.cs
1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
6 using System.Linq;
7 using System.Runtime.InteropServices;
8 using System.Runtime.InteropServices.ComTypes;
9 using System.Text;
10
11 namespace Pithos.ShellExtensions.Menus
12 {
13     [ClassInterface(ClassInterfaceType.None)]
14     [Guid("B1F1405D-94A1-4692-B72F-FC8CAF8B8700"), ComVisible(true)]
15     public class FileContextMenu : IShellExtInit, IContextMenu
16     {
17         private const string MenuHandlername = "CSShellExtContextMenuHandler.FileContextMenuExt";
18
19
20         private readonly Dictionary<string, MenuItem> _items;
21
22
23         private FileContext _context =new FileContext();
24         public FileContext Context
25         {
26             get { return _context; }
27             set { _context = value; }
28         }
29
30         public FileContextMenu()
31         {
32             _items = new Dictionary<string, MenuItem>{
33                 {"gotoPithos",new MenuItem{
34                                            MenuText = "&Go to Pithos",
35                                             Verb = "gotoPithos",
36                                              VerbCanonicalName = "PITHOSGoTo",
37                                               VerbHelpText = "Go to Pithos",
38                                                MenuDisplayId = 0,
39                                                MenuCommand=OnVerbDisplayFileName,
40                                                DisplayFlags=DisplayFlags.All
41                                            }},
42                 {"prevVersions",new MenuItem{
43                                            MenuText = "&Show Previous Versions",
44                                             Verb = "prevVersions",
45                                              VerbCanonicalName = "PITHOSPrevVersions",
46                                               VerbHelpText = "Go to Pithos and display previous versions",
47                                                MenuDisplayId = 1,
48                                                MenuCommand=OnVerbDisplayFileName,
49                                                DisplayFlags=DisplayFlags.File
50                                            }}
51             };
52
53             
54         }
55
56
57         void OnVerbDisplayFileName(IntPtr hWnd)
58         {
59             string message = String.Format("The selected file is {0}\r\n\r\nThe selected Path is {1}",
60                                            Context.CurrentFile,
61                                            Context.CurrentFolder);
62
63             System.Windows.Forms.MessageBox.Show(
64                 message,
65                 "CSShellExtContextMenuHandler");
66             NativeMethods.SHChangeNotify(HChangeNotifyEventID.SHCNE_ASSOCCHANGED, HChangeNotifyFlags.SHCNF_IDLIST,
67                                              IntPtr.Zero, IntPtr.Zero);
68         }
69         
70
71         #region Shell Extension Registration
72
73         [ComRegisterFunction]
74         public static void Register(Type t)
75         {
76             try
77             {
78                 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, ".cs",
79                     MenuHandlername);
80                 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "Directory",
81                     MenuHandlername);
82                 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, @"Directory\Background",
83                     MenuHandlername);
84                 ShellExtReg.RegisterShellExtContextMenuHandler(t.GUID, "*",
85                     MenuHandlername);
86
87                 //ShellExtReg.MarkApproved(t.GUID, MenuHandlername);
88             }
89             catch (Exception ex)
90             {
91                 Console.WriteLine(ex.Message); // Log the error
92                 throw;  // Re-throw the exception
93             }
94         }
95
96         [ComUnregisterFunction]
97         public static void Unregister(Type t)
98         {
99             try
100             {
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);
105
106                 //ShellExtReg.RemoveApproved(t.GUID, MenuHandlername);
107             }
108             catch (Exception ex)
109             {
110                 Console.WriteLine(ex.Message); // Log the error
111                 throw;  // Re-throw the exception
112             }
113         }
114
115         #endregion
116
117
118         #region IShellExtInit Members
119
120         /// <summary>
121         /// Initialize the context menu handler.
122         /// </summary>
123         /// <param name="pidlFolder">
124         /// A pointer to an ITEMIDLIST structure that uniquely identifies a folder.
125         /// </param>
126         /// <param name="pDataObj">
127         /// A pointer to an IDataObject interface object that can be used to retrieve 
128         /// the objects being acted upon.
129         /// </param>
130         /// <param name="hKeyProgID">
131         /// The registry key for the file object or folder type.
132         /// </param>
133         public void Initialize(IntPtr pidlFolder, IntPtr pDataObj, IntPtr hKeyProgID)
134         {
135             
136             if(pDataObj == IntPtr.Zero && pidlFolder == IntPtr.Zero)
137             {
138                 throw new ArgumentException("pidlFolder and pDataObj shouldn't be null at the same time");
139             }
140
141
142             Trace.Write("Initializing");
143
144             if (pDataObj != IntPtr.Zero)
145             {
146                 Trace.Write("Got a data object");
147
148                 FORMATETC fe = new FORMATETC();
149                 fe.cfFormat = (short)CLIPFORMAT.CF_HDROP;
150                 fe.ptd = IntPtr.Zero;
151                 fe.dwAspect = DVASPECT.DVASPECT_CONTENT;
152                 fe.lindex = -1;
153                 fe.tymed = TYMED.TYMED_HGLOBAL;
154                 STGMEDIUM stm = new STGMEDIUM();
155
156                 // The pDataObj pointer contains the objects being acted upon. In this 
157                 // example, we get an HDROP handle for enumerating the selected files 
158                 // and folders.
159                 IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(pDataObj);
160                 dataObject.GetData(ref fe, out stm);
161
162                 try
163                 {
164                     // Get an HDROP handle.
165                     IntPtr hDrop = stm.unionmember;
166                     if (hDrop == IntPtr.Zero)
167                     {
168                         throw new ArgumentException();
169                     }
170
171                     // Determine how many files are involved in this operation.
172                     uint nFiles = NativeMethods.DragQueryFile(hDrop, UInt32.MaxValue, null, 0);
173
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. 
177                     if (nFiles == 1)
178                     {
179                         // Get the path of the file.
180                         var fileName = new StringBuilder(260);
181                         if (0 == NativeMethods.DragQueryFile(hDrop, 0, fileName,
182                                                              fileName.Capacity))
183                         {
184                             Marshal.ThrowExceptionForHR(WinError.E_FAIL);
185                         }
186                         Context.CurrentFile = fileName.ToString();
187                     }
188                     /* else
189                      {
190                          Marshal.ThrowExceptionForHR(WinError.E_FAIL);
191                      }*/
192
193                     // [-or-]
194
195                     // Enumerate the selected files and folders.
196                     //if (nFiles > 0)
197                     //{
198                     //    StringCollection selectedFiles = new StringCollection();
199                     //    StringBuilder fileName = new StringBuilder(260);
200                     //    for (uint i = 0; i < nFiles; i++)
201                     //    {
202                     //        // Get the next file name.
203                     //        if (0 != NativeMethods.DragQueryFile(hDrop, i, fileName,
204                     //            fileName.Capacity))
205                     //        {
206                     //            // Add the file name to the list.
207                     //            selectedFiles.Add(fileName.ToString());
208                     //        }
209                     //    }
210                     //
211                     //    // If we did not find any files we can work with, throw 
212                     //    // exception.
213                     //    if (selectedFiles.Count == 0)
214                     //    {
215                     //        Marshal.ThrowExceptionForHR(WinError.E_FAIL);
216                     //    }
217                     //}
218                     //else
219                     //{
220                     //    Marshal.ThrowExceptionForHR(WinError.E_FAIL);
221                     //}
222                 }
223                 finally
224                 {
225                     NativeMethods.ReleaseStgMedium(ref stm);
226                 }
227             }
228
229             if (pidlFolder != IntPtr.Zero)
230             {
231                 Trace.Write("Got a folder");
232                 StringBuilder path = new StringBuilder();
233                 if (!NativeMethods.SHGetPathFromIDList(pidlFolder, path))
234                 {
235                     int error = Marshal.GetHRForLastWin32Error();
236                     Marshal.ThrowExceptionForHR(error);
237                 }
238                 Context.CurrentFolder = path.ToString();
239                 Trace.Write(String.Format("Folder is {0}", Context.CurrentFolder));
240             }
241         }
242
243         #endregion
244
245
246         #region IContextMenu Members
247
248         /// <summary>
249         /// Add commands to a shortcut menu.
250         /// </summary>
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.
254         /// </param>
255         /// <param name="idCmdFirst">
256         /// The minimum value that the handler can specify for a menu item ID.
257         /// </param>
258         /// <param name="idCmdLast">
259         /// The maximum value that the handler can specify for a menu item ID.
260         /// </param>
261         /// <param name="uFlags">
262         /// Optional flags that specify how the shortcut menu can be changed.
263         /// </param>
264         /// <returns>
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.
268         /// </returns>
269         public int QueryContextMenu(
270             IntPtr hMenu,
271             uint iMenu,
272             uint idCmdFirst,
273             uint idCmdLast,
274             uint uFlags)
275         {
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));
279
280             if (((uint)CMF.CMF_DEFAULTONLY & uFlags) != 0)
281             {
282                 Trace.Write("Default only flag, returning");
283                 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
284             }
285
286             if (!Context.IsManaged)
287             {
288                 Trace.Write("Not a PITHOS folder");
289                 return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
290             }
291
292             /*
293                         if (!selectedFolder.ToLower().Contains("pithos"))
294                             return WinError.MAKE_HRESULT(WinError.SEVERITY_SUCCESS, 0, 0);
295             */
296
297             // Use either InsertMenu or InsertMenuItem to add menu items.
298
299             uint largestID = 0;
300
301             DisplayFlags itemType = (Context.IsFolder) ? DisplayFlags.Folder : DisplayFlags.File;
302
303             Trace.Write(String.Format("Item Flags {0}", itemType));
304
305             foreach (var menuItem in _items.Values)
306             {
307                 Trace.Write(String.Format("Menu Flags {0}", menuItem.DisplayFlags));
308                 if ((itemType & menuItem.DisplayFlags) != DisplayFlags.None)
309                 {
310                     Trace.Write("Adding Menu");
311
312                     MENUITEMINFO mii = menuItem.CreateInfo(idCmdFirst);
313                     if (!NativeMethods.InsertMenuItem(hMenu, iMenu, true, ref mii))
314                     {
315                         return Marshal.GetHRForLastWin32Error();
316                     }
317                     if (largestID < menuItem.MenuDisplayId)
318                         largestID = menuItem.MenuDisplayId;
319                 }
320             }
321
322             Trace.Write("Adding Separator 1");
323             // Add a separator.
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))
329             {
330                 Trace.TraceError("Error adding separator 1\r\n{0}", Marshal.GetLastWin32Error());
331                 return Marshal.GetHRForLastWin32Error();
332             }
333
334
335
336
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,
342                 largestID + 1);
343         }
344
345         /// <summary>
346         /// Carry out the command associated with a shortcut menu item.
347         /// </summary>
348         /// <param name="pici">
349         /// A pointer to a CMINVOKECOMMANDINFO or CMINVOKECOMMANDINFOEX structure 
350         /// containing information about the command. 
351         /// </param>
352         public void InvokeCommand(IntPtr pici)
353         {
354             bool isUnicode = false;
355
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)))
366             {
367                 if ((ici.fMask & CMIC.CMIC_MASK_UNICODE) != 0)
368                 {
369                     isUnicode = true;
370                     iciex = (CMINVOKECOMMANDINFOEX)Marshal.PtrToStructure(pici,
371                         typeof(CMINVOKECOMMANDINFOEX));
372                 }
373             }
374
375             // Determines whether the command is identified by its offset or verb.
376             // There are two ways to identify commands:
377             // 
378             //   1) The command's verb string 
379             //   2) The command's identifier offset
380             // 
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.
385
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)
389             {
390                 // Is the verb supported by this context menu extension?
391                 string verbAnsi = Marshal.PtrToStringAnsi(ici.verb);
392                 if (_items.ContainsKey(verbAnsi))
393                 {
394                     _items[verbAnsi].MenuCommand(ici.hwnd);
395                 }
396                 else
397                 {
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);
402                 }
403             }
404
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)
408             {
409                 // Is the verb supported by this context menu extension?
410                 string verbUTF = Marshal.PtrToStringUni(iciex.verbW);
411                 if (_items.ContainsKey(verbUTF))
412                 {
413                     _items[verbUTF].MenuCommand(ici.hwnd);
414                 }
415                 else
416                 {
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);
421                 }
422             }
423
424                 // If the command cannot be identified through the verb string, then 
425             // check the identifier offset.
426             else
427             {
428                 // Is the command identifier offset supported by this context menu 
429                 // extension?
430                 int menuID = NativeMethods.LowWord(ici.verb.ToInt32());
431                 var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
432                 if (menuItem != null)
433                 {
434                     menuItem.MenuCommand(ici.hwnd);
435                 }
436                 else
437                 {
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);
442                 }
443             }
444         }
445
446         /// <summary>
447         /// Get information about a shortcut menu command, including the help string 
448         /// and the language-independent, or canonical, name for the command.
449         /// </summary>
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.
455         /// </param>
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 
459         /// retrieved.
460         /// </param>
461         /// <param name="cchMax">
462         /// Size of the buffer, in characters, to receive the null-terminated string.
463         /// </param>
464         public void GetCommandString(
465             UIntPtr idCmd,
466             uint uFlags,
467             IntPtr pReserved,
468             StringBuilder pszName,
469             uint cchMax)
470         {
471             uint menuID = idCmd.ToUInt32();
472             var menuItem = _items.FirstOrDefault(item => item.Value.MenuDisplayId == menuID).Value;
473             if (menuItem != null)
474             {
475                 switch ((GCS)uFlags)
476                 {
477                     case GCS.GCS_VERBW:
478                         if (menuItem.VerbCanonicalName.Length > cchMax - 1)
479                         {
480                             Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
481                         }
482                         else
483                         {
484                             pszName.Clear();
485                             pszName.Append(menuItem.VerbCanonicalName);
486                         }
487                         break;
488
489                     case GCS.GCS_HELPTEXTW:
490                         if (menuItem.VerbHelpText.Length > cchMax - 1)
491                         {
492                             Marshal.ThrowExceptionForHR(WinError.STRSAFE_E_INSUFFICIENT_BUFFER);
493                         }
494                         else
495                         {
496                             pszName.Clear();
497                             pszName.Append(menuItem.VerbHelpText);
498                         }
499                         break;
500                 }
501             }
502         }
503
504         #endregion
505     }
506 }