Improving Discoverability with our Clipboard History DXCore Plug-in for Visual Studio

Welcome to Part 11 of the series showing how to build plug-ins for Visual Studio using DXCore.

So far we've:

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.

So let's try that. From the Toolbox, select the StatusStrip control.

SelectStatusStrip

Drop the StatusStrip onto our FrmClipHistory form...

 DropStatusStrip

Click the Add drop down button.

ClickAddLabel 

Select the StatusLabel.

AddStatusLabel

Set the following properties for the new StatusLabel:

Property Value
(Name) lblStatusText
Enabled Arrow keys to select, Enter to paste, Escape to cancel. Hold down Ctrl to maximize code view.

Next, let's update that AllViewsRect property to take this new status bar into consideration:

private static Rectangle AllViewsRect
{
 
get
 
{
    if (_FrmClipHistory == null)
      return Rectangle.Empty;
    Rectangle rect = _FrmClipHistory.ClientRectangle;
    if (_FrmClipHistory.statusStrip1.Visible)
      rect.Height -= _FrmClipHistory.statusStrip1.Height;
    return rect;
  }
}

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....

Adding a Context Menu

Let's add a context menu with items to:

  • Paste the selected entry
  • Make it easy to discover and access our options page
  • Toggle the visibility of the status bar

If you're still running a debugger session, close it now.

Activate FrmClipHistory.cs [Design].

ActivateFrmClipHistoryDesign

From the Toolbox, select a ContextMenuStrip control...

SelectContextMenu

Drop the ContextMenuStrip onto the form...

DropContextMenuStrip

Using the in-place editor...

AddNewOptionsMenuItem

Add four new menu items:  "Paste", "-", "Toggle status bar visibility", and "Options...".

AddMenuItems 

Let's create event handlers for each menu item. First, double-click on the Paste menu item, and inside that event handler add a call to CloseAndPaste, like this:

private void pasteToolStripMenuItem_Click(object sender, EventArgs e)
{
  CloseAndPaste();
}

Activate the FrmClipHistory.cs [Design] form again, and then double-click the "Toggle status bar visibility" menu item. Add the following code to that event handler:

private void toggleStatusBarVisibilityToolStripMenuItem_Click(object sender, EventArgs e)
{
  statusStrip1.Visible = !statusStrip1.Visible;
  PositionViews();
  using (DecoupledStorage storage = OptClipboardHistory.Storage)
    storage.WriteBoolean("Settings", "ShowStatusBar", statusStrip1.Visible);
}

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:

public static void ShowClipboardHistory()
{
  if (_FrmClipHistory != null)
   
return;
 
_FrmClipHistory = new FrmClipHistory();
 
PasteOnClose = false;
 
try
 
{
    Application.AddMessageFilter(_FrmClipHistory);
 

    LoadSettings(); 

   
CreateViews();
   
PositionViews();
   
UpdateViews();
   
ShowCursor();
   
PositionForm();
   
_FrmClipHistory.ShowDialog(CodeRush.IDE);
 
}
 
finally
 
{
   
CleanUpViews();
   
Application.RemoveMessageFilter(_FrmClipHistory);
   
if (_FrmClipHistory != null)
     
_FrmClipHistory.Dispose();
   
_FrmClipHistory = null;
 
}
 
if (PasteOnClose)
   
Paste();
}

It is important to call LoadSettings before calling PositionViews, since CodeView position depends upon that AllViewsRect property, which changes based on status bar visibility.

LoadSettings looks like this:

private static void LoadSettings()
{
  using (DecoupledStorage storage = OptClipboardHistory.Storage)
    _FrmClipHistory.statusStrip1.Visible = storage.ReadBoolean("Settings", "ShowStatusBar", true);
}

Activate the FrmClipHistory.cs [Design] form again, and then double-click the "Options..." menu item. Add the following code to that event handler:

private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
{
  OptClipboardHistory.Show();
}

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:

AssignContextMenuStripProperty

...

ContextMenuStripAssigned

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):

bool IMessageFilter.PreFilterMessage(ref Message m)
{
  bool isDoubleClick;
  if (m.Msg == (int)WindowMessage.WM_LBUTTONDOWN
|| m.Msg == (int)WindowMessage.WM_RBUTTONDOWN
)
    isDoubleClick = false;
  else if (m.Msg == (int)WindowMessage.WM_LBUTTONDBLCLK)
    isDoubleClick = true;
  else
// Not a message we're interested in.
    return false;
 
  Control target = Control.FromHandle(m.HWnd);
  if (target is WheelPanel)
// WheelPanel is the child of the CodeView that holds the text.
    target = target.Parent;

  if (target != null)
    for (int i = 0; i <= ClipboardHistory.LastIndex; i++)
      if (_Borders[ i ] == target || _CodeViews[ i ] == target)
      {
        MoveCursorTo(i);
        if (isDoubleClick)
          CloseAndPaste();
        return true;
      }
  return false;
}

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...".

InvokeOptions

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)...

ChangeOnlyColor

and click OK.

ClickOKOptionsDialog

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

CursorColorNotChanged

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

CursorColorChanged

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:

private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
{
  Color previousColor = SelectedBorderColor;

 
OptClipboardHistory.Show();

  if (previousColor != SelectedBorderColor)
    ShowCursor();
}

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.

CursorColorChangedImmediately

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:

NotGood

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:

OutOfRange

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

VisualStudioIsClosing

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:

private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
{
 
Color previousColor = SelectedBorderColor;

  int previousRowCount = ClipboardHistory.RowCount;
  int previousColumnCount = ClipboardHistory.ColumnCount;

 
OptClipboardHistory.Show();

 
if (previousRowCount != ClipboardHistory.RowCount || previousColumnCount != ClipboardHistory.ColumnCount)
  {
    int previousCount = previousRowCount * previousColumnCount;
    for (int i = 0; i < previousCount; i++)
    {
      Controls.Remove(_Borders[ i ]);
      _CodeViews[ i ] = null;
      _Borders[ i ] = null;
    }
    CreateViews();
    PositionViews();
    UpdateViews();
    ShowCursor();
  }
  else
if (previousColor != SelectedBorderColor)
   
ShowCursor();
}

I put the else in 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:

private void optionsToolStripMenuItem_Click(object sender, EventArgs e)
{
 
Color previousColor = SelectedBorderColor;
  int previousRowCount = ClipboardHistory.RowCount;
 
int previousColumnCount = ClipboardHistory.ColumnCount;
 
OptClipboardHistory.Show();
 
if (previousRowCount != ClipboardHistory.RowCount || previousColumnCount != ClipboardHistory.ColumnCount)
 
{
   
int previousCount = previousRowCount * previousColumnCount;
   
for (int i = 0; i < previousCount; i++)
   
{
     
Controls.Remove(_Borders[ i ]);
     
_CodeViews[ i ] = null;
     
_Borders[ i ] = null;
   
}
   
CreateViews();
   
PositionViews();
   
UpdateViews();
   
ShowCursor();

    Focus();

  }
 
else if (previousColor != SelectedBorderColor)
   
ShowCursor();
}

I suspect a few minutes spent with Reflector may reveal what's happening here.

Test this new code and see what you think.

Tomorrow, we'll add one of the last features on my list, the ability to paste in a different language (e.g., copy from a VB file and paste into a C# file). See you then!

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
No Comments

Please login or register to post comments.