In today's post, we'll improve shortcut discoverability and add a context menu to our Clipboard History. We'll also learn how to programmatically invoke the options page we created earlier, and see yet another way to work with DecoupledStorage.

Improving Shortcut Key Discoverability

So far the shortcut and visual interaction feel good, but discoverability for the shortcut keys could be better. To make shortcuts easier to discover, I'm thinking about placing a status bar below the views, revealing available keys.

That should be all we need. Run and test to make sure that it's working as expected.

Now that we have a status bar, it might be nice to allow developers familiar with the shortcuts to hide the status bar to regain some screen real estate for clipboard entries. It would also be nice to provide a fast and easy way to get directly to our new options page from the Clipboard History UI. Both of these problems can be solved with a right-click context menu....

I think it's important to save this preference out whenever it changes. That way developers will only have to hide it once.

Using DecoupledStorage

One of the cool things about this plug-in is that it uses DecoupledStorage in three different but interesting ways:

To save and load the clipboard history.

To save and load options page settings.

To save configuration settings that are not on any options page.

This last bullet point is what we're doing now. While we could place a new check box on our options page, having an option to show or hide the status bar is pretty trivial, and may introduce some confusion. Instead, we can provide contextual access to the setting through the right-click menu. This also has the benefit of giving developers immediate feedback on the setting (to get the same effect with a check box on an options page we would have to modify the preview to draw a status bar -- again, more work than seems justified).

Next, let's make sure the status bar is hidden if needed on startup. First, add a call to LoadSettings from inside ShowClipboardHistory:

Now I think this is really cool. Take a look at that code in the event handler. It's calling a static method inside our options page. That method was built courtesy of the wizard, and it brings up the DevExpress options dialog and displays this page.

One last important step: We need to assign our new context menu to the form's ContextMenuStrip property, like this:

...

Nice. Let's give this a try.

Testing the Context Menu

Click Run. In the second instance of Visual Studio, press Ctrl+Shift+Insert to bring up the Clipboard History form. Right-click the form to bring up the context menu. Try the Paste and Toggle status bar visibility menu items to verify expected behavior. Try hiding the status bar, closing the form, and then bringing it up again to verify that the status bar visibility setting is in fact preserved.

Not bad. Although I am noticing that when I right-click, the CodeView under the mouse is not selected, and I think it should be.

We can fix that while we're running if edit and continue is enabled. Just set a breakpoint in the PreFilterMessage method, and when it hits make this change (add the check for the right mouse button):

And then clear out the breakpoint and run again. Now you should be able to right-click any CodeView, and have it become selected before the context menu pops up.

Next, let's test bringing up the options page. Right-click the Clipboard History form, and choose "Options...".

Remember this line of code?

OptClipboardHistory.Show();

That static method is all it takes to bring up the options dialog and have our new options page displayed.

With the Options dialog up, change only the Selector Color (don't touch the dimensions just yet)...

and click OK.

And yet the cursor color does not appear to have changed:

Press one of the arrow keys to move the cursor, and you'll see the cursor color correctly drawn.

OK, so it seems we need to call ShowCursor on the form after returning from the options page. I'm a big fan of efficient code, so let's only call ShowCursor if the color changes, by adding the following code to our Options... menu item event handler:

You can add this code by setting a breakpoint at the opening brace of the handler (bringing up the options page again through the context menu) and using Edit and Continue, or you can shut down the debugging session, make the change, and then start a new debugging session (Edit and Continue is much faster).

With the change made, let's repeat the test again. Right-click the Clipboard History, select "Options...", and then change only the Selector Color (we'll test dimension changes in a bit), and then click OK.

This time the cursor color is updated immediately when the options page closes.

Excellent.

Now, let's try changing the dimensions. Specifically, try increasing one or both of the dimensions. I've been holding off testing a dimension change because I'm pretty sure we're going to see some issues. Much of the code in FrmClipHistory depends upon a certain synchronization between the values in ClipboardHistory (e.g., RowCount, ColumnCount), and the CodeViews and border Panels on the form.

So, after right-clicking the context menu and bringing up the Options dialog one more time, increasing the dimensions and clicking OK, we might see something like this:

Not only is there no indication that our change in dimensions has taken place, but there also appears to be two cursors! Interacting further with the Clipboard History and you might find yourself suddenly staring at a dialog back in the first instance of Visual Studio, looking something like this:

And if you click Continue, you might eventually see something like this:

So clearly we have at least one issue that requires attention.

One solution might be to close the Clipboard History and reopen it again after returning from the call to OptClipboardHistory.Show. However I think we can clean things up and rebuild the dialog while it's still up, without closing the form.

We should be able to fix this by adding the following code to our Options... click event handler:

I put the elsein front of the last if-statement (that checks for color change) for efficiency, as there is already a ShowCursor call in the previous block (there's no need to call this twice if the dimensions change).

Now, let's try to test this again.

Click Run. Press Ctrl+Shift+Insert to bring up the Clipboard History.

Right-click the Clipboard History and choose "Options...".

Change the dimensions of the clipboard history and click OK. Try changing dimensions again. Be sure to test changes where you increase the dimensions.

This time changes to row or column count appear to be reflected immediately in the Clipboard History upon closing the options dialog.

Not bad, but in testing you've probably noticed that this new code appears to have introduced a very strange bug. After clicking OK on the options dialog and returning to the Clipboard History, the arrow keys no longer move the cursor! If I switch focus to another application and then return to this instance of Visual Studio, the arrow keys work normally again as expected.

What's going on?

If I set a breakpoint inside the ProcessCmdKey method, and then repeat the steps to reproduce the bug (switching to the first instance of Visual Studio to set the breakpoint fixes the problem when I switch back -- so that's why I need to repeat the steps), I find that the ProcessCmdKey breakpoint is NEVER HIT when the problem is reproduced.

This is a very strange problem indeed.

After playing with this more, I theorized that the form is somehow no longer considering itself active. It doesn't make sense to me why this would happen, however the behavior certainly seems to indicate that.

My first stab at fixing this was to place a call to Activate at the end of the block that rebuilds and repositions the CodeViews (right after the first ShowCursor call).

Unfortunately this call to Activate had no effect on the problem. Changing the dimensions through the right-click context menu still resulted in completely disabled keyboard functionality.

Then I tried adding a call to Focus instead, and remarkably, THAT WORKED.

Here's the final version of the event handler, with that added call to Focus that restores keyboard functionality: