Skip to content

Commit 369c23d

Browse files
authored
Merge pull request #3100 from BentoBoxWorld/fix/panel-player-inventory-clicks
Let players use their own inventory while a panel is open
2 parents fbf6906 + 49a2fe4 commit 369c23d

5 files changed

Lines changed: 199 additions & 1 deletion

File tree

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,19 @@ default boolean hasClickCooldown() {
5353
default boolean isActionableSlot(int rawSlot) {
5454
return true;
5555
}
56+
57+
/**
58+
* Whether the player may use their own inventory while this panel is open: pick up, place,
59+
* swap and drop items, and use bundles. When {@code true}, such clicks are left to vanilla and
60+
* are <b>not</b> passed to {@link #onInventoryClick(User, InventoryClickEvent)}. Clicks that
61+
* could move an item into the panel, such as shift-clicks, are always blocked.
62+
* <p>
63+
* Defaults to {@code false} because some panels read clicks in the player's inventory, for
64+
* example to let them pick an icon. Panels without a listener always allow it.
65+
* @return {@code true} to let the player use their own inventory while the panel is open
66+
* @since 3.23.4
67+
*/
68+
default boolean allowsPlayerInventoryClicks() {
69+
return false;
70+
}
5671
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,14 @@ public void onInventoryClose(InventoryCloseEvent event) {
207207
}
208208
}
209209

210+
/**
211+
* Tabs and flag buttons are all in the panel, so the player may use their own inventory.
212+
*/
213+
@Override
214+
public boolean allowsPlayerInventoryClicks() {
215+
return true;
216+
}
217+
210218
@Override
211219
public void onInventoryClick(User user, InventoryClickEvent event) {
212220
// Note: click cooldown is now applied in PanelListenerManager before handlers run.

‎src/main/java/world/bentobox/bentobox/listeners/PanelListenerManager.java‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package world.bentobox.bentobox.listeners;
22

33
import java.util.ArrayList;
4+
import java.util.EnumSet;
45
import java.util.HashMap;
56
import java.util.Map;
67
import java.util.Optional;
@@ -10,8 +11,10 @@
1011
import org.bukkit.event.EventHandler;
1112
import org.bukkit.event.EventPriority;
1213
import org.bukkit.event.Listener;
14+
import org.bukkit.event.inventory.InventoryAction;
1315
import org.bukkit.event.inventory.InventoryClickEvent;
1416
import org.bukkit.event.inventory.InventoryCloseEvent;
17+
import org.bukkit.event.inventory.InventoryDragEvent;
1518
import org.bukkit.event.inventory.InventoryType.SlotType;
1619
import org.bukkit.event.player.PlayerQuitEvent;
1720
import org.bukkit.event.server.PluginDisableEvent;
@@ -28,6 +31,22 @@ public class PanelListenerManager implements Listener {
2831

2932
private static final Map<UUID, Panel> openPanels = new HashMap<>();
3033

34+
/**
35+
* Actions that, when performed on a slot of the player's own inventory, only move items
36+
* within that inventory, to the cursor, or out of the world via a drop. None of them can put
37+
* an item into the panel. Shift-click ({@link InventoryAction#MOVE_TO_OTHER_INVENTORY}) and
38+
* double-click ({@link InventoryAction#COLLECT_TO_CURSOR}, which also gathers matching panel
39+
* icons) are deliberately absent.
40+
*/
41+
private static final EnumSet<InventoryAction> PLAYER_INVENTORY_ACTIONS = EnumSet.of(
42+
InventoryAction.PICKUP_ALL, InventoryAction.PICKUP_SOME, InventoryAction.PICKUP_HALF,
43+
InventoryAction.PICKUP_ONE, InventoryAction.PLACE_ALL, InventoryAction.PLACE_SOME,
44+
InventoryAction.PLACE_ONE, InventoryAction.SWAP_WITH_CURSOR, InventoryAction.DROP_ALL_SLOT,
45+
InventoryAction.DROP_ONE_SLOT, InventoryAction.HOTBAR_SWAP,
46+
InventoryAction.PICKUP_FROM_BUNDLE, InventoryAction.PICKUP_ALL_INTO_BUNDLE,
47+
InventoryAction.PICKUP_SOME_INTO_BUNDLE, InventoryAction.PLACE_FROM_BUNDLE,
48+
InventoryAction.PLACE_ALL_INTO_BUNDLE, InventoryAction.PLACE_SOME_INTO_BUNDLE);
49+
3150
@EventHandler(priority = EventPriority.LOW)
3251
public void onInventoryClick(InventoryClickEvent event) {
3352
User user = User.getInstance(event.getWhoClicked()); // The player that clicked the item
@@ -43,6 +62,13 @@ public void onInventoryClick(InventoryClickEvent event) {
4362
// Get the panel itself
4463
Panel panel = openPanels.get(user.getUniqueId());
4564

65+
// Let the player use their own inventory while the panel is open, unless the panel
66+
// reads clicks there (e.g. "click an item in your inventory" icon pickers).
67+
if (isPlayerInventoryClickAllowed(panel, event)) {
68+
event.setCancelled(false);
69+
return;
70+
}
71+
4672
// Apply the click cooldown for throttled panels (tabbed and settings panels) first,
4773
// before any other (potentially expensive) work. A rejected spam click must do the
4874
// absolute minimum: without this early return, every click - even the throttled ones -
@@ -74,7 +100,9 @@ public void onInventoryClick(InventoryClickEvent event) {
74100
if (pi != null) {
75101
pi.getClickHandler().ifPresent(handler ->
76102
// Execute the handler's onClick method and optionally cancel the event if the handler returns true
77-
event.setCancelled(handler.onClick(panel, user, event.getClick(), event.getSlot())));
103+
event.setCancelled(handler.onClick(panel, user, event.getClick(), event.getSlot())
104+
// An item picked up from the player's inventory must never be placed in the panel
105+
|| hasCursorItem(event)));
78106
}
79107
// If there is a listener, then run it and refresh the panel
80108
panel.getListener().ifPresent(l -> {
@@ -92,6 +120,36 @@ public void onInventoryClick(InventoryClickEvent event) {
92120
}
93121
}
94122

123+
/**
124+
* Whether a click can be left to vanilla: it is in the player's own (bottom) inventory, it
125+
* cannot move an item into the panel, and the panel does not want to see it.
126+
*/
127+
private boolean isPlayerInventoryClickAllowed(Panel panel, InventoryClickEvent event) {
128+
return event.getClickedInventory() != null
129+
&& event.getClickedInventory().equals(event.getView().getBottomInventory())
130+
&& PLAYER_INVENTORY_ACTIONS.contains(event.getAction())
131+
&& panel.getListener().map(PanelListener::allowsPlayerInventoryClicks).orElse(true);
132+
}
133+
134+
private boolean hasCursorItem(InventoryClickEvent event) {
135+
return event.getCursor() != null && !event.getCursor().getType().isAir();
136+
}
137+
138+
/**
139+
* Stops items being dragged into a panel. Panels never accept items, and anything that got
140+
* into one would be lost when it closes. Drags confined to the player's own inventory are fine.
141+
*/
142+
@EventHandler(priority = EventPriority.LOW)
143+
public void onInventoryDrag(InventoryDragEvent event) {
144+
Panel panel = openPanels.get(event.getWhoClicked().getUniqueId());
145+
if (panel != null && panel.getInventory().equals(event.getInventory())) {
146+
int topSize = event.getView().getTopInventory().getSize();
147+
if (event.getRawSlots().stream().anyMatch(slot -> slot < topSize)) {
148+
event.setCancelled(true);
149+
}
150+
}
151+
}
152+
95153
@EventHandler(priority = EventPriority.LOWEST)
96154
public void onInventoryClose(InventoryCloseEvent event) {
97155
if (openPanels.containsKey(event.getPlayer().getUniqueId())) {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ public void onInventoryClick(User user, InventoryClickEvent event) {
9090
// Clicks are handled by the buttons
9191
}
9292

93+
/**
94+
* All buttons are in the panel, so the player may use their own inventory.
95+
*/
96+
@Override
97+
public boolean allowsPlayerInventoryClicks() {
98+
return true;
99+
}
100+
93101
@Override
94102
public void refreshPanel() {
95103
if (closed || panel == null) {

‎src/test/java/world/bentobox/bentobox/listeners/PanelListenerManagerTest.java‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,27 @@
1414

1515
import java.util.HashMap;
1616
import java.util.Map;
17+
import java.util.Set;
1718
import java.util.Optional;
1819
import java.util.UUID;
1920

2021
import org.bukkit.ChatColor;
22+
import org.bukkit.Material;
2123
import org.bukkit.entity.HumanEntity;
2224
import org.bukkit.entity.Player;
2325
import org.bukkit.event.inventory.ClickType;
2426
import org.bukkit.event.inventory.InventoryAction;
2527
import org.bukkit.event.inventory.InventoryClickEvent;
2628
import org.bukkit.event.inventory.InventoryCloseEvent;
29+
import org.bukkit.event.inventory.InventoryDragEvent;
2730
import org.bukkit.event.inventory.InventoryType;
2831
import org.bukkit.event.inventory.InventoryType.SlotType;
2932
import org.bukkit.event.player.PlayerQuitEvent;
3033
import org.bukkit.inventory.Inventory;
3134
import org.bukkit.inventory.InventoryView;
3235
import org.bukkit.inventory.ItemStack;
3336
import org.bukkit.inventory.MenuType;
37+
import org.bukkit.inventory.PlayerInventory;
3438
import org.jetbrains.annotations.Nullable;
3539
import org.junit.jupiter.api.AfterEach;
3640
import org.junit.jupiter.api.BeforeEach;
@@ -341,6 +345,111 @@ void testOnInventoryClickOpenPanelsRightPanelRightSlot() {
341345
verify(pl).onInventoryClick(any(), any());
342346
}
343347

348+
/**
349+
* A view whose raw slots 0-8 are the panel and 9 onwards are the player's inventory.
350+
*/
351+
class SplitView extends MyView {
352+
353+
private final Inventory bottom = mock(PlayerInventory.class);
354+
private ItemStack cursor;
355+
356+
SplitView() {
357+
super(ChatColor.RED + PANEL_NAME, panel.getInventory());
358+
}
359+
360+
@Override
361+
public Inventory getBottomInventory() {
362+
return bottom;
363+
}
364+
365+
@Override
366+
public Inventory getInventory(int rawSlot) {
367+
return rawSlot < 9 ? getTopInventory() : bottom;
368+
}
369+
370+
@Override
371+
public ItemStack getCursor() {
372+
return cursor;
373+
}
374+
375+
@Override
376+
public void setCursor(ItemStack item) {
377+
cursor = item;
378+
}
379+
}
380+
381+
private InventoryClickEvent clickPlayerInventory(InventoryAction action) {
382+
PanelListenerManager.getOpenPanels().put(uuid, panel);
383+
InventoryClickEvent e = new InventoryClickEvent(new SplitView(), type, 20, click, action);
384+
plm.onInventoryClick(e);
385+
return e;
386+
}
387+
388+
@Test
389+
void testPlayerInventoryClickRoutedToListenerByDefault() {
390+
// A listener that has not opted in (e.g. an icon picker) still gets the click, cancelled
391+
InventoryClickEvent e = clickPlayerInventory(InventoryAction.PICKUP_ALL);
392+
assertTrue(e.isCancelled());
393+
verify(pl).onInventoryClick(any(), eq(e));
394+
}
395+
396+
@Test
397+
void testPlayerInventoryClickAllowedWhenListenerOptsIn() {
398+
when(pl.allowsPlayerInventoryClicks()).thenReturn(true);
399+
InventoryClickEvent e = clickPlayerInventory(InventoryAction.PICKUP_ALL);
400+
assertFalse(e.isCancelled());
401+
verify(pl, never()).onInventoryClick(any(), any());
402+
verify(pl, never()).refreshPanel();
403+
}
404+
405+
@Test
406+
void testPlayerInventoryClickAllowedWithoutListener() {
407+
when(panel.getListener()).thenReturn(Optional.empty());
408+
assertFalse(clickPlayerInventory(InventoryAction.DROP_ONE_SLOT).isCancelled());
409+
assertFalse(clickPlayerInventory(InventoryAction.PICKUP_FROM_BUNDLE).isCancelled());
410+
}
411+
412+
@Test
413+
void testPlayerInventoryShiftAndDoubleClickBlocked() {
414+
when(panel.getListener()).thenReturn(Optional.empty());
415+
assertTrue(clickPlayerInventory(InventoryAction.MOVE_TO_OTHER_INVENTORY).isCancelled());
416+
assertTrue(clickPlayerInventory(InventoryAction.COLLECT_TO_CURSOR).isCancelled());
417+
}
418+
419+
@Test
420+
void testPanelClickWithCursorItemStaysCancelled() {
421+
PanelListenerManager.getOpenPanels().put(uuid, panel);
422+
// The handler asks for the click not to be cancelled
423+
when(ch.onClick(any(), any(), any(), any(Integer.class))).thenReturn(false);
424+
SplitView splitView = new SplitView();
425+
ItemStack held = mock(ItemStack.class);
426+
when(held.getType()).thenReturn(Material.STONE);
427+
splitView.setCursor(held);
428+
InventoryClickEvent e = new InventoryClickEvent(splitView, type, 0, click, InventoryAction.PLACE_ALL);
429+
plm.onInventoryClick(e);
430+
verify(ch).onClick(eq(panel), any(User.class), eq(click), eq(0));
431+
assertTrue(e.isCancelled());
432+
}
433+
434+
@Test
435+
void testDragIntoPanelCancelled() {
436+
PanelListenerManager.getOpenPanels().put(uuid, panel);
437+
InventoryDragEvent e = new InventoryDragEvent(new SplitView(), null, mock(ItemStack.class), false,
438+
Map.of(3, mock(ItemStack.class), 20, mock(ItemStack.class)));
439+
plm.onInventoryDrag(e);
440+
assertTrue(e.isCancelled());
441+
}
442+
443+
@Test
444+
void testDragWithinPlayerInventoryAllowed() {
445+
PanelListenerManager.getOpenPanels().put(uuid, panel);
446+
InventoryDragEvent e = new InventoryDragEvent(new SplitView(), null, mock(ItemStack.class), false,
447+
Map.of(20, mock(ItemStack.class), 21, mock(ItemStack.class)));
448+
plm.onInventoryDrag(e);
449+
assertFalse(e.isCancelled());
450+
assertEquals(Set.of(20, 21), e.getRawSlots());
451+
}
452+
344453
/**
345454
* Test method for {@link world.bentobox.bentobox.listeners.PanelListenerManager#onInventoryClose(org.bukkit.event.inventory.InventoryCloseEvent)}.
346455
*/

0 commit comments

Comments
 (0)