├── README-En.txt ├── README-Fr.txt ├── Screenshots ├── context-menu-en.png └── install-prompt-en.png ├── FileActionsManager ├── Icons │ ├── Mattahan-Buuf-Menu.ico │ ├── Mattahan-Buuf-MDI-Text-Editor.ico │ └── Readme.txt ├── Properties │ └── AssemblyInfo.cs ├── FileActionsManager.csproj ├── INIFile.cs └── Program.cs ├── .gitignore ├── FileActionsManager.sln ├── FileActionsConsole ├── Properties │ └── AssemblyInfo.cs ├── FileActionsConsole.csproj ├── Program.cs └── ShellFileType.cs └── README.md /README-En.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORelio/FileActionsManager/HEAD/README-En.txt -------------------------------------------------------------------------------- /README-Fr.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORelio/FileActionsManager/HEAD/README-Fr.txt -------------------------------------------------------------------------------- /Screenshots/context-menu-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORelio/FileActionsManager/HEAD/Screenshots/context-menu-en.png -------------------------------------------------------------------------------- /Screenshots/install-prompt-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORelio/FileActionsManager/HEAD/Screenshots/install-prompt-en.png -------------------------------------------------------------------------------- /FileActionsManager/Icons/Mattahan-Buuf-Menu.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORelio/FileActionsManager/HEAD/FileActionsManager/Icons/Mattahan-Buuf-Menu.ico -------------------------------------------------------------------------------- /FileActionsManager/Icons/Mattahan-Buuf-MDI-Text-Editor.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORelio/FileActionsManager/HEAD/FileActionsManager/Icons/Mattahan-Buuf-MDI-Text-Editor.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | Other 3 | FileActionsConsole/bin 4 | FileActionsConsole/obj 5 | FileActionsConsole/*.user 6 | FileActionsManager/bin 7 | FileActionsManager/obj 8 | FileActionsManager/*.user 9 | .vs 10 | -------------------------------------------------------------------------------- /FileActionsManager/Icons/Readme.txt: -------------------------------------------------------------------------------- 1 | = Developer's note = 2 | 3 | After compiling FileActionsManager.exe, the icon groups need to be added manually 4 | using Resource Hacker: http://www.angusj.com/resourcehacker/ 5 | 6 | MAIN (1033) => Mattahan-Buuf-MDI-Text-Editor.ico 7 | MENU (1033) => Mattahan-Buuf-Menu.ico 8 | 9 | Visual Studio do not offer easy embedding of more than 1 native icon group resource, 10 | so adding it manually is the most simple and straightforward way. 11 | 12 | = Credits = 13 | 14 | http://www.iconarchive.com/show/buuf-icons-by-mattahan.html 15 | 16 | = License = 17 | 18 | https://creativecommons.org/licenses/by-nc-sa/4.0/ -------------------------------------------------------------------------------- /FileActionsManager.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileActionsConsole", "FileActionsConsole\FileActionsConsole.csproj", "{9E2133B2-F1E4-4BDD-B105-1797C8F703F5}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileActionsManager", "FileActionsManager\FileActionsManager.csproj", "{9DADC942-E157-4FAB-8636-956ED2C08729}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9E2133B2-F1E4-4BDD-B105-1797C8F703F5}.Debug|x86.ActiveCfg = Debug|x86 15 | {9E2133B2-F1E4-4BDD-B105-1797C8F703F5}.Debug|x86.Build.0 = Debug|x86 16 | {9E2133B2-F1E4-4BDD-B105-1797C8F703F5}.Release|x86.ActiveCfg = Release|x86 17 | {9E2133B2-F1E4-4BDD-B105-1797C8F703F5}.Release|x86.Build.0 = Release|x86 18 | {9DADC942-E157-4FAB-8636-956ED2C08729}.Debug|x86.ActiveCfg = Debug|x86 19 | {9DADC942-E157-4FAB-8636-956ED2C08729}.Debug|x86.Build.0 = Debug|x86 20 | {9DADC942-E157-4FAB-8636-956ED2C08729}.Release|x86.ActiveCfg = Release|x86 21 | {9DADC942-E157-4FAB-8636-956ED2C08729}.Release|x86.Build.0 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /FileActionsManager/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Shell Extensions Manager")] 9 | [assembly: AssemblyDescription("Easily manage shell menu items")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("By ORelio - Microzoom.fr")] 12 | [assembly: AssemblyProduct("File Actions Manager")] 13 | [assembly: AssemblyCopyright("Copyright © ORelio 2015-2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("bdba6ebc-d81c-4365-a7ab-9bb4de4fa4d6")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /FileActionsConsole/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("FileActionsManager")] 9 | [assembly: AssemblyDescription("Manage shell menu items through command line")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("By ORelio - Microzoom.fr")] 12 | [assembly: AssemblyProduct("File Actions Console")] 13 | [assembly: AssemblyCopyright("Copyright © ORelio 2015-2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("edecdee5-b0d2-46a1-902d-7506c2610be7")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /FileActionsConsole/FileActionsConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {9E2133B2-F1E4-4BDD-B105-1797C8F703F5} 9 | Exe 10 | Properties 11 | FileActionsConsole 12 | FileActionsConsole 13 | v4.0 14 | Client 15 | 512 16 | 17 | 18 | x86 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | x86 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 58 | -------------------------------------------------------------------------------- /FileActionsManager/FileActionsManager.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {9DADC942-E157-4FAB-8636-956ED2C08729} 9 | WinExe 10 | Properties 11 | FileActionsManager 12 | FileActionsManager 13 | v4.0 14 | Client 15 | 512 16 | 17 | 18 | x86 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | x86 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ShellFileType.cs 55 | 56 | 57 | 58 | 59 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /FileActionsConsole/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using SharpTools; 8 | 9 | namespace FileActionsConsole 10 | { 11 | /// 12 | /// Command-line utility for creating/deleting file context menu actions 13 | /// By ORelio - (c) 2015-2018 - Available under the CDDL-1.0 license 14 | /// 15 | class Program 16 | { 17 | static void Main(string[] args) 18 | { 19 | try 20 | { 21 | //ShellFileType.AddAction("txt", "testaction", "Testing FileActionsManager", "cmd.exe /C echo %1 && pause > nul"); 22 | //ShellFileType.RemoveAction("txt", "testaction"); 23 | 24 | if (args.Length == 3 && args[0] == "del") 25 | { 26 | ShellFileType.RemoveAction(args[1].Split(','), args[2]); 27 | } 28 | else if ((args.Length == 5 || args.Length == 6) && args[0] == "add") 29 | { 30 | ShellFileType.AddAction(args[1].Split(','), args[2], args[3], args[4], args.Length == 6 && args[5] == "default"); 31 | } 32 | else 33 | { 34 | string exeName = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName); 35 | Console.WriteLine(" - Windows Shell Menu Action Setter v1.0 - By ORelio -\n"); 36 | Console.WriteLine("Usage: " + exeName + " add [def]"); 37 | Console.WriteLine("Usage: " + exeName + " del \n"); 38 | Console.WriteLine("add : Add or update the action based on internal name"); 39 | Console.WriteLine("del : Remove the action based on internal name"); 40 | Console.WriteLine("ext : List of file extensions to affect eg mp3 or mp3,mp4 and so on"); 41 | Console.WriteLine("int : internal name to designate the action"); 42 | Console.WriteLine("dsp : display name of the shell menu item"); 43 | Console.WriteLine("cmd : command to execute when selecting item. File is provided as %1"); 44 | Console.WriteLine("def : add `default' as last argument for setting the action as the default one"); 45 | } 46 | } 47 | catch (UnauthorizedAccessException) 48 | { 49 | RelaunchAsAdmin(args, true); 50 | } 51 | } 52 | 53 | public static void RelaunchAsAdmin(string[] args, bool waitforexit = false) 54 | { 55 | ProcessStartInfo startInfo = new ProcessStartInfo(); 56 | 57 | startInfo.Verb = "runas"; 58 | startInfo.Arguments = String.Join(" ", args.Select(arg => "\"" + args + '"').ToArray()); 59 | startInfo.FileName = Process.GetCurrentProcess().MainModule.FileName; 60 | 61 | Process process = Process.Start(startInfo); 62 | 63 | if (waitforexit) 64 | process.WaitForExit(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /FileActionsManager/INIFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.IO; 6 | 7 | namespace SharpTools 8 | { 9 | /// 10 | /// INI File tools for parsing and generating user-friendly INI files 11 | /// By ORelio (c) 2014 - CDDL 1.0 12 | /// 13 | static class INIFile 14 | { 15 | /// 16 | /// Parse a INI file into a dictionary. 17 | /// Values can be accessed like this: dict["section"]["setting"] 18 | /// 19 | /// INI file to parse 20 | /// INI sections and keys will be converted to lowercase unless this parameter is set to false 21 | /// If failed to read the file 22 | /// Parsed data from INI file 23 | public static Dictionary> ParseFile(string iniFile, bool lowerCase = true) 24 | { 25 | var iniContents = new Dictionary>(); 26 | string[] lines = File.ReadAllLines(iniFile, Encoding.UTF8); 27 | string iniSection = "default"; 28 | foreach (string lineRaw in lines) 29 | { 30 | string line = lineRaw.Split('#')[0].Trim(); 31 | if (line.Length > 0) 32 | { 33 | if (line[0] == '[' && line[line.Length - 1] == ']') 34 | { 35 | iniSection = line.Substring(1, line.Length - 2); 36 | if (lowerCase) 37 | iniSection = iniSection.ToLower(); 38 | } 39 | else 40 | { 41 | string argName = line.Split('=')[0]; 42 | if (lowerCase) 43 | argName = argName.ToLower(); 44 | if (line.Length > (argName.Length + 1)) 45 | { 46 | string argValue = line.Substring(argName.Length + 1); 47 | if (!iniContents.ContainsKey(iniSection)) 48 | iniContents[iniSection] = new Dictionary(); 49 | iniContents[iniSection][argName] = argValue; 50 | } 51 | } 52 | } 53 | } 54 | return iniContents; 55 | } 56 | 57 | /// 58 | /// Write given data into an INI file 59 | /// 60 | /// File to write into 61 | /// Data to put into the file 62 | /// INI file description, inserted as a comment on first line of the INI file 63 | /// Automatically change first char of section and keys to uppercase 64 | public static void WriteFile(string iniFile, Dictionary> contents, string description = null, bool autoCase = true) 65 | { 66 | List lines = new List(); 67 | if (!String.IsNullOrWhiteSpace(description)) 68 | lines.Add('#' + description); 69 | foreach (var section in contents) 70 | { 71 | if (lines.Count > 0) 72 | lines.Add(""); 73 | if (!String.IsNullOrEmpty(section.Key)) 74 | { 75 | lines.Add("[" + (autoCase ? char.ToUpper(section.Key[0]) + section.Key.Substring(1) : section.Key) + ']'); 76 | foreach (var item in section.Value) 77 | if (!String.IsNullOrEmpty(item.Key)) 78 | lines.Add((autoCase ? char.ToUpper(item.Key[0]) + item.Key.Substring(1) : item.Key) + '=' + item.Value); 79 | } 80 | } 81 | File.WriteAllLines(iniFile, lines, Encoding.UTF8); 82 | } 83 | 84 | /// 85 | /// Convert an integer to string or return 0 if failed to parse 86 | /// 87 | /// String to parse 88 | /// Int value 89 | public static int Str2Int(string str) 90 | { 91 | try 92 | { 93 | return Convert.ToInt32(str); 94 | } 95 | catch 96 | { 97 | return 0; 98 | } 99 | } 100 | 101 | /// 102 | /// Convert a 0/1 or True/False value to boolean 103 | /// 104 | /// String to parse 105 | /// Boolean value 106 | public static bool Str2Bool(string str) 107 | { 108 | return str.ToLower() == "true" || str == "1"; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # FileActionsManager 4 | 5 | This program allows to create context menu actions in the Windows file explorer using configuration files. 6 | 7 | ![Context menu](Screenshots/context-menu-en.png) 8 | 9 | The main purpose of FileActionsManager is to allow easy installation of the action and associated standalone program, if any, on several machines. If you are looking to manually configure file type actions using a nice GUI, see [FileTypesMan](https://www.nirsoft.net/utils/file_types_manager.html) from Nirsoft instead. 10 | 11 | ## How to use 12 | 13 | ### General use 14 | 15 | When opening a configuration file with FileActionsManager, the program will offer to register the provided context menu action, or unregister it if the action is already registered. 16 | 17 | ![Install prompt](Screenshots/install-prompt-en.png) 18 | 19 | Otherwise, FileActionsManager offers to associate or unassociate itself with `.seinf` files. 20 | 21 | ### Configuration file syntax 22 | 23 | FileActionsManager works using [INI](https://en.wikipedia.org/wiki/INI_file) files with the `.seinf` file extension. 24 | The following example creates an action to optimize various image formats with [RIOT](http://luci.criosweb.ro/riot/). 25 | 26 | ````ini 27 | [ShellExtension] 28 | Ext=bmp,png,gif,jpg 29 | Name=optimizeimagesize 30 | DisplayName=Optimize image size 31 | Command=Riot.exe "%1" 32 | Requires=Riot.exe,FreeImage.dll 33 | Default=false 34 | ```` 35 | 36 | Each field has the following purpose: 37 | 38 | - `Ext`: A list of comma-separated file extensions for which the action needs to be created. 39 | - `Name`: Internal name of the action, must be unique to avoid overwriting another action. 40 | - `DisplayName`: The label displayed in file context menu inside Windows Explorer. 41 | - `Command`: The command associated with the action, as typed in `cmd.exe` for instance. 42 | - `Requires`: _Optional_. Files required for the action to work. See below for details. 43 | - `Default`: _Optional_. Defines whether the action will become the default action (default: `false`). 44 | 45 | **Remark:** Some built-in file types in Windows do not have a default action set. Adding a new non-default action to such a file type may make it appear as default since Windows will pick the first action as default when no default action was specified in registry. As a workaround, you can use an action name like `zzmyaction`. 46 | 47 | ### Providing additional files with `Requires` 48 | 49 | When using the `Requires` feature, FileActionsManager will look for the required files in the same directory as your `.seinf` file. For instance, the following architecture is valid: 50 | 51 | ```` 52 | OptimizeImages 53 | |- OptimizeImages.seinf 54 | |- FreeImage.dll 55 | `- Riot.exe 56 | ```` 57 | 58 | When opening `OptimizeImages.seinf`, FileActionsManager will copy `FreeImage.dll` and `Riot.exe` to ``%appdata%\ShellExtensions``. Furthermore, the `Command` will be adapted to the full path to `Riot.exe` before being set in registry. 59 | 60 | It is possible to share the same executable between two actions, for instance it is possible to provide `ffmpeg.exe` with both `ConvertMP3.seinf` and `ConvertAAC.seinf`. `ffmpeg.exe` will be stored in `%appdata%\ShellExtensions`, keeping track of which actions depends on `ffmpeg.exe`. The executable will be deleted only when the last action requiring it is removed. 61 | 62 | Different executables or libraries with the same name are not handled since only one version is stored. 63 | 64 | ### Command-line usage 65 | 66 | Alternatively to configuration files, FileActionsConsole can be used to register and remove file actions: 67 | 68 | ```` 69 | :: Usage 70 | FileActionsConsole.exe add [default=false] 71 | FileActionsConsole.exe del 72 | ```` 73 | 74 | ```` 75 | :: Example 76 | FileActionsConsole.exe add bmp,png,gif,jpg optimizeimagesize "Optimize image size" "Riot.exe "%1"" 77 | FileActionsConsole.exe del bmp,png,gif,jpg optimizeimagesize 78 | ```` 79 | 80 | The command-line utility does not support additional files, you'll have to install them by other means. 81 | If you do not need FileActionsConsole.exe, you can safely delete it as it is fully independent from FileActionsManager.exe 82 | 83 | ## How it works 84 | 85 | ### File types in the Windows registry 86 | 87 | FileActionsManager works by manipulating software classes defined in registry with the Registry editor. These are stored in two places: 88 | 89 | - System-wide file types are defined in `HKEY_CLASSES_ROOT` 90 | - Users-defined file types are defined in `HKEY_CURRENT_USER\Software\Classes` 91 | 92 | As user-defined file types will override system-wide file types, FileActionsManager will copy file type information to the current user section of the registry when installing a new context menu action. This way, no administrator privileges are required since only the `HKEY_CURRENT_USER` section of the registry is opened for writing. 93 | 94 | See [How to add context menu item to Windows Explorer](https://stackoverflow.com/questions/20449316/how-add-context-menu-item-to-windows-explorer-for-folders) for more details. 95 | 96 | ### Additional dependencies 97 | 98 | When specifying a dependency with`Requires`, FileActionsManager will copy the file in the Application Data folder of the current user: ``%appdata%\ShellExtensions``. 99 | 100 | A file called `deps.ini` is used to track dependencies. This INI file contains a reverse index of actions for each file present in the directory. When the last action item is removed, the file is deleted. 101 | 102 | ````ini 103 | [Dependencies] 104 | Ffmpeg.exe=aacconvert,mp3convert 105 | Riot.exe=optimizeimagesize 106 | Freeimage.dll=optimizeimagesize 107 | ```` 108 | 109 | ## Credits 110 | 111 | The following icons are used within FileActionsManager: 112 | 113 | - Buuf icons by Mattahan: [Text Editor icon](http://www.iconarchive.com/show/buuf-icons-by-mattahan/MDI-Text-Editor-icon.html) 114 | - Buuf icons by Mattahan: [Menu icon](http://www.iconarchive.com/show/buuf-icons-by-mattahan/Menu-icon.html) 115 | 116 | These icons are licensed under [CC Attribution-Noncommercial-Share Alike 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/). 117 | 118 | ## License 119 | 120 | FileActionsManager is provided under [CDDL-1.0](http://opensource.org/licenses/CDDL-1.0) ([Why?](http://qstuff.blogspot.fr/2007/04/why-cddl.html)). 121 | 122 | Basically, you can use it or its source for any project, free or commercial, but if you improve it or fix issues, 123 | the license requires you to contribute back by submitting a pull request with your improved version of the code. 124 | Also, credit must be given to the original project, and license notices may not be removed from the code. 125 | -------------------------------------------------------------------------------- /FileActionsManager/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Windows.Forms; 4 | using System.IO; 5 | using System.Diagnostics; 6 | using System.Collections.Generic; 7 | using SharpTools; 8 | using System.ComponentModel; 9 | using System.Reflection; 10 | 11 | namespace FileActionsManager 12 | { 13 | /// 14 | /// Small utility for installing/uninstalling file context menu actions using a configuration file 15 | /// By ORelio - (c) 2015-2018 - Available under the CDDL-1.0 license 16 | /// 17 | static class Program 18 | { 19 | private static string AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); 20 | private static string AppFolder = AppData + "\\ShellExtensions"; 21 | private static string AppDepsCacheFile = AppFolder + "\\deps.ini"; 22 | private static string AppDepsSection = "dependencies"; 23 | 24 | public const string AppVer = "1.0"; 25 | public static readonly string AppName = typeof(Program).Assembly.GetName().Name; 26 | public static readonly string AppDesc = AppName + " v" + AppVer; 27 | 28 | [STAThread] 29 | static void Main(string[] args) 30 | { 31 | Application.EnableVisualStyles(); 32 | Application.SetCompatibleTextRenderingDefault(false); 33 | 34 | try 35 | { 36 | if (args.Length > 0 && !args[0].StartsWith("--")) 37 | { 38 | if (File.Exists(args[0])) 39 | { 40 | try 41 | { 42 | var cfgFile = INIFile.ParseFile(args[0]); 43 | if (cfgFile != null && cfgFile.ContainsKey("shellextension")) 44 | { 45 | var settings = cfgFile["shellextension"]; 46 | string[] required = new[] { "ext", "name", "displayname", "command" }; 47 | if (required.All(setting => settings.ContainsKey(setting))) 48 | { 49 | string[] extensions = settings["ext"].Split(','); 50 | string actionName = settings["name"]; 51 | string displayName = settings["displayname"]; 52 | string command = settings["command"]; 53 | string[] dependencies = settings.ContainsKey("requires") ? 54 | settings["requires"].Split(new string[] { "," }, 55 | StringSplitOptions.RemoveEmptyEntries) : new string[0]; 56 | bool defaultAction = settings.ContainsKey("default") ? 57 | settings["default"].ToLower() == "true" : false; 58 | 59 | if (ShellFileType.ActionExistsAny(extensions, actionName)) 60 | { 61 | if ((args.Contains("--yes") || MessageBox.Show("Action '" + displayName + "' already exists.\nUninstall?", 62 | AppDesc, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)) 63 | { 64 | if (dependencies.Length > 0) 65 | HandleDependencies(dependencies, args[0], actionName, displayName, ref command, false); 66 | ShellFileType.RemoveAction(extensions, actionName); 67 | MessageBox.Show("Action '" + displayName + "' has been successfully removed.", 68 | AppDesc, MessageBoxButtons.OK, MessageBoxIcon.Information); 69 | } 70 | } 71 | 72 | else if (args.Contains("--yes") || MessageBox.Show( 73 | "Install action '" + displayName + "' to the following file extensions?\n" 74 | + String.Join(", ", extensions), AppDesc, MessageBoxButtons.YesNo, 75 | MessageBoxIcon.Question) == DialogResult.Yes) 76 | { 77 | if (dependencies.Length > 0 78 | && !HandleDependencies(dependencies, args[0], actionName, displayName, ref command, true)) 79 | return; 80 | ShellFileType.AddAction(extensions, actionName, displayName, command, defaultAction); 81 | MessageBox.Show("Action '" + displayName + "' has been successfully installed.", 82 | AppDesc, MessageBoxButtons.OK, MessageBoxIcon.Information); 83 | } 84 | } 85 | else MessageBox.Show("File '" + Path.GetFileName(args[0]) + "' has missing required fields: " 86 | + String.Join(", ", required.Where(setting => !settings.ContainsKey(setting)).ToArray()), 87 | AppDesc, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); 88 | } 89 | else MessageBox.Show("File '" + Path.GetFileName(args[0]) + "' is not a valid shell extension file.", 90 | AppDesc, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); 91 | } 92 | catch (IOException) 93 | { 94 | MessageBox.Show("Cannot read '" + Path.GetFileName(args[0]) + "'.", 95 | AppDesc, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); 96 | } 97 | } 98 | else MessageBox.Show("Cannot find '" + Path.GetFileName(args[0]) + "'.", 99 | AppDesc, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); 100 | } 101 | else 102 | { 103 | if (!args.Contains("--install") 104 | && (args.Contains("--uninstall") || ShellFileType.ActionExists("seinf", "open"))) 105 | { 106 | if (args.Contains("--yes") || MessageBox.Show(AppName 107 | + " is currently associated with .seinf files.\nRemove association?", 108 | AppDesc, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) 109 | { 110 | args = new string[] { "--uninstall" }; 111 | using (ShellFileType seinf = ShellFileType.GetType("seinf")) 112 | { 113 | seinf.ProgId = null; 114 | } 115 | MessageBox.Show("File association has been removed.", AppDesc, 116 | MessageBoxButtons.OK, MessageBoxIcon.Information); 117 | } 118 | } 119 | else 120 | { 121 | if (args.Contains("--yes") || MessageBox.Show(AppName 122 | + " is currently not associated with .seinf files. Associate?", 123 | AppDesc, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) 124 | { 125 | args = new string[] { "--install" }; 126 | using (ShellFileType seinf = ShellFileType.GetOrCreateType("seinf")) 127 | { 128 | seinf.ProgId = AppName + ".File"; 129 | seinf.Description = "Shell Extension Information File"; 130 | seinf.DefaultIcon = Assembly.GetEntryAssembly().Location + ",1"; 131 | seinf.DefaultAction = "open"; 132 | seinf.MenuItems.Clear(); 133 | seinf.MenuItems.Add("open", 134 | new ShellFileType.MenuItem("&Install", "\"" + Assembly.GetEntryAssembly().Location + "\" \"%1\"")); 135 | seinf.MenuItems.Add("edit", 136 | new ShellFileType.MenuItem("&Edit", "notepad \"%1\"")); 137 | } 138 | MessageBox.Show("File association has been installed.", AppDesc, 139 | MessageBoxButtons.OK, MessageBoxIcon.Information); 140 | } 141 | } 142 | } 143 | } 144 | catch (Exception e) 145 | { 146 | MessageBox.Show(e.ToString(), "Something went wrong!", MessageBoxButtons.OK, MessageBoxIcon.Error); 147 | } 148 | } 149 | 150 | /// 151 | /// Handle shell extension dependency files 152 | /// 153 | /// List of files required by this shell extension 154 | /// File describing the shell extension 155 | /// Internal name of the shell extension 156 | /// Display name of the shell extension 157 | /// Command of the shell extension 158 | /// TRUE to INSTALL, FALSE to UNINSTALL the dependencies 159 | /// True if the (un)install procedure successfuly completed 160 | private static bool HandleDependencies( 161 | string[] dependencies, string actionFile, string actionName, 162 | string displayName, ref string command, bool install) 163 | { 164 | //Create dependencies directory if first dependencies install 165 | if (install) 166 | { 167 | if (!Directory.Exists(AppFolder)) 168 | Directory.CreateDirectory(AppFolder); 169 | } 170 | 171 | //Load or dependencies dictionary 172 | var deps = new Dictionary>(); 173 | if (File.Exists(AppDepsCacheFile)) 174 | deps = INIFile.ParseFile(AppDepsCacheFile); 175 | if (!deps.ContainsKey(AppDepsSection)) 176 | deps[AppDepsSection] = new Dictionary(); 177 | 178 | foreach (string dep in dependencies) 179 | { 180 | //Install depencency 181 | if (install) 182 | { 183 | if (File.Exists(AppFolder + dep)) { } 184 | else if (File.Exists(Path.GetDirectoryName(actionFile) + '\\' + dep)) 185 | File.Copy(Path.GetDirectoryName(actionFile) + '\\' + dep, AppFolder + '\\' + dep, true); 186 | else 187 | { 188 | MessageBox.Show("Action '" + displayName + "' requires '" + dep 189 | + "', which is not installed and cannot be found.", AppDesc, 190 | MessageBoxButtons.OK, MessageBoxIcon.Error); 191 | return false; 192 | } 193 | } 194 | 195 | //Update dependencies dictionary 196 | HashSet depstmp = new HashSet(); 197 | if (deps[AppDepsSection].ContainsKey(dep)) 198 | foreach (string t in deps[AppDepsSection][dep].Split(',')) 199 | if (!depstmp.Contains(t)) 200 | depstmp.Add(t); 201 | if (install && !depstmp.Contains(actionName)) 202 | depstmp.Add(actionName); 203 | else if (!install) 204 | depstmp.RemoveWhere(item => item == actionName); 205 | deps[AppDepsSection][dep] 206 | = String.Join(",", depstmp.ToArray()); 207 | 208 | //Remove dependency if no longer needed 209 | if (!install && String.IsNullOrEmpty(deps[AppDepsSection][dep])) 210 | { 211 | File.Delete(AppFolder + '\\' + dep); 212 | deps[AppDepsSection].Remove(dep); 213 | } 214 | 215 | //Auto-prepend app directory path to dependencies 216 | string[] commandSplitted = command.Split(' '); 217 | for (int i = 0; i < commandSplitted.Length; i++) 218 | if (commandSplitted[i] == dep) 219 | commandSplitted[i] = "\"" + AppFolder + '\\' + dep + '"'; 220 | command = String.Join(" ", commandSplitted); 221 | } 222 | 223 | //Write back dependencies dictionary 224 | if (deps[AppDepsSection].Values.Count > 0) 225 | INIFile.WriteFile(AppDepsCacheFile, deps); 226 | else File.Delete(AppDepsCacheFile); 227 | 228 | //Remove dependencies directory if it's no longer required 229 | if (!install && !Directory.EnumerateFiles(AppFolder).Any()) 230 | Directory.Delete(AppFolder, true); 231 | 232 | return true; 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /FileActionsConsole/ShellFileType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using Microsoft.Win32; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace SharpTools 8 | { 9 | /// 10 | /// Standalone class representing a shell file type information for the Windows operating system. 11 | /// By ORelio - (c) 2015-2018 - Available under the CDDL-1.0 license 12 | /// 13 | public class ShellFileType : IDisposable 14 | { 15 | private const string UserExtensionsPathPrefix = @"Software\\Classes\\"; 16 | private const string UserChoicePathTemplate = @"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.{0}\UserChoice"; 17 | 18 | private static RegistryKey ClassesRoot = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Default); 19 | private static RegistryKey CurrentUser = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default); 20 | 21 | /// 22 | /// Utility method for notifying the operating system that file types were updated 23 | /// 24 | [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)] 25 | private static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2); 26 | 27 | //Extension eg .txt 28 | public readonly string Extension; 29 | 30 | //Extension related items 31 | public string ContentType { get; set; } 32 | public string PerceivedType { get; set; } 33 | private string _progId; 34 | private bool _progIdChanged = false; 35 | public string ProgId 36 | { 37 | get 38 | { 39 | return _progId; 40 | } 41 | set 42 | { 43 | _progIdChanged = true; 44 | _progId = value; 45 | } 46 | } 47 | 48 | //ProgId related items, can be shared between extensions 49 | public string Description { get; set; } 50 | public string DefaultIcon { get; set; } 51 | public string DefaultAction { get; set; } 52 | public readonly Dictionary MenuItems; 53 | 54 | //Init ShellFileType item with readonly list 55 | public ShellFileType(string extension, string progId = null) 56 | { 57 | ProgId = progId; 58 | Extension = extension; 59 | MenuItems = new Dictionary(); 60 | } 61 | 62 | /// 63 | /// Represents a file type shell menu action 64 | /// 65 | public class MenuItem 66 | { 67 | //Elements of the menu item 68 | public string DisplayName { get; set; } 69 | public string Command { get; set; } 70 | 71 | //Init FileTypeMenuItem object 72 | public MenuItem(string displayName, string command) 73 | { 74 | DisplayName = displayName; 75 | Command = command; 76 | } 77 | } 78 | 79 | /// 80 | /// Get a file type item representing a given file extension from the windows registry 81 | /// 82 | /// extension, without the preceding dot, eg "txt" 83 | /// Thrown if the extension does not exist 84 | /// File type representing the extension 85 | public static ShellFileType GetType(string extension) 86 | { 87 | //Basic checks and registry key reading 88 | if (extension == null) 89 | throw new ArgumentNullException("The given extension must not be null"); 90 | if (extension.Length > 0 && !extension.All(c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))) 91 | throw new ArgumentException("The given extension must be non-empty and composed of letters or digits only"); 92 | extension = extension.ToLower(); 93 | RegistryKey result = CurrentUser.OpenSubKey(UserExtensionsPathPrefix + '.' + extension); 94 | if (result == null) 95 | result = ClassesRoot.OpenSubKey("." + extension); 96 | if (result == null) 97 | throw new KeyNotFoundException("The given extension does not exists in registry"); 98 | 99 | //Read basic information about the file extension 100 | ShellFileType fileType = new ShellFileType(extension); 101 | fileType.ProgId = result.GetValue(null) as string; 102 | fileType.ContentType = result.GetValue("Content Type") as string; 103 | fileType.PerceivedType = result.GetValue("PerceivedType") as string; 104 | 105 | //Retrieve ProgId from alternative locations 106 | RegistryKey altProgIds = result.OpenSubKey("OpenWithProgids"); 107 | if (fileType.ProgId == null && altProgIds != null) 108 | if ((fileType.ProgId = altProgIds.GetValue(null) as string) == null) 109 | if (altProgIds.GetValueNames().Length > 0) 110 | fileType.ProgId = altProgIds.GetValueNames()[0]; 111 | 112 | //Update ProgId if the current user has chosen a specifig program for this file extension 113 | RegistryKey userChoice = CurrentUser.OpenSubKey(String.Format(UserChoicePathTemplate, extension)); 114 | if (userChoice != null) 115 | fileType.ProgId = userChoice.GetValue("Progid", fileType.ProgId) as string; 116 | fileType._progIdChanged = false; 117 | 118 | //Read program-related information about file extension 119 | if (fileType.ProgId == null) 120 | return fileType; 121 | RegistryKey progInfo = CurrentUser.OpenSubKey(UserExtensionsPathPrefix + fileType.ProgId); 122 | if (progInfo == null) 123 | progInfo = ClassesRoot.OpenSubKey(fileType.ProgId); 124 | if (progInfo == null) 125 | return fileType; 126 | fileType.Description = progInfo.GetValue(null) as string; 127 | 128 | //Read file type icon 129 | RegistryKey iconInfo = progInfo.OpenSubKey("DefaultIcon"); 130 | if (iconInfo != null) 131 | fileType.DefaultIcon = iconInfo.GetValue(null) as string; 132 | 133 | //Read default shell menu action 134 | RegistryKey shellInfo = progInfo.OpenSubKey("shell"); 135 | if (shellInfo == null) 136 | return fileType; 137 | fileType.DefaultAction = shellInfo.GetValue(null) as string; 138 | 139 | //Read shell menu actions 140 | foreach (string actionName in shellInfo.GetSubKeyNames()) 141 | { 142 | RegistryKey shellAction = shellInfo.OpenSubKey(actionName); 143 | if (shellAction != null) 144 | { 145 | string actionDisplayName = shellAction.GetValue(null) as string; 146 | RegistryKey shellCommand = shellAction.OpenSubKey("command"); 147 | if (shellCommand != null) 148 | { 149 | string actionCommand = shellCommand.GetValue(null) as string; 150 | fileType.MenuItems[actionName] = new ShellFileType.MenuItem(actionDisplayName, actionCommand); 151 | } 152 | } 153 | } 154 | 155 | //All data have been read for registry 156 | return fileType; 157 | } 158 | 159 | /// 160 | /// Get a file type item representing a given file extension a create a default one 161 | /// 162 | /// extension, without the preceding dot, eg "txt" 163 | /// File type representing the extension 164 | public static ShellFileType GetOrCreateType(string extension) 165 | { 166 | try 167 | { 168 | return GetType(extension); 169 | } 170 | catch (KeyNotFoundException) 171 | { 172 | return new ShellFileType(extension, extension + "_auto_file"); 173 | } 174 | } 175 | 176 | /// 177 | /// Save the given type to the registry 178 | /// 179 | /// File type to save in registry 180 | /// Thrown if the provided file type is invalid 181 | public static void SaveType(ShellFileType fileType) 182 | { 183 | //Basic checks on provided file type 184 | if (fileType == null || String.IsNullOrEmpty(fileType.Extension) 185 | || !fileType.Extension.All(c => (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) 186 | throw new ArgumentException("The provided file type object is invalid."); 187 | 188 | //Delete user choice if ProgId was changed 189 | if (fileType._progIdChanged) 190 | CurrentUser.DeleteSubKey(String.Format(UserChoicePathTemplate, fileType.Extension), false); 191 | 192 | //Update extension-related info 193 | RegistryKey result = CurrentUser.CreateSubKey(UserExtensionsPathPrefix + "." + fileType.Extension); 194 | if (fileType._progIdChanged) 195 | result.DeleteValue(null, false); 196 | result.DeleteValue("Content Type", false); 197 | result.DeleteValue("PerceivedType", false); 198 | if (!String.IsNullOrWhiteSpace(fileType.ProgId) 199 | && (fileType._progIdChanged 200 | || String.IsNullOrWhiteSpace(result.GetValue(null) as string))) 201 | result.SetValue(null, fileType.ProgId.Trim()); 202 | if (!String.IsNullOrWhiteSpace(fileType.ContentType)) 203 | result.SetValue("Content Type", fileType.ContentType.Trim()); 204 | if (!String.IsNullOrWhiteSpace(fileType.PerceivedType)) 205 | result.SetValue("PerceivedType", fileType.PerceivedType.Trim()); 206 | 207 | //Noting more to do if no program id 208 | if (String.IsNullOrEmpty(fileType.ProgId)) 209 | return; 210 | 211 | //Update program-related info 212 | RegistryKey progInfo = CurrentUser.CreateSubKey(UserExtensionsPathPrefix + fileType.ProgId); 213 | progInfo.DeleteValue(null, false); 214 | if (!String.IsNullOrWhiteSpace(fileType.Description)) 215 | progInfo.SetValue(null, fileType.Description.Trim()); 216 | 217 | //Update icon info 218 | RegistryKey iconInfo = progInfo.CreateSubKey("DefaultIcon"); 219 | iconInfo.DeleteValue(null, false); 220 | if (!String.IsNullOrWhiteSpace(fileType.DefaultIcon)) 221 | iconInfo.SetValue(null, fileType.DefaultIcon); 222 | 223 | //Update default shell menu action 224 | RegistryKey shellInfo = progInfo.CreateSubKey("shell"); 225 | shellInfo.DeleteValue(null, false); 226 | if (!String.IsNullOrWhiteSpace(fileType.DefaultAction)) 227 | shellInfo.SetValue(null, fileType.DefaultAction); 228 | 229 | //Delete removed shell menu actions 230 | foreach (string actionName in shellInfo.GetSubKeyNames()) 231 | if (!fileType.MenuItems.ContainsKey(actionName)) 232 | shellInfo.DeleteSubKeyTree(actionName); 233 | 234 | //Update shell menu actions 235 | foreach (var menuItem in fileType.MenuItems) 236 | { 237 | RegistryKey shellAction = shellInfo.CreateSubKey(menuItem.Key); 238 | shellAction.DeleteValue(null, false); 239 | if (!String.IsNullOrWhiteSpace(menuItem.Value.DisplayName)) 240 | shellAction.SetValue(null, menuItem.Value.DisplayName.Trim()); 241 | RegistryKey shellCommand = shellAction.CreateSubKey("command"); 242 | shellCommand.DeleteValue(null, false); 243 | if (!String.IsNullOrWhiteSpace(menuItem.Value.Command)) 244 | shellCommand.SetValue(null, menuItem.Value.Command.Trim()); 245 | } 246 | 247 | //Notify the operating system that file type was updated 248 | SHChangeNotify(0x08000000, 0x0000, (IntPtr)null, (IntPtr)null); 249 | } 250 | 251 | /// 252 | /// Add or update a menu item to the specified file extension 253 | /// 254 | /// action name, will overwrite action if already exists 255 | /// display name of the action visible in shell menu 256 | /// command to associate to the given action name 257 | /// file type to add or update action 258 | /// set to true to set the action as the default action 259 | public static void AddAction(string fileExtension, string actionName, string actionDisplayName, string actionCommand, bool isDefault = false) 260 | { 261 | using (ShellFileType fileType = ShellFileType.GetOrCreateType(fileExtension)) 262 | { 263 | fileType.MenuItems[actionName] = new MenuItem(actionDisplayName, actionCommand); 264 | if (isDefault) { fileType.DefaultAction = actionName; } 265 | } 266 | } 267 | 268 | /// 269 | /// Add or update the same menu item to several file extensions at once 270 | /// 271 | /// action name, will overwrite action if already exists 272 | /// display name of the action visible in shell menu 273 | /// command to associate to the given action name 274 | /// file types to add or update action 275 | /// set to true to set the action as the default action 276 | public static void AddAction(IEnumerable fileExtensions, string actionName, 277 | string actionDisplayName, string actionCommand, bool isDefault = false) 278 | { 279 | foreach (string fileExtension in fileExtensions) 280 | AddAction(fileExtension, actionName, actionDisplayName, actionCommand, isDefault); 281 | } 282 | 283 | /// 284 | /// Remove the same menu item from the specified file extensions 285 | /// 286 | /// internal action name tp remove 287 | /// file type to remove action from 288 | public static void RemoveAction(string fileExtension, string actionName) 289 | { 290 | try 291 | { 292 | using (ShellFileType fileType = ShellFileType.GetType(fileExtension)) 293 | { 294 | fileType.MenuItems.Remove(actionName); 295 | } 296 | } 297 | catch (KeyNotFoundException) { /* Nothing to remove */ } 298 | } 299 | 300 | /// 301 | /// Remove the same menu item from several file extensions at once 302 | /// 303 | /// internal action name tp remove 304 | /// file types to remove action from 305 | public static void RemoveAction(IEnumerable fileExtensions, string actionName) 306 | { 307 | foreach (string fileExtension in fileExtensions) 308 | RemoveAction(fileExtension, actionName); 309 | } 310 | 311 | /// 312 | /// Check if the provided menu item exists for the provided file extension 313 | /// 314 | /// File extension 315 | /// Action name 316 | /// True if the action exists for the specified file extension 317 | public static bool ActionExists(string fileExtension, string actionName) 318 | { 319 | try 320 | { 321 | return ShellFileType.GetType(fileExtension).MenuItems.ContainsKey(actionName); 322 | } 323 | catch (KeyNotFoundException) { return false; } 324 | } 325 | 326 | /// 327 | /// Check if the provided menu item exists for all the provided file extensions 328 | /// 329 | /// File extensions 330 | /// Action name 331 | /// True if the action exists for all the specified file extensions 332 | public static bool ActionExistsAll(IEnumerable fileExtensions, string actionName) 333 | { 334 | foreach (string fileExtension in fileExtensions) 335 | if (!ActionExists(fileExtension, actionName)) 336 | return false; 337 | return true; 338 | } 339 | 340 | /// 341 | /// Check if the provided menu item exists for at least one of the provided file extensions 342 | /// 343 | /// File extensions 344 | /// Action name 345 | /// True if the action exists for at least one of the specified file extensions 346 | public static bool ActionExistsAny(IEnumerable fileExtensions, string actionName) 347 | { 348 | foreach (string fileExtension in fileExtensions) 349 | if (ActionExists(fileExtension, actionName)) 350 | return true; 351 | return false; 352 | } 353 | 354 | /// 355 | /// Save changes to the registry immediately 356 | /// 357 | public void Save() 358 | { 359 | SaveType(this); 360 | } 361 | 362 | /// 363 | /// Save changes to the registry before disposing the object 364 | /// 365 | public void Dispose() 366 | { 367 | SaveType(this); 368 | } 369 | } 370 | } 371 | --------------------------------------------------------------------------------