using Microsoft.Win32; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using Vanara.Extensions; using Vanara.Extensions.Reflection; using static Vanara.PInvoke.Shell32; namespace Vanara.Configuration; /// A class that manages a Most Recently Used file listing. [DefaultEvent(nameof(RecentFileMenuItemClick))] public class MRUManager : Component { /// This value should contain the appropriate image for the clear list menu item in derived classes. protected object clearListMenuItemImage; private const string defClearListMenuItemText = "Clear List"; private const int defMaxHistoryCount = 10; private MenuPlacement clearListMenuItemPlacement; private string clearListMenuItemText = defClearListMenuItemText; private string[] exts; private IFileListStorage storage; /// Initializes a new instance of the class. public MRUManager() { } /// Initializes a new instance of the class. /// The file list storage. /// The menu builder. public MRUManager(IFileListStorage fileListStorage, IMenuBuilder menuBuilder) { StorageHandler = fileListStorage; MenuBuilderHandler = menuBuilder; } /// Occurs when the clear recent files menu item is clicked. [Category("Behavior"), Description("Occurs when the clear recent files menu item is clicked.")] public event Action ClearListMenuItemClick; /// Occurs when [get menu image for file]. [Category("Behavior"), Description("Occurs when a file menu item is about to be drawn and is requesting an image.")] public event Func GetMenuImageForFile; /// Occurs when one of the automatically added recent file menu items is clicked. [Category("Behavior"), Description("Occurs when one of the automatically added recent file menu items is clicked.")] public event Action RecentFileMenuItemClick; /// The placement of a menu item in a list. public enum MenuPlacement { /// The bottom of the list, after all items. Bottom, /// The top of the list, before all items. Top } /// Defines a class that implements storage for an MRU file list. public interface IFileListStorage { /// Gets or sets the files declared in this storage instance. /// The file listing. Each file should declare its full path. IEnumerable Files { get; set; } /// Adds a new file to the list. /// The file name with full path. void AddRecentFile(string fileNameWithFullPath); /// Initializes this instance and sets up any local settings required for execution. void Initialize(); /// Removes the file from the list. /// The file name with full path. void RemoveRecentFile(string fileNameWithFullPath); } /// Defines a class that implements a menu handler for an MRU file list. public interface IMenuBuilder { /// Clears the menu items of all files. void ClearRecentFiles(); /// Rebuilds the menus. /// The file listing. /// The handler for when one of the automatically added recent file menu items is clicked.. /// /// The clear list menu item text. A null value indicates that this menu item should not be shown. /// /// The handler for when the clear recent files menu item is clicked. /// /// The clear list menu item image. A null value indicates that this menu item's image should not be shown. /// /// if set to , the clear list menu item precedes the files. /// The menu image callback delegate. void RebuildMenus(IEnumerable files, Action fileMenuItemClick, string clearListMenuItemText = null, Action clearListMenuItemClick = null, object clearListMenuItemImage = null, bool clearListMenuItemOnTop = false, Func menuImageCallback = null); } /// Gets or sets the clear list menu item placement relative to the MRU items. /// The clear list menu item placement. [Category("Appearance"), DefaultValue(typeof(MenuPlacement), "Bottom"), Description("The clear list menu item text."), Localizable(true)] public MenuPlacement ClearListMenuItemPlacement { get => clearListMenuItemPlacement; set { clearListMenuItemPlacement = value; RefreshRecentFilesMenu(); } } /// Gets or sets the clear list menu item text. /// The clear list menu item text. Set this value to null to hide this menu item. [Category("Appearance"), DefaultValue(defClearListMenuItemText), Description("The clear list menu item text."), Localizable(true)] public string ClearListMenuItemText { get => clearListMenuItemText; set { clearListMenuItemText = value; RefreshRecentFilesMenu(); } } /// Gets or sets the vertical bar ('|') separated list of extensions without periods that are supported. /// The file extensions. [DefaultValue(null), Category("Behavior"), Description("Required. Vertical bar ('|') separated list of extensions without periods that are supported.")] public string FileExtensions { get => exts == null ? string.Empty : string.Join("|", exts); set { exts = value?.ToLower().Replace(".", "").Split('|', ';', ',', ' '); RefreshRecentFilesMenu(); } } /// Gets the list of most recently used files. /// The files in reverse chronological order. [Browsable(false)] public IEnumerable Files { get { // Pre-load with values from local settings var recentFiles = StorageHandler != null ? new List(StorageHandler.Files) : new List(); // Augment with values from Recently Used Files system folder if (exts != null && exts.Length > 0) { try { var type = Type.GetTypeFromProgID("Wscript.Shell"); if (type != null) { var script = Activator.CreateInstance(type); foreach (var file in Directory.GetFiles(Environment.GetFolderPath(Environment.SpecialFolder.Recent), "*.lnk").Where(s => exts.Contains(Path.GetExtension(s.Substring(0, s.Length - 4)).Trim('.').ToLower()))) { var sc = script.InvokeMethod("CreateShortcut", file); var targetPath = sc.GetPropertyValue("TargetPath"); if (targetPath != null) recentFiles.Add(targetPath); } } } catch (Exception ex) { Debug.WriteLine(ex.ToString()); } } // Sort files by last write time var res = recentFiles.Distinct().Where(s => !string.IsNullOrEmpty(s) && File.Exists(s)).OrderByDescending(File.GetLastWriteTimeUtc).Take(MaxHistoryCount).ToList(); // Update settings with updated list if (StorageHandler != null) StorageHandler.Files = res; return res; } } /// Gets or sets the maximum number of files to maintain in the history. /// The maximum number of files to maintain in the history. [DefaultValue(defMaxHistoryCount), Category("Behavior"), Description("The maximum number of files to maintain in the history.")] public int MaxHistoryCount { get; set; } = defMaxHistoryCount; /// Gets or sets the menu builder handler. /// The menu builder handler. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IMenuBuilder MenuBuilderHandler { get; set; } /// Gets or sets the storage handler. /// The storage handler. [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IFileListStorage StorageHandler { get => storage; set { storage = value; if (LicenseManager.UsageMode != LicenseUsageMode.Designtime) storage?.Initialize(); } } /// Adds the recent file. /// The file name with full path. public void AddRecentFile(string fileNameWithFullPath) { if (string.IsNullOrEmpty(fileNameWithFullPath) || !File.Exists(fileNameWithFullPath)) throw new FileNotFoundException("Unable to add a file that doesn't exist.", fileNameWithFullPath); try { SHAddToRecentDocs(SHARD.SHARD_PATHA, fileNameWithFullPath); StorageHandler?.AddRecentFile(fileNameWithFullPath); } catch (Exception ex) { Debug.WriteLine(ex.ToString()); } RefreshRecentFilesMenu(); } /// Removes the recent file. /// The file name with full path. public void RemoveRecentFile(string fileNameWithFullPath) { try { StorageHandler?.RemoveRecentFile(fileNameWithFullPath); File.Delete(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Recent), Path.GetFileName(fileNameWithFullPath) + ".lnk")); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } RefreshRecentFilesMenu(); } /// Refreshes the recent files menu. protected void RefreshRecentFilesMenu() { if (!DesignMode) MenuBuilderHandler?.RebuildMenus(Files, OnRecentFileMenuItemClick, ClearListMenuItemText, OnClearListMenuItemClick, clearListMenuItemImage, clearListMenuItemPlacement == MenuPlacement.Top, GetMenuImageForFile); } private void OnClearListMenuItemClick() { var fileArray = new StringCollection(); fileArray.AddRange(Files.ToArray()); try { if (StorageHandler != null) { foreach (var s in StorageHandler.Files) try { File.Delete(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Recent), Path.GetFileName(s) + ".lnk")); } catch { } StorageHandler.Files = null; } MenuBuilderHandler?.ClearRecentFiles(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } ClearListMenuItemClick?.Invoke(fileArray); } private void OnRecentFileMenuItemClick(string file) => RecentFileMenuItemClick?.Invoke(file); /// Storage in the local application settings. public class AppSettingsFileListStorage : IFileListStorage { private const string propName = "__MRUList__"; private ApplicationSettingsBase settings; /// Initializes a new instance of the class. /// /// The settings object to use. If , the component will search all loaded assemblies for an instance. /// public AppSettingsFileListStorage(ApplicationSettingsBase settings = null) { if (settings != null) Settings = settings; } /// Gets or sets the files. /// The files. public IEnumerable Files { get => new List(SettingsValue.Cast()); set { var col = new StringCollection(); if (value != null) col.AddRange(value.ToArray()); SettingsValue = col; } } private ApplicationSettingsBase Settings { get => settings; set { settings = value; AddMRUPropToSettings(); } } private StringCollection SettingsValue { get => settings != null ? settings[propName] as StringCollection : new StringCollection(); set { if (settings != null) settings[propName] = value; } } /// Adds the recent file. /// The file name with full path. public void AddRecentFile(string fileNameWithFullPath) { var col = new StringCollection { fileNameWithFullPath }; col.AddRange(SettingsValue.Cast().ToArray()); SettingsValue = col; } /// Initializes this instance. public void Initialize() => TryLoadAppSettings(); /// Removes the recent file. /// The file name with full path. public void RemoveRecentFile(string fileNameWithFullPath) { var col = SettingsValue; col.Remove(fileNameWithFullPath); SettingsValue = col; } private void AddMRUPropToSettings() { if (Settings?.Properties[propName] != null) return; object defValue = new StringCollection(); var d = new SettingsAttributeDictionary { { typeof(UserScopedSettingAttribute), new UserScopedSettingAttribute() } }; Settings?.Properties.Add(new SettingsProperty(propName, typeof(StringCollection), Settings.Providers["LocalFileSettingsProvider"], false, defValue, SettingsSerializeAs.Xml, d, false, false)); Settings?.Reload(); var conf = SettingsValue; if (conf != null) return; SettingsValue = new StringCollection(); Settings?.Save(); } private void TryLoadAppSettings() { if (Settings != null) return; var appSettingsType = AppDomain.CurrentDomain.GetAllTypes().FirstOrDefault(t => t.IsSubclassOf(typeof(ApplicationSettingsBase))); if (appSettingsType != null) { var defProp = appSettingsType.GetProperty("Default"); if (defProp != null) Settings = (ApplicationSettingsBase)defProp.GetValue(null, null); else Settings = (ApplicationSettingsBase)SettingsBase.Synchronized((ApplicationSettingsBase)Activator.CreateInstance(appSettingsType)); } else throw new ApplicationException("Assembly hosting MRUManager must already have a settings instance derived from ApplicationSettingsBase."); } } /// public class RegistryFileListStorage : IFileListStorage { /// Gets or sets the files. /// The files. public IEnumerable Files { get => GetFiles(GetKey()); set => SetFiles(GetKey(), value); } /// Gets or sets the name of the sub key. /// The name of the sub key. public string SubKeyName { get; set; } /// Adds the recent file. /// The file name with full path. public void AddRecentFile(string fileNameWithFullPath) { try { using var rK = GetKey(); var l = new List(GetFiles(rK)); l.Insert(0, fileNameWithFullPath); SetFiles(rK, l.Distinct()); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } /// Initializes this instance. public void Initialize() { if (SubKeyName == null) { var company = GetAssemblyAttribute()?.Company; var product = GetAssemblyAttribute()?.Product; string s; if (company != null && product != null) s = $"{company}\\{product}"; else if (company != null) s = company; else if (product != null) s = product; else s = Assembly.GetExecutingAssembly().GetName().Name; SubKeyName = $"Software\\{s}\\MRU"; } } /// Removes the recent file. /// The file name with full path. public void RemoveRecentFile(string fileNameWithFullPath) { try { using var rK = GetKey(); var l = new List(GetFiles(rK)); l.RemoveAll(s => string.Equals(s, fileNameWithFullPath, StringComparison.InvariantCultureIgnoreCase)); SetFiles(rK, l.Distinct()); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } private static T GetAssemblyAttribute() => (T)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(T), true).FirstOrDefault(); private IEnumerable GetFiles(SafeRegKey rK) { for (var i = 0; ; i++) { if (rK.Key.GetValue(i.ToString(), null) is not string s) break; yield return s; } } private SafeRegKey GetKey() => new(Registry.CurrentUser, SubKeyName, RegistryKeyPermissionCheck.ReadWriteSubTree); private void SetFiles(SafeRegKey rK, IEnumerable value) { var i = 0; while (true) { try { rK.Key.DeleteValue(i++.ToString(), true); } catch { break; } } i = 0; foreach (var s in value) rK.Key.SetValue(i++.ToString(), s); } private class SafeRegKey : IDisposable { public readonly RegistryKey Key; public SafeRegKey(RegistryKey root, string subKeyName, RegistryKeyPermissionCheck opt) => Key = root.CreateSubKey(subKeyName, opt); public static implicit operator RegistryKey(SafeRegKey k) => k.Key; public void Dispose() => Key.Close(); } } }