While going through the theme's various XAML files I noticed things like
color="#FF123456"
, and I looked and I couldn't figure out what colour I was looking at. There are a lot of these hex notation colours and they all seemed opaque to me.It struck me that it would be nice if I could just hover my mouse over the hex and get the colour to pop up. Sounded like an easy enough task. So I set out to write an extension for Visual Studio to do just that.
My first attempt to write an extension to Firefox met with disappointment - I couldn't figure out how to get started - so I was a little apprehensive about writing an extension for Visual Studio. Luckily extensions for Visual Studio are easy to create (so long as you have Visual Studio).
- Download the Visual Studio 2010 SDK (or this one for Service Pack 1)
- Install the SDK
- Start a new project by selecting from the C#/Extensibility templates - I chose Editor Text Adornment, because I wanted to adorn the editor text with something.
So my task, now that I have the ability to write an extension, was to write an extension that does what I want - i.e. show a colour swatch of the hex notation I'm hovering over.
Step 1: create a regex that picks out the hex. I tried one or two and settled on this one:
#(([0-9A-F]{6})|([0-9A-F]{8})|([0-9A-F]{3}))["<;]
. There might be ways to write it shorter, and I'm willing to hear them, but I'm not a regex guru, so I'll stick with simple. You'll notice that I've constrained the hex to start with a # and end with ", <, or ;. This way the regex will only pick up hex that is the right length, and not any old length, and is most likely meant to be a colour. All the colour hexes I could see ended in ", < or ;. I could have missed an edge case, but not so far!Step 2: turn that string into a colour. There might be a library function for doing this, but I couldn't find it (would be glad if someone were to tell me about it!). I wrote my own:
private Tuple<byte, byte, byte, byte> BytesFromColourString(string colour) { string alpha; string red; string green; string blue; if (colour.Length == 8) { alpha = colour.Substring(0, 2); red = colour.Substring(2, 2); green = colour.Substring(4, 2); blue = colour.Substring(6, 2); } else if (colour.Length == 6) { red = colour.Substring(0, 2); green = colour.Substring(2, 2); blue = colour.Substring(4, 2); alpha = "FF"; } else if (colour.Length == 3) { red = colour.Substring(0, 1) + colour.Substring(0, 1); green = colour.Substring(1, 1) + colour.Substring(1, 1); blue = colour.Substring(2, 1) + colour.Substring(2, 1); alpha = "FF"; } else { throw new ArgumentException(String.Format("The colour string may be 8, 6 or 3 characters long, the one passed in is {0}", colour.Length)); } return new Tuple<byte, byte, byte, byte>( Convert.ToByte(alpha, 16) , Convert.ToByte(red, 16) , Convert.ToByte(green, 16) , Convert.ToByte(blue, 16)); }
OK, so this actually returns a
Tuple<byte, byte, byte, byte>
. I'm not entirely sure why I chose that over returning an actual colour. I might refactor that later. Anyway, turning the tuple into a System.Windows.Media.Color
is a trivial call to the static method Color.FromArgb(byte, byte, byte, byte)
. Also, the above method is a brute force approach to breaking down the colour string into bytes, there could well be a better way. I'm sticking with what works until I'm shown something better.My next hurdle was figuring out how to place the colour swatch where I wanted it. I was able to return the position in text the mouse was hovering over, which would give me a single character, but I couldn't think of how to use that position and character to get the hex colour string.
In the end I opted for a two stage approach. Stage one: when the layout updates, find the start and end positions for any colours in the view. Stage two: when the mouse is hovering somewhere, see if it's position is in any of the ranges previously stored.
Stage one looks like this:
private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { _colourPositions = new List<Tuple<int, int, Color>>(); var matches = Regex.Matches(_view.TextSnapshot.GetText(), "#(([0-9A-F]{6})|([0-9A-F]{8})|([0-9A-F]{3}))[\"<;]", RegexOptions.IgnoreCase); foreach(var m in matches) { var match = m as Match; var mgrp = match.Groups[1] as Group; var colourbytes = BytesFromColourString(mgrp.Value); var colour = Color.FromArgb(colourbytes.Item1, colourbytes.Item2, colourbytes.Item3, colourbytes.Item4); _colourPositions.Add(new Tuple<int,int,Color>(mgrp.Index, mgrp.Index + mgrp.Length, colour)); } }I went with a list to store the position of the colours because I think it makes cleaner code than a dictionary would.
Stage two's like this:
private void ShowColourSwatch(int position, IMappingPoint textPosition, ITextView textView) { _layer.RemoveAllAdornments(); SnapshotPoint? snapPoint = textPosition.GetPoint(textPosition.AnchorBuffer, PositionAffinity.Predecessor); if (snapPoint.HasValue) { SnapshotSpan charSpan = textView.GetTextElementSpan(snapPoint.Value); var colourPos = _colourPositions.Find(cp => (cp.Item1 <= charSpan.Start) && (cp.Item2 >= charSpan.Start)); if(colourPos != null) { Image image = CreateSwatchImage(colourPos, charSpan); _layer.AddAdornment(AdornmentPositioningBehavior.TextRelative, charSpan, null, image, null); Thread t = new Thread(p => { Thread.Sleep(3500); lock (lockObject) { Application.Current.Dispatcher.Invoke(new Action(() => { _layer.RemoveAdornmentsByVisualSpan(charSpan); }), new object[]{}); } }); t.Start(); } } }
The
Thread
in there just makes sure that the colour swatch disappears after three and a half seconds. CreateSwatchImage
uses a lot of the code from the example project that Visual Studio gives you to start with, and just draws the colour swatch on a black and white background for contrast.That is pretty much all the important code that I wrote in constructing the extension. There is one last snippet, I had to modify a single line in the auto-generated factory class so that the swatch would be above the text:
[Order(After = PredefinedAdornmentLayers.Text, Before = PredefinedAdornmentLayers.Caret)]
. Before that the property made the adornment go behind the text, which looked silly for my purposes.The last thing that tripped me up was installing the extension. Obviously I can't sign my extension because I'm too cheap to pay for a certificate to do that with, so I can't get it put on the online extensions thing. However I was sure I could find a way. My first attempt was to double click on the .vsix file that Visual Studio had generated for me. This looked promising - it ran me through an install process and told me it had been successful, so I loaded up Visual Studio but my extension was no where to be found. I tried rebooting my computer, just in case, but to no avail. So I sought out where the extension had been placed and deleted it - which is how you are meant to uninstall extension, by the way - and went online to find out The Right Way™. A few places told me to put the extension in a folder under %appdata%, but that didn't seem to work. Eventually I found an MSDN page that explained I should be putting it under %localappdata%, which sorted me right out. Essentially the path should go something like
%localappdata%Microsoft\VisualStudio\10.0\Extensions\[company]\[extensionName]\[version]\
although you can probably leave out [company] and [version] and it will still work. Once I put the extension there and loaded up Visual Studio, I checked the Extensions Manager in the tools menu and it was there, but needed enabling. After being enabled, and restarting Visual Studio, the extension was working like a charm! No more wondering about what a hex colour string means for me.To view all the code for my extension, and download it for yourself, visit my Github repository.