Collecting URLs part 2
Using Ctrl-Shift-C is quick and convenient, but only if you remember it; and moving the mouse pointer all the way down to the Notification Area really is far too much like hard work. How about adding a control to the browser itself? Well, it's pretty easy to add something to Internet Explorer's menu or toolbar, but neither of those is visible in IE9 and above by default. A marginally more complex alternative is to add something to IE's context menu, which is what I'll describe here.
First of all, I need to adapt the program from last time so that it operates in two modes: as a resident application, as before, and a single shot "capture URLs and exit" mode. (There are other ways to support invoking from an external source such as the browser, but this is definitely the simplest.) I've replaced last time's Main with:
[STAThread]
static void Main()
{
var args = Environment.GetCommandLineArgs();
if (args.Length > 1)
switch (args[1][0]) // Just look at first letter and ignore the rest
{
case 'c':
case 'C':
CopyInternetExplorerTabsToClipboard();
break;
}
else
{
bool mutexCreated;
using (var mutex = new Mutex(false, "BrowserClip.Started", out mutexCreated))
{
if (mutexCreated)
{
<Body of the previous version>
}
else
MessageBox.Show("The URL clipper is already running");
}
}
}
The first modification is to check for command line arguments: if something starting with an upper or lowercase 'c' is given, perform the URL clip immediately, otherwise run as before. The second is a small bit of tidiness, to ensure that at most only a single resident instance of the application is running at any given time: the way this works is for the program to attempt to create a kernel object, a Mutex in this case; if this succeeds, then this is the first instance of the program, whereas if the Mutex already exists, another copy is already running.
Adding something to IE's context menu is a two stage operation: a bit of registry manipulation to install the menu item and, because that can point only to an HTML file, such a file to invoke the clipping application. According to the MSDN page, you add the following to the registry to define a context menu item:
[HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt\Clip all URLs]
"Contexts"=dword:00000001
@=" <SOMEPATH> \RunBrowserClip.htm"
I.e., add a key under HKCR\Software\Microsoft\Internet Explorer\MenuExt with the name of the menu item, giving its default value the absolute path to the HTML file to "run." The context defines when the menu item appears, 1 being the default - see the MSDN page for more details.
The HTML file contains nothing more than some JavaScript to run the application (with 'c' as the argument):
<html>
<script type="text/javascript">
var shell = new ActiveXObject("WScript.Shell");
shell.Run('" <EXEPATH> \\BrowserClip.exe" c');
</script>
</html>
Again, this has to specify a complete path, to the executable this time, but note that backslashes have to be escaped here (i.e., doubled) or could be switched to forward slashes.
With this in place, you should now see "Clip all URLs" on IE's right click menu, and selecting that will place all URLs on the clipboard, just the same as using the Notification Area icon or hotkey. (The first time you use it from the context menu, you'll get a warning that the web page wants to execute something, but you can suppress that warning thereafter.)
It is slightly annoying that full paths are needed in both the registry and the HTML file: a nice setup program could insert the correct paths at install time but that seems like a lot of work. Instead, I chose to add registration operations to the program. The argument switch statement is augmented with 'r' to register and 'u' to unregister:
case 'r':
case 'R':
RegisterContextMenu();
break;
case 'u':
case 'U':
UnregisterContextMenu();
break;
And the functions look like this:
private const string regPath = @"Software\Microsoft\Internet Explorer\MenuExt\Clip all URLs";
private static void RegisterContextMenu()
{
var exePath = Assembly.GetExecutingAssembly().Location;
var directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var htmlPath = GetHtmlPath();
using (var w = new StreamWriter(htmlPath))
{
w.WriteLine("<html><script type='text/javascript'>");
w.WriteLine("var shell = new ActiveXObject('WScript.Shell');");
w.WriteLine("shell.Run('\"{0}\" c');", new Uri(exePath).AbsoluteUri);
w.WriteLine("</script></html>");
}
using (var key = Registry.CurrentUser.CreateSubKey(regPath))
{
key.SetValue(null, htmlPath, RegistryValueKind.String);
key.SetValue("Contexts", 1, RegistryValueKind.DWord);
}
}
private static void UnregisterContextMenu()
{
Registry.CurrentUser.DeleteSubKeyTree(regPath);
var htmlPath = GetHtmlPath();
File.Delete(htmlPath);
}
private static string GetHtmlPath()
{
var directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(directory, "RunBrowserClip.htm");
}
I write the HTML file into the current user's LocalAppData space (somewhere I can write without requiring admin permissions), and the contents of both the registry value and that HTML file are built on the fly to point to the appropriate places. Unregistering simply removes both parts again.