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