Several screens and dialogs
Open sub-screens, go back, close the whole menu and ask yes-or-no questions with ConfirmDialog before destructive actions.
Steps
A menu can have any number of screens. This page shows how to move between them and how to confirm a destructive action.
The screen stack#
The UiHost your create method receives keeps a stack of screens:
| Call | Effect |
|---|---|
host.open(screen) |
Pushes screen on top. Its back() returns to the current one. |
host.back() |
Pops the current screen. From the first screen, returns to wherever the menu was opened from (the Options screen, the mod list, or your own parent). |
host.close() |
Pops every screen at once, returning to wherever the menu was opened from. |
Keep the host (pass it to the methods that build screens) so buttons can navigate.
Open a sub-screen#
Build each screen in a method, and build it fresh every time it opens:
@Override
public Screen create(UiHost host) {
return MenuScreen.builder("main")
.title(Text.translatable("mymod.title", "My Mod"))
.add(Button.builder("advanced")
.label(Text.translatable("mymod.advanced", "Advanced..."))
.onPress(() -> host.open(advancedScreen(host)))
.build())
.footer(Button.builder("done").label(UiTexts.DONE).onPress(host::back).build())
.build();
}
private Screen advancedScreen(UiHost host) {
return MenuScreen.builder("advanced")
.title(Text.translatable("mymod.advanced.title", "Advanced"))
.add(Toggle.builder("debug")
.label(Text.translatable("mymod.debug", "Debug output"))
.value(() -> config.debug)
.onChange(v -> config.debug = v)
.build())
.footer(Button.builder("back").label(UiTexts.DONE).onPress(host::back).build())
.build();
}
Pass data into the screen-building method when a sub-screen is about one item:
.onPress(() -> host.open(profileScreen(host, profiles.selected())))
Ask before a destructive action#
ConfirmDialog is a yes/no question on its own screen, drawn like vanilla's "Delete world?" screen:
StatusLine status = StatusLine.builder("status").build();
Button reset = Button.builder("reset")
.label(Text.translatable("mymod.reset", "Reset..."))
.onPress(() -> host.open(ConfirmDialog.builder("reset.confirm")
.title(Text.translatable("mymod.reset.title", "Reset every setting?"))
.message(Text.translatable("mymod.reset.message", "This can't be undone."))
.yesLabel(Text.translatable("mymod.reset.yes", "Reset"))
.noLabel(UiTexts.CANCEL)
.onYes(() -> {
config.reset();
status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.reset.done", "Settings reset"));
})
.build()))
.build();
| Builder method | Required | Default |
|---|---|---|
title(Text) |
yes | |
message(Text) |
yes | |
onYes(Runnable) |
yes | |
yesLabel(Text) |
no | UiTexts.YES. A verb ("Delete", "Overwrite") is clearer. |
noLabel(Text) |
no | UiTexts.NO |
onNo(Runnable) |
no | does nothing; the dialog just closes |
When the player answers, the dialog closes first and then the answer's action runs. So the action runs with
the screen that opened the dialog as the current one: it can write to that screen's StatusLine (as above) or
call host.open to go somewhere else. That screen is refreshed afterwards.
Leave the whole menu#
host.close() from a deep screen returns straight to where the menu was opened, for example after a "Save
and exit" button:
Button.builder("saveExit")
.label(Text.translatable("mymod.saveExit", "Save and exit"))
.onPress(() -> {
config.save();
host.close();
})
.build();
Test it#
FakeHost keeps the same stack. host.current().id() names the open screen and host.depth() counts the
stack. On a ConfirmDialog, click yes or no:
host.click("reset");
assertEquals("reset.confirm", host.current().id());
host.click("yes");
assertEquals("main", host.current().id());
Next steps#
- Slow work: files and network: when a button's action takes time.
- A list manager: a complete menu with sub-screens and a delete dialog.