-
Notifications
You must be signed in to change notification settings - Fork 84
How to: Add a command
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.
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
}
}
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.
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.
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.