Skip to content

Commit c504b01

Browse files
authored
Merge pull request #3094 from BentoBoxWorld/feature/settings-improvements-3092
Settings panel improvements (#3092)
2 parents 88f8ebd + 7c67722 commit c504b01

9 files changed

Lines changed: 197 additions & 14 deletions

File tree

‎src/main/java/world/bentobox/bentobox/api/panels/PanelItem.java‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
import java.util.List;
55
import java.util.Optional;
66

7+
import io.papermc.paper.datacomponent.DataComponentType;
78
import io.papermc.paper.datacomponent.DataComponentTypes;
89
import io.papermc.paper.datacomponent.item.TooltipDisplay;
910
import net.kyori.adventure.text.Component;
1011
import net.kyori.adventure.text.format.TextDecoration;
1112

13+
import org.bukkit.NamespacedKey;
14+
import org.bukkit.Registry;
1215
import org.bukkit.enchantments.Enchantment;
1316
import org.bukkit.event.inventory.ClickType;
1417
import org.bukkit.inventory.ItemFlag;
1518
import org.bukkit.inventory.ItemStack;
1619
import org.bukkit.inventory.meta.ItemMeta;
20+
import org.eclipse.jdt.annotation.Nullable;
1721

1822
import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder;
1923
import world.bentobox.bentobox.api.user.User;
@@ -269,7 +273,7 @@ private void hideAdditionalTooltip() {
269273
if (existing != null) {
270274
builder.hideTooltip(existing.hideTooltip());
271275
builder.addHiddenComponents(
272-
existing.hiddenComponents().toArray(io.papermc.paper.datacomponent.DataComponentType[]::new));
276+
existing.hiddenComponents().toArray(DataComponentType[]::new));
273277
}
274278
// Hide additional components that don't have non-deprecated ItemFlag equivalents
275279
builder.addHiddenComponents(
@@ -282,6 +286,26 @@ private void hideAdditionalTooltip() {
282286
DataComponentTypes.BUNDLE_CONTENTS,
283287
DataComponentTypes.MAP_DECORATIONS
284288
);
289+
DataComponentType blockEntityData = blockEntityData();
290+
if (blockEntityData != null) {
291+
builder.addHiddenComponents(blockEntityData);
292+
}
285293
icon.setData(DataComponentTypes.TOOLTIP_DISPLAY, builder.build());
286294
}
295+
296+
/**
297+
* Hiding this component drops the "Interact with Spawn Egg: Sets Mob Type" lines that a
298+
* spawner or trial spawner icon otherwise shows (#3092). Paper has no constant for it because
299+
* it has no API value, so it is looked up in the registry.
300+
* @return the {@code minecraft:block_entity_data} component type, or null if unavailable
301+
*/
302+
@Nullable
303+
private static DataComponentType blockEntityData() {
304+
try {
305+
return Registry.DATA_COMPONENT_TYPE.get(NamespacedKey.minecraft("block_entity_data"));
306+
} catch (RuntimeException e) {
307+
// Registry not available, e.g., in a test environment
308+
return null;
309+
}
310+
}
287311
}

‎src/main/java/world/bentobox/bentobox/database/objects/Players.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,22 @@ public void setLastLogin(Long lastLogin) {
296296
this.lastLogin = lastLogin;
297297
}
298298

299+
/**
300+
* @return the display mode the player last chose in the settings panel, never
301+
* {@link Flag.Mode#TOP_ROW}
302+
* @since 3.23.2
303+
*/
304+
public Flag.Mode getFlagsDisplayMode() {
305+
return flagsDisplayMode == null || flagsDisplayMode == Flag.Mode.TOP_ROW ? Flag.Mode.BASIC
306+
: flagsDisplayMode;
307+
}
308+
309+
/**
310+
* @param flagsDisplayMode the display mode to show the settings panel in
311+
* @since 3.23.2
312+
*/
313+
public void setFlagsDisplayMode(Flag.Mode flagsDisplayMode) {
314+
this.flagsDisplayMode = flagsDisplayMode;
315+
}
316+
299317
}

‎src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandRankClickListener.java‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,25 +150,32 @@ private List<String> getCommands(World world, User user) {
150150
return plugin.getCommandsManager().getCommands().values().stream()
151151
.filter(c -> c.getWorld() != null && c.getWorld().equals(world)) // Only allow commands in this world
152152
.filter(c -> c.testPermission(user.getSender())) // Only allow them to see commands they have permission to see
153-
.flatMap(c -> getCmdRecursively("/", c).stream())
153+
.flatMap(c -> getCmdRecursively("/", c, user).stream())
154154
.filter(label -> user.isOp() || !hiddenItems.contains(CommandCycleClick.COMMAND_RANK_PREFIX + label)) // Hide any hidden commands
155155
.limit(49) // Silently limit to 49
156156
.toList();
157157
}
158158

159159
/**
160-
* Recursively traverses the command tree looking for any configurable rank command and returns a string list of commands
160+
* Recursively traverses the command tree looking for any configurable rank command and returns a string list of commands.
161+
* Commands the user has no permission for are skipped, along with their sub-commands, because setting a rank for a
162+
* command nobody on the island can run is pointless (#3092).
161163
* @param labels - preceding command's label list
162164
* @param cc - composite command
165+
* @param user - the user viewing the panel
163166
* @return string list of commands
164167
*/
165-
private List<String> getCmdRecursively(String labels, CompositeCommand cc) {
168+
private List<String> getCmdRecursively(String labels, CompositeCommand cc, User user) {
166169
List<String> result = new ArrayList<>();
170+
if (!user.isOp() && cc.getPermission() != null && !cc.getPermission().isEmpty()
171+
&& !user.hasPermission(cc.getPermission())) {
172+
return result;
173+
}
167174
String newLabel = labels + cc.getName();
168175
if (cc.isConfigurableRankCommand()) {
169176
result.add(newLabel);
170177
}
171-
cc.getSubCommands().values().forEach(s -> result.addAll(getCmdRecursively(newLabel + " ", s)));
178+
cc.getSubCommands().values().forEach(s -> result.addAll(getCmdRecursively(newLabel + " ", s, user)));
172179
return result;
173180
}
174181

‎src/main/java/world/bentobox/bentobox/managers/PlayersManager.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import world.bentobox.bentobox.BentoBox;
2626
import world.bentobox.bentobox.api.events.player.PlayerEvent;
27+
import world.bentobox.bentobox.api.flags.Flag;
2728
import world.bentobox.bentobox.api.user.User;
2829
import world.bentobox.bentobox.database.Database;
2930
import world.bentobox.bentobox.database.objects.Island;
@@ -349,6 +350,32 @@ public int getDeaths(World world, UUID playerUUID) {
349350
return getPlayer(playerUUID).getDeaths(Util.getWorld(world));
350351
}
351352

353+
/**
354+
* Returns the display mode the player last chose in the settings panel, so it opens the
355+
* way they left it.
356+
* @param playerUUID - the player's UUID
357+
* @return the basic, advanced or expert display mode
358+
* @since 3.23.2
359+
*/
360+
public Flag.Mode getFlagsDisplayMode(UUID playerUUID) {
361+
return getPlayer(playerUUID).getFlagsDisplayMode();
362+
}
363+
364+
/**
365+
* Remembers the display mode the player chose in the settings panel. Saves only when it
366+
* changes.
367+
* @param playerUUID - the player's UUID
368+
* @param mode - the basic, advanced or expert display mode
369+
* @since 3.23.2
370+
*/
371+
public void setFlagsDisplayMode(UUID playerUUID, Flag.Mode mode) {
372+
Players p = getPlayer(playerUUID);
373+
if (p.getFlagsDisplayMode() != mode) {
374+
p.setFlagsDisplayMode(mode);
375+
handler.saveObjectAsync(p);
376+
}
377+
}
378+
352379
/**
353380
* Sets if a player is mid-teleport or not
354381
* @param uniqueId - unique ID

‎src/main/java/world/bentobox/bentobox/panels/customizable/SettingsPanel.java‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,14 @@ public boolean isReadOnly() {
172172
@Nullable
173173
private final Island island;
174174
private final List<TabType> tabs;
175-
private final Flag.Mode defaultMode;
175+
/** The display mode the viewer chose; shared by every tab. */
176+
private Flag.Mode selectedMode;
177+
/** Whether a mode change is saved as the viewer's preference for next time. */
178+
private boolean remembersMode;
179+
/**
180+
* Per-tab overrides of {@link #selectedMode}, set when a tab has nothing to show in the
181+
* chosen mode and skips ahead.
182+
*/
176183
private final Map<TabType, Flag.Mode> modes = new EnumMap<>(TabType.class);
177184
private final Map<TabType, ItemTemplateRecord> tabTemplates = new EnumMap<>(TabType.class);
178185
/**
@@ -198,7 +205,7 @@ public boolean isReadOnly() {
198205
* @param world the world the settings are for
199206
* @param island the island, or null if the viewer is not on one
200207
* @param tabs the tabs to show, in order; the first the viewer may see is shown initially
201-
* @param defaultMode the display mode each tab starts in
208+
* @param defaultMode the display mode the panel starts in
202209
*/
203210
protected SettingsPanel(@NonNull CompositeCommand command, @NonNull User user, @NonNull String templateName,
204211
@NonNull World world, @Nullable Island island, @NonNull List<TabType> tabs, Flag.Mode defaultMode) {
@@ -210,13 +217,14 @@ protected SettingsPanel(@NonNull CompositeCommand command, @NonNull User user, @
210217
this.tabs = tabs.stream()
211218
.filter(t -> t.getPermission(prefix).isEmpty() || user.hasPermission(t.getPermission(prefix)))
212219
.toList();
213-
this.defaultMode = defaultMode;
220+
this.selectedMode = defaultMode;
214221
this.activeTab = this.tabs.isEmpty() ? tabs.get(0) : this.tabs.get(0);
215222
}
216223

217224
/**
218225
* Opens the settings panel for a player. With an island the protection and settings tabs are
219-
* shown; without one, only the read-only view of the world's protection flags.
226+
* shown; without one, only the read-only view of the world's protection flags. It opens in the
227+
* display mode the player last chose.
220228
* @param command the island settings command
221229
* @param user the viewer
222230
* @param island the island, or null if the viewer is not on one
@@ -226,7 +234,10 @@ public static boolean openPanel(@NonNull CompositeCommand command, @NonNull User
226234
List<TabType> tabs = island == null ? List.of(TabType.WORLD_PROTECTION)
227235
: List.of(TabType.PROTECTION, TabType.SETTING);
228236
World world = island == null ? command.getWorld() : island.getWorld();
229-
return new SettingsPanel(command, user, SETTINGS_PANEL, world, island, tabs, Mode.BASIC).open();
237+
SettingsPanel settingsPanel = new SettingsPanel(command, user, SETTINGS_PANEL, world, island, tabs,
238+
command.getPlugin().getPlayers().getFlagsDisplayMode(user.getUniqueId()));
239+
settingsPanel.remembersMode = true;
240+
return settingsPanel.open();
230241
}
231242

232243
/**
@@ -441,7 +452,7 @@ private List<Flag> getFlags(Flag.Mode mode) {
441452
}
442453

443454
private Flag.Mode currentMode() {
444-
return modes.getOrDefault(activeTab, defaultMode);
455+
return modes.getOrDefault(activeTab, selectedMode);
445456
}
446457

447458
private boolean isVisibleToUser(Flag flag) {
@@ -623,7 +634,12 @@ private PanelItem createModeButton(@NonNull ItemTemplateRecord template, Templat
623634
user.getTranslation(PROTECTION_PANEL + "mode." + nextKey + ".name")))
624635
.clickHandler((p, u, clickType, s) -> {
625636
if (clickAllowed(template, clickType)) {
626-
modes.put(activeTab, currentMode().getNext());
637+
// One mode for every tab, remembered for the next time the player opens it
638+
selectedMode = currentMode().getNext();
639+
modes.clear();
640+
if (remembersMode) {
641+
plugin.getPlayers().setFlagsDisplayMode(u.getUniqueId(), selectedMode);
642+
}
627643
pageIndex = 0;
628644
u.getPlayer().playSound(u.getLocation(), Sound.BLOCK_STONE_BUTTON_CLICK_OFF, 1F, 1F);
629645
}

‎src/test/java/world/bentobox/bentobox/api/commands/island/IslandSettingsCommandTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import world.bentobox.bentobox.CommonTestSetup;
3030
import world.bentobox.bentobox.Settings;
3131
import world.bentobox.bentobox.api.commands.CompositeCommand;
32+
import world.bentobox.bentobox.api.flags.Flag.Mode;
3233
import world.bentobox.bentobox.api.flags.Flag.Type;
3334
import world.bentobox.bentobox.api.panels.Tab;
3435
import world.bentobox.bentobox.api.panels.TabbedPanel;
@@ -153,6 +154,7 @@ void testExecuteOpensTemplatedPanel() {
153154
when(user.getTranslationOrNothing(anyString())).thenReturn("");
154155
when(im.getIslandAt(any(Location.class))).thenReturn(Optional.of(island));
155156
when(island.getWorld()).thenReturn(world);
157+
when(plugin.getPlayers().getFlagsDisplayMode(any())).thenReturn(Mode.BASIC);
156158
isc.canExecute(user, "settings", Collections.emptyList());
157159

158160
assertTrue(isc.execute(user, "settings", Collections.emptyList()));

‎src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/CommandRankClickListenerTest.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import java.util.Collections;
1414
import java.util.HashMap;
15+
import java.util.LinkedHashMap;
1516
import java.util.Map;
1617
import java.util.Optional;
1718

@@ -197,6 +198,34 @@ void testOnClickTooManyCommands() {
197198
"protection.panel.flag-item.command-instructions.");
198199
}
199200

201+
/**
202+
* Sub-commands the viewer has no permission for are not listed, so their rank cannot be set (#3092).
203+
* The listed commands are "/test" and "/test allowed", so slot 1 is "/test allowed".
204+
*/
205+
@Test
206+
void testOnClickHidesSubCommandsWithoutPermission() {
207+
when(user.isOp()).thenReturn(false);
208+
when(user.hasPermission("oneblock.island.border")).thenReturn(false);
209+
CompositeCommand cc = cm.getCommands().get("test");
210+
Map<String, CompositeCommand> subCommands = new LinkedHashMap<>();
211+
subCommands.put("border", subCommand("border", "oneblock.island.border"));
212+
subCommands.put("allowed", subCommand("allowed", "oneblock.island.allowed"));
213+
when(cc.getSubCommands()).thenReturn(subCommands);
214+
215+
assertTrue(crcl.onClick(panel, user, ClickType.LEFT, 1));
216+
verify(user).getTranslationOrNothing("protection.panel.flag-item.command-instructions.allowed");
217+
verify(user, never()).getTranslationOrNothing("protection.panel.flag-item.command-instructions.border");
218+
}
219+
220+
private CompositeCommand subCommand(String name, String permission) {
221+
CompositeCommand sub = mock(CompositeCommand.class);
222+
when(sub.isConfigurableRankCommand()).thenReturn(true);
223+
when(sub.getName()).thenReturn(name);
224+
when(sub.getPermission()).thenReturn(permission);
225+
when(sub.getSubCommands()).thenReturn(Collections.emptyMap());
226+
return sub;
227+
}
228+
200229
/**
201230
* Test method for {@link world.bentobox.bentobox.listeners.flags.clicklisteners.CommandRankClickListener#getPanelItem(java.lang.String, world.bentobox.bentobox.api.user.User, org.bukkit.World)}.
202231
*/

‎src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.mockito.Mockito;
5555

5656
import world.bentobox.bentobox.CommonTestSetup;
57+
import world.bentobox.bentobox.api.flags.Flag;
5758
import world.bentobox.bentobox.api.user.User;
5859
import world.bentobox.bentobox.database.AbstractDatabaseHandler;
5960
import world.bentobox.bentobox.database.Database;
@@ -687,6 +688,17 @@ void testSetLocale() {
687688
assertEquals("en-UK", pm.getLocale(uuid));
688689
}
689690

691+
/**
692+
* Test method for
693+
* {@link world.bentobox.bentobox.managers.PlayersManager#setFlagsDisplayMode(java.util.UUID, world.bentobox.bentobox.api.flags.Flag.Mode)}.
694+
*/
695+
@Test
696+
void testFlagsDisplayMode() {
697+
assertEquals(Flag.Mode.BASIC, pm.getFlagsDisplayMode(uuid));
698+
pm.setFlagsDisplayMode(uuid, Flag.Mode.EXPERT);
699+
assertEquals(Flag.Mode.EXPERT, pm.getFlagsDisplayMode(uuid));
700+
}
701+
690702
/**
691703
* Test method for
692704
* {@link world.bentobox.bentobox.managers.PlayersManager#setPlayerName(world.bentobox.bentobox.api.user.User)}.

‎src/test/java/world/bentobox/bentobox/panels/customizable/SettingsPanelTest.java‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public void setUp() throws Exception {
118118
return flags.stream().filter(f -> f.getID().equals(id)).findFirst();
119119
});
120120
when(iwm.getHiddenFlags(any())).thenReturn(hidden);
121+
when(plugin.getPlayers().getFlagsDisplayMode(any())).thenReturn(Mode.BASIC);
121122

122123
// Command
123124
when(command.getPlugin()).thenReturn(plugin);
@@ -330,15 +331,49 @@ void testModeCycling() {
330331
}
331332

332333
/**
333-
* The mode is remembered per tab.
334+
* One mode is shared by every tab (#3092).
334335
*/
335336
@Test
336-
void testModePerTab() {
337+
void testModeSharedAcrossTabs() {
337338
SettingsPanel sp = openWithIsland();
338339
click(items(sp), MODE, user, ClickType.LEFT);
339340
assertEquals(Mode.ADVANCED, sp.getMode());
340341
click(items(sp), SETTING_TAB, user, ClickType.LEFT);
342+
assertEquals(Mode.ADVANCED, sp.getMode());
343+
}
344+
345+
/**
346+
* The panel opens in the mode the player last chose, and a change is saved for next time (#3092).
347+
*/
348+
@Test
349+
void testModeRemembered() {
350+
when(plugin.getPlayers().getFlagsDisplayMode(uuid)).thenReturn(Mode.EXPERT);
351+
SettingsPanel sp = openWithIsland();
352+
assertEquals(Mode.EXPERT, sp.getMode());
353+
assertEquals(Material.NETHER_BRICK, material(items(sp), MODE));
354+
355+
click(items(sp), MODE, user, ClickType.LEFT);
341356
assertEquals(Mode.BASIC, sp.getMode());
357+
verify(plugin.getPlayers()).setFlagsDisplayMode(uuid, Mode.BASIC);
358+
}
359+
360+
/**
361+
* A tab with nothing to show in the chosen mode skips ahead for display only; the player's
362+
* choice is neither changed nor saved.
363+
*/
364+
@Test
365+
void testSkippedModeNotSaved() {
366+
when(iwm.getAddon(any())).thenReturn(Optional.of(gma));
367+
flags.removeIf(f -> f.getID().equals("S_GAMMA"));
368+
flags.add(flag("S_ADVANCED", Type.SETTING, Mode.ADVANCED));
369+
SettingsPanel sp = openWithIsland();
370+
click(items(sp), SETTING_TAB, user, ClickType.LEFT);
371+
sp.refreshPanel();
372+
assertEquals(Mode.ADVANCED, sp.getMode());
373+
click(items(sp), PROTECTION_TAB, user, ClickType.LEFT);
374+
sp.refreshPanel();
375+
assertEquals(Mode.BASIC, sp.getMode());
376+
verify(plugin.getPlayers(), never()).setFlagsDisplayMode(any(), any());
342377
}
343378

344379
/**
@@ -493,6 +528,19 @@ void testUnknownPinnedFlagIsSkipped(@TempDir Path addonFolder) throws IOExceptio
493528
// Section: Admin panel
494529
// ---------------------------------------------------------------------
495530

531+
/**
532+
* The admin panel always opens in expert mode and does not touch the admin's own preference.
533+
*/
534+
@Test
535+
void testAdminModeNotRemembered() {
536+
SettingsPanel sp = openAdmin(island);
537+
assertEquals(Mode.EXPERT, sp.getMode());
538+
click(items(sp), MODE, user, ClickType.LEFT);
539+
assertEquals(Mode.BASIC, sp.getMode());
540+
verify(plugin.getPlayers(), never()).getFlagsDisplayMode(any());
541+
verify(plugin.getPlayers(), never()).setFlagsDisplayMode(any(), any());
542+
}
543+
496544
private SettingsPanel openAdmin(Island target) {
497545
assertTrue(SettingsPanel.openAdminPanel(command, user, target));
498546
return (SettingsPanel) PanelListenerManager.getOpenPanels().get(uuid).getListener().orElseThrow();

0 commit comments

Comments
 (0)