Argument suggestions with tooltips

A /warp command with suggestions of various locations. Hovering over the suggestions with the mouse cursor displays tooltips describing what the locations are

The CommandAPI can also display tooltips for specific argument suggestions. These are shown to the user when they hover over a given suggestion and can be used to provide more context to a user about the suggestions that are shown to them. In this section, we'll outline the two ways of creating suggestions with tooltips:

  • Normal (String) suggestions with tooltips
  • Safely typed suggestions with tooltips

Tooltips can have formatting to change how the text is displayed by using the ChatColor class.


Tooltips with normal (String-based) suggestions

To use these features, the CommandAPI includes the stringsWithTooltips methods for arguments, that accept IStringTooltip objects instead of String objects:

ArgumentSuggestions stringsWithTooltips(IStringTooltip... suggestions);
ArgumentSuggestions stringsWithTooltips(Function<SuggestionInfo, IStringTooltip[]> suggestions);

The StringTooltip class is the CommandAPI's default implementation of IStringTooltip, which has some static methods to construct tooltips easily:

StringTooltip none(String suggestion);
StringTooltip ofString(String suggestion, String tooltip);
StringTooltip ofMessage(String suggestion, Message tooltip);
StringTooltip ofBaseComponents(String suggestion, BaseComponent... tooltip);
StringTooltip ofAdventureComponent(String suggestion, Component tooltip);

The first method, StringTooltip.none(String) creates a normal suggestion entry with no tooltip. The other methods create a suggestion with the provided tooltip text in either String, Brigadier Message, Spigot BaseComponent[] or Adventure Component format.

Example - An emotes command with string suggestion tooltips

Say we want to create a simple command to provide in-game emotes between players. For example, if you did /emote wave Bob, you'll "wave" to the player Bob. For this example, we'll use the following command syntax:

/emote <emote> <target>

First, we'll declare our arguments. Here, we'll use the stringsWithTooltips method, along with the StringTooltip.ofString(String, String) method to create emote suggestions and include suitable descriptions:

List<Argument<?>> arguments = new ArrayList<>();
arguments.add(new StringArgument("emote")
    .replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(info ->
        new IStringTooltip[] {
            StringTooltip.ofString("wave", "Waves at a player"),
            StringTooltip.ofString("hug", "Gives a player a hug"),
            StringTooltip.ofString("glare", "Gives a player the death glare")
        }
    ))
);
arguments.add(new PlayerArgument("target"));
val arguments = mutableListOf<Argument<*>>()
arguments.add(StringArgument("emote")
    .replaceSuggestions(ArgumentSuggestions.stringsWithTooltips { info ->
        arrayOf<IStringTooltip>(
            StringTooltip.ofString("wave", "Waves at a player"),
            StringTooltip.ofString("hug", "Gives a player a hug"),
            StringTooltip.ofString("glare", "Gives a player the death glare")
        )
    })
)
arguments.add(PlayerArgument("target"))

Finally, we declare our command as normal:

new CommandAPICommand("emote")
    .withArguments(arguments)
    .executesPlayer((player, args) -> {
        String emote = (String) args[0];
        Player target = (Player) args[1];
        
        switch(emote) {
        case "wave":
            target.sendMessage(player.getName() + " waves at you!");
            break;
        case "hug":
            target.sendMessage(player.getName() + " hugs you!");
            break;
        case "glare":
            target.sendMessage(player.getName() + " gives you the death glare...");
            break;
        }
    })
    .register();
CommandAPICommand("emote")
    .withArguments(*arguments.toTypedArray())
    .executesPlayer(PlayerCommandExecutor { player, args ->
        val emote = args[0] as String
        val target = args[1] as Player

        when (emote) {
            "wave" -> target.sendMessage("${player.name} waves at you!")
            "hug" -> target.sendMessage("${player.name} hugs you!")
            "glare" -> target.sendMessage("${player.name} gives you the death glare...")
        }
    })
    .register()

Using IStringTooltip directly

The IStringTooltip interface can be implemented by any other class to provide tooltips for custom objects. The IStringTooltip interface has the following methods:

public interface IStringTooltip {
    public String getSuggestion();
    public Message getTooltip();
}

Note that the Message class is from the Brigadier library, which you will have to add as a dependency to your plugin. Information on how to do that can be found here.

This is incredibly useful if you are using suggestions with custom objects, such as a plugin that has custom items.

Example - Using IStringTooltip for custom items

Let's say we've created a simple plugin which has custom items. For a custom item, we'll have a simple class CustomItem that sets its name, lore and attached itemstack:

public @SuppressWarnings("deprecation")
class CustomItem implements IStringTooltip {

    private ItemStack itemstack;
    private String name;
    
    public CustomItem(ItemStack itemstack, String name, String lore) {
        ItemMeta meta = itemstack.getItemMeta();
        meta.setDisplayName(name);
        meta.setLore(Arrays.asList(lore));
        itemstack.setItemMeta(meta);
        this.itemstack = itemstack;
        this.name = name;
    }
    
    public String getName() {
        return this.name;
    }
    
    public ItemStack getItem() {
        return this.itemstack;
    }
    
    @Override
    public String getSuggestion() {
        return this.itemstack.getItemMeta().getDisplayName();
    }

    @Override
    public Message getTooltip() {
        return Tooltip.messageFromString(this.itemstack.getItemMeta().getLore().get(0));
    }
    
}
class CustomItem(val item: ItemStack, val name: String, lore: String): IStringTooltip {

    init {
        val meta = item.itemMeta
        meta.setDisplayName(name)
        meta.setLore(listOf(lore))
        item.setItemMeta(meta)
    }

    override fun getSuggestion(): String = this.item.itemMeta.displayName

    override fun getTooltip(): Message = Tooltip.messageFromString(this.item.itemMeta.lore?.get(0) ?: "")

}

We make use of the Tooltip.messageFromString() method to generate a Brigadier Message object from our string tooltip.

Let's also say that our plugin has registered lots of CustomItems and has this stored in a CustomItem[] in our plugin. We could then use this as our input for suggestions:

CustomItem[] customItems = new CustomItem[] {
    new CustomItem(new ItemStack(Material.DIAMOND_SWORD), "God sword", "A sword from the heavens"),
    new CustomItem(new ItemStack(Material.PUMPKIN_PIE), "Sweet pie", "Just like grandma used to make")
};
    
new CommandAPICommand("giveitem")
    .withArguments(new StringArgument("item").replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(customItems))) // We use customItems[] as the input for our suggestions with tooltips
    .executesPlayer((player, args) -> {
        String itemName = (String) args[0];
        
        // Give them the item
        for (CustomItem item : customItems) {
            if (item.getName().equals(itemName)) {
                player.getInventory().addItem(item.getItem());
                break;
            }
        }
    })
    .register();
val customItems = arrayOf<CustomItem>(
    CustomItem(ItemStack(Material.DIAMOND_SWORD), "God sword", "A sword from the heavens"),
    CustomItem(ItemStack(Material.PUMPKIN_PIE), "Sweet pie", "Just like grandma used to make")
)

CommandAPICommand("giveitem")
    .withArguments(StringArgument("item").replaceSuggestions(ArgumentSuggestions.stringsWithTooltips(*customItems))) // We use customItems[] as the input for our suggestions with tooltips
    .executesPlayer(PlayerCommandExecutor { player, args ->
        val itemName = args[0] as String

        // Give them the item
        for (item in customItems) {
            if (item.name == itemName) {
                player.inventory.addItem(item.item)
                break
            }
        }
    })
    .register()

Tooltips with safe suggestions

Using tooltips with safe suggestions is almost identical to the method described above for normal suggestions, except for two things. Firstly, you must use tooltips method instead of the stringsWithTooltips method and secondly, instead of using StringTooltip, you must use Tooltip<S>. Let's look at these differences in more detail.

The tooltips methods are fairly similar to the stringsWithTooltips methods, except instead of using StringTooltip, it simply uses Tooltip<S>:

SafeSuggestions<T> tooltips(Tooltip<T>... suggestions);
SafeSuggestions<T> tooltips(Function<SuggestionInfo, Tooltip<T>[]> suggestions);

The Tooltip<S> class represents a tooltip for a given object S. For example, a tooltip for a LocationArgument would be a Tooltip<Location> and a tooltip for an EnchantmentArgument would be a Tooltip<Enchantment>.

Just like the StringTooltip class, the Tooltip<S> class provides the following static methods, which operate exactly the same as the ones in the StringTooltip class:

Tooltip<S> none(S object);
Tooltip<S> ofString(S object, String tooltip);
Tooltip<S> ofMessage(S object, Message tooltip);
Tooltip<S> ofBaseComponents(S object, BaseComponent... tooltip);
Tooltip<S> ofAdventureComponent(S object, Component tooltip);

Tooltip<S>[] arrayOf(Tooltip<S>... tooltips);

The use of arrayOf is heavily recommended as it provides the necessary type safety for Java code to ensure that the correct types are being passed to the tooltips method.

Example - Teleportation command with suggestion descriptions

Say we wanted to create a custom teleport command which suggestions a few key locations. In this example, we'll use the following command syntax:

/warp <location>

First, we'll declare our arguments. Here, we use a LocationArgument and use the tooltips method, with a parameter for the command sender, so we can get information about the world. We populate the suggestions with tooltips using Tooltip.ofString(Location, String) and collate them together with Tooltip.arrayOf(Tooltip<Location>...):

List<Argument<?>> arguments = new ArrayList<>();
arguments.add(new LocationArgument("location")
    .replaceSafeSuggestions(SafeSuggestions.tooltips(info -> {
        // We know the sender is a player if we use .executesPlayer()
        Player player = (Player) info.sender();
        return Tooltip.arrayOf(
            Tooltip.ofString(player.getWorld().getSpawnLocation(), "World spawn"),
            Tooltip.ofString(player.getBedSpawnLocation(), "Your bed"),
            Tooltip.ofString(player.getTargetBlockExact(256).getLocation(), "Target block")
        );
    })));
val arguments = listOf<Argument<*>>(
    LocationArgument("location")
        .replaceSafeSuggestions(SafeSuggestions.tooltips( { info ->
            // We know the sender is a player if we use .executesPlayer()
            val player = info.sender() as Player
            Tooltip.arrayOf(
                Tooltip.ofString(player.world.spawnLocation, "World spawn"),
                Tooltip.ofString(player.bedSpawnLocation, "Your bed"),
                Tooltip.ofString(player.getTargetBlockExact(256)?.location, "Target block")
            )
        }))
)

In the arguments declaration, we've casted the command sender to a player. To ensure that the command sender is definitely a player, we'll use the executesPlayer command execution method in our command declaration:

new CommandAPICommand("warp")
    .withArguments(arguments)
    .executesPlayer((player, args) -> {
        player.teleport((Location) args[0]);
    })
    .register();
CommandAPICommand("warp")
    .withArguments(arguments)
    .executesPlayer(PlayerCommandExecutor { player, args ->
        player.teleport(args[0] as Location)
    })
    .register()