Skip to content

Commit 934b386

Browse files
bastianbeckeruXephi
authored andcommitted
feat(dialog): add option to stack dialog buttons vertically
1 parent 0092252 commit 934b386

8 files changed

Lines changed: 94 additions & 17 deletions

File tree

‎authme-core/src/main/java/fr/xephi/authme/platform/DialogWindowSpec.java‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* @param secondaryButtonCommand command template the secondary button should execute in post-join dialogs
1616
* (null for pre-join dialogs where the secondary button is a cancel action)
1717
* @param body optional translated body text shown below the title (null = no body)
18+
* @param stackButtonsVertically whether buttons should be stacked vertically instead of shown side by side
1819
*/
1920
public record DialogWindowSpec(String title,
2021
List<DialogInputSpec> inputs,
@@ -23,7 +24,8 @@ public record DialogWindowSpec(String title,
2324
boolean showSecondaryButton,
2425
boolean canCloseWithEscape,
2526
String secondaryButtonCommand,
26-
String body) {
27+
String body,
28+
boolean stackButtonsVertically) {
2729

2830
public DialogWindowSpec {
2931
Objects.requireNonNull(title, "title");
@@ -42,12 +44,17 @@ public DialogWindowSpec(String title,
4244
boolean canCloseWithEscape,
4345
String secondaryButtonCommand) {
4446
this(title, inputs, primaryButtonLabel, secondaryButtonLabel,
45-
showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, null);
47+
showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, null, false);
4648
}
4749

4850
/** Returns a copy of this spec with the given body text (may be null). */
4951
public DialogWindowSpec withBody(String newBody) {
5052
return new DialogWindowSpec(title, inputs, primaryButtonLabel, secondaryButtonLabel,
51-
showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, newBody);
53+
showSecondaryButton, canCloseWithEscape, secondaryButtonCommand, newBody, stackButtonsVertically);
54+
}
55+
56+
/** Returns the number of columns the dialog buttons should be arranged in. */
57+
public int buttonColumns() {
58+
return stackButtonsVertically ? 1 : 2;
5259
}
5360
}

‎authme-core/src/main/java/fr/xephi/authme/service/DialogWindowService.java‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ public DialogWindowSpec createPreJoinLoginDialog(String playerName) {
6767
showRecovery || showCancelButton,
6868
canCloseWithEscape,
6969
showRecovery ? "forgot_password" : null,
70-
showBody ? text.apply(MessageKey.DIALOG_LOGIN_BODY) : null);
70+
showBody ? text.apply(MessageKey.DIALOG_LOGIN_BODY) : null,
71+
commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY));
7172
}
7273

7374
public DialogWindowSpec createPreJoinRecoveryDialog(String playerName) {
@@ -82,7 +83,8 @@ public DialogWindowSpec createPreJoinRecoveryDialog(String playerName) {
8283
true,
8384
false,
8485
null,
85-
showBody ? text.apply(MessageKey.DIALOG_RECOVERY_BODY) : null);
86+
showBody ? text.apply(MessageKey.DIALOG_RECOVERY_BODY) : null,
87+
commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY));
8688
}
8789

8890
public DialogWindowSpec createTotpDialog(Player player) {
@@ -99,7 +101,8 @@ public DialogWindowSpec createTotpDialog(Player player) {
99101
false,
100102
false,
101103
null,
102-
showBody ? text.apply(MessageKey.DIALOG_TWO_FACTOR_BODY) : null);
104+
showBody ? text.apply(MessageKey.DIALOG_TWO_FACTOR_BODY) : null,
105+
commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY));
103106
}
104107

105108
public DialogWindowSpec createRegisterDialog(Player player,
@@ -144,7 +147,8 @@ private DialogWindowSpec createLoginDialogSpec(Function<MessageKey, String> text
144147
showSecondaryButton,
145148
canCloseWithEscape,
146149
showRecovery ? "email recover $(email)" : null,
147-
showBody ? textResolver.apply(MessageKey.DIALOG_LOGIN_BODY) : null);
150+
showBody ? textResolver.apply(MessageKey.DIALOG_LOGIN_BODY) : null,
151+
commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY));
148152
}
149153

150154
private DialogWindowSpec createRegisterDialogSpec(RegistrationType type,
@@ -194,7 +198,8 @@ private DialogWindowSpec createRegisterDialogSpec(RegistrationType type,
194198
showSecondaryButton,
195199
canCloseWithEscape,
196200
null,
197-
showBody ? textResolver.apply(MessageKey.DIALOG_REGISTER_BODY) : null);
201+
showBody ? textResolver.apply(MessageKey.DIALOG_REGISTER_BODY) : null,
202+
commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY));
198203
}
199204

200205
private String getPostJoinMessage(Player player, MessageKey key) {

‎authme-core/src/main/java/fr/xephi/authme/settings/properties/RegistrationSettings.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ public final class RegistrationSettings implements SettingsHolder {
120120
public static final Property<Boolean> DIALOG_SHOW_BODY =
121121
newProperty("settings.registration.dialog.showBody", true);
122122

123+
@Comment({
124+
"Stack dialog buttons vertically, with the primary button on top.",
125+
"If disabled, dialog buttons are shown side by side."})
126+
public static final Property<Boolean> DIALOG_STACK_BUTTONS_VERTICALLY =
127+
newProperty("settings.registration.dialog.stackButtonsVertically", false);
128+
123129
@Comment({
124130
"Show the login/register dialog during the Paper/Folia pre-join phase.",
125131
"Independent from postJoin.enable; you may enable either or both options.",

‎authme-core/src/test/java/fr/xephi/authme/service/DialogWindowServiceTest.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ void shouldBuildPostJoinLoginDialogWithForgotPasswordButtonWhenEmailServiceConfi
4949
.willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey());
5050
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(true);
5151
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(true);
52+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false);
5253
given(emailService.hasAllInformation()).willReturn(true);
5354

5455
// when
@@ -73,6 +74,7 @@ void shouldNotShowForgotPasswordButtonWhenEmailServiceNotConfigured() {
7374
.willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey());
7475
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(true);
7576
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false);
77+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false);
7678
given(emailService.hasAllInformation()).willReturn(false);
7779

7880
// when — email service not configured: recovery must not appear regardless of the flag
@@ -92,6 +94,7 @@ void shouldBuildPostJoinLoginDialogWithoutForgotPasswordButtonWhenDisabledByConf
9294
.willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey());
9395
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(false);
9496
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false);
97+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false);
9598

9699
// when — disabled by config even when email service is available
97100
DialogWindowSpec dialog = dialogWindowService.createLoginDialog(player);
@@ -110,6 +113,7 @@ void shouldBuildPostJoinRegisterDialogWithEmailBeforePasswordForMandatoryEmailFl
110113
given(commonService.retrieveSingleMessage(eq(player), any(MessageKey.class)))
111114
.willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey());
112115
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false);
116+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false);
113117

114118
// when
115119
DialogWindowSpec dialog = dialogWindowService.createRegisterDialog(
@@ -126,6 +130,7 @@ void shouldBuildPostJoinRegisterDialogForOptionalEmailFlow() {
126130
given(commonService.retrieveSingleMessage(eq(player), any(MessageKey.class)))
127131
.willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey());
128132
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false);
133+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false);
129134

130135
// when
131136
DialogWindowSpec dialog = dialogWindowService.createRegisterDialog(
@@ -148,6 +153,7 @@ void shouldBuildPreJoinLoginDialogWithCancelButtonWhenEmailServiceNotConfigured(
148153
given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_SHOW_CANCEL_BUTTON)).willReturn(true);
149154
given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_ALLOW_CLOSE_WITH_ESCAPE)).willReturn(true);
150155
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(true);
156+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false);
151157
given(emailService.hasAllInformation()).willReturn(false);
152158

153159
// when — email not configured: no recovery, cancel button shown instead
@@ -160,6 +166,7 @@ void shouldBuildPreJoinLoginDialogWithCancelButtonWhenEmailServiceNotConfigured(
160166
assertThat(dialog.showSecondaryButton(), is(true));
161167
assertThat(dialog.canCloseWithEscape(), is(true));
162168
assertThat(dialog.body(), is("dialog.login.body"));
169+
assertThat(dialog.buttonColumns(), is(2));
163170
assertThat(dialog.inputs().stream().map(input -> input.id()).toList(), contains("password"));
164171
assertThat(dialog.secondaryButtonCommand(), is(nullValue()));
165172
}
@@ -171,6 +178,7 @@ void shouldBuildPreJoinLoginDialogWithRecoveryButtonWhenEmailServiceConfigured()
171178
.willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey());
172179
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_FORGOT_PASSWORD_BUTTON)).willReturn(true);
173180
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false);
181+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(false);
174182
given(emailService.hasAllInformation()).willReturn(true);
175183

176184
// when — email configured: recovery button shown, cancel suppressed, email field present
@@ -183,4 +191,24 @@ void shouldBuildPreJoinLoginDialogWithRecoveryButtonWhenEmailServiceConfigured()
183191
assertThat(dialog.secondaryButtonCommand(), is(notNullValue()));
184192
assertThat(dialog.inputs().stream().map(input -> input.id()).toList(), contains("password"));
185193
}
194+
195+
@Test
196+
void shouldKeepStackedButtonsInPreJoinRegisterDialogWhenBodyIsReplaced() {
197+
// given
198+
given(messages.retrieveSingle(eq("Bobby"), any(MessageKey.class)))
199+
.willAnswer(invocation -> invocation.getArgument(1, MessageKey.class).getKey());
200+
given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_SHOW_CANCEL_BUTTON)).willReturn(true);
201+
given(commonService.getProperty(RegistrationSettings.PRE_JOIN_DIALOG_ALLOW_CLOSE_WITH_ESCAPE)).willReturn(true);
202+
given(commonService.getProperty(RegistrationSettings.DIALOG_SHOW_BODY)).willReturn(false);
203+
given(commonService.getProperty(RegistrationSettings.DIALOG_STACK_BUTTONS_VERTICALLY)).willReturn(true);
204+
205+
// when
206+
DialogWindowSpec dialog = dialogWindowService.createPreJoinRegisterDialog(
207+
"Bobby", RegistrationType.PASSWORD, RegisterSecondaryArgument.CONFIRMATION).withBody("error");
208+
209+
// then
210+
assertThat(dialog.body(), is("error"));
211+
assertThat(dialog.showSecondaryButton(), is(true));
212+
assertThat(dialog.buttonColumns(), is(1));
213+
}
186214
}

‎authme-paper-common/src/main/java/fr/xephi/authme/platform/PaperDialogHelper.java‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public static Dialog createPreJoinLoginDialog(DialogWindowSpec dialog) {
6363

6464
return Dialog.create(factory -> factory.empty()
6565
.base(base)
66-
.type(DialogType.multiAction(createPreJoinButtons(dialog, loginButton, secondaryKey)).build()));
66+
.type(createButtonDialogType(dialog, createPreJoinButtons(dialog, loginButton, secondaryKey))));
6767
}
6868

6969
public static Dialog createPreJoinRecoveryDialog(DialogWindowSpec dialog) {
@@ -80,8 +80,8 @@ public static Dialog createPreJoinRecoveryDialog(DialogWindowSpec dialog) {
8080

8181
return Dialog.create(factory -> factory.empty()
8282
.base(base)
83-
.type(DialogType.multiAction(createPreJoinButtons(dialog, submitButton,
84-
PaperDialogActionKeys.PRE_JOIN_RECOVERY_CANCEL)).build()));
83+
.type(createButtonDialogType(dialog, createPreJoinButtons(dialog, submitButton,
84+
PaperDialogActionKeys.PRE_JOIN_RECOVERY_CANCEL))));
8585
}
8686

8787
public static Dialog createPreJoinRegisterDialog(DialogWindowSpec dialog) {
@@ -98,9 +98,9 @@ public static Dialog createPreJoinRegisterDialog(DialogWindowSpec dialog) {
9898

9999
return Dialog.create(factory -> factory.empty()
100100
.base(base)
101-
.type(DialogType.multiAction(createPreJoinButtons(dialog,
101+
.type(createButtonDialogType(dialog, createPreJoinButtons(dialog,
102102
registerButton,
103-
PaperDialogActionKeys.PRE_JOIN_REGISTER_CANCEL)).build()));
103+
PaperDialogActionKeys.PRE_JOIN_REGISTER_CANCEL))));
104104
}
105105

106106
private static Dialog createInGameCommandDialog(DialogWindowSpec dialog, String commandTemplate) {
@@ -124,7 +124,11 @@ private static Dialog createInGameCommandDialog(DialogWindowSpec dialog, String
124124

125125
return Dialog.create(factory -> factory.empty()
126126
.base(base)
127-
.type(DialogType.multiAction(buttons).build()));
127+
.type(createButtonDialogType(dialog, buttons)));
128+
}
129+
130+
private static DialogType createButtonDialogType(DialogWindowSpec dialog, List<ActionButton> buttons) {
131+
return DialogType.multiAction(buttons).columns(dialog.buttonColumns()).build();
128132
}
129133

130134
private static List<ActionButton> createPreJoinButtons(DialogWindowSpec dialog,

‎authme-spigot-1.21/src/main/java/fr/xephi/authme/platform/SpigotDialogHelper.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ static void showLoginDialog(Player player, DialogWindowSpec dialog) {
4040
buttons.add(new ActionButton(toTextComponent(dialog.secondaryButtonLabel()),
4141
new RunCommandAction(dialog.secondaryButtonCommand())));
4242
}
43-
player.showDialog(new MultiActionDialog(base, buttons.toArray(new ActionButton[0])));
43+
player.showDialog(new MultiActionDialog(base, buttons.toArray(new ActionButton[0]))
44+
.columns(dialog.buttonColumns()));
4445
}
4546

4647
static void showTotpDialog(Player player, DialogWindowSpec dialog) {

‎authme-spigot-1.21/src/test/java/fr/xephi/authme/platform/SpigotDialogHelperTest.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ public void showLoginDialogSendsCorrectCommandTemplate() {
2929
assertThat(action.template(), is("login $(password)"));
3030
}
3131

32+
@Test
33+
public void showLoginDialogStacksButtonsWhenEnabled() {
34+
// given
35+
Player player = mock(Player.class);
36+
DialogWindowSpec dialog = new DialogWindowSpec("Login",
37+
java.util.List.of(new DialogInputSpec("password", "Password", 100)),
38+
"Login",
39+
"Forgot Password?",
40+
true,
41+
false,
42+
"email recover $(email)",
43+
null,
44+
true);
45+
46+
// when
47+
SpigotDialogHelper.showLoginDialog(player, dialog);
48+
49+
// then
50+
ArgumentCaptor<MultiActionDialog> captor = ArgumentCaptor.forClass(MultiActionDialog.class);
51+
verify(player).showDialog(captor.capture());
52+
assertThat(captor.getValue().columns(), is(1));
53+
}
54+
3255
@Test
3356
public void showLoginDialogIncludesForgotPasswordButtonWhenSpecified() {
3457
// given

‎docs/config.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!-- AUTO-GENERATED FILE! Do not edit this directly -->
2-
<!-- File auto-generated on Fri Sep 18 18:25:33 CST 2026. See authme-tools/src/test/java/tools/docs/config/config.tpl.md -->
2+
<!-- File auto-generated on Mon Nov 02 14:57:12 CLST 2026. See authme-tools/src/test/java/tools/docs/config/config.tpl.md -->
33

44
## AuthMe Configuration
55
The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder,
@@ -335,6 +335,9 @@ settings:
335335
# Show a short description body in login, register, and two-factor dialogs.
336336
# The body text helps players understand what the dialog is asking them to do.
337337
showBody: true
338+
# Stack dialog buttons vertically, with the primary button on top.
339+
# If disabled, dialog buttons are shown side by side.
340+
stackButtonsVertically: false
338341
preJoin:
339342
# Show the login/register dialog during the Paper/Folia pre-join phase.
340343
# Independent from postJoin.enable; you may enable either or both options.
@@ -650,4 +653,4 @@ To change settings on a running server, save your changes to config.yml and use
650653

651654
---
652655

653-
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Sep 18 18:25:33 CST 2026
656+
This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Nov 02 14:57:12 CLST 2026

0 commit comments

Comments
 (0)