Dialogs
Paper's dialogs are the pop-up screens with text, inputs and buttons. They're great for forms, confirmations and info screens, but the raw API is a pile of builders. SporeAPI wraps it so a dialog is just a few lines:
player.openDialog {
title("Rename your pet")
message("Pick a new name for <white>${pet.name}</white>.")
val name = textField("Name") {
initial = pet.name
maxLength = 16
}
val glowing = checkbox("Glowing")
button("Save") { click ->
pet.rename(click[name])
pet.isGlowing = click[glowing]
}
}
All text is MiniMessage, with the same tags as .mm(). Every input gives you back a handle, so click[name] is a String and click[glowing] is a Boolean - no keys to keep track of.
Reusable dialogs
For a dialog you open from more than one place, make it a class or an object. build runs every time it opens, so the content can depend on the player:
object SettingsDialog : SporeDialog() {
override fun DialogBuilder.build(player: Player) {
val settings = Settings.of(player)
title("Settings")
val volume = slider("Volume", 0..100) { initial = settings.volume }
val mode = choice("Mode") {
option("easy", "<green>Easy", selected = settings.mode == "easy")
option("hard", "<red>Hard", selected = settings.mode == "hard")
}
button("Save") { click ->
settings.volume = click[volume]
settings.mode = click[mode]
}
}
override fun onClose(player: Player, reason: DialogCloseReason) {
// saved, cancelled, replaced, left...
}
}
SettingsDialog.open(player)
val shop = dialog { player -> ... } does the same without a class.
Inputs
| Input | Gives you | Options |
|---|---|---|
textField("Name") | String | initial, maxLength, width, showLabel, multiline(maxLines, height) |
checkbox("Glowing") | Boolean | initial |
slider("Volume", 0..100) | Int | initial, step, width, format |
slider("Speed", 0.5f..2f) | Float | the same |
choice("Mode") { option(...) } | the chosen option's id | width, showLabel |
There's also message("...") for text and item(itemStack, description = "...") for an item in the body.
Buttons
button("Save") { click -> }runs your code on the server.commandButton("Spawn", "/spawn")runs a command as the player.linkButton("Website", "https://...")opens a link. The server isn't told about this one.exitButton("Cancel") { }is the footer button, and it's also what Escape does. Dialogs with buttons get a "Close" one by default;noExitButton()removes it, but then closing with Escape can't be noticed.confirm(yes = "Delete", no = "Keep") { click -> }makes a yes/no dialog instead of a button list.- A dialog without any buttons is a notice with a single OK button. Change it with
okButton("Got it") { }.
columns sets how many buttons fit on a row (2 by default).
Closing
A dialog closes when a button is clicked. Inside a handler you can also send the player somewhere else:
button("Save") { click ->
if (click[name].isBlank()) {
click.player.sendMessage("You need a name!")
click.reopen()
return@button
}
click.open(ConfirmRenameDialog)
}
Set afterClick = AfterClick.KEEP_OPEN to leave it open after a click, or AfterClick.WAIT_FOR_RESPONSE to show a waiting screen while you check something. It closes on its own if your handler doesn't open or reopen anything.
From anywhere else, use SporeDialogs.close(player), SporeDialogs.isOpen(player) and SporeDialogs.current(player).
onClose { reason -> }, or overriding onClose in a class, tells you why it closed: BUTTON, EXIT (the exit button, "no", or Escape), CLOSED, REPLACED or QUIT. Open dialogs are closed when your plugin disables, and errors in your handlers show up as incidents.
Loading dialogs
Some dialogs need data first - a player's profile from the database, a page of punishments. openLoaded shows a loading dialog straight away, runs your loader in a coroutine, and swaps in the real dialog once it's done:
openLoaded(viewer, "<s_blue>$name", "<gray>Loading <white>$name</white>...", "profile.open", back = { LookupDialog() }) {
val user = withAsyncCtx { UserRepository.findBlocking(uuid.toString()) }
?: return@openLoaded noticeDialog("<s_blue>$name", "<s_red>Couldn't load $name's data.") { LookupDialog() }
ProfileDialog(user)
}
- The loader starts on the main thread, so do your database work in
withAsyncCtx(or through aSuspendCache). - If the player clicks Cancel, closes the dialog, opens something else or leaves before it finishes, the result is thrown away instead of popping up over whatever they're doing now.
- If the loader throws, it becomes an incident under
operation, and the player gets a notice with the ref. backis where Cancel and the error notice take the player. Leave it out and they just close the dialog.
noticeDialog(title, text) { back } is the small "message + one button" dialog it uses for errors. It's handy on its own for "No access" or "Not found" screens.