Mark Miller

September 2008 - Posts

  • IDE Team Discussion - Using CodeRush Templates to Generate Code

    CodeRush users interested in creating templates that generate custom code based on elements inside a container (e.g., fields in a class, methods in a type, types in a namespace, comments in a file, etc.), might want to check out this YouTube video.

    In it, the IDE Team discusses some work we're doing, where we need to add custom code to serialize and deserialize the fields of around 30 classes. The solution came in creating a template that iterates through the fields in each class and generates the appropriate serialization or deserialization code for each field.

    The main template looks like this:

    «:ccsr»

    public override void WriteData(BinaryWriter writer)
    {
     base.WriteData(writer);
     «ForEach(Field in this, WriteField)»}
     
    public override void ReadData(BinaryReader reader)
    {
     base.ReadData(reader);
     «ForEach(Field in this, ReadField)»}

    Both the ReadField and WriteField templates will be called once for each field in the active class. Both of these templates have a number of alternate expansions. The expansion ultimately selected for a particular field is determined by context. You can set context with the Context Picker on the lower right of the Template options page.

    To make this work, we created a new context, called TypeImplements, because many of the scenarios we needed to respond to were dependent upon the type of the field. For example, one of the alternate expansions for ReadField has this context:

    TypeImplements(«?Get(itemType)»,System.Boolean)

    You can pass parameters to contexts (like we've done here), by right-clicking the context in the Context Picker, and selecting "Parameters...".

    The expansion for the ReadField template associated with the context above looks like this:

    «?Get(itemName)» = reader.ReadBoolean();

    «?Get(itemName)» returns the name of the field we're iterating over, while «?Get(itemType)» returns the full type name. Get is a custom StringProvider that you can use to retrieve the value of a template variable stored with the Set StringProvider (the ForEach TextCommand stores the itemName and itemType variables for you automatically before calling the ReadField and WriteField templates).

    The new TypeImplements context added to solve this code generation challenge will ship with the next version of CodeRush.

  • Language Translation on Paste with the Clipboard History Plug-in

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

    So far we've:

    Today, in our final post of the 12-part series, we'll add a neat feature that shows how to paste code copied from one language into another.

    Paste As Language Conversions

    I know a number of developers who work in more than one language. And sometimes when researching a question online I'll find example code in the wrong language. While the DXCore was never specifically built to convert from one language to another, it has a pretty decent (but not always 100% perfect) ability to do so. And so I'm thinking it might be nice to offer the ability to convert code copied from one language into another just before pasting.

    I also like this feature because it shows how to convert source code from one language to another using the DXCore.

    So let's add a "Paste as..." menu item which can convert code in the clipboard history into the active language. This menu item would be available when the active language is different from the language of the entry.

    Right-click the horizontal bar in our ContextMenuStrip and choose Insert | MenuItem.

    InsertMenuItem

    Set these properties:

    Property Value
    (Name) itmPasteAs
    Text Paste as

    PasteAsMenuItem

    Create an event handler for this menu item's Click event....

    itmPasteAsClickEventHandler

    In the handler, add a call to PasteAsTargetLanguage (we'll implement this soon):

    private void itmPasteAs_Click(object sender, EventArgs e)
    {
      PasteAsTargetLanguage();
    }

    Next, let's ensure this menu item is only available under the right conditions, e.g., when the selected clipboard entry contains source code in a language that is different from the language of the active source file.

    In the FrmClipHistory.cs [Design] file, click the contextMenuStrip1 control.

    SelectContextMenu2

    In the Properties grid, click the Events tab.

    contextMenuStripClickEvents

    Create a handler for the Opening event. This event fires just before the context menu appears.

    contextMenuStripOpeningEventHandler

    Inside the Opening handler, add the following code:

    private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
    {
      itmPasteAs.Visible = false;
      LanguageExtensionBase sourceLanguage;
      LanguageExtensionBase targetLanguage;
      GetLanguagesForConversion(out sourceLanguage, out targetLanguage);
      if (sourceLanguage == null || targetLanguage == null)
        return;
      itmPasteAs.Visible = true;
      itmPasteAs.Text = "Paste as " + targetLanguage.LanguageID;
    }

    This code will make the itmPasteAs menu item visible only if the language from which the source code in the current entry was copied.... LEFT OFF HERE

    GetLanguagesForConversion determines the source and target languages necessary for conversion, and looks like this:

    private static void GetLanguagesForConversion(out LanguageExtensionBase sourceLanguage, out LanguageExtensionBase targetLanguage)
    {
      sourceLanguage = null;
      targetLanguage = null;
      ClipboardHistoryEntry entry = ClipboardHistory.GetEntry(CursorIndex);
      if (entry == null || String.IsNullOrEmpty(entry.Language) || entry.Language == CodeRush.Language.Active)
        return;
      sourceLanguage = CodeRush.Language.GetLanguageExtension(entry.Language);
      targetLanguage = CodeRush.Language.ActiveExtension;
     
    // Only return languages that supports types....
     
    if (sourceLanguage != null && !sourceLanguage.SupportsTypes)
        sourceLanguage = null;
      if (targetLanguage != null && !targetLanguage.SupportsTypes)
        targetLanguage = null;
    }

    Notice the check in the code above to make sure both languages return true in their SupportsTypes properties. This check is performed because there are languages supported by the DXCore that do not support types, such as HTML, and converting from HTML to Visual Basic, for example, would probably not yield useful results. So this simple check should ensure that both the language of the clipboard entry and the language of the active file we're about to paste into, both have a good chance of producing a relatively useful conversion.

    Finally, our PasteAsTargetLanguage method looks like this:

    private void PasteAsTargetLanguage()
    {
      LanguageExtensionBase sourceLanguage;

      LanguageExtensionBase
    targetLanguage;
      GetLanguagesForConversion(out sourceLanguage, out targetLanguage);
      if (sourceLanguage != null && targetLanguage != null)
      {
        ParserBase parser = CodeRush.Language.GetParserFromLanguageID(sourceLanguage.LanguageID);
        if (parser != null)
        {
          LanguageElement rootNode = parser.ParseString(ClipboardHistory.GetText(CR_ClipboardHistory.FrmClipHistory.CursorIndex));
          if (rootNode != null)
          {
            CloseAndPaste(CodeRush.Language.GenerateElement(rootNode, targetLanguage));
            return;
          }
        }
      }
      CloseAndPaste();
    }

    This method gets the source languages necessary for conversion, and then gets a parser for the source language, calling ParseString to turn that code into a parse tree. Then it takes that parse tree and feeds it to GenerateElement to get the source code equivalent in the target language. With the conversion complete all that's needed is to pass those results on to CloseAndPaste. And speaking of CloseAndPaste, I needed to refactor that method so it could optionally accept the text to paste (instead of taking it directly from the ClipboardHistory). The refactored CloseAndPaste and its corresponding overload (to prevent existing code from breaking) look like this:

    private void CloseAndPaste()
    {
      string thisText = ClipboardHistory.GetText(CursorIndex);
      CloseAndPaste(thisText);
    }

    private void CloseAndPaste(string textToPaste)
    {
      Application.RemoveMessageFilter(this);
      PasteOnClose = true;
      if (textToPaste != Clipboard.GetText() && !String.IsNullOrEmpty(textToPaste))
    // Need to put text on clipboard.
       
    Clipboard.SetText(textToPaste);
     
    // Simply calling close here will fail when a mouse double-clicks on a CodeView.
     
    NativeMethods.PostMessage(this.Handle, WindowMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    }

    Adding this optional parameter makes it possible for us to condition the text before pasting (such as what we're doing here in converting code from one language to another).

    Finally, let's add keyboard support for this so developers can paste translated source code without being forced to reach for the mouse. We've already assigned the Enter key to our Paste functionality, so it seems logical to make Alt+Enter be our alternate "Paste as..." shortcut. This requires a small addition to our ProcessCmdKey method:

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
     
    const int WM_KEYDOWN = 0x100;
     
    if (msg.Msg == WM_KEYDOWN)
     
    {
       
    switch (keyData)
        {
         
    case Keys.Enter:
           
    CloseAndPaste();
           
    return true;

          case Keys.Enter | Keys.Alt:
            PasteAsTargetLanguage();
            return true;

         
    case Keys.Escape:
           
    Close();
           
    return true;
       
    }
       
    switch (keyData & ~Keys.Control)
       
    {
         
    case Keys.Left:
           
    MoveCursor(0, -1);
           
    return true;

         
    case Keys.Right:
           
    MoveCursor(0, 1);
           
    return true;

         
    case Keys.Up:
           
    MoveCursor(-1, 0);
           
    return true;

         
    case Keys.Down:
           
    MoveCursor(1, 0);
           
    return true;
       
    }
      }
      return base.ProcessCmdKey(ref msg, keyData);
    }

    And that's it for the code changes. One thing I've noticed, however, is when we bring up the context menu on the Clipboard History, there are no shortcuts shown in the menu. For example, Enter is already our Paste command trigger, however you see no mention of that when the menu is up:

    NoShortcuts

    So let's fix this. I noticed that the ShortcutKeys property of the ToolStripMenuItem control seems to be missing an option to compose a shortcut with the Enter key. Fortunately this control also has a string property ShortcutKeyDisplayString, which will work.

    Set the ShortcutKeyDisplayString properties of the two Paste and Paste as menu items to "Enter" and "Alt+Enter", respectively.

     ShortcutKeyDisplayStringAltEnter

    The form should now look like this:

     ShortcutKeysOnContextMenuAltEnter

    Now it's time to test. Click Run. Open a file in one language (e.g., C#), and copy some code to the clipboard. For example, I've selected this method with a C# initializer in it:

    private static Form CreateNewWindow()
    {
      var window = new Form { Name = "New window", Width = 640, Height = 480 };
      return window;
    }

    Open a file in a different language (e.g., VB), press Ctrl+Shift+Insert to bring up the Clipboard History, right click the entry to paste, and select "Paste as Basic" (or whatever language you've selected.

     PasteAsBasic3

    Here's the code I get when pasting in Visual Basic:

    Private Shared Function CreateNewWindow() As Form
      Dim window = New Form() With {.Name = "New window", .Width = 640, .Height = 480}
      Return window
    End Function

    Excellent! As I alluded to at the beginning of this post, language conversion has never been the number one priority with the DXCore. It's really more a side-effect of the architecture that you get for free, so you may find examples where the results require some editing (especially in areas where features exist in the source language but not the target language). So it's not always perfect, but it's still pretty cool.

    Wrap Up

    For now I think this Clipboard History feature is done. We've come a long way since part one of this series, where we had an idea for a simple plug-in to provide quick access to a history of clipboard operations. Now we have syntax highlighting, ease of use, discoverability, persistence, customization (with preview) and language conversion, all in a professional package with concise code that should be easy to maintain. And this series should have given you a sense of what it takes to produce a high quality feature that many developers will enjoy.

    Speaking of which, CodeRush customers will get this feature (and full source code) wrapped up into the next release (3.2). For eveyone else, you'll need to download the DXCore and follow the steps of this series (or you could purchase a license CodeRush -- it rocks on multiple levels)....

    So what's next? Well, the next step is up to you. What cool feature will you build and share with your peers? Let us know what you're working on and what we can do to make writing plug-ins even easier and more enjoyable. Also, let us know what parts of the DXCore we should cover in more depth with tutorials like this.

  • 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!

  • Maximizing the Active CodeView in our Clipboard History Plug-in for Visual Studio

    Welcome to Part 10 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 the ability to quickly maximize and later restore the active CodeView in the Clipboard History.

    Maximizing the Active CodeView

    I want to improve the Clipboard History UI a bit. While we have increased flexibility by allowing developers the ability to change the dimensions of the Clipboard History, having many rows and/or columns may lead to views that clip a portion of their content, forcing developers to scroll to see everything they contain.

    So what I would like to do is make it possible to maximize the active CodeView temporarily, by holding down the Ctrl key. This allows developers working in space-constrained environments the ability to quickly see the contents of a selected clipboard history entry, without having to reach for the mouse to scroll.

    Unfortunately I'm not aware of any elegant code which can notify me the moment the Ctrl key is pressed or released, so I'm going to hack this the old-fashioned way: by polling with a Timer (shudder).

    Activate FrmClipHistory.cs [Design].

    ActivateFrmClipHistoryDesign

    From the Toolbox, select a System.Windows.Forms.Timer control...

    SelectTimer

    Drop the Timer onto the FrmClipHistory design surface...

    DropTimer

    Set the following properties:

    Property Value
    (Name) tmrCheckControl
    Enabled True

    In the Properties grid, click the Events button.

    tmrCheckControlClickEventsButton

    Double-click the Tick event to add a new handler.

    HandleTickEvent

    Add the following code...

    private static bool _ActiveViewMaximized;

    private
    void tmrCheckControl_Tick(object sender, EventArgs e)
    {
      Keys controlState = Control.ModifierKeys & Keys.Control;
      if (!_ActiveViewMaximized && controlState == Keys.Control ||
          _ActiveViewMaximized && controlState != Keys.Control)
      {
        _ActiveViewMaximized = !_ActiveViewMaximized;
        PositionViews();
      }
    }

    And let's now modify PositionViews so it can distribute the CodeViews evenly or maximize the view under the cursor. Speaking of PositionViews, this method is getting a little meaty, and with this new code it's going to get even meatier, so I think we should refactor it a bit. Here's the new PositionViews method:

    private static void PositionViews()
    {
      if (_FrmClipHistory == null)
        return;

      if (_ActiveViewMaximized)
        MaximizeActiveView();
      else 
        DistributeViewsEvenly();
    }

    DistributeViewsEvenly contains essentially the code from the old PositionViews method:

    private static void DistributeViewsEvenly()
    {
      int width = AllViewsRect.Width / ClipboardHistory.ColumnCount;
      int height = AllViewsRect.Height / ClipboardHistory.RowCount;
      Size viewSize = new Size(width - CursorBorder * 2, height - CursorBorder * 2);
      Size borderSize = new Size(width, height);
      for (int row = 0; row < ClipboardHistory.RowCount; row++)
        for (int column = 0; column < ClipboardHistory.ColumnCount; column++)
        {
          int index = GetIndex(row, column);
          CodeView thisView = _CodeViews[index];
          Panel thisBorder = _Borders[index];
          if (thisView == null || thisBorder == null)
            continue;

          thisBorder.Size = borderSize;
          thisBorder.Location = new Point(width * column, height * row);
          thisView.Size = viewSize;
        }
    }

    I replaced access to _FrmClipHistory.ClientRectangle from inside that old PositionViews code with an AllViewsRect property access. The AllViewsRect property simply returns the client rectangle of the form, like this:

    private static Rectangle AllViewsRect
    {
      get { return _FrmClipHistory.ClientRectangle; }
    }

    MaximizeActiveView needs this rectangle as well, and now seems like a good time to consolidate this access anyway, because I'm thinking about adding a status bar soon to assist with discoverability. If we do this, that status bar will reduce available space to be something less than ClientRectangle. Later, if/when we need to calculate the new available space (e.g., due to this new status bar I'm thinking about adding to the form), we can make that calculation from a single location.

    MaximizeActiveView looks like this:

    private static void MaximizeActiveView()
    {
      int cursorIndex = GetIndex(_CursorRow, _CursorColumn);
      CodeView cursorView = _CodeViews[cursorIndex];
      Panel cursorBorder = _Borders[cursorIndex];
      if (cursorView == null || cursorBorder == null)
        return;

      Rectangle viewRect = AllViewsRect;
      viewRect.Inflate(-CursorBorder, -CursorBorder);
      cursorView.Size = viewRect.Size;
      cursorBorder.Size = AllViewsRect.Size;
      cursorBorder.Location = new Point(0, 0);
      cursorBorder.BringToFront();
    }

    That last BringToFront call is important -- it allows us to only size and position the active view and hide all the others behind it (regardless of their positions).

    There's one more change required. If the active view is maximized, and the arrow keys are pressed, I still want the cursor to move and the active view to reflect that. To do this, we'll need to add the following code to MoveCursor:

    private static void MoveCursor(int rowDelta, int columnDelta)
    {
     
    HideCursor();
     
    _CursorRow += rowDelta;
     
    _CursorColumn += columnDelta;
     
    KeepCursorInBounds();

      if (_ActiveViewMaximized)
        PositionViews();

     
    ShowCursor();
    }

    If Ctrl is held down and the arrow keys are pressed, I still want MoveCursor to be called. So we'll need to make a small modification to ProcessCmdKey, splitting its switch statement in two, one that switches on keyData, and the other that switches on keyData with the control key removed, like this:

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
     
    const int WM_KEYDOWN = 0x100;
     
    if (msg.Msg == WM_KEYDOWN)
     
    {
       
    switch (keyData)
        {
         
    case Keys.Enter:
           
    CloseAndPaste();
           
    return true;
         
         
    case Keys.Escape:
           
    Close();
           
    return true;

        }
        switch (keyData & ~Keys.Control)
        {

         
    case Keys.Left:
           
    MoveCursor(0, -1);
           
    return true;

         
    case Keys.Right:
           
    MoveCursor(0, 1);
           
    return true;

         
    case Keys.Up:
           
    MoveCursor(-1, 0);
           
    return true;

         
    case Keys.Down:
           
    MoveCursor(1, 0);
           
    return true;
       
    }
      }
      return base.ProcessCmdKey(ref msg, keyData);
    }

     The second switch statement strips out the Control key if present in the keyData parameter (so, for example, Ctrl+Left would be treated like a Left when execution enters the second switch).

    That should do it. After making these changes, let's give it a try...

    Testing the New CodeView Maximize/Distribute Feature

    Click Run. In the second instance of Visual Studio, press Ctrl+Shift+Insert to bring up the Clipboard History.

    Use the arrow keys to select an entry with scrollbars...

    CodeViewsDistributedEvenly

    Now hold down the Ctrl key to maximize the active view...

     ActiveCodeViewMaximized

    Nice. Now release the Ctrl key to restore the distributed view default. Try holding down the Ctrl key and using the arrow keys.

    Interact with that a bit. See if it feels right to you.

    Tomorrow we'll improve discoverability into this new Ctrl-key feature, and add a right-click context menu to our form. See you then!

  • Adding Default Settings and a Custom Title to the DXCore Options page of our Clipboard History plug-in for Visual Studio

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

    So far we've:

    In today's post, we'll take the options page up a notch, adding support for reverting to default settings, improving the quality of the code, and specifying a custom title for the options page.

    A Bit of Cleanup

    There are two things I want to clean up on the options page. The first -- notice that whenever we bring up the Editor\Clipboard\History options page, the Default Settings... button is disabled.

    DefaultSettingsIsDisabled

    We recommend providing a way for customers to restore default settings for each options page. I'll show you how to do that in a moment.

    The second issue -- notice that the title of our clipboard history options page is simply "History".

    TitleJustHistory

    It might be nicer to change this to something a bit more explanatory, like "Clipboard History". We can fix this second issue easy enough.

    Close down the debugging session if it's still running.

    Activate the OptClipboardHistory.cs [Design] file...

    ActivateOptClipboardHistory

    Click the options page itself (so no controls are selected).

    In the Properties grid, click the Properties button.

    OptionsPageClickPropertiesButton

    Change the Title property to its default of empty (which means the Title value will come from the options page name specified in the wizard) to the new value, "Clipboard History".

    ChangeTitleToClipboardHistory

    Next, let's fix that disabled Default Settings... button challenge.

    Activate the OptClipboardHistory.cs [Design] file one more time...

    ActivateOptClipboardHistory

    Make sure no controls are selected (click the options page design surface if necessary).

    In the Properties grid, click the Events button.

    OptClipboardHistoryClickEvents

    Click the Categorized button if needed to make sure events are grouped together.

    ClickCategorized

    In the CodeRush category, create a handler for the RestoreDefaults event.

    HandleRestoreDefaults

    Now we can specify default values for our controls. The code here is going to be a little bit similar to the code in our PreparePage handler (just without any storage access), and looks like this:

    private void OptClipboardHistory_RestoreDefaults(object sender, OptionsPageEventArgs ea)
    {
      ckbPersistHistory.Checked = true;
      clrSelector.Color = Color.Red;
      NumRows = 3;
      NumColumns = 3;
      UpdateDimensions();
    }

    There's one final bit of cleanup on my mind that I'll only mention but will not walk you through, and that is to make the observation that these initial values for the settings are showing up (duplicated) in several places. Specifically they appear at these locations:

    • RestoreDefaults event handler
    • PreparePage event handler
    • LoadSettings method
    • Initial values for fields in the ClipboardHistory class
    • Initial value for the SelectedBorderColor field of FrmClipHistory.

    This all works for now because these initial values are all the same. But just like copying code, duplicating initial values can lead to maintenance issues later on. For example, changing one without changing the others can introduce hard-to-find (or at the very least somewhat confusing) bugs. So it makes sense, whenever you're dealing with default options, to create a class to hold those options. That way if you ever want to change an initial value, you can do it in one place.

    So, our new class to hold default options looks like this:

    public static class Defaults
    {
      public const bool PersistHistory = true;
      public const int RowCount = 3;
      public const int ColumnCount = 3;
      public static readonly Color CursorColor = Color.Red;
    }

    And the corresponding changes to the code follow...

    In our RestoreDefaults handler:

    private void OptClipboardHistory_RestoreDefaults(object sender, OptionsPageEventArgs ea)
    {
      ckbPersistHistory.Checked =
    Defaults.PersistHistory
    ;
      clrSelector.Color =
    Defaults.CursorColor
    ;
      NumRows =
    Defaults.RowCount
    ;
      NumColumns =
    Defaults.ColumnCount
    ;
      UpdateDimensions();

    }

    In our PreparePage handler:

    private void OptClipboardHistory_PreparePage(object sender, OptionsPageStorageEventArgs ea)
    {
     
    ckbPersistHistory.Checked = ea.Storage.ReadBoolean("Settings", "PersistClipboardHistory", Defaults.PersistHistory);
     
    clrSelector.Color = ea.Storage.ReadColor("Settings", "SelectorColor", Defaults.CursorColor);
     
    NumRows = ea.Storage.ReadInt32("Settings", "RowCount", Defaults.RowCount);
     
    NumColumns = ea.Storage.ReadInt32("Settings", "ColumnCount", Defaults.ColumnCount);
      UpdateDimensions();
    }

    In PlugIn1.cs, in the LoadSettings method:

    private void LoadSettings()
    {
      using (DecoupledStorage settings = OptClipboardHistory.Storage)
     
    {
       
    ClipboardHistory.PersistHistory = settings.ReadBoolean("Settings", "PersistClipboardHistory", Defaults.PersistHistory
    );
       
    FrmClipHistory.SelectedBorderColor = settings.ReadColor("Settings", "SelectorColor", Defaults.CursorColor
    );
       
    ClipboardHistory.RowCount = settings.ReadInt32("Settings", "RowCount", Defaults.RowCount
    );
       
    ClipboardHistory.ColumnCount = settings.ReadInt32("Settings", "ColumnCount", Defaults.ColumnCount
    );
       
    ClipboardHistory.BuildEntries();
     
    }
    }

    At the top of ClipboardHistory.cs:

    public static class ClipboardHistory
    {
     
    public static bool PersistHistory = Defaults.PersistHistory
    ;
     
    public static int RowCount = Defaults.RowCount
    ;
     
    public static int ColumnCount = Defaults.ColumnCount;

    And the last change is at the top of FrmClipHistory.cs

    public partial class FrmClipHistory : Form, IMessageFilter
    {
      internal static Color SelectedBorderColor =
    Defaults.CursorColor;

    I feel better about the code now. It has a more solid feel to it. Let's test these changes.

    Testing the Cleanup Changes

    Click Run.

    Run

    In the second instance of Visual Studio, from the DevExpress menu, select Options....

    DevExpressOptions2

    Activate the Editor\Clipboard\History options page, if it's not already active.

    The first thing you should notice is that the title has changed to the new value we've specified:

    ClipboardHistoryTitle

    By now your eyes have no doubt glanced down at the Default Settings... button. Sure enough, it is now enabled:

    DefaultSettingsIsEnabled

    Good!

    Before testing this button, make sure your Selector Color is set to something other than Color.Red, or your Columns and Rows settings are a value other than 3.

    Click the Default Settings... button.

    ConfirmRestoreDefaultSettings

    Click Yes, and settings will be restored to their defaults:

    ClipboardHistoryRestoredSettings

    Excellent!

    Note: You can revert to your older settings if you like by clicking the Cancel button on the Options dialog.

    Tomorrow, we'll add a really cool usability feature -- the ability to quickly maximize the active CodeView in the Clipboard History. See you then!

  • Creating an Options Page for the Clipboard History, our DXCore/Visual Studio Plug-in

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

    So far we've:

    In today's rather ambitious post (brace for impact), we'll add a user-friendly options page to the DevExpress Options dialog so developers can easily customize features of the clipboard history.

    Customizing Behavior

    So, we've done a pretty good job up to this point of implementing a relatively useful clipboard history feature. Now I think it's time to make it easier for developers to customize behavior.

    For example, we need a setting to determine if the clipboard history should be persisted between sessions (currently the default behavior) or not. And it might be nice to have an option to let developers change the color of the cursor from the currently very bright and very saturated red to something perhaps a bit more aesthetically pleasing. And I'm also starting to think that it might be really nice if we allowed developers to change the dimensions of the history from the current hard-coded 3x3 to something else (like 1x4, or perhaps 5x5).

    So we need an options page. Traditionally, adding an options page to any application is a painful experience. Thankfully, the DXCore makes this significantly easier. Here's an overview of what we'll need to accomplish:

    • Run the options page wizard
    • Layout controls
    • Add runtime behavior code for controls if necessary
    • Prepare the options page (set values on the controls based on settings in storage)
    • Optionally specify default values (enables the "Restore Defaults" button)
    • Commit changes (transfer values on the controls to storage)
    • Read settings when necessary (read settings from storage and apply to feature behavior)

    The Options Page Wizard

    In the Solution Explorer tool window, right-click the project and choose Add | New Item....

    ProjectAddNewItem

    The Add New Item dialog appears.

    Select the DXCore category.

    ClickDXCore

    Select the Options Page template.

    ClickOptionsPageTemplate

    Specify a meaningful name. As you've already seen I like to prefix my form names with "Frm", and I like to similarly prefix my options pages with "Opt". So we can call this file "OptClipboardHistory.cs".

    OptClipboardHistoryFileName

    Click Add.

    ClickAdd

    The New Options Page dialog will appear. There are three options to set here. We'll leave User Level at its default (New User). This option determines when the page will be seen (the Options dialog has three different user levels -- at Expert level all options pages are visible).

    Let's change the Category. This list is populated with all the existing categories. If you have CodeRush installed, then you already have an Editor\Clipboard category, and you can select it from the list:

    ChooseTheEditorClipboardCategory

    If you don't see this entry, you can enter "Editor\Clipboard " into the Category text box.

    For the Page Name, specify "History".

    NewOptionsPageFilledOut

    Click OK. Astute readers will notice the similarity between the category and page name specified in this form, and the code used to get a new DecoupledStorage instance in the ClipboardHistory's Save and Load methods:

    DecoupledStorage settings = CodeRush.Options.GetStorage("Editor\\Clipboard", "History")

    This is not a coincidence. To make things easier, I wanted to use a single storage instance for both the options, and the persisted entries. It's also possible to have different DecoupledStorage instances pointing to different settings locations, but I think this will be a bit easier.

    Click OK.

    ClickOK

    The new options page is now part of our project:

    OptClipboardHistoryFileHasAppeared

    And an Options Page design surface is activated in the editor.

    NewOptionPageDesignSurface 

    Why can't I resize the options page at design-time?
    At runtime, options pages can be resized, but at design-time, the ability to resize is disabled. The size of this options page is the minimum size that it will be when it appears in the DXCore Options dialog. So everything needs to fit in this space. If you need more room for your options controls, you might consider a second options page, or more traditional strategies for dealing with space constraints, such as scroll bars or perhaps a tab control.

    Note that even though the options page is a fixed-size at design-time, controls on your options page can resize or move as the options page is resized at runtime, simply by setting the Anchor property accordingly.

    Next, we need to layout the controls that we want to appear on this page.

    Control Layout and Runtime Behavior

    In the Toolbox, select a standard Checkbox...

    ClickCheckbox

    and drop it onto the options page...

    DropCheckbox

    and set these properties:

    Property Value
    (Name) ckbPersistHistory
    Text Persist clipboard history across Visual Studio sessions

    Next, let's drop a label...

    DropLabel

    and position it under the checkbox...

    DropLabelOnForm

    and set its Text property to "Selector Color:".

    Next, we need a color picker. The DXCore actually ships with a custom color picker control, however it is not installed onto the Toolbox by default (at least not yet, as of version 3.0.8). So if you don't see a ColorPicker control in the DXCore section of the Toolbox, we'll need to add it. Here's how:

    In the Toolbox, right-click the DXCore category title and select "Choose Items...".

    ChooseItems

    After a few moments, the Choose Toolbox Items dialog will appear.

    CheckColorSwatch

    Scroll down and place a check in the box to the left of the ColorSwatch control (in the DevExpress.CodeRush.UserControls namespace), and click OK.

    Now, in the Toolbox, select the ColorSwatch control...

    SelectColorSwatch

    and drop it on the options page, to the right of our label. Change the following properties in the ColorSwatch:

    Property Value
    (Name) clrSelector
    Color Red

    To compile with the ColorSwatch, we'll need to add an assembly reference to our project.

    Right-click the References folder of our project, and choose Add Reference....

    AddReference

    Select the DevExpress.DXCore.Controls.Data assembly...

    AddReferenceToControlsData

    Click OK.

    Next, let's create a preview panel, to show the impact of the changes before we make them. We can put TrackBars above and to the left of the panel, to make it quick and easy to change the number of rows and columns that we'll track.

    Drop a Panel onto the options page, and size it so it looks something like this:

    DropPanel

    Change the following properties in the Panel:

    Property Value
    (Name) pnlPreview

    Next, select a TrackBar control...

    SelectTrackbar

    and drop it above the Panel.

    DropColumnTrackbar

    Change the following properties in the new TrackBar:

    Property Value
    (Name) trkColumnCount
    Maximum 8
    Minimum 1
    Value 3

    Now we need another TrackBar, just like this one. Click this control on the options page to ensure it has focus, and press Ctrl+C to copy it to the clipboard, and then Ctrl+V to paste it onto the form.

    Change the following properties in the newly-pasted TrackBar:

    Property Value
    (Name) trkRowCount
    Orientation Vertical

    Move and size this vertical TrackBar so it lines up with the left side of the preview Panel.

    DropRowTrackBar

    Make sure the top and left edges of the preview Panel are not obscured by the TrackBar controls.

    Important: Ctrl+Click on the horizontal TrackBar so both TrackBars are selected.

    Click the Events button on the Properties grid.

    ClickEventsButton

    Select the Scroll event (don't double-click), then type "DimensionsChanged"...

    TypeDimensionsChanged

    and press Enter. In the code, add a call to refresh our preview panel:

    private void DimensionsChanged(object sender, EventArgs e)
    {
      pnlPreview.Refresh();
    }

    On the options page design surface, click the clrSelector ColorSwatch control to select it.

    In the Properties grid, click the Events button.

    Double-click the ColorChange event to generate a handler for it. Add a call here as well to pnlPreview.Refresh...

    private void clrSelector_ColorChange(object sender, EventArgs e)
    {
      pnlPreview.Refresh();
    }

    For symmetry on the form, I want to invert the range that trkRowCount runs through. Instead of the bottom position resulting in a value of 1, and the top position resulting in a value of 8, I want those values returned to be the reverse of that (e.g., 1-8 from top to bottom). In the code I've created a simple NumRows property that performs this inversion:

    private int NumRows
    {
      get { return 1 + trkRowCount.Maximum - trkRowCount.Value; }
      set { trkRowCount.Value = 1 + trkRowCount.Maximum - value; }
    }

    And for symmetry in the code, I've created a similar property for NumColumns, although this property does not invert the TrackBar values since this TrackBar is already running through the range of values I want (e.g., 1-8 from left to right):

    private int NumColumns
    {
      get { return trkColumnCount.Value; }
      set { trkColumnCount.Value = value; }
    }

    On the options page design surface, double-click the Panel to create a new handler for it's Paint event. Now it's time to write some code to show the preview. I've come up with something like this:

    private void pnlPreview_Paint(object sender, PaintEventArgs e)
    {
      int width = pnlPreview.ClientRectangle.Width / NumColumns - 2;
      int height = pnlPreview.ClientRectangle.Height / NumRows - 2;
      for (int row = 0; row < NumRows; row++)
        for (int column = 0; column < NumColumns; column++)
        {
          Rectangle thisRect = new Rectangle(2 + column * width, 2 + row * height, width - 2, height - 2);
          e.Graphics.FillRectangle(Brushes.White, thisRect);
          thisRect.Inflate(-1, -1);
          using (Pen borderPen = new Pen(Color.LightGray, 2))
          e.Graphics.DrawRectangle(borderPen, thisRect);
        }
      Rectangle selectionRect = new Rectangle(2, 2, width - 2, height - 2);
      using (Pen selectionPen = new Pen(clrSelector.Color, 2))
        e.Graphics.DrawRectangle(selectionPen, selectionRect);
    }

    Activate the options page design surface one more time. Let's drop two more Labels on the options page...

    DropLabel

    and set their Text properties to "Rows" and "Columns", and center them next to the TrackBars, like so...

    DropRowAndColumnLabels

    We'll drop one final Label to show column and row counts...

    DropLabel

    and center it below the preview panel.

    Change the following properties in the newly-dropped Label:

    Property Value
    (Name) lblDimensions
    Text 3x3

    Your final UI should look like this:

    OptionsPageFinalUI 

    Can I use third-party controls in my plug-in?

    You can, but it's really only safe to do this if you're the only developer who will use your plug-in. If you expect to share plug-ins with other developers who might not have a license to your third-party control, we recommend using only the standard .NET controls, or controls that ship with the DXCore (the controls located in the "DXCore..." groups of the Toolbox).

    Inside our DimensionsChanged event handler, let's add one more line of code to update the Text on this recently dropped label:

    private void DimensionsChanged(object sender, EventArgs e)
    {
     
    pnlPreview.Refresh();
      lblDimensions.Text = String.Format("{0}x{1}", NumColumns, NumRows);
    }

    We still need to add the code to actually persist settings corresponding to these controls, but I think now would be a good time to save our work and test this interactive behavior to make sure it's working just like we want it to.

    Testing the Dimension Preview

    Click Run.

    Run

    In the second instance of Visual Studio, from the DevExpress menu, select Options....

     DevExpressOptions2

    In the tree view on the left, open the Editor\Clipboard folder, and select the History options page:

    EditorClipboardHistoryOptionsTree

    This location corresponds to the Category and Page Name fields specified when we created this options page.

    Remember this step?
    NewOptionsPageFilledOut

    Now interact with our new options page. Change the Selector Color, and drag the TrackBar thumbs to change the preview grid's dimensions.

    ClipboardHistoryOptionsPage

    I like the interactive preview. It delivers a satisfying level of discoverability, and conveys the impact the TrackBars have on the dimensions of the clipboard history.

    Note that if you make changes to this page and click OK and then view the Options dialog once again, your previous changes are not preserved.

    Let's fix that right now (close down that second instance of Visual Studio if you still have it running).

    Saving and Loading Settings

    Let's start with code to save the settings. Activate the OptClipboardHistory.cs [Design] file...

    ActivateOptClipboardHistory

    Click the options page itself (so no controls are selected).

    In the Properties grid, click the Events button.

    OptClipboardHistoryClickEvents

    The OptionsPage design surface exposes a number of interesting events, in addition to the events you might expect from a form.

    Click the Categorized button to see these events grouped together.

    ClickCategorized

    In the CodeRush category, create a handler for the CommitChanges event.

     CreateHandlerForCommitChanges

    The CommitChanges event is called when the options page has been modified (or simply shown and the OptionPage's AutoModify property is set to true) and the OK or Apply button has been clicked. Like many of the DXCore events, CommitChanges comes with its own EventArgs descendant that passes useful properties and/or methods to our handler.

    In our handler, we'll write code that transfers values in the controls to storage. One of the properties available in the event arguments is called Storage. The Storage property allows you to read and write values to and from the DecoupledStorage instance associated with this options page. Storage WriteXxxxx methods expect two strings to identify the section and the key to write, and a third parameter which is the value to write. There are a wide variety of ReadXxxx and WriteXxxx methods to read and write many different types.

    So with that in mind, add the following code to the CommitChanges event handler, to transfer values from the controls on our options page out to storage:

    private void OptClipboardHistory_CommitChanges(object sender, OptionsPageStorageEventArgs ea)
    {
      ea.Storage.WriteBoolean("Settings", "PersistClipboardHistory", ckbPersistHistory.Checked);
      ea.Storage.WriteColor("Settings", "SelectorColor", clrSelector.Color);
      ea.Storage.WriteInt32("Settings", "RowCount", NumRows);
      ea.Storage.WriteInt32("Settings", "ColumnCount", NumColumns);
    }

    Next, let's add the code to read settings from DecoupledStorage back into the controls on our options page....

    Activate OptClipboardHistory.cs [Design] once more...

    ActivateOptClipboardHistory

    Add a handler for the PreparePage event...

    CreateHandlerForPreparePage

    Inside our PreparePage event handler, add code similar to the following:

    private void OptClipboardHistory_PreparePage(object sender, OptionsPageStorageEventArgs ea)
    {
      ckbPersistHistory.Checked = ea.Storage.ReadBoolean("Settings", "PersistClipboardHistory", true);
      clrSelector.Color = ea.Storage.ReadColor("Settings", "SelectorColor", Color.Red);
      NumRows = ea.Storage.ReadInt32("Settings", "RowCount", 3);
      NumColumns = ea.Storage.ReadInt32("Settings", "ColumnCount", 3);
    }

    The third parameter for each of these ReadXxxxx calls is the default value to return if the storage item is not found (which could happen the first time this plug-in is loaded).

    Testing Options Page Persistence

    Click Run.

    Run

    In the second instance of Visual Studio, from the DevExpress menu, select Options....

     DevExpressOptions2

    The Editor\Clipboard\History options page should already be selected from the last time we ran (if not, activate this page).

    Change the settings on this options page.

    ClipboardHistoryOptionsTestPersistence

    Click OK.

    Whenever you close down the options dialog, any options pages that were created get destroyed. So to test the persistence of our settings, all we need to do is simply open the options dialog again. I am noticing one problem...

    MinorProblemWithPersistence

    StillThreeByThree

    Notice that the text of the dimensions label still reads "3x3" (instead of the 2x4 I changed it to the first time). This makes sense because this text is only updated in the Scroll event for the TrackBars, and all we're doing in our PreparePage event handler is assigning a value to the NumRows and NumColumns properties, which simply pass that on to the Value property of both TrackBars. No scrolling involved, whatsoever. We'll fix that in a moment.

    There's one other important missing component to these settings. Note that even though we've changed the settings to persist clipboard history across Visual Studio sessions, and we've changed the selector color and the dimensions of the clipboard history, and even though those settings are persisted, they still have no impact on our actual UI.

    If you press Ctrl+Shift+Insert to bring up the Clipboard History, you'll still see the original 3x3 dimensions and the red selector color.

    So let's stop the debugging session and address these issues.

    First, let's extract the line of code from DimensionsChanged that sets the lblDimensions.Text property into its own method, called UpdateDimensions:

    private void UpdateDimensions()
    {
      lblDimensions.Text = String.Format("{0}x{1}", NumColumns, NumRows);
    }

    private void DimensionsChanged(object sender, EventArgs e)
    {
      pnlPreview.Refresh();
      UpdateDimensions();
    }

    Then, let's add a call to UpdateDimensions from our PreparePage event handler:

    private void OptClipboardHistory_PreparePage(object sender, OptionsPageStorageEventArgs ea)
    {
     
    ckbPersistHistory.Checked = ea.Storage.ReadBoolean("Settings", "PersistClipboardHistory", true);
     
    clrSelector.Color = ea.Storage.ReadColor("Settings", "SelectorColor", Color.Red);
     
    NumRows = ea.Storage.ReadInt32("Settings", "RowCount", 3);
     
    NumColumns = ea.Storage.ReadInt32("Settings", "ColumnCount", 3);
      UpdateDimensions();
    }

    Good. That should fix the "3x3" problem. Next, let's figure out how to get those persisted settings into something that changes the behavior of our Clipboard History plug-in.

    Reading Settings from Storage

    There are two times when we should read settings from storage in our plug-in:

    • When the plug-in first initializes.
    • When the options change.

    So let's go back to PlugIn1.cs, and inside the InitializePlugIn method, add a call to LoadSettings (which we'll implement in a moment).

    public override void InitializePlugIn()
    {
     
    base.InitializePlugIn();
      LoadSettings();
     
    ClipboardHistory.Load();
    }

    It is important that the LoadSettings call comes before the ClipboardHistory.Load call, since the LoadSettings call will determine whether we'll actually read the history of clipboard entries stored on disk (this step is performed in ClipboardHistory.Load).

    Whenever the DevExpress Options dialog is up and the OK or Apply buttons are clicked, the OptionsChanged event is fired.

    We need to listen to this event, so let's add a handler.

    Activate the PlugIn1.cs [Design] surface.

    ActivateDesignSurface2

    Click the design surface itself (so no controls are selected).

    ClickOnDesignSurface

    Switch to the Events view of the Properties grid.

    ActivateEventsTabForSurface

    Create a handler for the OptionsChanged event.

    HandleOptionsChangedEvent

    As mentioned above, the OptionsChanged event is fired whenever the OK or Apply buttons on the Options dialog are clicked. But how can we tell if the options that changed included our Clipboard History options page? Fortunately the OptionsChanged event passes an OptionsPages parameter that answers this question.

    Inside the handler we can add the following code:

    private void PlugIn1_OptionsChanged(OptionsChangedEventArgs ea)
    {
      if (ea.OptionsPages.Contains(typeof(OptClipboardHistory)))
        LoadSettings();
    }

    Now it's time to implement that LoadSettings method.

    In the previously discussed Persisting History Across Sessions section, we used the following code to create a DecoupledStorage instance:

    using (DecoupledStorage settings = CodeRush.Options.GetStorage("Editor\\Clipboard", "History")) 

    That code can still work for us now, since the storage path specified there happens to match the path to our new options page (which means both the clipboard history and our new custom settings will appear in a single file). However, since we now have an OptionsPage descendant (OptClipboardHistory), we can use this easier-to-read code instead:

    using (DecoupledStorage settings = OptClipboardHistory.Storage) 

    Both lines of code will produce a DecoupledStorage instance, and will both provide access to the same settings.

    Our LoadSettings method in PlugIn1.cs looks like this (it's somewhat similar to the code we wrote for the PreparePage handler of our OptionsPage a few steps back):

    private void LoadSettings()
    {
      using (DecoupledStorage settings = OptClipboardHistory.Storage)
      {
        ClipboardHistory.PersistHistory = settings.ReadBoolean("Settings", "PersistClipboardHistory", true);
        FrmClipHistory.SelectedBorderColor = settings.ReadColor("Settings", "SelectorColor", Color.Red);
        ClipboardHistory.RowCount = settings.ReadInt32("Settings", "RowCount", 3);
        ClipboardHistory.ColumnCount = settings.ReadInt32("Settings", "ColumnCount", 3);
      }
    }

    Everything in that code block exists with the exception of that PersistHistory field in ClipboardHistory. Let's declare it:

    public static class ClipboardHistory
    {


      public static bool PersistHistory = true;

     
    public static int RowCount = 5;
      public static int ColumnCount = 3;

    Next, let's add the code to work with our new settings. Changes to the SelectedBorderColor should work automatically -- I'm not expecting any code changes necessary there. PersistHistory is easy. We just need to add a line of code to the top of the Save and Load methods inside ClipboardHistory.cs, like this:

    public static void Save()
    {
      if (!PersistHistory)
        return;

     
    using (DecoupledStorage settings = CodeRush.Options.GetStorage("Editor\\Clipboard", "History"))
      {
       
    settings.WriteInt32("Summary", "LastIndex", LastIndex);
       
    settings.WriteInt32("Summary", "Cursor", FrmClipHistory.CursorIndex);
       
    for (int index = 0; index <= LastIndex; index++)
       
    {
         
    string indexStr = index.ToString();
         
    ClipboardHistoryEntry entry = Entries[index];
         
    settings.WriteString("Entries", "Text" + indexStr, entry.Text, true);  // string is encoded
         
    settings.WriteString("Entries", "Language" + indexStr, entry.Language);
       
    }
      }
    }

    and this:

    public static void Load()
    {
      if (!PersistHistory)
        return;
     
     
    using (DecoupledStorage settings = CodeRush.Options.GetStorage("Editor\\Clipboard", "History"))
     
    {
       
    int lastIndex = settings.ReadInt32("Summary", "LastIndex", -1);
       
    FrmClipHistory.CursorIndex = settings.ReadInt32("Summary", "Cursor", 0);
       
    if (lastIndex > LastIndex) // Don't read more entries than we can support.
         
    lastIndex = LastIndex;
       
    for (int index = 0; index <= lastIndex; index++)
       
    {
         
    string indexStr = index.ToString();
         
    string text = settings.ReadString("Entries", "Text" + indexStr, String.Empty, true);  // string is encoded
         
    string languageID = settings.ReadString("Entries", "Language" + indexStr, String.Empty);
         
    Entries[index] = new ClipboardHistoryEntry { Text = text, Language = languageID };
       
    }
      }
    }

    Changes to ClipboardHistory's RowCount and ColumnCount fields are a little more involved. It seems to me the easiest solution is to add a method that creates the Entries array at the correct size, and call that whenever we change the dimensions. So inside our LoadSettings method, let's add this line of code:

    private void LoadSettings()
    {
     
    using (DecoupledStorage settings = OptClipboardHistory.Storage)
     
    {
       
    ClipboardHistory.PersistHistory = settings.ReadBoolean("Settings", "PersistClipboardHistory", true);
       
    FrmClipHistory.SelectedBorderColor = settings.ReadColor("Settings", "SelectorColor", Color.Red);
       
    ClipboardHistory.RowCount = settings.ReadInt32("Settings", "RowCount", 3);
       
    ClipboardHistory.ColumnCount = settings.ReadInt32("Settings", "ColumnCount", 3);

        ClipboardHistory.BuildEntries();
      }
    }

    At first I was thinking BuildEntries inside ClipboardHistory.cs would be simple, like this:

    public static void BuildEntries()
    {
      Entries = = new ClipboardHistoryEntry[RowCount * ColumnCount];
      LastIndex = numEntries - 1;
    }

    Then I realized that re-dimensioning the clipboard history through this options page would result in the loss of all previous entries. So to preserve as many entries as possible, I changed BuildEntries to look like this:

    public static void BuildEntries()
    {
      int numEntries = RowCount * ColumnCount;
      ClipboardHistoryEntry[] newEntries = new ClipboardHistoryEntry[numEntries];
      int numEntriesToCopy = Math.Min(numEntries, Entries.Length);
      for (int i = 0; i < numEntriesToCopy; i++)
        newEntries[ i ] = Entries[ i ];
      Entries = newEntries;
      LastIndex = numEntries - 1;
    }

    Now, there's one more small challenge, and that's over in FrmClipHistory. At the top of that class, we have these two fields:

    static CodeView[] _CodeViews = new CodeView[ClipboardHistory.LastIndex + 1];
    static Panel[] _Borders = new Panel[ClipboardHistory.LastIndex + 1];

    These static fields worked well when the size of the clipboard history wasn't changing. But now that the size is dynamic, we will need to update these as well.

    First, let's change the CreateViews method, so it initializes these arrays correctly:

    private static void CreateViews()
    {

      _CodeViews = new CodeView[ClipboardHistory.LastIndex + 1];
      _Borders = new Panel[ClipboardHistory.LastIndex + 1];

     
    for (int i = 0; i <= ClipboardHistory.LastIndex; i++)
     
    {
       
    _CodeViews[ i ] = new CodeView();
       
    _CodeViews[ i ].Location = new Point(CursorBorder, CursorBorder);
       
    _Borders[ i ] = new Panel();
       
    _Borders[ i ].Controls.Add(_CodeViews[ i ]);
       
    _FrmClipHistory.Controls.Add(_Borders[ i ]);
     
    }
    }

    Second, for efficiency and readability, let's clean up those field declarations at the top of the form, removing their initialization, like this:

    static CodeView[] _CodeViews;
    static Panel[] _Borders;

    Cool. Let's see how this works.

    Testing Reading from Storage

    I'm excited to try this out. We're almost done with the Clipboard History, and this options page will give developers a nice level of customization.

    Click Run.

    Run

    Now, you might be thinking we need to bring up the options page and change some settings to test. But we don't really need to do that, since our options have already been stored from the last test session we ran.

    So instead of bringing up the options page, press Ctrl+Shift+Insert to bring up the Clipboard History form.

    ClipboardHistoryRespectsSettings

    Excellent! This is really cool. The clipboard history is now 2x4, and the cursor selector color is blue, just as I set it from the last time I tested.

    Let's push this a bit, and see what the extremes might look like. From the DevExpress menu, click Options....

     DevExpressOptions2

    The Editor\Clipboard\History options page should be selected. Increase the row and column dimensions to their maximum levels, change the color of the cursor, and click OK.

    Then press Ctrl+Shift+Insert once more. This time the Clipboard History should look something like this:

    UpTo64ClipboardEntries

    Wow! That's a lot of clipboard entries. Developers with large monitors might appreciate the ability to keep a history of entries that long.

    Now might be a great time to interact with the Clipboard History options page and set the Clipboard History options to be the way you want them (I prefer something substantially smaller than 8x8).

    Tomorrow, we'll add support for reverting to default settings, improve the quality of the code, and specify a custom title for the options page.

  • Persisting Clipboard History Across Sessions with our DXCore Visual Studio Plug-in

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

    So far we've:

    In today's relatively short post (don't worry - it'll be intense tomorrow), we'll show how to persist the clipboard history across Visual Studio sessions.

    Persisting History Across Sessions

    One of the things you've no doubt noticed is that when we start a new session and immediately bring up the Clipboard History, it is completely empty. Some developers might find it handy to have access to entries from the last session of Visual Studio. We'll add this as an optional behavior, and show how to use the DXCore's DecoupledStorage class to write and read data to and from the user's settings location.

    About DecoupledStorage

    The DecoupledStorage class embodies methods that make it easier to read and write settings. Storage locations can be specified with category and page name strings, or with a type reference to an options page. You can create a new DecoupledStorage instance by calling CodeRush.Options.GetStorage, or if you have a type reference to an options page, you can get the associated storage for that options page directly by calling its static GetStorage method.

    Whenever I work with persisting settings I like to start with the code that saves. Inside ClipboardHistory.cs, let's create a new method that persists the important settings:

    public static void Save()
    {
      using (DecoupledStorage settings = CodeRush.Options.GetStorage("Editor\\Clipboard", "History"))
      {
        settings.WriteInt32("Summary", "LastIndex", LastIndex);
        settings.WriteInt32("Summary", "Cursor", FrmClipHistory.CursorIndex);
        for (int index = 0; index <= LastIndex; index++)
        {
          string indexStr = index.ToString();
          ClipboardHistoryEntry entry = Entries[index];
          settings.WriteString("Entries", "Text" + indexStr, entry.Text, true);  // string is encoded
          settings.WriteString("Entries", "Language" + indexStr, entry.Language);
        }
      }
    }

    So the code above effectively saves the LastIndex (so we know how many entries are stored), and the Cursor position (so we can restore that as well in the next session), and all the entries. When the DecoupledStorage instance is disposed (at the end of the using block above), all data is flushed out to disk. For performance reasons, calls to WriteXxxx generally do not write to disk until the DecoupledStorage instance is disposed.

    IMPORTANT: There are several ways to store strings, including a dedicated WriteStrings method which is useful if you have an array of strings. The overload we're using to save the Text in the code above has an additional parameter, encoded, which allows us to specify true for HTML encoding. This option is useful for saving strings containing new line characters or other special characters.

    Next, let's add a Load method that reads the persistent settings and creates a new ClipboardHistoryEntry for each entry persisted:

    public static void Load()
    {
      using (DecoupledStorage settings = CodeRush.Options.GetStorage("Editor\\Clipboard", "History"))
      {
        int lastIndex = settings.ReadInt32("Summary", "LastIndex", -1);
        FrmClipHistory.CursorIndex = settings.ReadInt32("Summary", "Cursor", 0);
        if (lastIndex > LastIndex)
    // Don't read more entries than we can support.
         
    lastIndex = LastIndex;
        for (int index = 0; index <= lastIndex; index++)
        {
          string indexStr = index.ToString();
          string text = settings.ReadString("Entries", "Text" + indexStr, String.Empty, true);  // string is encoded
          string languageID = settings.ReadString("Entries", "Language" + indexStr, String.Empty);
          Entries[index] = new ClipboardHistoryEntry { Text = text, Language = languageID };
        }
      }
    }

    In the code above note that we're setting the static property FrmClipHistory.CursorIndex, but that property is currently read-only. Let's change that and add a setter to the CursorIndex property, inside FrmClipHistory.cs

    internal static int CursorIndex
    {
      get
     
    {
        return
    GetIndex(_CursorRow, _CursorColumn);
      }

      set
     
    {
        SetIndex(value
    );
        KeepCursorInBounds();
      }
    }
     


    That should properly restore the cursor position.

    Finally, inside PlugIn1.cs, let's add calls to our new Load and Save methods from our plug-in's InitializePlugIn and FinalizePlugIn methods, like this:

    public override void InitializePlugIn()
    {
     
    base.InitializePlugIn();
      ClipboardHistory.Load();
    }

    and this:

    public override void FinalizePlugIn()
    {

      ClipboardHistory.Save();
     
    base.FinalizePlugIn();
    }

    Let's test it.

    Testing Persistent History

    Alright, you know the drill by now.

    Run

    Click Run, and in the second instance of Visual Studio, open a source file and perform several copy operations to populate the clipboard history.

    Press Ctrl+Shift+Insert to bring up the Clipboard History form. Move the cursor to a distinct position.

    Press Escape to close the Clipboard History and then exit Visual Studio.

    Click Run one more time and in the second instance of Visual Studio press Ctrl+Shift+Insert to bring up the clipboard history. This time you should see all the entries from the previous session, and the cursor should still be selecting the previously selected entry. Excellent.

    Tomorrow we'll add a professional-quality options page to the DevExpress Options dialog so developers can customize clipboard history behavior (including changing the dimensions of the grid).

  • Adding Custom Menu Items to Visual Studio, and Mouse Support for the Clipboard History Plug-in

    Welcome to Part 6 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 an item to Visual Studio's Edit menu so developers can easily discover and access the Clipboard History, and we'll add mouse support so developers can select clipboard entries with the mouse.

    Adding a Custom Entry to the Edit Menu

    To further integrate our new feature into Visual Studio, and to improve discoverability, we should add a new menu item to Visual Studio's Edit menu. Fortunately this is pretty easy.

    Activate Plugin1.cs [Design] and then click the actClipboardHistory control.

    ActivateClipboardHistoryAction

    In the Properties grid, change the following properties:

    Property Value Comments
    ButtonText Clipboard History This is the text of the menu item.
    ParentMenu Edit Specify the name of the parent menu toolbar.
    Position 15 This menu item will show up at position 15, which falls after the existing Paste menu item. This number is achieved somewhat through trial and error, as some menus will contain menu items that are not visible under all conditions.

    Next, let's add an image to the menu item. In the Properties grid, select the Image property and then click the "..." button to the right.

    ClickImage

    The Select Resource dialog will appear.

    SelectResource

    Click the Import... button.

    Select an image to use for this menu item entry. It should be 16 pixels by 16 pixels, and for backwards compatibility with previous versions of Visual Studio, we recommend using only the original 16 Windows colors, with the addition of one more color that can be used to indicate transparency (the default transparency color is very close to lime green, 0, 254, 0). Here's the image I created for this step:

    ClipboardHistory

    That's all we need to do. Save your work and get ready to test....

    Testing the New Menu Item

    Click Run....

    Run

    In the second instance of Visual Studio, open the Edit menu...

     EditMenuItemAdded

    Nice! Now our new feature is easier to discover. Notice the shortcut binding we added to the DXCore appears automatically here next to the menu item.

    Adding Mouse Support

    Since we have just added a menu item, there's now a chance that developers may bring this form up using the mouse. And so it follows that we should add mouse support. :-)

    Here's how I'd like it to work: A single-click should select the clipboard history element, while a double-click should close the form and paste that entry into the code.

    But I also want a click and drag on each CodeView's scroll bars to still drag the scroll bars as expected. For this reason I've decided to implement mouse support on the form level rather than try to hook up individual MouseDown and MouseDoubleClick event handlers on the CodeViews and the Panel borders.

    Unfortunately, trapping mouse messages at the form level is not as easy in Windows Forms as it is in WPF. But it's not impossible. We just need to implement an IMessageFilter and listen to the mouse click & double-click windows messages.

    First, our form needs to be an implementer of IMessageFilter... 

    public partial class FrmClipHistory : Form, IMessageFilter

    Next, we need to add and remove the message filter in the ShowClipboardHistory method:

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

       
    CreateViews();
       
    PositionViews();
       
    UpdateViews();
       
    ShowCursor();
       
    PositionForm();
       
    _FrmClipHistory.ShowDialog(CodeRush.IDE);
     
    }
     
    finally
     
    {
       
    CleanUpViews();

        Application
    .RemoveMessageFilter(_FrmClipHistory);

       
    if (_FrmClipHistory != null)
         
    _FrmClipHistory.Dispose();
       
    _FrmClipHistory = null;
     
    }
      if (PasteOnClose)
        Paste();
    }

    And finally, here's the filter implementation...

    bool IMessageFilter.PreFilterMessage(ref Message m)
    {
      bool isDoubleClick;
      if (m.Msg == (int)WindowMessage.WM_LBUTTONDOWN)
        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;
    }

    Note that the WheelPanel type reference in the code above refers to the control inside the CodeView that displays the actual text. This check allows clicks to the scrollbars to get through.

    MoveCursorTo and its overload look like this: 

    private static void MoveCursorTo(int index)
    {
     
    int newRow = index / ClipboardHistory.ColumnCount;
     
    int newColumn = index % ClipboardHistory.ColumnCount;
     
    MoveCursorTo(newRow, newColumn);
    }

    private
    static void MoveCursorTo(int newRow, int newColumn)
    {
      HideCursor();
     
    _CursorRow = newRow;
     
    _CursorColumn = newColumn;
      KeepCursorInBounds();
     
    ShowCursor();
    }
     

    And that's it!

    Run & test to verify mouse interaction works as expected.

    Tomorrow we'll learn how to persist the clipboard settings across Visual Studio versions. See you then!

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

  • Adding a Cursor and Keyboard Support to the Clipboard History Plug-in

    Welcome to Part 4 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 a cursor to indicate the selected clipboard entry. We'll also add keyboard support so we can move the cursor around. By the end of today's post, we should be pasting entries from the clipboard history into the editor (or any other text field inside Visual Studio)!

    Adding a Cursor

    We need a way to show which clipboard history element is selected. I want to keep this simple, so I'm thinking about placing each CodeView inside a slightly larger Panel, and then change the BackColor of the panel that corresponds to the selected cursor position.

    First, let's declare two static fields to represent the cursor position:

    static int _CursorRow = 0;
    static int _CursorColumn = 0;

    Now that we have these fields, we can add a SetIndex method to compliment the GetIndex method that we added previously:

    private static void SetIndex(int index)
    {
      _CursorRow = index / ClipboardHistory.ColumnCount;
      _CursorColumn = index %
    ClipboardHistory
    .ColumnCount;
    }

    And we need fields to hold the size and color of the border:

    static int CursorBorder = 3;
    internal static Color SelectedBorderColor = Color.Red;

    I'm making SelectedBorderColor internal because I'm thinking we might want to eventually allow developers to set this from an options page or settings file.

    Next, an array of Panels to go with our _CodeViews array:

    static Panel[] _Borders = new Panel[ClipboardHistory.LastIndex + 1];

    The new borders will need to be created. We can do that inside a revised CreateViews method:

    private static void CreateViews()
    {
      for (int i = 0; i <= ClipboardHistory.LastIndex; i++)
      {

       
    _CodeViews[ i ] = new CodeView();
        _CodeViews[ i ].Location = new Point(CursorBorder, CursorBorder);            
        _Borders[ i ] = new Panel();
        _Borders[ i ].Controls.Add(_CodeViews[ i ]);
        _FrmClipHistory.Controls.Add(_Borders[ i ]);
      }
    }

    Notice I'm setting the Location of each CodeView in the CreateViews method since that will never change.

    Next, we need to change our PositionViews method so it works with the parenting borders we just added:

    private static void PositionViews()
    {
      if (_FrmClipHistory == null)
        return;
      Rectangle clientRect = _FrmClipHistory.ClientRectangle;
      int width = clientRect.Width / ClipboardHistory.ColumnCount;
      int height = clientRect.Height / ClipboardHistory
    .RowCount;
      Size viewSize = new Size(width - CursorBorder * 2, height - CursorBorder * 2);
      Size borderSize = new Size(width, height);

      for (int row = 0; row < ClipboardHistory.RowCount; row++)
        for (int column = 0; column < ClipboardHistory.ColumnCount; column++)
        {

          int index = GetIndex(row, column);
          CodeView thisView = _CodeViews[index];
          Panel thisBorder = _Borders[index];
          if (thisView == null || thisBorder == null)
            continue;
          thisBorder.Size = borderSize;
          thisBorder.Location = new Point(width * column, height * row);
          thisView.Size = viewSize;
        }
    }

    The CleanUpViews method will also need to clean up our _Borders array:

    private static void CleanUpViews()
    {
      for (int i = 0; i <= ClipboardHistory.LastIndex; i++)

      {
        _CodeViews[ i ] = null;
        _Borders[ i ] = null;
      }
    }

    And we'll need a few utility methods (and one property) to make it easier to work with the cursor:

    internal static int CursorIndex
    {
     
    get
     
    {
        return GetIndex(_CursorRow, _CursorColumn);
      }
     
    set
     
    {
        SetIndex(value);
        KeepCursorInBounds();
      }
    }

    private static void ShowCursor()
    {
      SetBorderColor(CursorIndex, SelectedBorderColor);
    }

    private static void HideCursor()
    {
      if (_FrmClipHistory == null)
        return;
      SetBorderColor(CursorIndex, _FrmClipHistory.BackColor);           
    }

    private static void SetBorderColor(int index, Color backgroundColor)
    {
      if (index >= 0 && index <= ClipboardHistory.LastIndex)
      {
        Panel thisBorder = _Borders[index];
        if (thisBorder != null)
          thisBorder.BackColor = backgroundColor;
      }
    }

    And finally, we'll call ShowCursor from inside our ShowClipboardHistory method:

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

        ShowCursor();

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

    We'll call HideCursor later, after we implement keyboard interaction. For now, let's run and test (remember to copy source code to the clipboard a few times before pressing Ctrl+Shift+Insert)....

    I get this:

    WithCursor

    That'll work. Next, we'll implement a keyboard interface so we can move that cursor around and ultimately insert text into the editor....

    Keyboard Interaction

    I have a pretty clear picture of how I want this to work: The arrow keys will drive the selection, and pressing Right arrow when the selected element is on the far right will simply wrap the cursor around to the far left (and vice versa) in the same row. Similar wrap-around behavior will occur in response to Up/Down arrow keys when the cursor is at the top/bottom, respectively (with the cursor staying in the same column). Pressing Enter will accept the entry, close the dialog, and insert that entry on the clipboard. And it would be nice if that text was then pasted into the edit window (or whatever text control had focus prior to bringing up this dialog). Pressing Escape will cancel and close the dialog.

    So, how do we get notified when those keys are pressed? It turns out in Windows Forms, it's not as straight forward to make this work as it is in WPF, which is where I've spent a lot of my time in the past few months. The existing WinForms key down and key pressed events remain ominously silent in spite of furious typing on the keyboard. To get a preview of keys pressed on the form, regardless of which control has focus, you need to override the ProcessCmdKey method in the form. Here we can check for the keys we are interested in (Enter, Escape, and the arrow keys), and in turn call methods to move the cursor, close the form, and paste the text.

    The ProcessCmdKey method looks like this:

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
      const int WM_KEYDOWN = 0x100;

      if (msg.Msg == WM_KEYDOWN)
      {
        switch (keyData)
        {
          case Keys.Enter:
            CloseAndPaste();
            return true;

          case Keys.Escape:
            Close();
            return true;

          case Keys.Left:
            MoveCursor(0, -1);
            return true;

          case Keys.Right:
            MoveCursor(0, 1);
            return true;

          case Keys.Up:
            MoveCursor(-1, 0);
            return true

          case Keys.Down:
            MoveCursor(1, 0);
            return true;
        }
      }
      return base.ProcessCmdKey(ref msg, keyData);
    }

    Note that returning true in each of the shortcut-trapping case blocks above prevents the control with focus from receiving the key.

    MoveCursor looks like this:

    private static void MoveCursor(int rowDelta, int columnDelta)
    {
      HideCursor();
      _CursorRow += rowDelta;
      _CursorColumn += columnDelta;
      KeepCursorInBounds();
      ShowCursor();
    }

    KeepCursorInBounds and its helper method KeepValueInBounds both ensure the _CursorRow and _CursorColumn fields hold legal values, even when the cursor position wraps from one side of the form to the another:

    private static void KeepValueInBounds(ref int value, int upperLimit)
    {
      if 
    (value < 0)
        value += upperLimit;
      if (value >= upperLimit)
        value -= upperLimit;
    }

    private static void KeepCursorInBounds()
    {
      KeepValueInBounds(ref _CursorRow, ClipboardHistory.RowCount);
      KeepValueInBounds(ref _CursorColumn, ClipboardHistory.ColumnCount);
    }

    CloseAndPaste needs to set the clipboard text to the selected item from the clipboard history, and it needs to set a new internal static field, PasteOnClose to true. We'll add code to check that field in a moment, but we don't want to do any actual pasting from here, because the form is still alive and will be active inside this method because it is being called from the ProcessCmdKey instance method.

    internal static bool PasteOnClose;

    private void CloseAndPaste()
    {
      PasteOnClose = true;
      string thisText = ClipboardHistory.GetText(CursorIndex);
      if (thisText != Clipboard.GetText() && !String.IsNullOrEmpty(thisText))
    // Need to put text on clipboard.
       
    Clipboard.SetText(thisText);
      NativeMethods.PostMessage(Handle, WindowMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    }

    Note that there is one small problem caused by the code above. The call to Clipboard.SetText is going to trigger our ClipboardChanged event handler back in our plug-in (and we'll want to ignore this one).

    Since PasteOnClose is internal, we can check that field from our ClipboardChanged handler, and if true, that means we can ignore this ClipboardChanged event. So our handler, over in PlugIn1.cs, can now look like this:

    private void PlugIn1_ClipboardChanged(ClipboardChangedEventArgs ea)
    {
      if (!FrmClipHistory.PasteOnClose)
       
    ClipboardHistory.AddEntry(ea.Details);
    }

    We need to remember to set PasteOnClose to false in our Paste method (more on this below) so we don't introduce any lingering state bugs. This will ensure the historical entries do not reorder (see the ClipboardHistory.AddEntry method) simply due to a paste from the Clipboard History form.

    Switching back to FrmClipHistory.cs, the IndexIsValid method looks like this:

    private static bool IndexIsValid(int index)
    {
      return index >= 0 && index <= ClipboardHistory.LastIndex;
    }

    And changes to ShowClipboardHistory look like this: 

    public static void ShowClipboardHistory()
    {
      if (_FrmClipHistory != null)
       
    return;
     
    _FrmClipHistory = new FrmClipHistory();

      PasteOnClose = false;

     
    try
     
    {
       
    CreateViews();
       
    PositionViews();
       
    UpdateViews();
       
    ShowCursor();
       
    _FrmClipHistory.ShowDialog(CodeRush.IDE);
     
    }
     
    finally
     
    {
       
    CleanUpViews();
       
    if (_FrmClipHistory != null)
         
    _FrmClipHistory.Dispose();
       
    _FrmClipHistory = null;
     
    }

      if (PasteOnClose)
        Paste();
    }

    Finally we come to the Paste method. This turned out to be tricky. At first I tried sending a WM_Paste message, but I found that many controls ignored this message. Next I tried sending the WM_KeyDown and WM_KeyUp messages for Ctrl+V, however, I found that some controls responded to the WM_KeyDown and others responded only to the WM_Char. Furthermore, I learned that the controls that handled WM_Char were expecting to see 22 ASCII, which is Ctrl+V, however the controls listening to WM_KeyDown were expecting to see simply the letter V (and they also check the state of the Control key at the time the message arrives). Ultimately I settled upon the following code, which appears to be the minimum needed to generate a paste with all the controls I tested with (including Visual Studio's main edit window):

    using DevExpress.CodeRush.Win32;

    private static void Paste()
    {
      PasteOnClose = false;
     
    if (CodeRush.Editor.HasFocus)
        CodeRush.Command.Execute("Edit.Paste");
      else
     
    {
        HWND
    activeControl = NativeMethods.GetFocus();
        IntPtr V = new IntPtr(NativeMethods.GetKeyValue(Keys.V));
        const int CtrlV = 22
    // ASCII for Ctrl+V.
        NativeMethods.SetKeyPressed(Keys.Control, true);
        NativeMethods.SendMessage(activeControl, WindowMessage.WM_KEYDOWN, V, IntPtr.Zero);
        NativeMethods.SendMessage(activeControl, WindowMessage.WM_CHAR, new IntPtr(CtrlV), IntPtr.Zero);
        NativeMethods.SendMessage(activeControl, WindowMessage.WM_KEYUP, V, IntPtr.Zero);
        NativeMethods.SetKeyPressed(Keys.Control, false);
      }
    }

    Note that since we're dealing with all these windows messages, we must add a namespace reference to DevExpress.CodeRush.Win32.

    Testing Keyboard Interaction

    Let's test our work. Click Run once again....

    Run

    In the second instance of Visual Studio, open some source code and copy different selections a number of times.

    Press Ctrl+Shift+Insert to bring up the Clipboard History. Select an entry with the arrow keys. Press Escape to cancel. The form should close. Repeat these steps, but press Enter instead to paste the selected entry. You should find the code correctly inserts itself into any active edit control inside Visual Studio.

    Tomorrow we'll add some neat code to calculate a good position for the form (based on what we'll be pasting into), and we'll show how we can update the clipboard history when the form is visible and the clipboard changes from another application. See you then!

1
2
LIVE CHAT

Chat is one of the many ways you can contact members of the DevExpress Team.
We are available Monday-Friday between 7:30am and 4:30pm Pacific Time.

If you need additional product information, write to us at info@devexpress.com or call us at +1 (818) 844-3383

FOLLOW US

DevExpress engineers feature-complete Presentation Controls, IDE Productivity Tools, Business Application Frameworks, and Reporting Systems for Visual Studio, along with high-performance HTML JS Mobile Frameworks for developers targeting iOS, Android and Windows Phone. Whether using WPF, ASP.NET, WinForms, HTML5 or Windows 10, DevExpress tools help you build and deliver your best in the shortest time possible.

Copyright © 1998-2017 Developer Express Inc.
All trademarks or registered trademarks are property of their respective owners