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 { /// Represents a Jump List item. /// public interface IJumpListItem : INotifyPropertyChanged { /// Gets the category to which the item belongs. /// The category name. string Category { get; } /// Creates a shell object based on this item. /// An instance of either or . object GetShellObject(); } /// Provides access to the jump list on the application's task bar icon. [TypeConverter(typeof(GenericExpandableObjectConverter))] [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 { /// Initializes a new instance of the class. public JumpList() => CollectionChanged += OnCollectionChanged; /// Gets the number of items in the collection. /// The count. [Browsable(false)] public new int Count => base.Count; /// Whether to show the special "Frequent" category. /// /// 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. /// [Category("Appearance"), DefaultValue(false)] [Description("Gets or sets Whether to show the special \"Frequent\" category.")] public bool ShowFrequentCategory { get; set; } /// Whether to show the special "Recent" category. /// /// 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. /// [Category("Appearance"), DefaultValue(false)] [Description("Gets or sets Whether to show the special \"Recent\" category.")] public bool ShowRecentCategory { get; set; } /// /// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently. /// /// The document path to add. public static void AddToRecentDocs(string docPath) { if (docPath is null) throw new ArgumentNullException(nameof(docPath)); SHAddToRecentDocs(SHARD.SHARD_PATHW, System.IO.Path.GetFullPath(docPath)); } /// /// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently. /// /// The to add. public static void AddToRecentDocs(IShellItem iShellItem) { if (iShellItem is null) throw new ArgumentNullException(nameof(iShellItem)); SHAddToRecentDocs(SHARD.SHARD_SHELLITEM, iShellItem); } /// /// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently. /// /// The to add. /// iShellLink public static void AddToRecentDocs(IShellLinkW iShellLink) { if (iShellLink is null) throw new ArgumentNullException(nameof(iShellLink)); SHAddToRecentDocs(SHARD.SHARD_LINK, iShellLink); } /// /// Notifies the system that an item has been accessed, for the purposes of tracking those items used most recently and most frequently. /// /// The to add. /// shellItem 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); } /// Clears the system usage data for recent documents. public static void ClearRecentDocs() => SHAddToRecentDocs(0, (string)null); /// Deletes a custom Jump List for a specified application. /// The AppUserModelID of the process whose taskbar button representation displays the custom Jump List. public static void DeleteList(string appId = null) { using var icdl = ComReleaserFactory.Create(new ICustomDestinationList()); icdl.Item.DeleteList(appId); } /// Applies the the current settings for the jumplist to the taskbar button. /// The application identifier. 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(out _)); var removedObjs = ioaRemoved.Item.ToArray(); var exceptions = new System.Collections.Generic.List(); 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().Any(d => string.IsNullOrEmpty(d.Category))) throw new InvalidOperationException("A JumpListDestination cannot have a null category."); } } /// A file-based destination for a jumplist with an associated category. public class JumpListDestination : JumpListItem, IJumpListItem { private string path; /// Initializes a new instance of the class. public JumpListDestination(string category, string path) { Category = category; Path = path; } /// The shell item to reference or execute. [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(); } } /// Converts to string. /// A that represents this instance. public override string ToString() => $"{Category}:{Path}"; /// Creates a shell object based on this item. /// An interface. object IJumpListItem.GetShellObject() => ShellUtil.GetShellItemForPath(System.IO.Path.GetFullPath(Path)); } /// An item in a Jump List. [TypeConverter(typeof(GenericExpandableObjectConverter))] public abstract class JumpListItem : INotifyPropertyChanged { private string category; /// Occurs when a property value changes. public event PropertyChangedEventHandler PropertyChanged; /// Gets or sets the category to which the item belongs. /// The category name. public string Category { get => category; set { if (category == value) return; category = value; OnPropertyChanged(); } } /// Called when a property has changed. /// Name of the property. protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } /// A separator which can be inserted into a custom list or task list. public class JumpListSeparator : JumpListItem, IJumpListItem { /// Initializes a new instance of the class and optionally assigns it to a category. /// The category name. If this value is , this separator will be inserted into the task list. public JumpListSeparator(string category = null) => Category = category; /// Creates a shell object based on this item. /// An instance of either or . object IJumpListItem.GetShellObject() { var link = new IShellLinkW(); var props = (IPropertyStore)link; props?.SetValue(PROPERTYKEY.System.AppUserModel.IsDestListSeparator, true); return link; } } /// A task for a jumplist. /// public class JumpListTask : JumpListItem, IJumpListItem { private int iconResIdx; private string title, description, path, args, dir, iconPath, appUserModelID; /// Initializes a new instance of the class. public JumpListTask(string title, string applicationPath) { Title = title; ApplicationPath = applicationPath; } /// Gets or sets the application path. /// The application path. /// ApplicationPath [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(); } } /// /// Gets or sets an explicit Application User Model ID used to associate processes, files, and windows with a particular application. /// /// The application path. /// /// 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. /// CompanyName.ProductName.SubProduct.VersionInformation /// /// 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. /// /// [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(); } } /// Gets or sets the arguments. /// The arguments. [DefaultValue(null)] public string Arguments { get => args; set { if (args == value) return; args = value; OnPropertyChanged(); } } /// Gets or sets the description. /// The description. [DefaultValue(null)] public string Description { get => description; set { if (description == value) return; description = value; OnPropertyChanged(); } } /// Gets or sets the index of the icon resource. /// The index of the icon resource. [DefaultValue(0)] public int IconResourceIndex { get => iconResIdx; set { if (iconResIdx == value) return; iconResIdx = value; OnPropertyChanged(); } } /// Gets or sets the icon resource path. /// The icon resource path. /// Length of path may not exceed 260 characters. - IconResourcePath [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(); } } /// Gets or sets the title. /// The title. /// Title [DefaultValue(null)] public string Title { get => title; set { if (value is null) throw new ArgumentNullException(nameof(Title)); if (title == value) return; title = value; OnPropertyChanged(); } } /// Gets or sets the working directory. /// The working directory. [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(); } } /// Converts to string. /// A that represents this instance. public override string ToString() => $"{Category}:{ApplicationPath}"; /// Creates a shell object based on this item. /// An interface. 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 : 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); } } }