Jonathon Reinhart
Jonathon Reinhart

Reputation: 137398

Visual Studio-style undo drop-down button - custom ToolStripSplitButton

I'm looking to implement a Visual Studio-style undo drop-down button:

Undo drop-down

I've looked all over the internet, and can't seem to find any real implementations of this.

I've started by deriving from ToolStripSplitButton, but don't really know where to go from there. Its DropDown property is a ToolStripDropDown, but that doesn't seem to have anything regarding multiple items being selected, much less scrolling, and the text at the bottom.

So instead of the default ToolStripDropDown, I'm thinking maybe the whole drop down part should be a custom control, based on a combobox. The question then, is how to cause the right-side (drop down arrow) button to do something other than show its default drop down?

Am I on the right track here? Thanks!

Upvotes: 3

Views: 4543

Answers (4)

Pavel Korsukov
Pavel Korsukov

Reputation: 959

Vs 2010 is a WPF application. If you are in the beginning of this application development than use WPF as a core technology. WPF drop down button is implemented in WPF ribbon. Source code is available on CodePlex.

Upvotes: 0

Josh Stribling
Josh Stribling

Reputation: 1078

Extra thanks to LarsTech! (I didn't know about ToolStripControlHost a few hours ago)

Here is my implementation, which is really close to the VS drop down...

UndoRedoDropDown

You should be able to just drop this delegate & function into your Form:

    public delegate void UndoRedoCallback(int count);

    private void DrawDropDown(ToolStripSplitButton button, string action, IEnumerable<string> commands, UndoRedoCallback callback)
    {
        int width = 277;
        int listHeight = 181;
        int textHeight = 29;

        Panel panel = new Panel()
        {
            Size = new Size(width, textHeight + listHeight),
            Padding = new Padding(0),
            Margin = new Padding(0),
            BorderStyle = BorderStyle.FixedSingle,
        };
        Label label = new Label()
        {
            Size = new Size(width, textHeight),
            Location = new Point(1, listHeight - 2),
            TextAlign = ContentAlignment.MiddleCenter,
            Text = String.Format("{0} 1 Action", action),
            Padding = new Padding(0),
            Margin = new Padding(0),
        };
        ListBox list = new ListBox()
        {
            Size = new Size(width, listHeight),
            Location = new Point(1,1),
            SelectionMode = SelectionMode.MultiSimple,
            ScrollAlwaysVisible = true,
            Padding = new Padding(0),
            Margin = new Padding(0),
            BorderStyle = BorderStyle.None,
            Font = new Font(panel.Font.FontFamily, 9),
        };
        foreach (var item in commands) { list.Items.Add(item); }
        if (list.Items.Count == 0) return;
        list.SelectedIndex = 0;

        ToolStripControlHost toolHost = new ToolStripControlHost(panel)
        {
            Size = panel.Size,
            Margin = new Padding(0),
        };
        ToolStripDropDown toolDrop = new ToolStripDropDown()
        {
            Padding = new Padding(0),
        };
        toolDrop.Items.Add(toolHost);

        panel.Controls.Add(list);
        panel.Controls.Add(label);
        toolDrop.Show(this, new Point(button.Bounds.Left + button.Owner.Left, button.Bounds.Bottom + button.Owner.Top));

        // *Note: These will be "up values" that will exist beyond the scope of this function
        int index = 1;
        int lastIndex = 1;

        list.Click += (sender, e) => { toolDrop.Close(); callback(index); };
        list.MouseMove += (sender, e) =>
        {
            index = Math.Max(1, list.IndexFromPoint(e.Location) + 1);
            if (lastIndex != index)
            {
                int topIndex = Math.Max(0, Math.Min(list.TopIndex + e.Delta, list.Items.Count - 1));
                list.BeginUpdate();
                list.ClearSelected();
                for (int i = 0; i < index; ++i) { list.SelectedIndex = i; }
                label.Text = String.Format("{0} {1} Action{2}", action, index, index == 1 ? "" : "s");
                lastIndex = index;
                list.EndUpdate();
                list.TopIndex = topIndex;
            }
        };
        list.Focus();
    }

You can set it up and test like this, assuming you have a blank form (Form1) with a toolStrip that has 1 ToolStripSplitButton (toolStripSplitButton1) added:

    public Form1()
    {
        InitializeComponent();

        // Call DrawDropDown with:
        //   The clicked ToolStripSplitButton
        //   "Undo" as the action
        //   TestDropDown for the enumerable string source for the list box
        //   UndoCommands for the click callback
        toolStripSplitButton1.DropDownOpening += (sender, e) => { DrawDropDown(
            toolStripSplitButton1,
            "Undo",
            TestDropDown,
            UndoCommands
        ); };
    }


    private IEnumerable<string> TestDropDown
    {
        // Provides a list of strings for testing the drop down
        get { for (int i = 1; i < 1000; ++i) { yield return "test " + i; } }
    }

    private void UndoCommands(int count)
    {
        // Do something with the count when an action is clicked
        Console.WriteLine("Undo: {0}", count);
    }

Here is a better example using the Undo/Redo system from: http://www.codeproject.com/KB/cs/AutomatingUndoRedo.aspx

    public Form1()
    {
        InitializeComponent();

        // Call DrawDropDown with:
        //   The Undo ToolStripSplitButton button on the Standard tool strip
        //   "Undo" as the action name
        //   The list of UndoCommands from the UndoRedoManager
        //   The Undo method of the UndoRedoManager
        m_TSSB_Standard_Undo.DropDownOpening += (sender, e) => { DrawDropDown(
            m_TSSB_Standard_Undo,
            "Undo",
            UndoRedoManager.UndoCommands,
            UndoRedoManager.Undo
        ); };
    }

*Note: I did modify the Undo & Redo methods in the UndoRedoManager to accept a count:

    // Based on code by Siarhei Arkhipenka (Sergey Arhipenko) (http://www.codeproject.com/KB/cs/AutomatingUndoRedo.aspx)
    public static void Undo(int count)
    {
        AssertNoCommand();
        if (CanUndo == false) return;
        for (int i = 0; (i < count) && CanUndo; ++i)
        {
            Command command = history[currentPosition--];
            foreach (IUndoRedo member in command.Keys)
            {
                member.OnUndo(command[member]);
            }
        }
        OnCommandDone(CommandDoneType.Undo);
    }

Upvotes: 4

LarsTech
LarsTech

Reputation: 81610

Yes, I think you're on the right track. And in this case, ToolStripControlHost is your friend.

You don't necessarily need to derive from it (unless you are making your own control), but try just subscribing to the ToolStripSplitButton's DropDownOpening event:

Working example:

private ListBox listBox1;

public Form1()
{
  InitializeComponent();

  listBox1 = new ListBox();
  listBox1.IntegralHeight = false;
  listBox1.MinimumSize = new Size(120, 120);  \\ <- important
  listBox1.Items.Add("Item 1");
  listBox1.Items.Add("Item 2");
}

private void toolStripSplitButton1_DropDownOpening(object sender, EventArgs e) {
  ToolStripControlHost toolHost = new ToolStripControlHost(listBox1);
  toolHost.Size = new Size(120, 120);
  toolHost.Margin = new Padding(0);
  ToolStripDropDown toolDrop = new ToolStripDropDown();
  toolDrop.Padding = new Padding(0);
  toolDrop.Items.Add(toolHost);
  toolDrop.Show(this, new Point(toolStripSplitButton1.Bounds.Left,
                                toolStripSplitButton1.Bounds.Bottom));
}

Here is the result:

enter image description here

For your application, you would need to replace the ListBox with your own UserControl, so you can contain whatever your want in it. The ToolStripControlHost can only hold one control, and it's important to set the MinimumSize property, or else the dropped control isn't sized correctly.

Upvotes: 4

Zarat
Zarat

Reputation: 2754

I'd suggest implementing the popup separately from the toolbar button. Popups are separate windows with a topmost-flag which auto-close when losing focus or pressing escape. If you code your own popup window that frees you from having to fit your behaviour to a preexisting model (which is going to be hard in your case). Just make a new topmost window with a listbox and status bar, then you are free to implement the selection behavior on the listbox like you need it.

Upvotes: 0

Related Questions