mirror of https://github.com/dahall/Vanara.git
470 lines
17 KiB
C#
470 lines
17 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using System.ComponentModel.Design.Serialization;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using Vanara.InteropServices;
|
|
using Vanara.PInvoke;
|
|
using static Vanara.PInvoke.Ole32;
|
|
using static Vanara.PInvoke.PropSys;
|
|
using static Vanara.PInvoke.Shell32;
|
|
|
|
namespace Vanara.Windows.Shell
|
|
{
|
|
/// <summary>Represents a Jump List item.</summary>
|
|
/// <seealso cref="INotifyPropertyChanged"/>
|
|
public interface IJumpListItem : INotifyPropertyChanged
|
|
{
|
|
/// <summary>Gets the category to which the item belongs.</summary>
|
|
/// <value>The category name.</value>
|
|
string Category { get; }
|
|
|
|
/// <summary>Creates a shell object based on this item.</summary>
|
|
/// <returns>An instance of either <see cref="IShellItem"/> or <see cref="IShellLinkW"/>.</returns>
|
|
object GetShellObject();
|
|
}
|
|
|
|
/// <summary>Provides access to the jump list on the application's task bar icon.</summary>
|
|
[TypeConverter(typeof(GenericExpandableObjectConverter<JumpList>))]
|
|
[Editor("Vanara.Windows.Shell.JumpListItemCollectionEditor, Vanara.Windows.Shell", "System.Drawing.Design.UITypeEditor, System.Drawing")]
|
|
[Description("Provides access to the jump list on the application's task bar icon.")]
|
|
public class JumpList : ObservableCollection<IJumpListItem>
|
|
{
|
|
/// <summary>Initializes a new instance of the <see cref="JumpList"/> class.</summary>
|
|
public JumpList() => CollectionChanged += OnCollectionChanged;
|
|
|
|
/// <summary>Gets the number of items in the collection.</summary>
|
|
/// <value>The count.</value>
|
|
[Browsable(false)]
|
|
public new int Count => base.Count;
|
|
|
|
/// <summary>Whether to show the special "Frequent" category.</summary>
|
|
/// <remarks>
|
|
/// This category is managed by the Shell and keeps track of items that are frequently accessed by this program. Applications can
|
|
/// request that specific items are included here by calling JumpList.AddToRecentCategory. Because of duplication, applications
|
|
/// generally should not have both ShowRecentCategory and ShowFrequentCategory set at the same time.
|
|
/// </remarks>
|
|
[Category("Appearance"), DefaultValue(false)]
|
|
[Description("Gets or sets Whether to show the special \"Frequent\" category.")]
|
|
public bool ShowFrequentCategory { get; set; }
|
|
|
|
/// <summary>Whether to show the special "Recent" category.</summary>
|
|
/// <remarks>
|
|
/// This category is managed by the Shell and keeps track of items that have been recently accessed by this program. Applications
|
|
/// can request that specific items are included here by calling JumpList.AddToRecentCategory Because of duplication, applications
|
|
/// generally should not have both ShowRecentCategory and ShowFrequentCategory set at the same time.
|
|
/// </remarks>
|
|
[Category("Appearance"), DefaultValue(false)]
|
|
[Description("Gets or sets Whether to show the special \"Recent\" category.")]
|
|
public bool ShowRecentCategory { get; set; }
|
|
|
|
/// <summary>
|
|
/// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently.
|
|
/// </summary>
|
|
/// <param name="docPath">The document path to add.</param>
|
|
public static void AddToRecentDocs(string docPath)
|
|
{
|
|
if (docPath is null) throw new ArgumentNullException(nameof(docPath));
|
|
SHAddToRecentDocs(SHARD.SHARD_PATHW, System.IO.Path.GetFullPath(docPath));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently.
|
|
/// </summary>
|
|
/// <param name="iShellItem">The <see cref="IShellItem"/> to add.</param>
|
|
public static void AddToRecentDocs(IShellItem iShellItem)
|
|
{
|
|
if (iShellItem is null) throw new ArgumentNullException(nameof(iShellItem));
|
|
SHAddToRecentDocs(SHARD.SHARD_SHELLITEM, iShellItem);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently.
|
|
/// </summary>
|
|
/// <param name="iShellLink">The <see cref="IShellLinkW"/> to add.</param>
|
|
/// <exception cref="ArgumentNullException">iShellLink</exception>
|
|
public static void AddToRecentDocs(IShellLinkW iShellLink)
|
|
{
|
|
if (iShellLink is null) throw new ArgumentNullException(nameof(iShellLink));
|
|
SHAddToRecentDocs(SHARD.SHARD_LINK, iShellLink);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently.
|
|
/// </summary>
|
|
/// <param name="shellItem">The <see cref="ShellItem"/> to add.</param>
|
|
/// <exception cref="ArgumentNullException">shellItem</exception>
|
|
public static void AddToRecentDocs(ShellItem shellItem)
|
|
{
|
|
if (shellItem is null) throw new ArgumentNullException(nameof(shellItem));
|
|
if (shellItem is ShellLink lnk)
|
|
AddToRecentDocs(lnk.link);
|
|
else
|
|
AddToRecentDocs(shellItem.IShellItem);
|
|
}
|
|
|
|
/// <summary>Clears the system usage data for recent documents.</summary>
|
|
public static void ClearRecentDocs() => SHAddToRecentDocs(0, (string)null);
|
|
|
|
/// <summary>Deletes a custom Jump List for a specified application.</summary>
|
|
/// <param name="appId">The AppUserModelID of the process whose taskbar button representation displays the custom Jump List.</param>
|
|
public static void DeleteList(string appId = null)
|
|
{
|
|
using var icdl = ComReleaserFactory.Create(new ICustomDestinationList());
|
|
icdl.Item.DeleteList(appId);
|
|
}
|
|
|
|
/// <summary>Applies the the current settings for the jumplist to the taskbar button.</summary>
|
|
/// <param name="appId">The application identifier.</param>
|
|
public void ApplySettings(string appId = null)
|
|
{
|
|
using var icdl = ComReleaserFactory.Create(new ICustomDestinationList());
|
|
if (!string.IsNullOrEmpty(appId))
|
|
icdl.Item.SetAppID(appId);
|
|
|
|
using var ioaRemoved = ComReleaserFactory.Create(icdl.Item.BeginList<IObjectArray>(out _));
|
|
var removedObjs = ioaRemoved.Item.ToArray<object>();
|
|
var exceptions = new System.Collections.Generic.List<Exception>();
|
|
foreach (var cat in this.GroupBy(i => i.Category))
|
|
{
|
|
using var poc = ComReleaserFactory.Create(new IObjectCollection());
|
|
foreach (var o in cat)
|
|
{
|
|
using var psho = ComReleaserFactory.Create(o.GetShellObject());
|
|
if (!IsRemoved(psho.Item))
|
|
poc.Item.AddObject(psho.Item);
|
|
}
|
|
if (cat.Key is null)
|
|
icdl.Item.AddUserTasks(poc.Item);
|
|
else
|
|
{
|
|
try { icdl.Item.AppendCategory(cat.Key, poc.Item); }
|
|
catch (COMException cex) when (cex.ErrorCode == DESTS_E_NO_MATCHING_ASSOC_HANDLER)
|
|
{ exceptions.Add(new InvalidOperationException($"At least one of the destinations in the '{cat.Key}' category has an extension that is not registered to this application.")); }
|
|
}
|
|
}
|
|
|
|
if (ShowFrequentCategory)
|
|
icdl.Item.AppendKnownCategory(KNOWNDESTCATEGORY.KDC_FREQUENT);
|
|
|
|
if (ShowRecentCategory)
|
|
icdl.Item.AppendKnownCategory(KNOWNDESTCATEGORY.KDC_RECENT);
|
|
|
|
icdl.Item.CommitList();
|
|
|
|
if (exceptions.Count > 0)
|
|
throw new AggregateException(exceptions);
|
|
|
|
bool IsRemoved(object shellObj)
|
|
{
|
|
if (shellObj is IShellItem shi)
|
|
{
|
|
return Array.Exists(removedObjs, o => o is IShellItem oi && ShellItem.Equals(shi, oi));
|
|
}
|
|
else if (shellObj is IShellLinkW shl)
|
|
{
|
|
var cstring = ShellLink.GetCompareString(shl);
|
|
return Array.Exists(removedObjs, o => o is IShellLinkW l && cstring == ShellLink.GetCompareString(l));
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems.OfType<JumpListDestination>().Any(d => string.IsNullOrEmpty(d.Category)))
|
|
throw new InvalidOperationException("A JumpListDestination cannot have a null category.");
|
|
}
|
|
}
|
|
|
|
/// <summary>A file-based destination for a jumplist with an associated category.</summary>
|
|
public class JumpListDestination : JumpListItem, IJumpListItem
|
|
{
|
|
private string path;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="JumpListDestination"/> class.</summary>
|
|
public JumpListDestination(string category, string path)
|
|
{
|
|
Category = category;
|
|
Path = path;
|
|
}
|
|
|
|
/// <summary>The shell item to reference or execute.</summary>
|
|
[Editor("System.Windows.Forms.Design.FileNameEditor, System.Design", "System.Drawing.Design.UITypeEditor, System.Drawing")]
|
|
[DefaultValue(null)]
|
|
public string Path
|
|
{
|
|
get => path;
|
|
set
|
|
{
|
|
if (value is null) throw new ArgumentNullException(nameof(ShellItem));
|
|
if (path == value) return;
|
|
path = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Converts to string.</summary>
|
|
/// <returns>A <see cref="string"/> that represents this instance.</returns>
|
|
public override string ToString() => $"{Category}:{Path}";
|
|
|
|
/// <summary>Creates a shell object based on this item.</summary>
|
|
/// <returns>An interface.</returns>
|
|
object IJumpListItem.GetShellObject() => ShellUtil.GetShellItemForPath(System.IO.Path.GetFullPath(Path));
|
|
}
|
|
|
|
/// <summary>An item in a Jump List.</summary>
|
|
[TypeConverter(typeof(GenericExpandableObjectConverter<JumpListItem>))]
|
|
public abstract class JumpListItem : INotifyPropertyChanged
|
|
{
|
|
private string category;
|
|
|
|
/// <summary>Occurs when a property value changes.</summary>
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
/// <summary>Gets or sets the category to which the item belongs.</summary>
|
|
/// <value>The category name.</value>
|
|
public string Category
|
|
{
|
|
get => category;
|
|
set
|
|
{
|
|
if (category == value) return;
|
|
category = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Called when a property has changed.</summary>
|
|
/// <param name="propertyName">Name of the property.</param>
|
|
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") =>
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
/// <summary>A separator which can be inserted into a custom list or task list.</summary>
|
|
public class JumpListSeparator : JumpListItem, IJumpListItem
|
|
{
|
|
/// <summary>Initializes a new instance of the <see cref="JumpListSeparator"/> class and optionally assigns it to a category.</summary>
|
|
/// <param name="category">The category name. If this value is <see langword="null"/>, this separator will be inserted into the task list.</param>
|
|
public JumpListSeparator(string category = null) => Category = category;
|
|
|
|
/// <summary>Creates a shell object based on this item.</summary>
|
|
/// <returns>An instance of either <see cref="IShellItem"/> or <see cref="IShellLinkW"/>.</returns>
|
|
object IJumpListItem.GetShellObject()
|
|
{
|
|
var link = new IShellLinkW();
|
|
var props = (IPropertyStore)link;
|
|
props?.SetValue(PROPERTYKEY.System.AppUserModel.IsDestListSeparator, true);
|
|
return link;
|
|
}
|
|
}
|
|
|
|
/// <summary>A task for a jumplist.</summary>
|
|
/// <seealso cref="JumpListItem"/>
|
|
public class JumpListTask : JumpListItem, IJumpListItem
|
|
{
|
|
private int iconResIdx;
|
|
private string title, description, path, args, dir, iconPath, appUserModelID;
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="JumpListTask"/> class.</summary>
|
|
public JumpListTask(string title, string applicationPath)
|
|
{
|
|
Title = title;
|
|
ApplicationPath = applicationPath;
|
|
}
|
|
|
|
/// <summary>Gets or sets the application path.</summary>
|
|
/// <value>The application path.</value>
|
|
/// <exception cref="ArgumentNullException">ApplicationPath</exception>
|
|
[DefaultValue(null)]
|
|
[Editor("System.Windows.Forms.Design.FileNameEditor, System.Design", "System.Drawing.Design.UITypeEditor, System.Drawing")]
|
|
public string ApplicationPath
|
|
{
|
|
get => path;
|
|
set
|
|
{
|
|
if (value is null) throw new ArgumentNullException(nameof(ApplicationPath));
|
|
if (path == value) return;
|
|
path = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets an explicit Application User Model ID used to associate processes, files, and windows with a particular application.
|
|
/// </summary>
|
|
/// <value>The application path.</value>
|
|
/// <remarks>
|
|
/// An application must provide its AppUserModelID in the following form. It can have no more than 128 characters and cannot contain
|
|
/// spaces. Each section should be camel-cased.
|
|
/// <para><c>CompanyName.ProductName.SubProduct.VersionInformation</c></para>
|
|
/// <para>
|
|
/// CompanyName and ProductName should always be used, while the SubProduct and VersionInformation portions are optional and depend
|
|
/// on the application's requirements. SubProduct allows a main application that consists of several subapplications to provide a
|
|
/// separate taskbar button for each subapplication and its associated windows. VersionInformation allows two versions of an
|
|
/// application to coexist while being seen as discrete entities. If an application is not intended to be used in that way, the
|
|
/// VersionInformation should be omitted so that an upgraded version can use the same AppUserModelID as the version that it replaced.
|
|
/// </para>
|
|
/// </remarks>
|
|
[DefaultValue(null)]
|
|
public string AppUserModelID
|
|
{
|
|
get => appUserModelID;
|
|
set
|
|
{
|
|
if (appUserModelID == value) return;
|
|
if (value != null && value.Length > 128 || value.Contains(" "))
|
|
throw new ArgumentException("Invalid format.");
|
|
appUserModelID = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the arguments.</summary>
|
|
/// <value>The arguments.</value>
|
|
[DefaultValue(null)]
|
|
public string Arguments
|
|
{
|
|
get => args;
|
|
set
|
|
{
|
|
if (args == value) return;
|
|
args = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the description.</summary>
|
|
/// <value>The description.</value>
|
|
[DefaultValue(null)]
|
|
public string Description
|
|
{
|
|
get => description;
|
|
set
|
|
{
|
|
if (description == value) return;
|
|
description = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the index of the icon resource.</summary>
|
|
/// <value>The index of the icon resource.</value>
|
|
[DefaultValue(0)]
|
|
public int IconResourceIndex
|
|
{
|
|
get => iconResIdx;
|
|
set
|
|
{
|
|
if (iconResIdx == value) return;
|
|
iconResIdx = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the icon resource path.</summary>
|
|
/// <value>The icon resource path.</value>
|
|
/// <exception cref="ArgumentException">Length of path may not exceed 260 characters. - IconResourcePath</exception>
|
|
[DefaultValue(null)]
|
|
[Editor("System.Windows.Forms.Design.FileNameEditor, System.Design", "System.Drawing.Design.UITypeEditor, System.Drawing")]
|
|
public string IconResourcePath
|
|
{
|
|
get => iconPath;
|
|
set
|
|
{
|
|
if (iconPath == value) return;
|
|
if (iconPath != null && iconPath.Length > Kernel32.MAX_PATH)
|
|
throw new ArgumentException("Length of path may not exceed 260 characters.", nameof(IconResourcePath));
|
|
iconPath = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the title.</summary>
|
|
/// <value>The title.</value>
|
|
/// <exception cref="ArgumentNullException">Title</exception>
|
|
[DefaultValue(null)]
|
|
public string Title
|
|
{
|
|
get => title;
|
|
set
|
|
{
|
|
if (value is null) throw new ArgumentNullException(nameof(Title));
|
|
if (title == value) return;
|
|
title = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets or sets the working directory.</summary>
|
|
/// <value>The working directory.</value>
|
|
[DefaultValue(null)]
|
|
[Editor("System.Windows.Forms.Design.FolderNameEditor, System.Design", "System.Drawing.Design.UITypeEditor, System.Drawing")]
|
|
public string WorkingDirectory
|
|
{
|
|
get => dir;
|
|
set
|
|
{
|
|
if (dir == value) return;
|
|
dir = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>Converts to string.</summary>
|
|
/// <returns>A <see cref="string"/> that represents this instance.</returns>
|
|
public override string ToString() => $"{Category}:{ApplicationPath}";
|
|
|
|
/// <summary>Creates a shell object based on this item.</summary>
|
|
/// <returns>An interface.</returns>
|
|
object IJumpListItem.GetShellObject()
|
|
{
|
|
var link = new IShellLinkW();
|
|
|
|
link.SetPath(ApplicationPath);
|
|
|
|
if (!string.IsNullOrEmpty(AppUserModelID))
|
|
(link as IPropertyStore)?.SetValue(PROPERTYKEY.System.AppUserModel.ID, AppUserModelID);
|
|
|
|
if (!string.IsNullOrEmpty(WorkingDirectory))
|
|
link.SetWorkingDirectory(WorkingDirectory);
|
|
|
|
if (!string.IsNullOrEmpty(Arguments))
|
|
link.SetArguments(Arguments);
|
|
|
|
// -1 is a sentinel value indicating not to use the icon.
|
|
if (IconResourceIndex != -1)
|
|
link.SetIconLocation(IconResourcePath ?? ApplicationPath, IconResourceIndex);
|
|
|
|
if (!string.IsNullOrEmpty(Description))
|
|
link.SetDescription(Description);
|
|
|
|
if (!string.IsNullOrEmpty(Title))
|
|
(link as IPropertyStore)?.SetValue(PROPERTYKEY.System.Title, Title);
|
|
|
|
return link;
|
|
}
|
|
}
|
|
|
|
internal class GenericExpandableObjectConverter<T> : ExpandableObjectConverter
|
|
{
|
|
public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
|
|
{
|
|
if (destType == typeof(InstanceDescriptor) || destType == typeof(string))
|
|
return true;
|
|
return base.CanConvertTo(context, destType);
|
|
}
|
|
|
|
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo info, object value, Type destType)
|
|
{
|
|
if (destType == typeof(InstanceDescriptor))
|
|
return new InstanceDescriptor(typeof(T).GetConstructor(new Type[0]), null, false);
|
|
if (destType == typeof(string))
|
|
return "";
|
|
return base.ConvertTo(context, info, value, destType);
|
|
}
|
|
}
|
|
} |