-
Notifications
You must be signed in to change notification settings - Fork 200
/
Copy pathShellBrowser.cs
959 lines (804 loc) · 37 KB
/
ShellBrowser.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Windows.Forms;
using Vanara.PInvoke;
using static Vanara.PInvoke.Shell32;
namespace Vanara.Windows.Shell;
/// <summary>The direction argument for NavigateFromHistory()</summary>
public enum NavigationLogDirection
{
/// <summary>Navigates forward through the navigation log</summary>
Forward,
/// <summary>Navigates backward through the travel log</summary>
Backward
}
/// <summary>Undocumented Flags used by <see cref="IShellFolderViewCB.MessageSFVCB"/> Callback Handler.</summary>
public enum SFVMUD
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
SFVM_SELECTIONCHANGED = 8,
SFVM_DRAWMENUITEM = 9,
SFVM_MEASUREMENUITEM = 10,
SFVM_EXITMENULOOP = 11,
SFVM_VIEWRELEASE = 12,
SFVM_GETNAMELENGTH = 13,
SFVM_WINDOWCLOSING = 16,
SFVM_LISTREFRESHED = 17,
SFVM_WINDOWFOCUSED = 18,
SFVM_REGISTERCOPYHOOK = 20,
SFVM_COPYHOOKCALLBACK = 21,
SFVM_ADDINGOBJECT = 29,
SFVM_REMOVINGOBJECT = 30,
SFVM_GETCOMMANDDIR = 33,
SFVM_GETCOLUMNSTREAM = 34,
SFVM_CANSELECTALL = 35,
SFVM_ISSTRICTREFRESH = 37,
SFVM_ISCHILDOBJECT = 38,
SFVM_GETEXTVIEWS = 40,
SFVM_GET_CUSTOMVIEWINFO = 77,
SFVM_ENUMERATEDITEMS = 79, // It seems this msg never gets sent, using Win 10 at least.
SFVM_GET_VIEW_DATA = 80,
SFVM_GET_WEBVIEW_LAYOUT = 82,
SFVM_GET_WEBVIEW_CONTENT = 83,
SFVM_GET_WEBVIEW_TASKS = 84,
SFVM_GET_WEBVIEW_THEME = 86,
SFVM_GETDEFERREDVIEWSETTINGS = 92,
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}
/// <summary>Indicates the viewing mode of the ShellBrowser</summary>
public enum ShellBrowserViewMode
{
/// <summary>Choose the best view mode for the folder</summary>
Auto = FOLDERVIEWMODE.FVM_AUTO,
/// <summary>(New for Windows7)</summary>
Content = FOLDERVIEWMODE.FVM_CONTENT,
/// <summary>Object names and other selected information, such as the size or date last updated, are shown.</summary>
Details = FOLDERVIEWMODE.FVM_DETAILS,
/// <summary>The view should display medium-size icons.</summary>
Icon = FOLDERVIEWMODE.FVM_ICON,
/// <summary>Object names are displayed in a list view.</summary>
List = FOLDERVIEWMODE.FVM_LIST,
/// <summary>The view should display small icons.</summary>
SmallIcon = FOLDERVIEWMODE.FVM_SMALLICON,
/// <summary>The view should display thumbnail icons.</summary>
Thumbnail = FOLDERVIEWMODE.FVM_THUMBNAIL,
/// <summary>The view should display icons in a filmstrip format.</summary>
ThumbStrip = FOLDERVIEWMODE.FVM_THUMBSTRIP,
/// <summary>The view should display large icons.</summary>
Tile = FOLDERVIEWMODE.FVM_TILE
}
/// <summary>
/// Encapsulates a <see cref="IShellBrowser"/>-Implementation within an <see cref="UserControl"/>. <br/><br/> Implements the following
/// Interfaces: <br/>
/// - <seealso cref="IWin32Window"/><br/>
/// - <seealso cref="IShellBrowser"/><br/>
/// - <seealso cref="Shell32.IServiceProvider"/><br/><br/> For more Information on used techniques see: <br/>
/// - <seealso href="https://www.codeproject.com/Articles/28961/Full-implementation-of-IShellBrowser"/><br/><br/><br/> Known Issues: <br/>
/// - Using windows 10, the virtual Quick-Access folder doesn't get displayed properly. It has to be grouped by "Group" (as shown in
/// Windows Explorer UI), but I couldn't find the OLE-Property for this. Also, if using Groups, the Frequent Files List doesn't have its
/// Icons. Maybe we have to bind to another version of ComCtrls to get this rendered properly - That's just an idea though, cause the
/// Collapse-/Expand-Icons of the Groups have the Windows Vista / Windows 7-Theme, not the Windows 10 Theme as I can see. <br/>
/// - DONE: Keyboard input doesn't work so far. <br/>
/// - DONE: Only Details-Mode should have column headers: (Using Shell32.FOLDERFLAGS.FWF_NOHEADERINALLVIEWS) <br/> https://stackoverflow.com/questions/11776266/ishellview-columnheaders-not-hidden-if-autoview-does-not-choose-details
/// - TODO: CustomDraw, when currently no shellView available <br/>
/// - DONE: Network folder: E_FAIL => DONE: Returning HRESULT.E_NOTIMPL from MessageSFVCB fixes this <br/>
/// - DONE: Disk Drive (empty): E_CANCELLED_BY_USER <br/>
/// - DONE: Disable header in Details view when grouping is enabled
/// - DONE: Creating ViewWindow using '.CreateViewWindow()' fails on Zip-Folders; => Fixed again by returning HRESULT.E_NOTIMPL from MessageSFVCB
/// - TODO: internal static readonly bool IsMinVista = Environment.OSVersion.Version.Major >= 6; // TODO: We use one interface,
/// afaik, that only works in vista and above: IFolderView2
/// - TODO: Windows 10' Quick Access folder has a special type of grouping, can't find out how this works yet. As soon as we would be
/// able to get all the available properties for an particular item, we would be able found out how this grouping works. However, it
/// seems to be a special group, since folders are Tiles, whereas files are shown in Details mode.
/// - NOTE: The grouping is done by 'Group'. Activate it using "Group by->More->Group", and then do the grouping. However, the
/// Icons for 'Recent Files'-Group get lost.
/// - TODO: ViewMode-Property, Thumbnailsize => Set ThumbnailSize for Large, ExtraLarge, etc.
/// - DONE: Keyboard-Handling
/// - DONE: BrowseObject ->Parent -> Relative
/// - TODO: Properties in design editor!!!
/// - TODO: Write History correctly!
/// - TODO: Check getting / losing Focus! again
/// - TODO: Context-Menu -> "Open File Location" doesn't work on folder "Quick Access"
/// - TODO: When columns get reordered in details mode, then navigate to another folder, then back => column content gets messed
///
/// NOTE: https://stackoverflow.com/questions/7698602/how-to-get-embedded-explorer-ishellview-to-be-browsable-i-e-trigger-browseobje
/// NOTE: https://stackoverflow.com/questions/54390268/getting-the-current-ishellview-user-is-interacting-with
/// NOTE: https://www.codeproject.com/Articles/35197/Undocumented-List-View-Features // IMPORTANT!
/// NOTE: https://answers.microsoft.com/en-us/windows/forum/windows_10-files-winpc/windows-10-quick-access-folders-grouped-separately/ecd4be4a-1847-4327-8c44-5aa96e0120b8
/// </summary>
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Description("A Shell object that displays a list of Shell Items.")]
[Guid("B8B0F852-9527-4CA8-AB1D-648AE95B618E")]
public class ShellBrowser : UserControl, IWin32Window, IShellBrowser, Shell32.IServiceProvider
{
private const string defEmptyFolderText = "This folder is empty.";
internal const int defaultThumbnailSize = 32;
private const string processCmdKeyClassNameEdit = "Edit";
private const int processCmdKeyClassNameMaxLength = 31;
private readonly StringBuilder processCmdKeyClassName = new(processCmdKeyClassNameMaxLength + 1);
/// <summary>Required designer variable.</summary>
private IContainer components;
private string emptyFolderText = defEmptyFolderText;
private FOLDERSETTINGS folderSettings = new(FOLDERVIEWMODE.FVM_AUTO, FOLDERFLAGS.FWF_NOHEADERINALLVIEWS | FOLDERFLAGS.FWF_NOWEBVIEW | FOLDERFLAGS.FWF_USESEARCHFOLDER);
private IStream? viewStateStream;
private string? viewStateStreamIdentifier;
/// <summary>Initializes a new instance of the <see cref="ShellBrowser"/> class.</summary>
public ShellBrowser() : base()
{
InitializeComponent();
History = new ShellNavigationHistory();
Items = new ShellItemCollection(this, SVGIO.SVGIO_ALLVIEW);
SelectedItems = new ShellItemCollection(this, SVGIO.SVGIO_SELECTION);
}
/// <summary>Fires when the Items collection changes.</summary>
[Category("Action"), Description("Items changed.")]
public event EventHandler? ItemsChanged;
/// <summary>Fires when ShellBrowser has navigated to a new folder.</summary>
[Category("Action"), Description("ShellBowser has navigated to a new folder.")]
public event EventHandler<ShellBrowserNavigatedEventArgs>? Navigated;
/// <summary>Fires when the SelectedItems collection changes.</summary>
[Category("Behavior"), Description("Selection changed.")]
public event EventHandler? SelectionChanged;
/// <summary>The default text that is displayed when an empty folder is shown</summary>
[Category("Appearance"), DefaultValue(defEmptyFolderText), Description("The default text that is displayed when an empty folder is shown.")]
public string EmptyFolderText
{
get => emptyFolderText;
set
{
emptyFolderText = value;
if (IsHandlerValid)
ViewHandler!.Text = value;
}
}
/// <summary>Contains the navigation history of the ShellBrowser</summary>
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ShellNavigationHistory History { get; private set; }
/// <summary>The set of ShellItems in the ShellBrowser</summary>
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IReadOnlyList<ShellItem> Items { get; }
/// <summary>The set of selected ShellItems in the ShellBrowser</summary>
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IReadOnlyList<ShellItem> SelectedItems { get; }
/// <summary>The size of the thumbnails in pixels.</summary>
[Category("Appearance"), DefaultValue(defaultThumbnailSize), Description("The size of the thumbnails in pixels.")]
public int ThumbnailSize
{
get => IsHandlerValid ? ViewHandler!.ThumbnailSize : defaultThumbnailSize;
set
{
if (IsHandlerValid)
ViewHandler!.ThumbnailSize = value;
}
}
/// <summary>The viewing mode of the ShellBrowser</summary>
/// <remarks>Internally, this uses LVM_SETVIEW and LVM_GETVIEW messages on the ListView control</remarks>
[Category("Appearance"), DefaultValue(typeof(ShellBrowserViewMode), "Auto"), Description("The viewing mode of the ShellBrowser.")]
public ShellBrowserViewMode ViewMode
{
get => (ShellBrowserViewMode)folderSettings.ViewMode;
set
{
// TODO: Set ThumbnailSize accordingly?
folderSettings.ViewMode = (FOLDERVIEWMODE)value;
if (IsHandlerValid)
ViewHandler!.ViewMode = folderSettings.ViewMode;
}
}
/// <summary>The Registry Key where Browser ViewStates get serialized</summary>
/// <example>Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Streams\\</example>
[Category("Behavior"), Description("The Registry Key where Browser ViewStates get serialized.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public string ViewStateRegistryKey { get; set; } =
$"Software\\{Application.CompanyName}\\{Application.ProductName}\\ShellBrowser\\ViewStateStreams";
/// <summary>
/// <inheritdoc/><br/><br/>
/// Note: I've tried using ComCtl32.ListViewMessage.LVM_SETBKIMAGE, but this doesn't work properly. That's why this property has
/// been hidden.
/// </summary>
[Bindable(false), Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override Image BackgroundImage => base.BackgroundImage!;
/// <inheritdoc/>
protected override Size DefaultSize => new(200, 150);
/// <summary>The <see cref="ShellBrowserViewHandler"/> that is currently in use.</summary>
protected ShellBrowserViewHandler? ViewHandler { get; private set; }
/// <summary></summary>
/// <param name="pidl"></param>
/// <param name="wFlags"></param>
/// <returns>HRESULT.STG_E_PATHNOTFOUND if path n found</returns>
public HRESULT BrowseObject(IntPtr pidl, SBSP wFlags)
{
ShellItem? shellObject = null;
// The given PIDL equals Desktop, so ignore the other flags
if (ShellFolder.Desktop.PIDL.Equals(pidl))
{
shellObject = new ShellItem(ShellFolder.Desktop.PIDL);
}
// SBSP_NAVIGATEBACK stands for the last item in the navigation history list (and ignores the pidl)
else if (wFlags.HasFlag(SBSP.SBSP_NAVIGATEBACK))
{
if (History.CanSeekBackward)
shellObject = History.SeekBackward();
else
return HRESULT.STG_E_PATHNOTFOUND;
}
// SBSP_NAVIGATEFORWARD stands for the next item in the navigation history list (and ignores the pidl)
else if (wFlags.HasFlag(SBSP.SBSP_NAVIGATEFORWARD))
{
if (History.CanSeekForward)
shellObject = History.SeekForward();
else
return HRESULT.STG_E_PATHNOTFOUND;
}
// SBSP_RELATIVE stands for a pidl relative to the current folder
else if (wFlags.HasFlag(SBSP.SBSP_RELATIVE))
{
ShellItem? currentObject = History.Current;
if (currentObject is null)
return HRESULT.STG_E_PATHNOTFOUND;
shellObject = new ShellItem(ILCombine((IntPtr)currentObject.PIDL, pidl));
}
// SBSP_PARENT stands for the parent folder (and ignores the pidl)
else if (wFlags.HasFlag(SBSP.SBSP_PARENT))
{
ShellItem? currentObject = History.Current;
ShellFolder? parentObject = currentObject?.Parent;
if ((parentObject is not null) && parentObject.PIDL.IsParentOf(currentObject!.PIDL))
shellObject = parentObject;
else
return HRESULT.STG_E_PATHNOTFOUND;
}
// SBSP_ABSOLUTE as the remaining option stands for an absolute pidl that is given
else
{
// Remember we are not the owner of this pidl, so clone it to have our own copy on the heap.
shellObject = new ShellItem(new PIDL(pidl, true));
}
if (InvokeRequired)
BeginInvoke(() => BrowseShellItemInternal(shellObject!));
else
BrowseShellItemInternal(shellObject!);
return HRESULT.S_OK;
void BrowseShellItemInternal(ShellItem shellItem)
{
// Save ViewState of current folder
GetValidHandler()?.ShellView?.SaveViewState();
if (viewStateStream is not null)
Marshal.ReleaseComObject(viewStateStream);
viewStateStreamIdentifier = shellItem.ParsingName;
var viewHandler = new ShellBrowserViewHandler(this, new ShellFolder(shellItem), folderSettings, emptyFolderText);
// Clone the PIDL, to have our own object copy on the heap!
if (!wFlags.HasFlag(SBSP.SBSP_WRITENOHISTORY))
History.Add(new ShellItem(new PIDL(viewHandler.ShellFolder.PIDL)));
ShellBrowserViewHandler? oldViewHandler = ViewHandler;
ViewHandler = viewHandler;
oldViewHandler?.UIDeactivate();
viewHandler.UIActivate();
oldViewHandler?.DestroyView();
OnNavigated(viewHandler.ShellFolder);
OnSelectionChanged();
}
}
/// <inheritdoc/>
public HRESULT ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL;
/// <inheritdoc/>
public HRESULT EnableModelessSB(bool fEnable) => HRESULT.E_NOTIMPL;
/// <inheritdoc/>
public HRESULT GetControlWindow(FCW id, out HWND hwnd)
{
hwnd = HWND.NULL;
return HRESULT.E_NOTIMPL;
}
/// <inheritdoc/>
public HRESULT GetViewStateStream(STGM grfMode, [MaybeNull] out IStream stream)
{
if (viewStateStream is not null)
Marshal.ReleaseComObject(viewStateStream);
#pragma warning disable IL2050 // Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed.
stream = viewStateStream = ShlwApi.SHOpenRegStream2(HKEY.HKEY_CURRENT_USER, ViewStateRegistryKey, viewStateStreamIdentifier, grfMode);
#pragma warning restore IL2050 // Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed.
return stream is null ? HRESULT.E_FAIL : HRESULT.S_OK;
}
/// <inheritdoc/>
public HRESULT GetWindow(out HWND phwnd)
{
phwnd = Handle;
return HRESULT.S_OK;
}
/// <inheritdoc/>
public HRESULT InsertMenusSB(HMENU hmenuShared, ref Ole32.OLEMENUGROUPWIDTHS lpMenuWidths) => HRESULT.E_NOTIMPL;
/// <summary>
/// Navigates to the last item in the navigation history list. This does not change the set of locations in the navigation log.
/// </summary>
/// <returns>True if the navigation succeeded, false if it failed for any reason.</returns>
public bool NavigateBack() => BrowseObject(IntPtr.Zero, SBSP.SBSP_NAVIGATEBACK).Succeeded;
/// <summary>
/// Navigates to the next item in the navigation history list. This does not change the set of locations in the navigation log.
/// </summary>
/// <returns>True if the navigation succeeded, false if it failed for any reason.</returns>
public bool NavigateForward() => BrowseObject(IntPtr.Zero, SBSP.SBSP_NAVIGATEFORWARD).Succeeded;
/// <summary>
/// Navigate within the navigation log in a specific direciton. This does not change the set of locations in the navigation log.
/// </summary>
/// <param name="direction">The direction to navigate within the navigation logs collection.</param>
/// <returns>True if the navigation succeeded, false if it failed for any reason.</returns>
public bool NavigateFromHistory(NavigationLogDirection direction) => direction switch
{
NavigationLogDirection.Backward => NavigateBack(),
NavigationLogDirection.Forward => NavigateForward(),
_ => false,
};
/// <summary>Navigates to the parent folder.</summary>
/// <returns>True if the navigation succeeded, false if it failed for any reason.</returns>
public bool NavigateParent() => BrowseObject(IntPtr.Zero, SBSP.SBSP_PARENT).Succeeded;
/// <summary>Navigate within the navigation log. This does not change the set of locations in the navigation log.</summary>
/// <param name="historyIndex">An index into the navigation logs Locations collection.</param>
/// <returns>True if the navigation succeeded, false if it failed for any reason.</returns>
public bool NavigateToHistoryIndex(int historyIndex)
{
using ShellItem? shellFolder = History.Seek(historyIndex, SeekOrigin.Current);
return shellFolder is not null && BrowseObject((IntPtr)shellFolder.PIDL, SBSP.SBSP_ABSOLUTE | SBSP.SBSP_WRITENOHISTORY).Succeeded;
}
/// <inheritdoc/>
public HRESULT OnViewWindowActive(IShellView ppshv) => HRESULT.E_NOTIMPL;
/// <inheritdoc/>
public HRESULT QueryActiveShellView([MaybeNull] out IShellView shellView)
{
if (IsHandlerValid)
{
Marshal.AddRef(Marshal.GetIUnknownForObject(ViewHandler!.ShellView!));
shellView = ViewHandler.ShellView;
return HRESULT.S_OK;
}
shellView = null;
return HRESULT.E_PENDING;
}
/// <inheritdoc/>
public HRESULT RemoveMenusSB(HMENU hmenuShared) => HRESULT.E_NOTIMPL;
/// <summary>Selects all items in the current view.</summary>
public void SelectAll()
{
ShellBrowserViewHandler? viewHandler = GetValidHandler();
if (viewHandler is not null)
{
// NOTE: The for-loop is rather slow, so send (Ctrl+A)-KeyDown-Message instead and let the ShellView do the work for (var i
// = 0; i < viewHandler.FolderView2.ItemCount(SVGIO.SVGIO_ALLVIEW); i++) viewHandler.FolderView2.SelectItem(i, SVSIF.SVSI_SELECT);
//
// TODO: Another way would be to use this Windows Message-Pattern (Workaround #2): https://stackoverflow.com/questions/9039989/how-to-selectall-in-a-winforms-virtual-listview
var msg = new Message()
{
HWnd = (IntPtr)viewHandler.ViewWindow,
Msg = (int)User32.WindowMessage.WM_KEYDOWN,
};
ProcessCmdKey(ref msg, Keys.Control | Keys.A);
}
}
/// <inheritdoc/>
public HRESULT SendControlMsg(FCW id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret)
{
pret = IntPtr.Zero;
return HRESULT.E_NOTIMPL;
}
/// <inheritdoc/>
public HRESULT SetMenuSB(HMENU hmenuShared, IntPtr holemenuRes, HWND hwndActiveObject) => HRESULT.E_NOTIMPL;
/// <inheritdoc/>
public HRESULT SetStatusTextSB(string pszStatusText) => HRESULT.E_NOTIMPL;
/// <inheritdoc/>
public HRESULT SetToolbarItems(ComCtl32.TBBUTTON[]? lpButtons, uint nButtons, FCT uFlags) => HRESULT.E_NOTIMPL;
/// <inheritdoc/>
public HRESULT TranslateAcceleratorSB(ref MSG pmsg, ushort wID) => HRESULT.E_NOTIMPL;
/// <summary>Unselects all items in the current view.</summary>
public void UnselectAll() => GetValidHandler()?.FolderView2?.SelectItem(-1, SVSIF.SVSI_DESELECTOTHERS);
/// <summary>
/// <see cref="Shell32.IServiceProvider"/>-Interface Implementation for <see cref="ShellBrowser"/>. <br/><br/> Responds to the
/// following Interfaces: <br/>
/// - <see cref="IShellBrowser"/><br/>
/// - <see cref="IShellFolderViewCB"/><br/>
/// </summary>
/// <param name="guidService">The service's unique identifier (SID).</param>
/// <param name="riid">The IID of the desired service interface.</param>
/// <param name="ppvObject">
/// When this method returns, contains the interface pointer requested riid. If successful, the calling application is responsible
/// for calling IUnknown::Release using this value when the service is no longer needed. In the case of failure, this value is NULL.
/// </param>
/// <returns><see cref="HRESULT.S_OK"/> or <br/><see cref="HRESULT.E_NOINTERFACE"/></returns>
HRESULT Shell32.IServiceProvider.QueryService(in Guid guidService, in Guid riid, out IntPtr ppvObject)
{
// IShellBrowser: Guid("000214E2-0000-0000-C000-000000000046")
if (riid.Equals(typeof(IShellBrowser).GUID))
{
ppvObject = Marshal.GetComInterfaceForObject(this, typeof(IShellBrowser));
return HRESULT.S_OK;
}
// IShellFolderViewCB: Guid("2047E320-F2A9-11CE-AE65-08002B2E1262")
if (riid.Equals(typeof(IShellFolderViewCB).GUID))
{
ShellBrowserViewHandler? shvwHandler = GetValidHandler();
if (shvwHandler is not null)
{
ppvObject = Marshal.GetComInterfaceForObject(shvwHandler, typeof(IShellFolderViewCB));
return HRESULT.S_OK;
}
}
ppvObject = IntPtr.Zero;
return HRESULT.E_NOINTERFACE;
}
/// <summary>Gets the items in the ShellBrowser as an IShellItemArray</summary>
/// <returns>An <see cref="IShellItemArray"/> instance or <see langword="null"/> if not available.</returns>
internal IShellItemArray? GetItemsArray(SVGIO opt)
{
try
{
return GetValidHandler()?.FolderView2?.Items<IShellItemArray>(opt) ?? null;
}
catch { return null; }
}
/// <summary>Raises the <see cref="ItemsChanged"/> event.</summary>
protected internal virtual void OnItemsChanged() => ItemsChanged?.Invoke(this, EventArgs.Empty);
/// <summary>Raises the <see cref="Navigated"/> event.</summary>
protected internal virtual void OnNavigated(ShellFolder shellFolder)
{
if (Navigated is not null)
{
ShellBrowserNavigatedEventArgs eventArgs = new(shellFolder);
Navigated.Invoke(this, eventArgs);
}
}
/// <summary>Raises the <see cref="SelectionChanged"/> event.</summary>
protected internal virtual void OnSelectionChanged() => SelectionChanged?.Invoke(this, EventArgs.Empty);
/// <summary>Clean up any resources being used.</summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components is not null))
{
components.Dispose();
}
base.Dispose(disposing);
}
/// <summary>Raises the <see cref="E:HandleDestroyed"/> event. Saves ViewState when ShellBrowser gets closed.</summary>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected override void OnHandleDestroyed(EventArgs e)
{
GetValidHandler()?.ShellView?.SaveViewState();
base.OnHandleDestroyed(e);
}
/// <summary>Raises the <see cref="E:Resize"/> event. Resize ViewWindow when ShellBrowser gets resized.</summary>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected override void OnResize(EventArgs e)
{
ViewHandler?.MoveWindow(0, 0, ClientRectangle.Width, ClientRectangle.Height, false);
base.OnResize(e);
}
/// <summary>Process known command keys of the ShellBrowser.</summary>
/// <param name="msg">Windows Message</param>
/// <param name="keyData">Key codes and modifiers</param>
/// <returns>true if character was processed by the control; otherwise, false</returns>
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
// We have to take special care when the ShellView is currently renaming an item: If message's sender equals class 'Edit', and
// its parent window is our ViewWindow, the ShellView is currently showing an Edit-field to let the User edit an item's name.
// Thus, we have to pass all Key Strokes directly to the ShellView.
if (IsHandlerValid)
{
// Note: I tried using the LVM_GETEDITCONTROL message for finding the edit control without luck
if (User32.GetClassName(msg.HWnd,
processCmdKeyClassName,
processCmdKeyClassNameMaxLength) > 0)
{
if (processCmdKeyClassName.ToString().Equals(processCmdKeyClassNameEdit))
{
// Try to get Edit field's parent 'SysListView32' handle
HWND hSysListView32 = User32.GetParent(msg.HWnd);
if (!hSysListView32.IsNull)
{
// Try to get SysListView32's parent 'SHELLDLL_DefView' handle
HWND hShellDllDefViewWindow = User32.GetParent(hSysListView32);
if (!hShellDllDefViewWindow.IsNull && (hShellDllDefViewWindow == ViewHandler!.ViewWindow))
{
ViewHandler.ShellView!.TranslateAccelerator(new MSG(msg.HWnd, (uint)msg.Msg, msg.WParam, msg.LParam));
return true;
}
}
}
}
}
// Process tab key for control focus cycle: Tab => Focus next control Tab + Shift => Focus previous control
if ((keyData & Keys.KeyCode) == Keys.Tab)
{
var forward = (keyData & Keys.Shift) != Keys.Shift;
Parent!.SelectNextControl(ActiveControl, forward: forward, tabStopOnly: true, nested: true, wrap: true);
return true;
}
// Process folder navigation shortcuts: Alt + Left OR BrowserBack => Navigate back in history Alt + Right OR BrowserForward =>
// Navigate forward in history Backspace => Navigate to parent folder
switch (keyData)
{
case Keys.BrowserBack:
case Keys.Alt | Keys.Left:
NavigateBack();
return true;
case Keys.BrowserForward:
case Keys.Alt | Keys.Right:
NavigateForward();
return true;
case Keys.Back:
NavigateParent();
return true;
}
// Let the ShellView process all other keystrokes
if (IsHandlerValid)
{
ViewHandler!.ShellView!.TranslateAccelerator(new MSG(msg.HWnd, (uint)msg.Msg, msg.WParam, msg.LParam));
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
/// <summary>Required method for Designer support - do not modify the contents of this method with the code editor.</summary>
[MemberNotNull(nameof(components))]
private void InitializeComponent()
{
components = new Container();
AutoScaleMode = AutoScaleMode.Font;
}
private ShellBrowserViewHandler? GetValidHandler() => IsHandlerValid ? ViewHandler : null;
private bool IsHandlerValid => ViewHandler is not null && IsHandlerValid;
/// <summary>Represents a collection of <see cref="ShellItem"/> attached to an <see cref="ShellBrowser"/>.</summary>
private class ShellItemCollection : IReadOnlyList<ShellItem>
{
private readonly SVGIO option;
private readonly ShellBrowser shellBrowser;
internal ShellItemCollection(ShellBrowser shellBrowser, SVGIO opt)
{
this.shellBrowser = shellBrowser;
option = opt;
}
/// <summary>Gets the number of elements in the collection.</summary>
/// <value>Returns a <see cref="int"/> value.</value>
public int Count => shellBrowser.GetValidHandler()?.FolderView2!.ItemCount(option) ?? 0;
private IShellItemArray? Array => shellBrowser.GetItemsArray(option);
private IEnumerable<IShellItem> Items
{
get
{
IShellItemArray? array = Array;
if (array is null)
yield break;
try
{
for (uint i = 0; i < array.GetCount(); i++)
yield return array.GetItemAt(i);
}
finally
{
Marshal.ReleaseComObject(array);
}
}
}
/// <summary>Gets the <see cref="ShellItem"/> at the specified index.</summary>
/// <value>The <see cref="ShellItem"/>.</value>
/// <param name="index">The zero-based index of the element to get.</param>
public ShellItem this[int index]
{
get
{
IShellItemArray? array = Array;
try
{
if (array is not null)
return ShellItem.Open(array.GetItemAt((uint)index));
}
catch { }
finally
{
if (array is not null)
Marshal.ReleaseComObject(array);
}
throw new ArgumentOutOfRangeException(nameof(index));
}
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<ShellItem> GetEnumerator() => Items.Select(ShellItem.Open).GetEnumerator();
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
/// <summary>Event argument for The Navigated event.</summary>
public class ShellBrowserNavigatedEventArgs : EventArgs
{
/// <summary>Initializes a new instance of the <see cref="ShellBrowserNavigatedEventArgs"/> class.</summary>
public ShellBrowserNavigatedEventArgs(ShellFolder currentFolder) => CurrentFolder = currentFolder ?? throw new ArgumentNullException(nameof(currentFolder));
/// <summary>The new location of the ShellBrowser</summary>
public ShellFolder CurrentFolder { get; }
}
/// <summary>
/// Encapsulates an <see cref="IShellFolderViewCB">IShellFolderViewCB</see>-Implementation within an <see cref="IDisposable"/>-Object.
/// Beside that it's implemented as a Wrapper-Object that is responsible for creating and disposing the following objects aka
/// Interface-Instances: <br/>
/// - <seealso cref="Shell.ShellFolder"/><br/>
/// - <seealso cref="IShellView"/><br/>
/// - <seealso cref="IFolderView2"/><br/><br/> While doing that, it also handles some common error cases: <br/>
/// - When there's no disk in a disk drive <br/><br/> Implements the following Interfaces: <br/>
/// - <seealso cref="IShellFolderViewCB"/><br/><br/> This class make use of some <see cref="SFVMUD">undocumented Messages</see> in its
/// <see cref="IShellFolderViewCB.MessageSFVCB"/> Callback Handler. <br/><br/> For more Information on these see: <br/>
/// - Google Drive Shell Extension: <seealso href="https://github.com/google/google-drive-shell-extension/blob/master/DriveFusion/ShellFolderViewCBHandler.cpp"> ShellFolderViewCBHandler.cpp</seealso><br/>
/// - ReactOS: <seealso href="https://doxygen.reactos.org/d2/dbb/IShellFolderViewCB_8cpp.html">IShellFolderViewCB.cpp File Reference
/// </seealso>, <seealso href="https://doxygen.reactos.org/d2/dbb/IShellFolderViewCB_8cpp_source.html">IShellFolderViewCB.cpp</seealso>
/// </summary>
public class ShellBrowserViewHandler : IShellFolderViewCB
{
/// <summary>
/// <code>{"The operation was canceled by the user. (Exception from HRESULT: 0x800704C7)"}</code>
/// is the result of a call to <see cref="IShellView.CreateViewWindow"/> on a Shell Item that targets a removable Disk Drive when
/// currently no Media is present. Let's catch these to use our own error handling for this.
/// </summary>
internal static readonly HRESULT HRESULT_CANCELLED = new(0x800704C7);
private string? text;
private int thumbnailSize = ShellBrowser.defaultThumbnailSize;
/// <summary>Create an instance of <see cref="ShellBrowserViewHandler"/> to handle Callback messages for the given ShellFolder.</summary>
/// <param name="owner">The <see cref="ShellBrowser"/> that is owner of this instance.</param>
/// <param name="shellFolder">The ShellFolder for the view.</param>
/// <param name="folderSettings">The folder settings for the view.</param>
/// <param name="emptyFolderText">Text to display if the folder is empty.</param>
public ShellBrowserViewHandler(ShellBrowser owner, ShellFolder shellFolder, FOLDERSETTINGS folderSettings, string emptyFolderText)
{
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
ShellFolder = shellFolder ?? throw new ArgumentNullException(nameof(shellFolder));
// Create ShellView and FolderView2 objects, then its ViewWindow
try
{
SFV_CREATE sfvCreate = new()
{
cbSize = (uint)Marshal.SizeOf(typeof(SFV_CREATE)),
pshf = shellFolder.IShellFolder,
psvOuter = null,
psfvcb = this,
};
#pragma warning disable IL2050 // Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed.
SHCreateShellFolderView(sfvCreate, out IShellView? shellView).ThrowIfFailed();
#pragma warning restore IL2050 // Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed.
ShellView = shellView ?? throw new InvalidComObjectException(nameof(ShellView));
FolderView2 = ShellView as IFolderView2 ?? throw new InvalidComObjectException(nameof(FolderView2));
// Try to create ViewWindow and take special care of Exception {"The operation was canceled by the user. (Exception from
// HRESULT: 0x800704C7)"} cause this happens when there's no disk in a drive.
try
{
ViewWindow = ShellView.CreateViewWindow(null, folderSettings, owner, owner.ClientRectangle);
IsValid = true;
Text = emptyFolderText;
}
catch (COMException ex)
{
// TODO: Check if the target folder IS actually a drive with removable disks in it!
if (HRESULT_CANCELLED.Equals(ex.ErrorCode))
NoDiskInDriveError = true;
throw;
}
}
catch (COMException ex)
{
// TODO: e.g. C:\Windows\CSC => Permission denied! 0x8007 0005 E_ACCESSDENIED
ValidationError = ex;
}
}
/// <summary>The <see cref="IFolderView2"/>.</summary>
public IFolderView2? FolderView2 { get; private set; }
/// <summary>Indicates that no error occured while creating this instance, i.e. the View is fully functional.</summary>
public bool IsValid { get; private set; }
/// <summary>Indicates that an "No Disk In Drive"-error occured while creating this instance.</summary>
public bool NoDiskInDriveError { get; }
/// <summary>The owner of this instance of <see cref="ShellBrowserViewHandler"/>, i.e. the <see cref="ShellBrowser"/>.</summary>
public ShellBrowser Owner { get; }
/// <summary>The <see cref="ShellFolder"/>.</summary>
public ShellFolder ShellFolder { get; private set; }
/// <summary>The <see cref="IShellView"/>.</summary>
public IShellView? ShellView { get; private set; }
/// <summary>The default text to be used when there are no items in the view.</summary>
public string? Text
{
get => text;
set
{
text = value;
if (IsValid)
FolderView2!.SetText(FVTEXTTYPE.FVST_EMPTYTEXT, value);
}
}
/// <summary>The size of the thumbnails in pixels.</summary>
public int ThumbnailSize
{
get
{
if (IsValid)
FolderView2!.GetViewModeAndIconSize(out _, out thumbnailSize);
return thumbnailSize;
}
set
{
if (IsValid)
{
FolderView2!.GetViewModeAndIconSize(out FOLDERVIEWMODE fvm, out _);
FolderView2!.SetViewModeAndIconSize(fvm, thumbnailSize = value);
}
}
}
/// <summary>The <see cref="COMException"/> that occured, if creation of the instance failed.</summary>
public COMException? ValidationError { get; private set; }
/// <summary>The viewing mode of the ShellBrowser.</summary>
public FOLDERVIEWMODE ViewMode
{
get
{
if (IsValid)
{
return FolderView2!.GetCurrentViewMode();
// TODO: Check ThumbNailSize for new ViewModes with larger sized icons
}
return FOLDERVIEWMODE.FVM_AUTO; // TODO!
}
set
{
if (IsValid)
FolderView2!.SetCurrentViewMode(value);
}
}
/// <summary>The ViewWindow.</summary>
public HWND ViewWindow { get; private set; }
/// <summary>Destroy the view.</summary>
public void DestroyView()
{
IsValid = false;
// TODO: Remove MessageSFVCB here!
// Destroy ShellView's ViewWindow
ViewWindow = HWND.NULL;
ShellView?.DestroyViewWindow();
FolderView2 = null;
ShellView = null;
//this.ShellFolder = null; // NOTE: I >>think<< this one causes RPC-Errors
}
/// <summary>Changes the position and dimensions of this <see cref="ViewWindow"/>.</summary>
/// <param name="X">Left</param>
/// <param name="Y">Top</param>
/// <param name="nWidth">Width</param>
/// <param name="nHeight">Height</param>
/// <param name="bRepaint">Force redraw</param>
/// <returns>If the function succeeds, the return value is nonzero.</returns>
public bool MoveWindow(int X, int Y, int nWidth, int nHeight, bool bRepaint) =>
ViewWindow != HWND.NULL && User32.MoveWindow(ViewWindow, X, Y, nWidth, nHeight, bRepaint);
/// <summary>Activate the ShellView of this ShellBrowser.</summary>
/// <param name="uState">The <seealso cref="SVUIA"/> to be set</param>
public void UIActivate(SVUIA uState = SVUIA.SVUIA_ACTIVATE_NOFOCUS) => ShellView?.UIActivate(uState);
/// <summary>Deactivate the ShellView of this ShellBrowser.</summary>
public void UIDeactivate() => UIActivate(SVUIA.SVUIA_DEACTIVATE);
/// <summary>Allows communication between the system folder view object and a system folder view callback object.</summary>
/// <param name="uMsg">One of the SFVM_* notifications.</param>
/// <param name="wParam">Additional information. See the individual notification pages for specific requirements.</param>
/// <param name="lParam">Additional information. See the individual notification pages for specific requirements.</param>
/// <param name="plResult">TODO: @dahall: Where does this come from?</param>
/// <returns><b>S_OK</b> if the notification has been handled. <b>E_NOTIMPL</b> otherwise.</returns>
HRESULT IShellFolderViewCB.MessageSFVCB(SFVM uMsg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult)
{
switch ((SFVMUD)uMsg)
{
case SFVMUD.SFVM_SELECTIONCHANGED:
Owner.OnSelectionChanged();
return HRESULT.S_OK;
case SFVMUD.SFVM_LISTREFRESHED:
Owner.OnItemsChanged();
return HRESULT.S_OK;
default:
// TODO: What happens when the ViewMode gets changed via Context-Menu? => Msg #33, #18
return HRESULT.E_NOTIMPL;
}
}
}