Vanara/Windows.Shell/ShellObjects/ShellBrowser.cs

1002 lines
38 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
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>Extension methods for <see cref="ShellBrowserViewHandler"/>.</summary>
public static class ShellBrowserViewHandlerExtension
{
/// <summary>
/// Returns the reference to the given <see cref="ShellBrowserViewHandler"/> if it is not null <b>and</b> valid, null otherwise
/// </summary>
/// <param name="shellBrowserViewHandler">A (possible) <see cref="ShellBrowserViewHandler"/> reference.</param>
/// <returns></returns>
public static ShellBrowserViewHandler GetValidInstance(this ShellBrowserViewHandler shellBrowserViewHandler) =>
((!(shellBrowserViewHandler is null)) && shellBrowserViewHandler.IsValid) ? shellBrowserViewHandler : null;
}
/// <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 =&gt; 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; =&gt; Fixed again by returning HRESULT.E_NOTIMPL from MessageSFVCB
/// - TODO: internal static readonly bool IsMinVista = Environment.OSVersion.Version.Major &gt;= 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-&gt;More-&gt;Group", and then do the grouping. However, the
/// Icons for 'Recent Files'-Group get lost.
/// - TODO: ViewMode-Property, Thumbnailsize =&gt; Set ThumbnailSize for Large, ExtraLarge, etc.
/// - DONE: Keyboard-Handling
/// - DONE: BrowseObject -&gt;Parent -&gt; Relative
/// - TODO: Properties in design editor!!!
/// - TODO: Write History correctly!
/// - TODO: Check getting / losing Focus! again
/// - TODO: Context-Menu -&gt; "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 =&gt; 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
{
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 = "This folder is empty.";
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("This folder is empty."), Description("The default text that is displayed when an empty folder is shown.")]
public string EmptyFolderText
{
get => emptyFolderText;
set
{
emptyFolderText = value;
if (ViewHandler.IsValid)
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 => ViewHandler.IsValid ? ViewHandler.ThumbnailSize : defaultThumbnailSize;
set
{
if (ViewHandler.IsValid)
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 (ViewHandler.IsValid)
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.")]
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;
PIDL targetObject = PIDLUtil.ILCombine((IntPtr)currentObject.PIDL, pidl);
shellObject = new ShellItem(targetObject);
}
// 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((Action)(() => BrowseShellItemInternal(shellObject)));
else
BrowseShellItemInternal(shellObject);
return HRESULT.S_OK;
#region BrowseShellItemInternal
void BrowseShellItemInternal(ShellItem shellItem)
{
// Save ViewState of current folder
ViewHandler.GetValidInstance()?.ShellView.SaveViewState();
if (viewStateStream is not null)
Marshal.ReleaseComObject(viewStateStream);
viewStateStreamIdentifier = shellItem.ParsingName;
var viewHandler = new ShellBrowserViewHandler(this,
new ShellFolder(shellItem),
ref folderSettings,
ref 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();
}
#endregion BrowseShellItemInternal
}
/// <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, out IStream stream)
{
if (viewStateStream is not null)
Marshal.ReleaseComObject(viewStateStream);
stream = viewStateStream = ShlwApi.SHOpenRegStream2(hkey: HKEY.HKEY_CURRENT_USER, pszSubkey: ViewStateRegistryKey, pszValue: viewStateStreamIdentifier,
grfMode: grfMode);
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(out IShellView shellView)
{
if (ViewHandler.GetValidInstance() is not null)
{
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 = ViewHandler.GetValidInstance();
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()
{
ShellBrowserViewHandler viewHandler = ViewHandler.GetValidInstance();
if (viewHandler is not null)
viewHandler.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 = ViewHandler.GetValidInstance();
if (!(shvwHandler is 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
{
ShellBrowserViewHandler viewHandler = ViewHandler.GetValidInstance();
return viewHandler is not null ? viewHandler.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="System.EventArgs"/> instance containing the event data.</param>
protected override void OnHandleDestroyed(EventArgs e)
{
ViewHandler.GetValidInstance()?.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="System.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 (ViewHandler.GetValidInstance() is not null)
{
// 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 (ViewHandler.GetValidInstance() is not null)
{
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>
private void InitializeComponent()
{
components = new Container();
AutoScaleMode = AutoScaleMode.Font;
}
/// <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
{
get
{
ShellBrowserViewHandler viewHandler = shellBrowser.ViewHandler.GetValidInstance();
return viewHandler is not null ? viewHandler.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
{
return array is null ? null : ShellItem.Open(array.GetItemAt((uint)index));
}
catch
{
return null;
}
finally
{
if (array is not null)
Marshal.ReleaseComObject(array);
}
}
}
/// <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, ref FOLDERSETTINGS folderSettings, ref 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
{
var sfvCreate = new SFV_CREATE()
{
cbSize = (uint)Marshal.SizeOf(typeof(SFV_CREATE)),
pshf = shellFolder.IShellFolder,
psvOuter = null,
psfvcb = this,
};
SHCreateShellFolderView(sfvCreate, out IShellView shellView).ThrowIfFailed();
ShellView = shellView ??
throw new InvalidComObjectException(nameof(ShellView));
FolderView2 = (IFolderView2)ShellView ??
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;
}
}
}
}