Fixing the Start Position and Dynamically Updating the Clipboard History Form

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

So far we've:

In today's post we'll add code to calculate a good start position when the Clipboard History first appears, and we'll add code to dynamically update the Clipboard History form when it's up and the clipboard is changed from another active application.

Fixing that Start Position

When we first created the form to show the Clipboard History, we set its StartPosition to Manual. Now it's time to change that.

Let's add a call to a new method, PositionForm, to ShowClipboardHistory:

public static void ShowClipboardHistory()
{
 
if (_FrmClipHistory != null)
   
return;
 
_FrmClipHistory = new FrmClipHistory();
 
PasteOnClose = false;
 
try
 
{
   
CreateViews();
   
PositionViews();
   
UpdateViews();
   
ShowCursor();

   
PositionForm();

   
_FrmClipHistory.ShowDialog(CodeRush.IDE);
 
}
 
finally
 
{
   
CleanUpViews();
   
if (_FrmClipHistory != null)
     
_FrmClipHistory.Dispose();
   
_FrmClipHistory = null;
 
}
  if (PasteOnClose)
    Paste();
}

And let's fill that PositionForm method out. When the Clipboard History appears and the code edit window has focus I would like it to cover the edit window, taking up the same size and position. If another control has focus, however, I want the clipboard history to appear next to that control (so the control remains visible) in the part of the screen that affords the most screen real estate (e.g., left, right, above, or below).

PositionForm looks like this:

private static void PositionForm()
{
  if (_FrmClipHistory == null)
    return;
  if (CodeRush.Editor.HasFocus)
    PositionFormOverTextView();
 
else
   
PositionFormNearActiveControl();
}

PositionFormOverTextView is simple enough, getting a reference to the active TextView (this is the corresponding view to the active document in the code editor), and converting its Bounds property to screen coordinates:

private static void PositionFormOverTextView()
{
  TextView activeView = CodeRush.TextViews.Active;
  if (activeView != null)
    _FrmClipHistory.Bounds = activeView.ToScreenRect(activeView.Bounds);
}

There are many methods inside the TextView that you may find useful for your plug-ins. You might want to take a moment to explore those now with Intellisense in the code.

PositionFormNearActiveControl is a bit meatier, calling IsLargest to determine which potential area (above, below, left, or right) yields the greatest amount of space:

const int _ActiveControlBorder = 8// Distance between edge of active control and this form.

private
static void PositionFormNearActiveControl()
{
  HWND activeWindow = NativeMethods.GetFocus();
  if (activeWindow == HWND.Empty)
    return;

 
Rectangle activeWindowRect = activeWindow.WindowRect;

  Screen targetScreen = Screen.FromRectangle(activeWindowRect);
  if (targetScreen == null)
    return;

  Rectangle targetScreenBounds = targetScreen.Bounds;

  int roomTop = (activeWindowRect.Top - targetScreenBounds.Top - _ActiveControlBorder) * targetScreenBounds.Width;
  int roomBottom = (targetScreenBounds.Bottom - activeWindowRect.Bottom - _ActiveControlBorder) * targetScreenBounds.Width;
  int roomLeft = (activeWindowRect.Left - targetScreenBounds.Left - _ActiveControlBorder) * targetScreenBounds.Height;
  int roomRight = (targetScreenBounds.Right - activeWindowRect.Right - _ActiveControlBorder) * targetScreenBounds.Height;

  if (IsLargest(roomBottom, roomTop, roomLeft, roomRight))
    _FrmClipHistory.Bounds = GetRectangleBelow(activeWindowRect, targetScreenBounds);
  else if (IsLargest(roomLeft, roomBottom, roomTop, roomRight))
    _FrmClipHistory.Bounds = GetRectangleLeft(activeWindowRect, targetScreenBounds);
  else if (IsLargest(roomRight, roomLeft, roomBottom, roomTop))
    _FrmClipHistory.Bounds = GetRectangleRight(activeWindowRect, targetScreenBounds);
 
else
   
_FrmClipHistory.Bounds = GetRectangleAbove(activeWindowRect, targetScreenBounds);
}

IsLargest is straightforward:

/// <summary>
///
Returns true if the first parameter is larger than the remaining three.
///
</summary>
private static bool IsLargest(int first, int second, int third, int fourth)
{
  return first > second && first > third && first > fourth;
}

And the support methods for getting screen rectangles above, below, or to the left or right of the active control are here:

private static Rectangle GetRectangleBelow(Rectangle activeWin, Rectangle screen)
{
  return Rectangle.FromLTRB(screen.Left, activeWin.Bottom + _ActiveControlBorder, screen.Right, screen.Bottom);
}

private
static Rectangle GetRectangleLeft(Rectangle activeWin, Rectangle screen)
{
  return Rectangle.FromLTRB(screen.Left, screen.Top, activeWin.Left - _ActiveControlBorder, screen.Bottom);
}

private
static Rectangle GetRectangleRight(Rectangle activeWin, Rectangle screen)
{
  return Rectangle.FromLTRB(activeWin.Right + _ActiveControlBorder, screen.Top, screen.Right, screen.Bottom);
}

private
static Rectangle GetRectangleAbove(Rectangle activeWin, Rectangle screen)
{
  return Rectangle.FromLTRB(screen.Left, screen.Top, screen.Right, activeWin.Top - _ActiveControlBorder);
}
 

Testing Form Placement

Let's see how all this works. Click Run....

Run

In the second instance of Visual Studio, open a source file and copy a few different selections to the clipboard.

With that source file active, press Ctrl+Shift+Insert. You should see the Clipboard History completely cover the source code.

Next, from the DevExpress menu, bring up the Options dialog. Navigate to a page with edit controls, like the Editor \ Templates page.

Click on one of the text boxes to give it focus, and press Ctrl+Shift+Insert. You should see the Clipboard History appear to the left, right, top, or bottom of the active control.

Excellent!

Dynamically Updating the Clipboard History Form

When the Clipboard History form is displayed, you may have noticed that changes to the clipboard (caused by a cut or copy operation performed in another application) are not reflected in the UI. It's not until the next time you show the Clipboard History that these changes are reflected. We can fix this pretty easily. At first, you might be thinking we could make the FrmClipHistory's UpdateViews method internal, and call that from our ClipboardChanged event handler in our plug-in. But that design is not so appealing, because it links two classes and requires explicit knowledge of the workings of the UI form from our plug-in. Also, it fails to illustrate the recommended way to handle Visual Studio events in forms....

First, let's activate the FrmClipHistory.cs [Design] surface.

ActivateFrmClipHistoryDesign

From the Toolbox, select the DXCoreEvents control. This control lets you listen to the same Visual Studio events that are already directly available from our plug-in's design surface, only now we can listen to those events from any form.

SelectDXCoreEvents

Drop the DXCoreEvents control onto our Clipboard History form ("FrmClipHistory.cs [Design]").

DropDXCoreEvents

Select this control.

On the Properties grid, click the Events tab.

dxCoreEventsClickEventsTab 

Double-click the ClipboardChanged event.

 DoubleClickClipboardChanged

In the event handler, add a call to UpdateViews, like this:

private void dxCoreEvents1_ClipboardChanged(ClipboardChangedEventArgs ea)
{
  UpdateViews();
}

That's it! Remember, if your plug-in has custom UI and your form needs to handle a Visual Studio event, just drop a DxCoreEvents control onto your custom form. We didn't need to do this at the beginning for our plug-in, since the plug-in design surface already exposes these same events.

Testing Dynamic Update

Click Run....

Run

In the second instance of Visual Studio, press Ctrl+Shift+Insert to bring up the Clipboard History. Now switch to another application that works with text, like a browser, notepad, or perhaps even the first instance of Visual Studio. Keeping the Clipboard History visible so you can see the updates as the text is changed, copy text from the second application. You should see that text appear inside the Clipboard History while it's up.

 ClipboardHistoryDynamicUpdate

Note that source code copied from a different instance of Visual Studio is not syntax highlighted. That's because there is no language ID with the clipboard change when it comes from a different application.

After seeing this in action, I'm thinking it might be a good idea to visually indicate when a clipboard entry is not associated with a particular programming language that can be syntax highlighted. We could do this with a different font, for example. Let's change the UpdateViews method so it looks like this:

private static void UpdateViews()
{
 
for (int i = 0; i <= ClipboardHistory
.LastIndex; i++)
 
{
    ClipboardHistoryEntry thisEntry = ClipboardHistory.Entries[ i ];
   
CodeView thisView = _CodeViews[ i ];
    if (thisEntry == null || thisView == null)
      continue;
    if (String.IsNullOrEmpty(thisEntry.Language))
    {
      thisView.LanguageID = "PlainText"
;
      thisView.UseEditorFont = false
;
      thisView.Font = SystemFonts
.DefaultFont;
    
}
    else
    {
     
thisView.LanguageID = thisEntry.Language;
      thisView.UseEditorFont = true
;
    }
    thisView.Text = thisEntry.Text;

  }
}

And I'll leave it to you to test this by copying or cutting text to the clipboard from a different application while the Clipboard History is up, and you can let me know what you think about this behavior.

Tomorrow we'll add an item to Visual Studio's Edit menu so developers can easily discover and access the Clipboard History, and we'll also add mouse support so developers can select clipboard entries with the mouse.

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.
Tags
No Comments

Please login or register to post comments.