├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------