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
{
/// The direction argument for NavigateFromHistory()
public enum NavigationLogDirection
{
/// Navigates forward through the navigation log
Forward,
/// Navigates backward through the travel log
Backward
}
/// Undocumented Flags used by Callback Handler.
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
}
/// Indicates the viewing mode of the ShellBrowser
public enum ShellBrowserViewMode
{
/// Choose the best view mode for the folder
Auto = FOLDERVIEWMODE.FVM_AUTO,
/// (New for Windows7)
Content = FOLDERVIEWMODE.FVM_CONTENT,
/// Object names and other selected information, such as the size or date last updated, are shown.
Details = FOLDERVIEWMODE.FVM_DETAILS,
/// The view should display medium-size icons.
Icon = FOLDERVIEWMODE.FVM_ICON,
/// Object names are displayed in a list view.
List = FOLDERVIEWMODE.FVM_LIST,
/// The view should display small icons.
SmallIcon = FOLDERVIEWMODE.FVM_SMALLICON,
/// The view should display thumbnail icons.
Thumbnail = FOLDERVIEWMODE.FVM_THUMBNAIL,
/// The view should display icons in a filmstrip format.
ThumbStrip = FOLDERVIEWMODE.FVM_THUMBSTRIP,
/// The view should display large icons.
Tile = FOLDERVIEWMODE.FVM_TILE
}
/// Extension methods for .
public static class ShellBrowserViewHandlerExtension
{
///
/// Returns the reference to the given if it is not null and valid, null otherwise
///
/// A (possible) reference.
///
public static ShellBrowserViewHandler GetValidInstance(this ShellBrowserViewHandler shellBrowserViewHandler) =>
((!(shellBrowserViewHandler is null)) && shellBrowserViewHandler.IsValid) ? shellBrowserViewHandler : null;
}
///
/// Encapsulates a -Implementation within an .
Implements the following
/// Interfaces:
/// -
/// -
/// -
For more Information on used techniques see:
/// -
Known Issues:
/// - 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.
/// - DONE: Keyboard input doesn't work so far.
/// - DONE: Only Details-Mode should have column headers: (Using Shell32.FOLDERFLAGS.FWF_NOHEADERINALLVIEWS)
https://stackoverflow.com/questions/11776266/ishellview-columnheaders-not-hidden-if-autoview-does-not-choose-details
/// - TODO: CustomDraw, when currently no shellView available
/// - DONE: Network folder: E_FAIL => DONE: Returning HRESULT.E_NOTIMPL from MessageSFVCB fixes this
/// - DONE: Disk Drive (empty): E_CANCELLED_BY_USER
/// - 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
///
[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);
/// Required designer variable.
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;
/// Initializes a new instance of the class.
public ShellBrowser()
: base()
{
InitializeComponent();
History = new ShellNavigationHistory();
Items = new ShellItemCollection(this, SVGIO.SVGIO_ALLVIEW);
SelectedItems = new ShellItemCollection(this, SVGIO.SVGIO_SELECTION);
}
/// Fires when the Items collection changes.
[Category("Action"), Description("Items changed.")]
public event EventHandler ItemsChanged;
/// Fires when ShellBrowser has navigated to a new folder.
[Category("Action"), Description("ShellBowser has navigated to a new folder.")]
public event EventHandler Navigated;
/// Fires when the SelectedItems collection changes.
[Category("Behavior"), Description("Selection changed.")]
public event EventHandler SelectionChanged;
/// The default text that is displayed when an empty folder is shown
[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;
}
}
/// Contains the navigation history of the ShellBrowser
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ShellNavigationHistory History { get; private set; }
/// The set of ShellItems in the ShellBrowser
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IReadOnlyList Items { get; }
/// The set of selected ShellItems in the ShellBrowser
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IReadOnlyList SelectedItems { get; }
/// The size of the thumbnails in pixels.
[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;
}
}
/// The viewing mode of the ShellBrowser
/// Internally, this uses LVM_SETVIEW and LVM_GETVIEW messages on the ListView control
[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;
}
}
/// The Registry Key where Browser ViewStates get serialized
/// Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Streams\\
[Category("Behavior"), Description("The Registry Key where Browser ViewStates get serialized.")]
public string ViewStateRegistryKey { get; set; } =
$"Software\\{ Application.CompanyName }\\{ Application.ProductName }\\ShellBrowser\\ViewStateStreams";
///
///
/// Note: I've tried using ComCtl32.ListViewMessage.LVM_SETBKIMAGE, but this doesn't work properly. That's why this property has
/// been hidden.
///
[Bindable(false), Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override Image BackgroundImage => base.BackgroundImage;
///
protected override Size DefaultSize => new(200, 150);
/// The that is currently in use.
protected ShellBrowserViewHandler ViewHandler { get; private set; }
///
///
///
/// HRESULT.STG_E_PATHNOTFOUND if path n found
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
}
///
public HRESULT ContextSensitiveHelp(bool fEnterMode) => HRESULT.E_NOTIMPL;
///
public HRESULT EnableModelessSB(bool fEnable) => HRESULT.E_NOTIMPL;
///
public HRESULT GetControlWindow(FCW id, out HWND hwnd)
{
hwnd = HWND.NULL;
return HRESULT.E_NOTIMPL;
}
///
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;
}
///
public HRESULT GetWindow(out HWND phwnd)
{
phwnd = Handle;
return HRESULT.S_OK;
}
///
public HRESULT InsertMenusSB(HMENU hmenuShared, ref Ole32.OLEMENUGROUPWIDTHS lpMenuWidths) => HRESULT.E_NOTIMPL;
///
/// Navigates to the last item in the navigation history list. This does not change the set of locations in the navigation log.
///
/// True if the navigation succeeded, false if it failed for any reason.
public bool NavigateBack() => BrowseObject(IntPtr.Zero, SBSP.SBSP_NAVIGATEBACK).Succeeded;
///
/// Navigates to the next item in the navigation history list. This does not change the set of locations in the navigation log.
///
/// True if the navigation succeeded, false if it failed for any reason.
public bool NavigateForward() => BrowseObject(IntPtr.Zero, SBSP.SBSP_NAVIGATEFORWARD).Succeeded;
///
/// Navigate within the navigation log in a specific direciton. This does not change the set of locations in the navigation log.
///
/// The direction to navigate within the navigation logs collection.
/// True if the navigation succeeded, false if it failed for any reason.
public bool NavigateFromHistory(NavigationLogDirection direction) => direction switch
{
NavigationLogDirection.Backward => NavigateBack(),
NavigationLogDirection.Forward => NavigateForward(),
_ => false,
};
/// Navigates to the parent folder.
/// True if the navigation succeeded, false if it failed for any reason.
public bool NavigateParent() => BrowseObject(IntPtr.Zero, SBSP.SBSP_PARENT).Succeeded;
/// Navigate within the navigation log. This does not change the set of locations in the navigation log.
/// An index into the navigation logs Locations collection.
/// True if the navigation succeeded, false if it failed for any reason.
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;
}
///
public HRESULT OnViewWindowActive(IShellView ppshv) => HRESULT.E_NOTIMPL;
///
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;
}
///
public HRESULT RemoveMenusSB(HMENU hmenuShared) => HRESULT.E_NOTIMPL;
/// Selects all items in the current view.
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);
}
}
///
public HRESULT SendControlMsg(FCW id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret)
{
pret = IntPtr.Zero;
return HRESULT.E_NOTIMPL;
}
///
public HRESULT SetMenuSB(HMENU hmenuShared, IntPtr holemenuRes, HWND hwndActiveObject) => HRESULT.E_NOTIMPL;
///
public HRESULT SetStatusTextSB(string pszStatusText) => HRESULT.E_NOTIMPL;
///
public HRESULT SetToolbarItems(ComCtl32.TBBUTTON[] lpButtons, uint nButtons, FCT uFlags) => HRESULT.E_NOTIMPL;
///
public HRESULT TranslateAcceleratorSB(ref MSG pmsg, ushort wID) => HRESULT.E_NOTIMPL;
/// Unselects all items in the current view.
public void UnselectAll()
{
ShellBrowserViewHandler viewHandler = ViewHandler.GetValidInstance();
if (viewHandler is not null)
viewHandler.FolderView2.SelectItem(-1, SVSIF.SVSI_DESELECTOTHERS);
}
///
/// -Interface Implementation for .
Responds to the
/// following Interfaces:
/// -
/// -
///
/// The service's unique identifier (SID).
/// The IID of the desired service interface.
///
/// 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.
///
/// or
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;
}
/// Gets the items in the ShellBrowser as an IShellItemArray
/// An instance or if not available.
internal IShellItemArray GetItemsArray(SVGIO opt)
{
try
{
ShellBrowserViewHandler viewHandler = ViewHandler.GetValidInstance();
return viewHandler is not null ? viewHandler.FolderView2.Items(opt) : null;
}
catch { return null; }
}
/// Raises the event.
protected internal virtual void OnItemsChanged() => ItemsChanged?.Invoke(this, EventArgs.Empty);
/// Raises the event.
protected internal virtual void OnNavigated(ShellFolder shellFolder)
{
if (Navigated is not null)
{
ShellBrowserNavigatedEventArgs eventArgs = new(shellFolder);
Navigated.Invoke(this, eventArgs);
}
}
/// Raises the event.
protected internal virtual void OnSelectionChanged() => SelectionChanged?.Invoke(this, EventArgs.Empty);
/// Clean up any resources being used.
/// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
if (disposing && (components is not null))
{
components.Dispose();
}
base.Dispose(disposing);
}
/// Raises the event. Saves ViewState when ShellBrowser gets closed.
/// The instance containing the event data.
protected override void OnHandleDestroyed(EventArgs e)
{
ViewHandler.GetValidInstance()?.ShellView.SaveViewState();
base.OnHandleDestroyed(e);
}
/// Raises the event. Resize ViewWindow when ShellBrowser gets resized.
/// The instance containing the event data.
protected override void OnResize(EventArgs e)
{
ViewHandler?.MoveWindow(0, 0, ClientRectangle.Width, ClientRectangle.Height, false);
base.OnResize(e);
}
/// Process known command keys of the ShellBrowser.
/// Windows Message
/// Key codes and modifiers
/// true if character was processed by the control; otherwise, false
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);
}
/// Required method for Designer support - do not modify the contents of this method with the code editor.
private void InitializeComponent()
{
components = new Container();
AutoScaleMode = AutoScaleMode.Font;
}
/// Represents a collection of attached to an .
private class ShellItemCollection : IReadOnlyList
{
private readonly SVGIO option;
private readonly ShellBrowser shellBrowser;
internal ShellItemCollection(ShellBrowser shellBrowser, SVGIO opt)
{
this.shellBrowser = shellBrowser;
option = opt;
}
/// Gets the number of elements in the collection.
/// Returns a 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 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);
}
}
}
/// Gets the at the specified index.
/// The .
/// The zero-based index of the element to get.
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);
}
}
}
/// Returns an enumerator that iterates through the collection.
/// An enumerator that can be used to iterate through the collection.
public IEnumerator GetEnumerator() => Items.Select(ShellItem.Open).GetEnumerator();
/// Returns an enumerator that iterates through the collection.
/// An enumerator that can be used to iterate through the collection.
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
/// Event argument for The Navigated event.
public class ShellBrowserNavigatedEventArgs : EventArgs
{
/// Initializes a new instance of the class.
public ShellBrowserNavigatedEventArgs(ShellFolder currentFolder) => CurrentFolder = currentFolder ?? throw new ArgumentNullException(nameof(currentFolder));
/// The new location of the ShellBrowser
public ShellFolder CurrentFolder { get; }
}
///
/// Encapsulates an IShellFolderViewCB-Implementation within an -Object.
/// Beside that it's implemented as a Wrapper-Object that is responsible for creating and disposing the following objects aka
/// Interface-Instances:
/// -
/// -
/// -
While doing that, it also handles some common error cases:
/// - When there's no disk in a disk drive
Implements the following Interfaces:
/// -
This class make use of some undocumented Messages in its
/// Callback Handler.
For more Information on these see:
/// - Google Drive Shell Extension: ShellFolderViewCBHandler.cpp
/// - ReactOS: IShellFolderViewCB.cpp File Reference
/// , IShellFolderViewCB.cpp
///
public class ShellBrowserViewHandler : IShellFolderViewCB
{
///
/// {"The operation was canceled by the user. (Exception from HRESULT: 0x800704C7)"}
/// is the result of a call to 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.
///
internal static readonly HRESULT HRESULT_CANCELLED = new(0x800704C7);
private string text;
private int thumbnailSize = ShellBrowser.defaultThumbnailSize;
/// Create an instance of to handle Callback messages for the given ShellFolder.
/// The that is owner of this instance.
/// The ShellFolder for the view.
/// The folder settings for the view.
/// Text to display if the folder is empty.
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;
}
}
/// The .
public IFolderView2 FolderView2 { get; private set; }
/// Indicates that no error occured while creating this instance, i.e. the View is fully functional.
public bool IsValid { get; private set; }
/// Indicates that an "No Disk In Drive"-error occured while creating this instance.
public bool NoDiskInDriveError { get; }
/// The owner of this instance of , i.e. the .
public ShellBrowser Owner { get; }
/// The .
public ShellFolder ShellFolder { get; private set; }
/// The .
public IShellView ShellView { get; private set; }
/// The default text to be used when there are no items in the view.
public string Text
{
get => text;
set
{
text = value;
if (IsValid)
FolderView2.SetText(FVTEXTTYPE.FVST_EMPTYTEXT, value);
}
}
/// The size of the thumbnails in pixels.
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);
}
}
}
/// The that occured, if creation of the instance failed.
public COMException ValidationError { get; private set; }
/// The viewing mode of the ShellBrowser.
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);
}
}
/// The ViewWindow.
public HWND ViewWindow { get; private set; }
/// Destroy the view.
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
}
/// Changes the position and dimensions of this .
/// Left
/// Top
/// Width
/// Height
/// Force redraw
/// If the function succeeds, the return value is nonzero.
public bool MoveWindow(int X, int Y, int nWidth, int nHeight, bool bRepaint) =>
ViewWindow != HWND.NULL && User32.MoveWindow(ViewWindow, X, Y, nWidth, nHeight, bRepaint);
/// Activate the ShellView of this ShellBrowser.
/// The to be set
public void UIActivate(SVUIA uState = SVUIA.SVUIA_ACTIVATE_NOFOCUS) => ShellView?.UIActivate(uState);
/// Deactivate the ShellView of this ShellBrowser.
public void UIDeactivate() => UIActivate(SVUIA.SVUIA_DEACTIVATE);
/// Allows communication between the system folder view object and a system folder view callback object.
/// One of the SFVM_* notifications.
/// Additional information. See the individual notification pages for specific requirements.
/// Additional information. See the individual notification pages for specific requirements.
/// TODO: @dahall: Where does this come from?
/// S_OK if the notification has been handled. E_NOTIMPL otherwise.
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;
}
}
}
}