From ae3d4dc52c463422d1404b6c4f015a73b6b5793f Mon Sep 17 00:00:00 2001 From: dahall Date: Wed, 26 Feb 2020 15:20:47 -0700 Subject: [PATCH] Finished intial work on TaskbarButton form extender that fully enables all interactions with the taskbar button in the UI. --- Windows.Shell/TaskBar/ExtenderProviderBase.cs | 126 +++++ Windows.Shell/TaskBar/HostedMessageWindow.cs | 68 +++ Windows.Shell/TaskBar/ImageIndexer.cs | 40 ++ Windows.Shell/TaskBar/JumpList.cs | 538 +++++++++++++++++++++ Windows.Shell/TaskBar/TaskbarButton.cs | 317 ++++++++++++ Windows.Shell/TaskBar/TaskbarButtonThumbnail.cs | 127 +++++ Windows.Shell/TaskBar/TaskbarButtonThumbnails.cs | 109 +++++ Windows.Shell/TaskBar/TaskbarList.cs | 34 +- Windows.Shell/TaskBar/ThumbnailToolbar.cs | 41 ++ Windows.Shell/TaskBar/ThumbnailToolbarButton.cs | 218 +++++++++ .../TaskBar/ThumbnailToolbarButtonCollection.cs | 64 +++ Windows.Shell/Vanara.Windows.Shell.csproj | 27 +- 12 files changed, 1682 insertions(+), 27 deletions(-) create mode 100644 Windows.Shell/TaskBar/ExtenderProviderBase.cs create mode 100644 Windows.Shell/TaskBar/HostedMessageWindow.cs create mode 100644 Windows.Shell/TaskBar/ImageIndexer.cs create mode 100644 Windows.Shell/TaskBar/JumpList.cs create mode 100644 Windows.Shell/TaskBar/TaskbarButton.cs create mode 100644 Windows.Shell/TaskBar/TaskbarButtonThumbnail.cs create mode 100644 Windows.Shell/TaskBar/TaskbarButtonThumbnails.cs create mode 100644 Windows.Shell/TaskBar/ThumbnailToolbar.cs create mode 100644 Windows.Shell/TaskBar/ThumbnailToolbarButton.cs create mode 100644 Windows.Shell/TaskBar/ThumbnailToolbarButtonCollection.cs diff --git a/Windows.Shell/TaskBar/ExtenderProviderBase.cs b/Windows.Shell/TaskBar/ExtenderProviderBase.cs new file mode 100644 index 00000000..e35bc3a6 --- /dev/null +++ b/Windows.Shell/TaskBar/ExtenderProviderBase.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Vanara.Windows +{ + /// A generic base to implement for a single extender type. + /// The type of the type that can be extended. + public abstract class ExtenderProviderBase : Component, IExtenderProvider, ISupportInitialize where TExtend : Component + { + /// A dictionary that holds a property bag for each extended type. + protected readonly Dictionary> propHash = new Dictionary>(); + + /// Initializes a new instance of the class. + protected ExtenderProviderBase() { } + + /// Initializes a new instance of the class. + /// The parent. + protected ExtenderProviderBase(TExtend parent) => OnAddingExtender(parent); + + /// Initializes a new instance of the class. + /// The container. + /// container + protected ExtenderProviderBase(IContainer container) : this() + { + if (container is null) + throw new ArgumentNullException(nameof(container)); + container.Add(this); + } + + /// Occurs when a new extender is being added. + protected event EventHandler AddingExtender; + + /// Sets the site. + /// The site. + public override ISite Site + { + set + { + base.Site = value; + var parent = (value?.GetService(typeof(IDesignerHost)) as IDesignerHost)?.RootComponent as TExtend; + if (parent != null) + OnAddingExtender(parent); + } + } + + /// Gets all extended components that have properties assigned. + /// Returns a value. + protected IEnumerable ExtendedComponents => propHash.Keys; + + /// Gets the known properties stored against all components. + /// Returns a value. + protected IEnumerable KnownProperties => propHash.Values.SelectMany(d => d.Keys).Distinct(); + + /// Signals the object that initialization is starting. + public virtual void BeginInit() { } + + /// Determines whether this instance can extend the specified extendee. + /// The extendee. + /// if this instance can extend the specified extendee; otherwise, . + public virtual bool CanExtend(object extendee) => extendee is TExtend; + + /// Signals the object that initialization is complete. + public virtual void EndInit() { } + + /// Gets the property value. + /// The type of the property to get. + /// The form. + /// The default value. + /// Name of the field. + /// + protected virtual T GetPropertyValue(TExtend form, T defaultValue = default, [CallerMemberName] string propName = "") + { + if (propName.StartsWith("Get")) + propName = propName.Remove(0, 3); + return propHash.TryGetValue(form, out var props) && props.TryGetValue(propName, out var prop) ? (T)prop : defaultValue; + } + + /// Calls the event. + /// The extender being added. + protected virtual void OnAddingExtender(TExtend extender) + { + var args = new AddExtenderEventArgs(extender); + AddingExtender?.Invoke(this, args); + propHash[extender] = args.ExtenderProperties; + } + + /// Sets the property value. + /// The type of the property to set. + /// The form. + /// The value. + /// Name of the field. + protected virtual bool SetPropertyValue(TExtend form, T value, [CallerMemberName] string propName = "") + { + if (!propHash.ContainsKey(form)) + OnAddingExtender(form); + if (propName.StartsWith("Set")) + propName = propName.Remove(0, 3); + if (propHash[form].TryGetValue(propName, out var prop) && Equals(prop, value)) + return false; + propHash[form][propName] = value; + return true; + } + + /// Arguments for the event. + public class AddExtenderEventArgs : EventArgs + { + internal AddExtenderEventArgs(TExtend parent) + { + Extender = parent; + ExtenderProperties = new Dictionary(); + } + + /// Gets the extender being added. + /// The extender. + public TExtend Extender { get; } + + /// Gets or sets the property bag to be associated with this extender. + /// The extender property bag. + public Dictionary ExtenderProperties { get; set; } + } + } +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/HostedMessageWindow.cs b/Windows.Shell/TaskBar/HostedMessageWindow.cs new file mode 100644 index 00000000..f4e81787 --- /dev/null +++ b/Windows.Shell/TaskBar/HostedMessageWindow.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; +using System.Windows.Forms; +using Vanara.PInvoke; + +namespace Vanara.Windows.Shell +{ + internal interface IMessageWindowHost + { + void WndProc(ref Message msg); + } + + [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] + [SecuritySafeCritical] + internal class HostedMessageWindow : NativeWindow where THost : IMessageWindowHost + { + private readonly THost host; + private GCHandle rooting; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")] + internal HostedMessageWindow(THost host) => this.host = host ?? throw new ArgumentNullException(nameof(host)); + + ~HostedMessageWindow() + { + if (Handle != default && !DoNotClose) + User32.PostMessage(Handle, (uint)User32.WindowMessage.WM_CLOSE); + } + + protected virtual bool DoNotClose => false; + + public void LockReference(bool locked) + { + if (locked) + { + if (!rooting.IsAllocated) + rooting = GCHandle.Alloc(host, GCHandleType.Normal); + } + else + { + if (rooting.IsAllocated) + rooting.Free(); + } + } + + protected override void OnThreadException(Exception e) => Application.OnThreadException(e); + + [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] + protected override void WndProc(ref Message m) + { + host.WndProc(ref m); + base.WndProc(ref m); + } + } + + [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] + internal class WindowMessageHook : HostedMessageWindow where THost : IMessageWindowHost + { + public WindowMessageHook(Control parent, THost host) : base(host) + { + if (parent is null) throw new ArgumentNullException(nameof(parent)); + parent.HandleCreated += (s, e) => AssignHandle(((Control)s).Handle); + parent.HandleDestroyed += (s, e) => ReleaseHandle(); + } + + protected override bool DoNotClose => true; + } +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/ImageIndexer.cs b/Windows.Shell/TaskBar/ImageIndexer.cs new file mode 100644 index 00000000..1e4ab6dc --- /dev/null +++ b/Windows.Shell/TaskBar/ImageIndexer.cs @@ -0,0 +1,40 @@ +using System.Windows.Forms; + +namespace Vanara.Windows.Shell +{ + public partial class ThumbnailToolbarButton + { + internal class ImageIndexer + { + private int index = -1; + private string key = string.Empty; + private bool useIntegerIndex = true; + + public virtual int ActualIndex => useIntegerIndex ? Index : (ImageList is null ? -1 : ImageList.Images.IndexOfKey(Key)); + + public virtual ImageList ImageList { get; set; } + + public virtual int Index + { + get => index; + set + { + key = string.Empty; + index = value; + useIntegerIndex = true; + } + } + + public virtual string Key + { + get => key; + set + { + index = -1; + key = value ?? string.Empty; + useIntegerIndex = false; + } + } + } + } +} diff --git a/Windows.Shell/TaskBar/JumpList.cs b/Windows.Shell/TaskBar/JumpList.cs new file mode 100644 index 00000000..bde28400 --- /dev/null +++ b/Windows.Shell/TaskBar/JumpList.cs @@ -0,0 +1,538 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.ComponentModel.Design.Serialization; +using System.Drawing.Design; +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(typeof(JumpListItemCollectionEditor), typeof(UITypeEditor))] + [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(typeof(System.Windows.Forms.Design.FileNameEditor), typeof(UITypeEditor))] + [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(typeof(System.Windows.Forms.Design.FileNameEditor), typeof(UITypeEditor))] + 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(typeof(System.Windows.Forms.Design.FileNameEditor), typeof(UITypeEditor))] + 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(typeof(System.Windows.Forms.Design.FolderNameEditor), typeof(UITypeEditor))] + 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); + } + } + + internal class JumpListItemCollectionEditor : System.ComponentModel.Design.CollectionEditor + { + /// Initializes a new instance of the class. + public JumpListItemCollectionEditor() : base(typeof(JumpList)) + { + } + + /// Creates the collection form. + /// + protected override CollectionForm CreateCollectionForm() + { + var f = base.CreateCollectionForm(); + f.Text = "JumpList Item Collection Editor"; + return f; + } + + /// Creates the new item types. + /// + protected override Type[] CreateNewItemTypes() => new[] { typeof(JumpListDestination), typeof(JumpListTask), typeof(JumpListSeparator) }; + + /// Sets the items. + /// The edit value. + /// The value. + /// + protected override object SetItems(object editValue, object[] value) + { + if (editValue is JumpList c) + { + c.Clear(); + foreach (var i in value.Cast()) + c.Add(i); + } + return editValue; + } + + protected override object CreateInstance(Type itemType) + { + if (itemType == typeof(JumpListDestination)) + return new JumpListDestination("[Category name]", "[File path]"); + if (itemType == typeof(JumpListSeparator)) + return new JumpListSeparator(); + if (itemType == typeof(JumpListTask)) + return new JumpListTask("[Title]", "[Application path]"); + return base.CreateInstance(itemType); + } + + protected override string GetDisplayText(object value) => value is JumpListSeparator ? "-----------" : value.ToString(); + + /*protected override string HelpTopic => base.HelpTopic; + + public override object EditValue(ITypeDescriptorContext context, System.IServiceProvider provider, object value) + { + return base.EditValue(context, provider, value); + } + + protected override CollectionForm CreateCollectionForm() => new JumpListItemCollectionEditorForm(this); + + protected class JumpListItemCollectionEditorForm : CollectionForm + { + public JumpListItemCollectionEditorForm(CollectionEditor editor) : base(editor) + { + } + + protected override void OnEditValueChanged(); + }*/ + } +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/TaskbarButton.cs b/Windows.Shell/TaskBar/TaskbarButton.cs new file mode 100644 index 00000000..1e1c49c8 --- /dev/null +++ b/Windows.Shell/TaskBar/TaskbarButton.cs @@ -0,0 +1,317 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Permissions; +using System.Windows.Forms; +using Vanara.PInvoke; + +namespace Vanara.Windows.Shell +{ + /// Provides access to the functionality of the taskbar button. + /// + [ProvideProperty("AppID", typeof(Form))] + [ProvideProperty("TaskbarButtonTooltip", typeof(Form))] + [ProvideProperty("TaskbarButtonOverlay", typeof(Form))] + [ProvideProperty("TaskbarButtonOverlayTooltip", typeof(Form))] + [ProvideProperty("TaskbarButtonProgressState", typeof(Form))] + [ProvideProperty("TaskbarButtonProgressValue", typeof(Form))] + [ProvideProperty("JumpList", typeof(Form))] + [ProvideProperty("TaskbarButtonThumbnails", typeof(Form))] + public class TaskbarButton : ExtenderProviderBase
, INotifyPropertyChanged + { + private const string category = "Taskbar Button"; + + static TaskbarButton() => Application.AddMessageFilter(new ThumbButtonMessageFilter()); + + /// Initializes a new instance of the class. + public TaskbarButton() + { + TaskbarButtonCreated += OnTaskbarButtonCreated; + ThumbnailButtonClick += OnThumbnailButtonClick; + } + + /// Initializes a new instance of the class. + /// The container of this component. + /// container + public TaskbarButton(IContainer container) : this() + { + if (container is null) + throw new ArgumentNullException(nameof(container)); + container.Add(this); + } + + /// + /// Occurs when the system reports a taskbar button has been created. The first parameter will contain the HWND of the window for + /// which the button was created. + /// + public static event Action TaskbarButtonCreated; + + /// + /// Occurs when the system reports a thumbnail button has been clicked. The first parameter contains the HWND of the window shown by + /// the thumbnail and the second contains the Command ID of the button that was clicked. + /// + public static event Action ThumbnailButtonClick; + + /// Occurs when a property has changed. + public event PropertyChangedEventHandler PropertyChanged; + + /// Gets a value indicating whether the taskbar button has been created. + /// if the taskbar button was created; otherwise, . + [Browsable(false)] + public bool IsTaskbarButtonCreated { get; internal set; } = false; + + /// Signals the object that initialization is starting. + public override void BeginInit() + { + base.BeginInit(); + if (Container is Form f && f.ShowInTaskbar) + TaskbarList.ActivateTaskbarItem(f); + } + + /// Gets the application identifier. + /// The form. + /// The application identifier. + [Category(category), DisplayName("AppID"), DefaultValue(null)] + [Description("Gets or sets the application identifier.")] + public string GetAppID(Form form) => GetPropertyValue(form); + + /// Gets the jumplist to display on this taskbar button. + /// The form. + /// The jumplist. + [Category(category), DisplayName("JumpList"), Localizable(true)] + [Description("Gets the jumplist to display with the taskbar button.")] + public JumpList GetJumpList(Form form) + { + var ret = GetPropertyValue(form); + if (ret is null) + { + ret = new JumpList(); + SetPropertyValue(form, ret, "JumpList"); + } + return ret; + } + + /// Gets the overlay icon to dispaly on a taskbar button to indicate application status or a notification to the user. + /// The form. + /// The overlay icon. + [Category(category), DisplayName("TaskbarButtonOverlay"), DefaultValue(null), Localizable(true)] + [Description("Gets or sets the overlay icon to dispaly on a taskbar button.")] + public Icon GetTaskbarButtonOverlay(Form form) => GetPropertyValue(form); + + /// + /// Gets the overlay tooltip to dispaly on a taskbar button to indicate application status or a notification to the user. + /// + /// The form. + /// The overlay tooltip. + [Category(category), DisplayName("TaskbarButtonOverlayTooltip"), DefaultValue(null), Localizable(true)] + [Description("Gets or sets the overlay tooltip to dispaly on a taskbar button.")] + public string GetTaskbarButtonOverlayTooltip(Form form) => GetPropertyValue(form); + + /// Gets the state of the progress indicator displayed on a taskbar button. + /// + /// The window in which the progress of an operation is being shown. This window's associated taskbar button will display the + /// progress bar. + /// + /// The current state of the progress button. + [Category(category), DisplayName("TaskbarButtonProgressState"), DefaultValue(TaskbarButtonProgressState.None)] + [Description("Gets or sets the state of the progress indicator displayed on a taskbar button.")] + public TaskbarButtonProgressState GetTaskbarButtonProgressState(Form form) => GetPropertyValue(form, TaskbarButtonProgressState.None); + + /// + /// Displays or updates a progress bar hosted in a taskbar button to show the specific percentage completed of the full operation. + /// + /// The window whose associated taskbar button is being used as a progress indicator. + /// The proportion of the operation that has been completed at the time the method is called. + [Category(category), DisplayName("TaskbarButtonProgressValue"), DefaultValue(0.0f)] + [Description("Gets or sets the percentage completion for the progress bar hosted in a taskbar button.")] + public float GetTaskbarButtonProgressValue(Form form) => GetPropertyValue(form, 0.0f); + + /// Gets the taskbar button thumbnails. + /// The window owning the taskbar button thumbnails. + /// A collection of thumbnails. + [Category(category), DisplayName("TaskbarButtonThumbnails")] + [Description("Gets the list of thumbnails associated with the taskbar button.")] + public TaskbarButtonThumbnails GetTaskbarButtonThumbnails(Form form) + { + var ret = GetPropertyValue(form); + if (ret is null) + { + ret = new TaskbarButtonThumbnails(form); + SetPropertyValue(form, ret, "TaskbarButtonThumbnails"); + } + return ret; + } + + /// Gets the description displayed on the tooltip of the taskbar button. + /// The form. + /// The description + [Category(category), DisplayName("TaskbarButtonTooltip"), DefaultValue(null), Localizable(true)] + [Description("Gets or sets the description displayed on the tooltip of the taskbar button.")] + public string GetTaskbarButtonTooltip(Form form) => GetPropertyValue(form); + + /// Sets the application identifier. + /// The form. + /// The value. + public void SetAppID(Form form, string value) + { + if (SetPropertyValue(form, value) && IsTaskbarButtonCreated) + ApplySetting(form, value); + } + + /// Sets the overlay icon to dispaly on a taskbar button to indicate application status or a notification to the user. + /// The form. + /// The overlay icon to apply. + public void SetTaskbarButtonOverlay(Form form, Icon value) + { + if (SetPropertyValue(form, value) && IsTaskbarButtonCreated) + ApplySetting(form, value); + } + + /// + /// Gets the overlay tooltip to dispaly on a taskbar button to indicate application status or a notification to the user. + /// + /// The form. + /// The overlay tooltip. + public void SetTaskbarButtonOverlayTooltip(Form form, string value) + { + if (SetPropertyValue(form, value) && IsTaskbarButtonCreated) + ApplySetting(form, value); + } + + /// Sets the type and state of the progress indicator displayed on a taskbar button. + /// + /// The window in which the progress of an operation is being shown. This window's associated taskbar button will display the + /// progress bar. + /// + /// The current state of the progress button. Specify only one of the enum values. + public void SetTaskbarButtonProgressState(Form form, TaskbarButtonProgressState value) + { + if (SetPropertyValue(form, value) && IsTaskbarButtonCreated) + ApplySetting(form, value); + } + + /// + /// Displays or updates a progress bar hosted in a taskbar button to show the specific percentage completed of the full operation. + /// + /// The window whose associated taskbar button is being used as a progress indicator. + /// + /// The proportion of the operation that has been completed at the time the method is called. This value must be between 0.0f and + /// + public void SetTaskbarButtonProgressValue(Form form, float value) + { + if (value < 0f || value > 1.0f) + throw new ArgumentOutOfRangeException(nameof(value), "Progress value must be a number between 0 and 1, inclusive."); + if (SetPropertyValue(form, value) && IsTaskbarButtonCreated) + ApplySetting(form, value); + } + + /// Sets the description displayed on the tooltip of the taskbar button. + /// The form. + /// The description. + public void SetTaskbarButtonTooltip(Form form, string value) + { + if (SetPropertyValue(form, value) && IsTaskbarButtonCreated) + ApplySetting(form, value); + } + + /// Calls the event. + /// The form. + /// Name of the changed property. + protected virtual void OnProperyChanged(Form form, string propName) => PropertyChanged?.Invoke(form, new PropertyChangedEventArgs(propName)); + + private static string GetUniqueProcessID() => System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name ?? null; + + private void ApplySetting(Form form, object value, [CallerMemberName] string propName = "") + { + if (propName.StartsWith("Set")) + propName = propName.Remove(0, 3); + switch (propName) + { + case "AppID": + TaskbarList.SetWindowAppId(form.Handle, (string)value); + break; + + case "TaskbarButtonTooltip": + TaskbarList.SetThumbnailTooltip(form, (string)value); + break; + + case "TaskbarButtonOverlay": + TaskbarList.SetOverlayIcon(form, (Icon)value, GetTaskbarButtonOverlayTooltip(form)); + break; + + case "TaskbarButtonOverlayTooltip": + TaskbarList.SetOverlayIcon(form, GetTaskbarButtonOverlay(form), (string)value); + break; + + case "TaskbarButtonProgressState": + TaskbarList.SetProgressState(form, (TaskbarButtonProgressState)value); + break; + + case "TaskbarButtonProgressValue": + TaskbarList.SetProgressValue(form, (ulong)(100000 * (float)value), 100000); + break; + + case "TaskbarButtonThumbnails": + GetTaskbarButtonThumbnails(form).ResetToolbar(); + break; + + case "JumpList": + GetJumpList(form).ApplySettings(GetAppID(form)); + break; + + default: + throw new InvalidOperationException("Unrecognized setting name."); + } + OnProperyChanged(form, propName); + } + + private void OnTaskbarButtonCreated(HWND hWnd) + { + IsTaskbarButtonCreated = true; + + // Apply any settings for this window + var form = ExtendedComponents.FirstOrDefault(f => f.Handle == hWnd); + if (form is null) return; + foreach (var kv in propHash[form]) + ApplySetting(form, kv.Value, kv.Key); + } + + private void OnThumbnailButtonClick(HWND hWnd, int buttonId) + { + var form = ExtendedComponents.FirstOrDefault(f => f.Handle == hWnd); + if (form is null) return; + var th = GetTaskbarButtonThumbnails(form); + if (th is null) return; + if (buttonId >= 0 && buttonId < th.Toolbar.Buttons.Count) + th.Toolbar.Buttons[buttonId].InvokeClick(); + } + + private void ResetJumpList(Form form) => propHash[form].Remove("JumpList"); + + private void ResetTaskbarButtonThumbnails(Form form) => propHash[form].Remove("TaskbarButtonThumbnails"); + + private bool ShouldSerializeJumpList(Form form) => GetJumpList(form).Count > 0; + + private bool ShouldSerializeTaskbarButtonThumbnails(Form form) => GetTaskbarButtonThumbnails(form).Count > 0; + + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + private class ThumbButtonMessageFilter : IMessageFilter + { + public bool PreFilterMessage(ref Message m) + { + if (m.Msg == Shell32.WM_TASKBARBUTTONCREATED) + TaskbarButtonCreated(m.HWnd); + else if (m.Msg == (int)User32.WindowMessage.WM_COMMAND && Macros.HIWORD(m.WParam) == Shell32.THBN_CLICKED) + { + ThumbnailButtonClick(m.HWnd, Macros.LOWORD(m.WParam)); + return true; + } + return false; + } + } + + // TODO: ??: SetActiveAlt, MarkFullscreenWindow + } +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/TaskbarButtonThumbnail.cs b/Windows.Shell/TaskBar/TaskbarButtonThumbnail.cs new file mode 100644 index 00000000..38201816 --- /dev/null +++ b/Windows.Shell/TaskBar/TaskbarButtonThumbnail.cs @@ -0,0 +1,127 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Forms; + +using static Vanara.PInvoke.Shell32; + +namespace Vanara.Windows.Shell +{ + /// Specifies taskbar button thumbnail tab properties. + public enum TaskbarItemTabThumbnailOption + { + /// The tab window provides a thumbnail and peek image, either live or static as appropriate. + TabWindow = 0, + + /// + /// Always use the thumbnail or peek image provided by the main application frame window rather than a thumbnail or peek image + /// provided by the individual tab window. + /// + MainWindow = 1, + + /// + /// When the application tab is active and a live representation of its window is available, use the main application's frame window + /// as the thumbnail or peek feature. At other times, use the tab window thumbnail or peek feature. + /// + MainWindowWhenActive = 2, + } + + //[TypeConverter(typeof(TaskbarItemTabConverter))] + //[Serializable] + /// + /// + public class TaskbarButtonThumbnail : INotifyPropertyChanged //, ISerializable + { + internal STPFLAG flag = 0; + private Control tabWin; + + /// Initializes a new instance of the class. + public TaskbarButtonThumbnail() + { + } + + /// Initializes a new instance of the class. + /// The tab window. + public TaskbarButtonThumbnail(Control tabWindow) => ChildWindow = tabWindow; + + /*private TaskbarItemTab(SerializationInfo info, StreamingContext context) + { + flag = (STPFLAG)info.GetValue(nameof(flag), flag.GetType()); + TabWindow = (Control)info.GetValue(nameof(ChildWindow), typeof(Control)); + } + + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(ChildWindow), tabWin, typeof(Control)); + info.AddValue(nameof(flag), flag, flag.GetType()); + }*/ + + /// Occurs when a property has changed. + public event PropertyChangedEventHandler PropertyChanged; + + /// Gets or sets the child window whose image will be displayed in this thumbnail. + /// The child window. + [DefaultValue(null), Category("Appearance")] + public Control ChildWindow + { + get => tabWin; + set + { + if (ReferenceEquals(tabWin, value ?? throw new ArgumentNullException(nameof(ChildWindow), "Child window must be specified for tab."))) + return; + tabWin = value; + //if (Parent != null) + //{ + // var idx = Parent.Thumbnails.IndexOf(this); + // var nextTab = (idx < (Parent.Thumbnails.Count - 1)) ? Parent.Thumbnails[idx + 1] : null; + // Register(nextTab); + //} + OnPropertyChanged(); + } + } + + /// Gets or sets the peek image provider. + /// The peek image provider. + [DefaultValue(typeof(TaskbarItemTabThumbnailOption), "TabWindow"), Category("Appearance")] + public TaskbarItemTabThumbnailOption PeekImageProvider + { + get => (TaskbarItemTabThumbnailOption)(((int)flag & 0xC) >> 2); + set { flag = (STPFLAG)(((int)flag & 0x3) | ((int)value << 2)); OnPropertyChanged(); } + } + + /// Gets or sets the thumbnail provider. + /// The thumbnail provider. + [DefaultValue(typeof(TaskbarItemTabThumbnailOption), "TabWindow"), Category("Appearance")] + public TaskbarItemTabThumbnailOption ThumbnailProvider + { + get => (TaskbarItemTabThumbnailOption)((int)flag & 0x3); + set { flag = (STPFLAG)(((int)flag & 0xC) | (int)value); OnPropertyChanged(); } + } + + /// Called when [property changed]. + /// Name of the property. + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /*internal class TaskbarItemTabConverter : TypeConverter + { + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == null) + throw new ArgumentNullException(nameof(destinationType)); + if (destinationType == typeof(InstanceDescriptor)) + { + TaskbarItemTab tab = (TaskbarItemTab)value; + var ci = typeof(TaskbarItemTab).GetConstructor((tab.ChildWindow == null) ? Type.EmptyTypes : new Type[] { typeof(Control) }); + if (ci != null) + return new InstanceDescriptor(ci, (tab.ChildWindow == null) ? null : new object[] { tab.ChildWindow }, false); + } + return base.ConvertTo(context, culture, value, destinationType); + } + }*/ +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/TaskbarButtonThumbnails.cs b/Windows.Shell/TaskBar/TaskbarButtonThumbnails.cs new file mode 100644 index 00000000..ecdf6712 --- /dev/null +++ b/Windows.Shell/TaskBar/TaskbarButtonThumbnails.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Windows.Forms; + +namespace Vanara.Windows.Shell +{ + /// The list of thumbnails to be displayed on the taskbar button. + /// + [Serializable] + [TypeConverter(typeof(ExpandableObjectConverter))] + public class TaskbarButtonThumbnails : ObservableCollection + { + internal IWin32Window parent; + private bool hasAddedButtons = false; + + internal TaskbarButtonThumbnails(IWin32Window parent) + { + this.parent = parent; + Toolbar = new ThumbnailToolbar(); + Toolbar.PropertyChanged += (s, e) => ResetToolbar(); + base.CollectionChanged += LocalCollectionChanged; + } + + /// Gets or sets the toolbar associated with the taskbar button. + /// The toolbar. + [Category("Appearance"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public ThumbnailToolbar Toolbar { get; } + + /// Gets the count. + /// The count. + [Browsable(false)] + public new int Count => base.Count; + + internal void ResetToolbar() + { + if (Toolbar?.ImageList != null) + TaskbarList.ThumbBarSetImageList(parent, Toolbar.ImageList); + if (Toolbar?.Buttons?.Count > 0) + { + if (!hasAddedButtons) + { + TaskbarList.ThumbBarAddButtons(parent, Toolbar.Buttons.ToArray()); + hasAddedButtons = true; + } + else + TaskbarList.ThumbBarUpdateButtons(parent, Toolbar.Buttons.ToArray()); + } + } + + private void ActivateThumbnail(TaskbarButtonThumbnail thumbnail) + { + if (parent != null) + TaskbarList.SetTabActive(parent, thumbnail?.ChildWindow ?? throw new ArgumentNullException(nameof(thumbnail), "The TaskbarItemTab.ChildWindow property must be set in order to be activated.")); + } + + private void LocalCollectionChanged(object _, NotifyCollectionChangedEventArgs e) + { + // If new thumbnails are added, they have to be registered + if (e.NewItems != null) + { + foreach (var item in e.NewItems.Cast()) + { + item.PropertyChanged += ThmbChanged; + RegisterThumbnail(item); + } + } + + // If new thumbnails are removed, they have to be unregistered + if (e.OldItems != null) + { + foreach (var item in e.OldItems.Cast()) + { + item.PropertyChanged -= ThmbChanged; + UnregisterThumbnail(item); + } + } + + void ThmbChanged(object s, PropertyChangedEventArgs e) => RefreshThumbnail(s as TaskbarButtonThumbnail); + } + + private void RefreshThumbnail(TaskbarButtonThumbnail thumbnail) + { + if (parent != null && thumbnail.ChildWindow != null) + TaskbarList.SetTabProperties(thumbnail.ChildWindow, thumbnail.flag); + } + + private void RegisterThumbnail(TaskbarButtonThumbnail thumbnail) + { + var idx = IndexOf(thumbnail); + var nxt = idx < Count - 1 ? this[idx + 1] : null; + + if (parent != null && thumbnail.ChildWindow != null) + { + TaskbarList.RegisterTab(parent, thumbnail.ChildWindow); + TaskbarList.SetTabOrder(thumbnail.ChildWindow, nxt?.ChildWindow); + TaskbarList.SetTabProperties(thumbnail.ChildWindow, thumbnail.flag); + } + } + + private void UnregisterThumbnail(TaskbarButtonThumbnail thumbnail) + { + if (thumbnail.ChildWindow != null) + TaskbarList.UnregisterTab(thumbnail.ChildWindow); + } + } +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/TaskbarList.cs b/Windows.Shell/TaskBar/TaskbarList.cs index 6005203f..ca3af12c 100644 --- a/Windows.Shell/TaskBar/TaskbarList.cs +++ b/Windows.Shell/TaskBar/TaskbarList.cs @@ -2,7 +2,10 @@ using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; +using Vanara.InteropServices; using Vanara.PInvoke; +using static Vanara.PInvoke.Ole32; +using static Vanara.PInvoke.PropSys; using static Vanara.PInvoke.Shell32; using static Vanara.PInvoke.User32; @@ -45,10 +48,6 @@ namespace Vanara.Windows.Shell taskbar4 = taskbar2 as ITaskbarList4; } - /// Gets the taskbar button created MSG identifier. - /// The taskbar button created MSG identifier. - public static uint TaskbarButtonCreatedWinMsgId => RegisterWindowMessage("TaskbarButtonCreated"); - /// /// Activates an item on the taskbar. The window is not actually activated; the window's item on the taskbar is merely displayed as active. /// @@ -467,6 +466,33 @@ namespace Vanara.Windows.Shell taskbar4?.UnregisterTab(hwnd); } + internal static IPropertyStore GetWindowPropertyStore(HWND hwnd) => SHGetPropertyStoreForWindow(hwnd); + + internal static void SetWindowAppId(HWND hwnd, string appId) => SetWindowProperty(hwnd, PROPERTYKEY.System.AppUserModel.ID, appId); + + internal static string GetWindowAppId(HWND hwnd) => GetWindowProperty(hwnd, PROPERTYKEY.System.AppUserModel.ID); + + internal static void SetWindowProperty(HWND hwnd, PROPERTYKEY propkey, string value) + { + // Get the IPropertyStore for the given window handle + using var pPropStore = ComReleaserFactory.Create(GetWindowPropertyStore(hwnd)); + + // Set the value + using var pv = new PROPVARIANT(value); + pPropStore.Item.SetValue(propkey, pv); + } + + internal static string GetWindowProperty(HWND hwnd, PROPERTYKEY propkey) + { + // Get the IPropertyStore for the given window handle + using var pPropStore = ComReleaserFactory.Create(GetWindowPropertyStore(hwnd)); + + // Get the value + using var pv = new PROPVARIANT(); + pPropStore.Item.GetValue(propkey, pv); + return pv.Value.ToString(); + } + private static void Validate7OrLater() { if (Environment.OSVersion.Version < Win7Ver) diff --git a/Windows.Shell/TaskBar/ThumbnailToolbar.cs b/Windows.Shell/TaskBar/ThumbnailToolbar.cs new file mode 100644 index 00000000..2100afcc --- /dev/null +++ b/Windows.Shell/TaskBar/ThumbnailToolbar.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; +using System.Windows.Forms; + +namespace Vanara.Windows.Shell +{ + /// The toolbar associated with thumbnails shown when hovering over an application's taskbar button. + /// + [TypeConverter(typeof(ExpandableObjectConverter))] + public class ThumbnailToolbar : INotifyPropertyChanged + { + private ImageList _imageList; + + /// Initializes a new instance of the class. + public ThumbnailToolbar() + { + Buttons = new ThumbnailToolbarButtonCollection(); + Buttons.CollectionChanged += (s, e) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Buttons))); + } + + /// Occurs when a property has changed. + public event PropertyChangedEventHandler PropertyChanged; + + /// Gets the buttons. + /// The buttons. + [Category("Appearance"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public ThumbnailToolbarButtonCollection Buttons { get; } + + /// Gets or sets the image list for use by the toolbar buttons. + /// The image list. + public ImageList ImageList + { + get => _imageList; + set + { + if (ReferenceEquals(_imageList, value)) return; + _imageList = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageList))); + } + } + } +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/ThumbnailToolbarButton.cs b/Windows.Shell/TaskBar/ThumbnailToolbarButton.cs new file mode 100644 index 00000000..feed70ce --- /dev/null +++ b/Windows.Shell/TaskBar/ThumbnailToolbarButton.cs @@ -0,0 +1,218 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Design; +using System.Runtime.CompilerServices; +using System.Windows.Forms; +using Vanara.Extensions; +using static Vanara.PInvoke.Shell32; + +namespace Vanara.Windows.Shell +{ + /// + public enum Visibility : byte + { + /// The collapsed + Collapsed = 2, + + /// The hidden + Hidden = 1, + + /// The visible + Visible = 0 + } + + /// A button in the toolbar associated with thumbnails displayed on a taskbar button. + /// + [DefaultProperty("Description"), DefaultEvent("Click")] + public partial class ThumbnailToolbarButton : INotifyPropertyChanged + { + internal THUMBBUTTON btn; + internal ImageIndexer indexer = new ImageIndexer(); + private Icon icon; + private ThumbnailToolbar parent; + private Visibility visibility; + + /// Initializes a new instance of the class. + public ThumbnailToolbarButton() + { + } + + /// Occurs when the button is clicked. + [Category("Behavior")] + public event EventHandler Click; + + /// Occurs when a property has changed. + public event PropertyChangedEventHandler PropertyChanged; + + /// Gets or sets the description displayed as a tooltip for the button. + /// The description text. + [DefaultValue(null), Category("Appearance")] + public string Description + { + get => btn.szTip; + set { btn.szTip = value; btn.dwMask |= THUMBBUTTONMASK.THB_TOOLTIP; OnPropertyChanged(); } + } + + /// Gets or sets a value indicating whether to dismiss the thumbnail when this button is clicked. + /// to dismiss when clicked; otherwise, . + [DefaultValue(false), Category("Behavior")] + public bool DismissWhenClicked + { + get => GetFlagValue(THUMBBUTTONFLAGS.THBF_DISMISSONCLICK); + set => SetFlagValue(THUMBBUTTONFLAGS.THBF_DISMISSONCLICK, value); + } + + /// Gets or sets a value indicating whether to draw the button's border. + /// if button border is drawn; otherwise, . + [DefaultValue(true), Category("Appearance")] + public bool DrawButtonBorder + { + get => !GetFlagValue(THUMBBUTTONFLAGS.THBF_NOBACKGROUND); + set => SetFlagValue(THUMBBUTTONFLAGS.THBF_NOBACKGROUND, !value); + } + + /// Gets or sets the icon shown on the toolbar button. + /// The button icon. + [Description("ButtonImageDescr"), Localizable(true), Category("Appearance")] + [DefaultValue(null)] + public Icon Icon + { + get => icon; + set + { + if (icon != value) + { + icon = value; + if (icon != null) + ImageIndex = -1; + UpdateImageInfo(); + OnPropertyChanged(); + } + } + } + + /// Gets or sets the index of the image from . + /// The index of the image. + /// ImageIndex + [TypeConverter(typeof(ImageIndexConverter)), Editor("System.Windows.Forms.Design.ImageIndexEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor)), Localizable(true), DefaultValue(-1), RefreshProperties(RefreshProperties.Repaint)] + [Description("ButtonImageIndexDescr"), Category("Appearance")] + public int ImageIndex + { + get + { + if (indexer.Index != -1 && Parent?.ImageList != null && indexer.Index >= Parent.ImageList.Images.Count) + return Parent.ImageList.Images.Count - 1; + return indexer.Index; + } + set + { + if (value < -1) + throw new ArgumentOutOfRangeException(nameof(ImageIndex)); + + if (indexer.Index != value) + { + if (value != -1) + icon = null; + indexer.Index = value; + UpdateImageInfo(); + OnPropertyChanged(); + } + } + } + + /// Gets or sets the image key of the image from . + /// The image key value. + [TypeConverter(typeof(ImageKeyConverter)), Editor("System.Windows.Forms.Design.ImageIndexEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor)), Localizable(true), DefaultValue(""), RefreshProperties(RefreshProperties.Repaint)] + [Description("ButtonImageIndexDescr"), Category("Appearance")] + public string ImageKey + { + get => indexer.Key; + set + { + if (indexer.Key != value) + { + if (value != null) + icon = null; + indexer.Key = value; + UpdateImageInfo(); + OnPropertyChanged(); + } + } + } + + /// Gets or sets a value indicating whether this button is enabled. + /// if this button is enabled; otherwise, . + [DefaultValue(true), Category("Behavior")] + public bool IsEnabled + { + get => !GetFlagValue(THUMBBUTTONFLAGS.THBF_DISABLED); + set => SetFlagValue(THUMBBUTTONFLAGS.THBF_DISABLED, !value); + } + + /// + /// Gets or sets a value indicating whether this button is interactive. If , no pressed button state is + /// drawn. This is intended for instances where the button is used in a notification. + /// + /// if this button is interactive; otherwise, . + [DefaultValue(true), Category("Behavior")] + public bool IsInteractive + { + get => !GetFlagValue(THUMBBUTTONFLAGS.THBF_NONINTERACTIVE); + set => SetFlagValue(THUMBBUTTONFLAGS.THBF_NONINTERACTIVE, !value); + } + + /// Gets or sets the visibility of the button. + /// The button visibility. + [DefaultValue(typeof(Visibility), "Visible"), Category("Appearance")] + public Visibility Visibility + { + get => visibility; + set { if (visibility != value) { visibility = value; OnPropertyChanged(); } } + } + + internal ThumbnailToolbar Parent + { + get => parent; + set + { + parent = value; + indexer.ImageList = value.ImageList; + } + } + + internal void InvokeClick() => Click?.Invoke(this, EventArgs.Empty); + + /// Called when a property has changed. + /// Name of the property. + protected virtual void OnPropertyChanged([CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); + + private bool GetFlagValue(THUMBBUTTONFLAGS f) => btn.dwFlags.IsFlagSet(f); + + private void SetFlagValue(THUMBBUTTONFLAGS f, bool value, [CallerMemberName] string memberName = "") + { + btn.dwFlags = btn.dwFlags.SetFlags(f, value); + btn.dwMask |= THUMBBUTTONMASK.THB_FLAGS; + OnPropertyChanged(memberName); + } + + private void UpdateImageInfo() + { + if (icon == null && indexer.ActualIndex >= 0) + { + btn.dwMask |= THUMBBUTTONMASK.THB_ICON | THUMBBUTTONMASK.THB_BITMAP; + btn.hIcon = IntPtr.Zero; + btn.iBitmap = (uint)indexer.ActualIndex; + } + else + { + btn.dwMask |= THUMBBUTTONMASK.THB_ICON | THUMBBUTTONMASK.THB_BITMAP; + if (icon == null) + btn.hIcon = IntPtr.Zero; + else + btn.hIcon = icon.Handle; + btn.iBitmap = 0; + } + } + } +} \ No newline at end of file diff --git a/Windows.Shell/TaskBar/ThumbnailToolbarButtonCollection.cs b/Windows.Shell/TaskBar/ThumbnailToolbarButtonCollection.cs new file mode 100644 index 00000000..e27c14ff --- /dev/null +++ b/Windows.Shell/TaskBar/ThumbnailToolbarButtonCollection.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +using static Vanara.PInvoke.Shell32; + +namespace Vanara.Windows.Shell +{ + /// A collection of thumbnail toolbar buttons. + public class ThumbnailToolbarButtonCollection : ObservableCollection + { + private const int maxBtns = 7; + private readonly ThumbnailToolbar Parent; + + internal ThumbnailToolbarButtonCollection() + { + CollectionChanged += OnCollectionChanged; + } + + /// Adds a sequence of instances to the collection. + /// The items to add. + public void AddRange(IEnumerable items) + { + foreach (var item in items) + Add(item); + } + + internal THUMBBUTTON[] ToArray() + { + var ret = new THUMBBUTTON[maxBtns]; + for (var i = 0; i < maxBtns; i++) + { + if (i < Count) + ret[i] = this[i].btn; + else + ret[i] = THUMBBUTTON.Default; + ret[i].iId = (uint)i; + } + return ret; + } + + /// Inserts the item into the collection. + /// The index at which to insert. + /// The item to insert. + protected override void InsertItem(int index, ThumbnailToolbarButton item) + { + if (Count >= maxBtns) + throw new InvalidOperationException($"A maximum of {maxBtns} buttons may be added to a {nameof(ThumbnailToolbarButtonCollection)}."); + item.indexer.ImageList = Parent.ImageList; + base.InsertItem(index, item); + } + + /// Called when the collection has changed. + /// The sender. + /// The instance containing the event data. + protected virtual void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e?.NewItems != null) + foreach (ThumbnailToolbarButton tbi in e.NewItems) + tbi.Parent = Parent; + } + } +} \ No newline at end of file diff --git a/Windows.Shell/Vanara.Windows.Shell.csproj b/Windows.Shell/Vanara.Windows.Shell.csproj index bd69bc64..9213af4c 100644 --- a/Windows.Shell/Vanara.Windows.Shell.csproj +++ b/Windows.Shell/Vanara.Windows.Shell.csproj @@ -47,38 +47,19 @@ ChangeFilters, ExecutableType, FolderItemFilter, LibraryFolderFilter, LibraryVie + - + + + 4.6.0 - - - - - - - - - - - - - - - - - - - - - -