Skip to content
This repository has been archived by the owner on Sep 16, 2019. It is now read-only.

How to: Add a command

Daniel Walder edited this page Aug 16, 2017 · 2 revisions

This is a short walkthrough of how to add a new command. The command in this tutorial will be the "Transform..." command of the BSP editor. This command prompts for user input before modifying the selection, so it is a good general example of a modification command.

Create the command class

Required reading: Commands

First, we should create an empty class for the command. I've pre-implemented some simple stuff in the command already:

[AutoTranslate]
[Export(typeof(ICommand))]
[MenuItem("Tools", "", "Transform", "D")]
[CommandID("BspEditor:Tools:Transform")]
[MenuImage(typeof(Resources), nameof(Resources.Menu_Transform))]
[DefaultHotkey("Ctrl+M")]
public class Transform : BaseCommand
{
    public override string Name { get; set; } = "Transform";
    public override string Details { get; set; } = "Transform the current selection";

    protected override bool IsInContext(IContext context, MapDocument document)
    {
        // The selection must be non-empty
        return base.IsInContext(context, document) && !document.Selection.IsEmpty;
    }

    protected override async Task Invoke(MapDocument document, CommandParameters parameters)
    {
        var objects = document.Selection.GetSelectedParents().ToList();

        throw new NotImplementedException(); // todo
    }
}

Menu setup (optional)

Required reading: Menu sections and groups

In the code above I'm using MenuItem and MenuImage tags to put the command into the menu. There's some additional setup when using these tags.

When adding a menu item, you should check if the menu section and group are declared so you can give them appropriate order hints. This is optional, but strongly encouraged. To do this, export an IMenuMetadataProvider class if you haven't got one already, and declare the menu structures in that class. In this example, there's already an implementation, so I will add a new line to declare the "Transform" menu group in MenuDataProvider.cs:

[Export(typeof(IMenuMetadataProvider))]
public class MenuDataProvider : IMenuMetadataProvider
{
    // ...

    public IEnumerable<MenuGroup> GetMenuGroups()
    {
        // ...
        yield return new MenuGroup("Tools", "", "Transform", "H"); // <- added
    }
}

The menu image is again optional, but encouraged. To add the image to your assembly, create an icon PNG and put it in your project. Then include the image in your assembly's resources file.

Translations setup (optional)

Required reading: Supporting translations

It's good practice to add the translation strings for your command. The command already has the [AutoTranslate] tag, so it's a simple matter of adding the keys to your assembly's translations file. In this case, this is added into Sledge.BspEditor.Editing.en.json:

{
    "Commands": {
        "Transform": {
            "Name": "Transform...",
            "Details": "Transform the current selection."
        }
}

You'll have noticed that the original class has some default values which look very similar to this. These values are what are used if the translation keys are not set up, or if translation fails for some reason. It's a good idea to give them defaults just in case, but it's not required if you're using translations.

Finish implementation

The only missing part in our class is the implementation in the Invoke method. The transform command must do the following:

  • Get the selected objects to transform
  • Prompt the user for transform parameters
  • If prompt isn't cancelled, transform the objects and commit the changes

The transform dialog is a fairly simple WinForms implementation, I won't go into details here. It does have translations which requires a bit of effort to set up, but that's for another article. The important thing to know is that small dialogs are often not instantiated in the MEF tree because you don't want hidden dialogs floating around for the lifetime of your application, so you need to manually translate them.

To do this, the dialog must implement IManualTranslate (but it shouldn't export it). Then import a ITranslationStringProvider into your command class and call Translate on your dialog before showing it:

public class Transform : BaseCommand
{
    [Import] private Lazy<ITranslationStringProvider> _translator;

    // ...

    protected override async Task Invoke(MapDocument document, CommandParameters parameters)
    {
        var objects = document.Selection.GetSelectedParents().ToList();
        var box = document.Selection.GetSelectionBoundingBox();

        using (var dialog = new TransformDialog(box))
        {
            _translator.Value.Translate(dialog);
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                // Do something 
            }
        }
    }
}

Now the only missing part is the section marked with // Do something. This is where the actual modifications to the map are constructed and committed. The code looks like this:

var transform = dialog.GetTransformation(box);
var op = new Modification.Operations.Mutation.Transform(transform, objects);
await MapDocumentOperation.Perform(document, op);

And that's all! The final class might have some extra error handling and such, but that's how a custom command is made.