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