Modifying context menu

Oct 11, 2012 at 5:21 AM

Hi,

What is the best way to modify the diagram's context menu? I need to do the following:

  • remove several standard menu items because they are meaningless for my application (e.g. all layers and aggregation stuff, "shape info...", "create template"),
  • add new custom menu items ("start service", "stop service" etc),
  • change behavior of several standard menu items (e.g. I want to prevent grouping in certain cases, depending upon the types of the selected shapes).

As far as I see, all menu items are created in the Dataweb.NShape.WinFormsUI.Display.GetMenuItemDefs() method. But it's not virtual - so I cannot comment out the unnecessary "yield return Create***MenuItemDef" lines. Besides, the PerformGroupShapes() method is also not virtual - so I cannot override it to prevent grouping when it is necessary.

Well, your project is an open source one - so, of course, I can simply modify the code of those methods. But maybe it's worth modifying the standard Display component and making the context menu customizable? What do you think?

Coordinator
Oct 11, 2012 at 7:18 AM

See documentation: "Programmer Tasks > Customizing Context Menus". All options are explained there.

In your case, "To Extend Custom Context Menus with NShape Commands" is the most suitable I think. Have a look at the sample code, it shows how to pick some wanted MenuItemDefs and fill a ContextMenuStrip with it. Implement it the other way round: Skip all unwanted items and build a context menu with the rest.
All this can be done without deriving a custom display.

I would disadvise from changing the code of framework classes (although you are allowed to do so) because it will be likely that there are changes to the code in future versions.

Oct 11, 2012 at 8:31 AM

Thanks again! If I were more attentive, I would discover that documentation topic myself. Sorry :)

Feb 3, 2013 at 11:44 AM
Edited Feb 3, 2013 at 11:58 AM
Hi,
I'm back to this task again.

I've studied the "Integrate NShape Commands in your Own Context Menus" help article and tried to use the described approach in my application.
I have several notes/questions regarding this article:
  • One item is missing (between #3 and #4): "Assign the ContextMenuStrip to the presenter component (e.g. a Display) and your items will show on top of the presenter component's context menu".
  • This article demonstrates how to change the Display's context menu - because loop iterates through display1 .GetMenuItemDefs(). It does not show how to change context menu of an arbitrary [currently selected] Shape.
  • It's not clear what to do in the case when several Shapes are selected. What should we iterate through?! Is it the DiagramController.GetMenuItemDefs(IShapeCollection) method?
  • Why doesn't this article make use of the WinFormHelpers.BuildContextMenu() method? Can we use it?
Thank You.

[UPDATE] Hmm... DiagramController.GetMenuItemDefs(IShapeCollection) method is not public and currently it contains only "yield break" instruction.
Coordinator
Feb 4, 2013 at 1:23 PM
Edited Feb 4, 2013 at 1:37 PM
In order to modify the shape-specific context menu items, you can override
Shape.GetMenuItemDefs(int mouseX, int mouseY, int range)
The parameters mouseX and mouseY specify the coordinates of the mouse cursor, range defines the size of the display presenter's (== Display component) grab handles. With these information, the shape can determine whether a control point was right-clicked or not.
Example:
ControlPointId clickedPointId = FindNearestControlPoint(mouseX, mouseY, range, ControlPointCapabilities.Resize);
Concerning your questions/remarks:

Comanche wrote:
One item is missing (between #3 and #4): "Assign the ContextMenuStrip to the presenter component (e.g. a Display) and your items will show on top of the presenter component's context menu".
I'm not sure how to interpret "One item is missing"... perhaps the phrasing of the words is misleading / misunderstandable?
New try:
2. (...)
3. Create all context menu items that should extend the presenter component (e.g. a Display) as sub items of the ContextMenuStrip created in [2].
4. Give all your context menu items created in [3] a descriptive name so you can identify them within the program code.
5. (...)
If you think there is missing a piece of necessary information, I need a hint which one... ;-)


Comanche wrote:
_This article demonstrates how to change the Display's context menu - because loop iterates through display1 .GetMenuItemDefs(). It does not show how to change context menu of an arbitrary [currently selected] Shape._
Good remark. I will add this one on our ToDo list.


Comanche wrote:
_It's not clear what to do in the case when several Shapes are selected. What should we iterate through?! Is it the DiagramController.GetMenuItemDefs(IShapeCollection) method?_
In case you right-click a selection of several shapes, only the menu items that both shapes have in common are displayed.
In my opinion, you need not to worry about this case.


Comanche wrote:
Why doesn't this article make use of the WinFormHelpers.BuildContextMenu() method? Can we use it?
As far as I know, the WinFormHelpers helper class is internal - as all other helper classes.
Feb 4, 2013 at 2:50 PM
Edited Feb 5, 2013 at 4:24 AM
Thank you for the feedback.

First let me comment your answers:
In order to modify the shape-specific context menu items, you can override Shape.GetMenuItemDefs(...)
I know that, but I need to modify context menu for any shape - not only "mine". I'll explain what I need to do in the end of this post.
If you think there is missing a piece of necessary information, I need a hint which one...
Current text of the help article:
_1.Set the ShowDefaultContextMenu property to false.
2.Create a ContextMenuStrip component by dragging it from the IDE's tool box onto your form.
3.Create all context menu items that should extend the presenter component (e.g. a Display) as sub items of the ContextMenuStrip created in 2.
4.Give all your context menu items a descriptive name so you can identify them within the program code.
5.Create an event handler for ContextMenuStrip.Opening. This is where you create menu items from NShape MenuItemDefs provided by the presenter component._
Should be:
_1.Set the ShowDefaultContextMenu property to false.
2.Create a ContextMenuStrip component by dragging it from the IDE's tool box onto your form.
3.Create all context menu items that should extend the presenter component (e.g. a Display) as sub items of the ContextMenuStrip created in 2.
3a. Assign the ContextMenuStrip to the presenter component (e.g. a Display) and your items will show on top of the presenter component's context menu.
4.Give all your context menu items a descriptive name so you can identify them within the program code.
5.Create an event handler for ContextMenuStrip.Opening. This is where you create menu items from NShape MenuItemDefs provided by the presenter component._
(item which I numbered as #3a should stand between #3 and #4 - this is what I meant)
Good remark. I will add this one on our ToDo list.
As for now - could You please give some explanations right here? (for one selected shape it's easy, but for several - not). But before please read the remaining of this post.
In case you right-click a selection of several shapes, only the menu items that both shapes have in common are displayed. In my opinion, you need not to worry about this case.
The point is that I MUST worry about this case :) Please see the remaining part of this post.

Second, let me tell what I'm trying to achieve:

(1) When selection contains ONE shape:

(1.1) For ANY shape (i.e. both for "mine" and for stock ones) - remove menu items related to layers, templates and selection, and leave only one "Delete" item which will be "smart" (will delete shape alone when there's no model and will delete "with model" when model exists),
(1.2) For ANY shape - add menu items related to selection "a la Visual Studio",
(1.3) For "my" shape (of any type) - remove menu items related to grouping/aggregation and cut/copy/paste,
(1.4) For "my" shape (of any type) - add menu items common for any type of "my" shapes,
(1.5) For "my" shape - add menu items specific for this shape type.

(2) When selection contains SEVERAL shapes:

(2.1) remove menu items related to layers, templates and selection, and leave only one "Delete" item,
(2.2) add menu items related to selection "a la Visual Studio",
(2.3) if selection contains at least one of "my" shapes - remove menu items related to grouping/aggregation and cut/copy/paste.

As You see, only items (1.3) - (1.5) can be done by overriding the Shape.GetMenuItemDefs() method. Items (1.1) and (1.2) can be possibly done using the approach described in the help article (but with display.Diagram.SelectedShape.GetMenuItemDefs instead of display.GetMenuItemDefs). But I have no idea how to implement the remaining items (2.1) - (2.3)
Coordinator
Feb 5, 2013 at 1:20 PM
Edited Apr 16, 2014 at 11:09 AM
First of all, I must correct my statement "In case you right-click a selection of several shapes, only the menu items that both shapes have in common are displayed." :
In an early development version, this was the case but in the current release version, MenuItemDefs are collected from the following components in this order:
  • from Display.CurrentTool.GetMenuItemDefs()
    If exactly one shape is selected:
    • from (selected) Shape.Template.GetMenuItemDefs()
    • from (selected) Shape.GetMenuItemDefs()
    • from (selected) Shape.ModelObject.GetMenuItemDefs()
  • from Display.GetMenuItemDefs()
As far as I know, you have a written a custom Tool - that's a very good place to modify the set of MenuItemDefs returned by the selected shapes.
An other good place to remove / add MenuItemDefs is to handle the Opening and Closing events of Display.ContextMenuStrip. You can use the Name property of the MenuItemDefs. The MenuItemDefs are stored in the ToolStripMenuItem.Tag property.
I admit this is not very nifty but it works and it is the only way to identify the MenuItemDefs at the moment.
Example:
private void ContextMenuStrip_Opening(object sender, CancelEventArgs e) {
    System.Windows.Forms.ContextMenuStrip ctxMenu = sender as System.Windows.Forms.ContextMenuStrip;
    if (ctxMenu != null) {
        for (int i = ctxMenu.Items.Count - 1; i >= 0; --i) {
            // Filter all MenuItems that modify layer assignment
            System.Windows.Forms.ToolStripItem item = ctxMenu.Items[i];
            string itemName = ((MenuItemDef)item.Tag).Name.ToLowerInvariant();
            if (item.Tag is MenuItemDef && !string.IsNullOrEmpty(itemName)) {
                if (itemName.Contains("layer")) {
                    // Remove item
                    ctxMenu.Items.RemoveAt(i);
                    // Avoid two subsequent seperators 
                    if (i > 0 && ctxMenu.Items[i] is System.Windows.Forms.ToolStripSeparator
                        && ctxMenu.Items[i - 1] is System.Windows.Forms.ToolStripSeparator)
                        ctxMenu.Items.RemoveAt(i);
                }
            }
        }
    }
}
I hope this helps more than the help article.
Feb 5, 2013 at 1:40 PM
Do I understand correctly that no matter what is selected - one shape or several shapes - ctxMenu.Items (in Your code) contains all MenuItemDefs collected by NShape "core"?
Coordinator
Feb 5, 2013 at 3:00 PM
Yes.
Feb 5, 2013 at 3:03 PM
Great!
Thank You very much!
Coordinator
Feb 5, 2013 at 3:13 PM
[Addendum to the post above]
But the number of items collected by the display is dependent of what is selected:
If exactly one shape is selected, the MenuItemDefs returned by the selected shape are added to the collected MenuItemDefs.

Example:
  • Place a PolyLine and an Ellipse on the display.
  • Select the polyline and right-click somewhere on the line.
    The context menu will contain shape-specific items like "Create Template", "Insert Vertex", etc.
  • Now select both shapes and right-click the polyline again.
    The context menu will not contain shape speciofic items any longer.
  • Select the Ellipse and right-click it
    The context menu will now contain the ellipse-specific menu items (only "Create Template")
Feb 5, 2013 at 3:26 PM
Yes, I understand this.
From my point of view, the most important conclusion is that I will be able to implement items (2.1) - (2.3)
Right? Will I?
Coordinator
Feb 7, 2013 at 7:33 AM
Sure.
The code sample is equal to the first part of (2.1): It removes all layer-specific menu items.
Feb 7, 2013 at 8:01 AM
Thank You!
With Your kind assistance I managed to implement a cool tuning of the stock ContextMenuStrip - now it contains only those items which are relevant both to selected shape(s) and to peculiarities (mainly of "restrictive" kind) of my application. That's great.

However, I want to ask You make one improvement in NShape framework: please add an ability to determine which MenuItemDefs are relevant to the given selection (i.e. to the given IShapeCollection). Provided this feature, we'll be able - instead of taking an already built stock menu and filtering out unwanted items - build a custom menu from scratch from a set of MenuItemDefs. In other words, I mean a method looking like this:
IEnumerable<MenuItemDef> GetContextMenuItems(IShapeCollection selectedShapes);
This method will encapsulate algorithm of "collecting" MenuItemDefs from different sources. As far as this algorithm, generally speaking, can be changed in future versions of NShape (who knows?), it will be good that developers will not need to re-implement it in their applications. Yes, you described this algorithm several posts above - but I do not want to use it in my application because of the risk of its future changes; that's why I had to filter out items from stock menu which is already built with that algorithm.
Apr 8, 2013 at 3:17 PM
Have You discussed the above mentioned idea (method GetContextMenuItems)?
Coordinator
Apr 11, 2013 at 7:14 AM
Not yet. But I don't think we will do any major changes to the framework in the near future.
May 15, 2013 at 7:32 AM
Any updates?
Coordinator
May 21, 2013 at 1:59 PM
The method that encapsulates the algorithm for collecting the MenuItemDefs is called Display.GetMenuItemDefs and is public. Doesn't it satisfy your needs?
Jun 10, 2013 at 4:42 PM
ppohmann wrote:
The method that encapsulates the algorithm for collecting the MenuItemDefs is called Display.GetMenuItemDefs and is public. Doesn't it satisfy your needs?
Not exactly - because it does not take security into account. And method WinFormHelpers.BuildContextMenu (the only current consumer of the Display.GetMenuItemDefs method) - does: it will skip actions which are not allowed. Unfortunately we cannot access this skipping logic because WinFormHelpers class is internal. Probably we can repeat (copy/paste) this logic - but it's not very nice :(

It's OK if You delay this wish until further releases. I have a satisfactory workaround for now - I'm just taking an already built stock menu and filtering out unwanted items. Not very obvious (as any solution of such "reversive" kind) - but the amount of code is approximately the same and... I've added lots of comments to this code :)