From b51b41206c338b48bedd3b9d7003112ba5a6f010 Mon Sep 17 00:00:00 2001 From: trunksbomb Date: Sun, 22 Mar 2026 21:44:01 -0400 Subject: [PATCH] tidying up UI and UX Add dock tab for locking the dock and opening settings, where menu dock location and offset can be moved. --- .../bagtabs/client/BagTabOverlay.java | 290 ++++++++++++++++-- .../bagtabs/client/DockConfigManager.java | 235 ++++++++++++++ .../bagtabs/client/DockConfigScreen.java | 128 ++++++++ .../resources/assets/bagtabs/lang/en_us.json | 18 +- .../bagtabs/textures/gui/bag_tabs_base.png | Bin 225 -> 426 bytes .../bagtabs/textures/gui/bag_tabs_dock.png | Bin 0 -> 429 bytes .../bagtabs/textures/gui/bag_tabs_overlay.png | Bin 227 -> 452 bytes .../assets/bagtabs/textures/gui/dock_gear.png | Bin 0 -> 592 bytes .../assets/bagtabs/textures/gui/dock_lock.png | Bin 0 -> 470 bytes .../bagtabs/textures/gui/dock_unlock.png | Bin 0 -> 477 bytes 10 files changed, 640 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java create mode 100644 src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java create mode 100644 src/main/resources/assets/bagtabs/textures/gui/bag_tabs_dock.png create mode 100644 src/main/resources/assets/bagtabs/textures/gui/dock_gear.png create mode 100644 src/main/resources/assets/bagtabs/textures/gui/dock_lock.png create mode 100644 src/main/resources/assets/bagtabs/textures/gui/dock_unlock.png diff --git a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java index 4f1455b..bdd8f01 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java @@ -16,7 +16,9 @@ import java.util.List; import java.util.Set; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.core.component.DataComponents; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; @@ -32,8 +34,15 @@ import net.neoforged.neoforge.network.PacketDistributor; public final class BagTabOverlay { private static final ResourceLocation TAB_BASE_TEXTURE = BagTabs.id("textures/gui/bag_tabs_base.png"); private static final ResourceLocation TAB_OVERLAY_TEXTURE = BagTabs.id("textures/gui/bag_tabs_overlay.png"); - private static final int TAB_WIDTH = 22; - private static final int TAB_HEIGHT = 22; + private static final ResourceLocation DOCK_TEXTURE = BagTabs.id("textures/gui/bag_tabs_dock.png"); + private static final ResourceLocation LOCK_ICON_TEXTURE = BagTabs.id("textures/gui/dock_lock.png"); + private static final ResourceLocation UNLOCK_ICON_TEXTURE = BagTabs.id("textures/gui/dock_unlock.png"); + private static final ResourceLocation GEAR_ICON_TEXTURE = BagTabs.id("textures/gui/dock_gear.png"); + private static final int BASE_TAB_WIDTH = 22; + private static final int BASE_TAB_HEIGHT = 22; + private static final int BASE_DOCK_WIDTH = 18; + private static final int BASE_DOCK_HEIGHT = 22; + private static final int BASE_DOCK_ICON_SIZE = 32; private static final int TAB_GAP = 0; private static final int TAB_Y_OFFSET = -3; private static final int TAB_X_OFFSET = -6; @@ -67,9 +76,15 @@ public final class BagTabOverlay { int mouseX = event.getMouseX(); int mouseY = event.getMouseY(); ItemStack carriedStack = screen.getMenu().getCarried(); + DockLayout layout = getDockLayout(screen, player, tabs.size()); + DockControl control = getDockControl(layout); refreshInsertTargets(screen, carriedStack); + if (control != null) { + renderDockControl(guiGraphics, control, mouseX, mouseY); + } + RenderedTab draggedTab = null; for (RenderedTab tab : tabs) { if (dragState != null && tab.entry().slot() == dragState.draggedSlot()) { @@ -77,7 +92,7 @@ public final class BagTabOverlay { continue; } renderTab(guiGraphics, tab, mouseX, mouseY, carriedStack, false); - guiGraphics.renderItem(tab.entry().stack(), tab.x() + 3, tab.y() + 2); + guiGraphics.renderItem(tab.entry().stack(), tab.itemX(), tab.itemY()); } if (draggedTab != null) { @@ -86,11 +101,24 @@ public final class BagTabOverlay { guiGraphics.pose().pushPose(); guiGraphics.pose().translate(0.0F, 0.0F, 200.0F); renderTab(guiGraphics, floatingTab, mouseX, mouseY, carriedStack, true); - guiGraphics.renderItem(draggedTab.entry().stack(), dragX + 3, draggedTab.y() + 2); + guiGraphics.renderItem(draggedTab.entry().stack(), floatingTab.itemX(), floatingTab.itemY()); guiGraphics.pose().popPose(); } if (dragState == null) { + if (control != null) { + DockIcon hoveredIcon = control.hoveredIcon(mouseX, mouseY); + if (hoveredIcon != null) { + guiGraphics.renderTooltip( + Minecraft.getInstance().font, + List.of(getDockTooltip(hoveredIcon).getVisualOrderText()), + mouseX, + mouseY + ); + return; + } + } + for (RenderedTab tab : tabs) { if (!tab.isHovered(mouseX, mouseY)) { continue; @@ -118,8 +146,32 @@ public final class BagTabOverlay { } List tabs = getRenderedTabs(screen, player); + DockControl control = getDockControl(getDockLayout(screen, player, tabs.size())); + + if (event.getButton() == 0) { + if (control != null) { + DockIcon clickedIcon = control.hoveredIcon(event.getMouseX(), event.getMouseY()); + if (clickedIcon == DockIcon.LOCK) { + DockConfigManager.toggleInteractionsLocked(); + dragState = null; + pendingClick = null; + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + event.setCanceled(true); + return; + } + if (clickedIcon == DockIcon.CONFIG) { + Minecraft.getInstance().setScreen(new DockConfigScreen(screen)); + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + event.setCanceled(true); + return; + } + } + } if (event.getButton() == 1) { + if (DockConfigManager.isInteractionsLocked()) { + return; + } for (RenderedTab tab : tabs) { if (!tab.isHovered(event.getMouseX(), event.getMouseY())) { continue; @@ -143,7 +195,7 @@ public final class BagTabOverlay { for (RenderedTab tab : tabs) { if (tab.isHovered(event.getMouseX(), event.getMouseY())) { - pendingClick = new PendingClick(tab.entry().slot(), event.getMouseX() - tab.x(), event.getMouseY() - tab.y(), tab.pinned()); + pendingClick = new PendingClick(tab.entry().slot(), event.getMouseX() - tab.x(), event.getMouseY() - tab.y(), tab.pinned() && !DockConfigManager.isInteractionsLocked()); event.setCanceled(true); return; } @@ -257,11 +309,10 @@ public final class BagTabOverlay { } List renderedTabs = new ArrayList<>(); int activeBagSlot = getActiveBagSlot(screen); - int leftBound = getInventoryLeftBound(screen, player); - int rightBound = getInventoryRightBound(screen, player); - int x = leftBound + TAB_X_OFFSET; - int y = screen.getGuiTop() + screen.getYSize() + TAB_Y_OFFSET; - int maxX = rightBound - TAB_WIDTH; + DockLayout layout = getDockLayout(screen, player, bags.size()); + int x = layout.firstTabX(); + int y = layout.tabY(); + int maxX = getMaxTabX(screen, player, layout); for (BagEntry bag : bags) { if (x > maxX) { @@ -269,8 +320,18 @@ public final class BagTabOverlay { } boolean pinned = TabPinManager.isPinned(bag, bags); - renderedTabs.add(new RenderedTab(bag, x, y, bag.slot() == activeBagSlot, pinned)); - x += TAB_WIDTH + TAB_GAP; + renderedTabs.add(new RenderedTab( + bag, + x, + y, + bag.slot() == activeBagSlot, + pinned, + x + Math.max(2, Math.round(layout.scale() * 3.0F)), + y + Math.max(1, Math.round(layout.scale() * 2.0F)), + layout.tabWidth(), + layout.tabHeight() + )); + x += layout.tabWidth() + TAB_GAP; } return renderedTabs; @@ -321,24 +382,29 @@ public final class BagTabOverlay { private static void renderTab(GuiGraphics guiGraphics, RenderedTab tab, int mouseX, int mouseY, ItemStack carriedStack, boolean dragged) { boolean hovered = tab.isHovered(mouseX, mouseY); boolean selected = tab.selected(); - int color = DyedItemColor.getOrDefault(tab.entry().stack(), BagItem.DEFAULT_COLOR); - float red = ((color >> 16) & 0xFF) / 255.0F; - float green = ((color >> 8) & 0xFF) / 255.0F; - float blue = (color & 0xFF) / 255.0F; - int uOffset = (hovered || selected) ? TAB_WIDTH : 0; + float red = 1.0F; + float green = 1.0F; + float blue = 1.0F; + if (tab.entry().stack().has(DataComponents.DYED_COLOR)) { + int color = DyedItemColor.getOrDefault(tab.entry().stack(), BagItem.DEFAULT_COLOR); + red = ((color >> 16) & 0xFF) / 255.0F; + green = ((color >> 8) & 0xFF) / 255.0F; + blue = (color & 0xFF) / 255.0F; + } + int uOffset = (hovered || selected) ? BASE_TAB_WIDTH : 0; RenderSystem.setShaderColor(red, green, blue, 1.0F); - guiGraphics.blit(TAB_BASE_TEXTURE, tab.x(), tab.y(), uOffset, 0, TAB_WIDTH, TAB_HEIGHT, TAB_WIDTH * 2, TAB_HEIGHT); + blitScaled(guiGraphics, TAB_BASE_TEXTURE, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, BASE_TAB_WIDTH, BASE_TAB_HEIGHT, BASE_TAB_WIDTH * 2, BASE_TAB_HEIGHT); RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - guiGraphics.blit(TAB_OVERLAY_TEXTURE, tab.x(), tab.y(), uOffset, 0, TAB_WIDTH, TAB_HEIGHT, TAB_WIDTH * 2, TAB_HEIGHT); + blitScaled(guiGraphics, TAB_OVERLAY_TEXTURE, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, BASE_TAB_WIDTH, BASE_TAB_HEIGHT, BASE_TAB_WIDTH * 2, BASE_TAB_HEIGHT); if (dragged) { - guiGraphics.fill(tab.x() + 1, tab.y(), tab.x() + TAB_WIDTH - 1, tab.y() + 1, 0xFF000000); - guiGraphics.fill(tab.x() + 2, tab.y() + 1, tab.x() + TAB_WIDTH - 2, tab.y() + 2, 0x80FFFFFF); + guiGraphics.fill(tab.x() + 1, tab.y(), tab.x() + tab.width() - 1, tab.y() + 1, 0xFF000000); + guiGraphics.fill(tab.x() + 2, tab.y() + 1, tab.x() + tab.width() - 2, tab.y() + 2, 0x80FFFFFF); } if (selected) { - guiGraphics.fill(tab.x() + 2, tab.y() + 2, tab.x() + TAB_WIDTH - 2, tab.y() + 4, 0x90FFFFFF); + guiGraphics.fill(tab.x() + 3, tab.y() + tab.height() - 4, tab.x() + tab.width() - 3, tab.y() + tab.height() - 2, 0xFFD94A4A); } if (tab.pinned()) { @@ -349,9 +415,9 @@ public final class BagTabOverlay { if (!carriedStack.isEmpty()) { if (INSERTABLE_SLOTS.contains(tab.entry().slot())) { - renderPlusIndicator(guiGraphics, tab.x() + 15, tab.y() + 3); + renderPlusIndicator(guiGraphics, tab.x() + tab.width() - 7, tab.y() + 3); } else { - renderXIndicator(guiGraphics, tab.x() + 15, tab.y() + 3); + renderXIndicator(guiGraphics, tab.x() + tab.width() - 7, tab.y() + 3); } } } @@ -410,10 +476,16 @@ public final class BagTabOverlay { lines.add(tab.entry().stack().getHoverName()); lines.add(Component.literal("Left-click: open")); if (tab.pinned()) { - lines.add(Component.literal("Drag: reorder pinned tabs")); - lines.add(Component.literal("Right-click: unpin")); + if (!DockConfigManager.isInteractionsLocked()) { + lines.add(Component.literal("Drag: reorder pinned tabs")); + lines.add(Component.literal("Right-click: unpin")); + } } else { String failureReason = TabPinManager.getPinFailureReason(tab.entry(), tabs.stream().map(RenderedTab::entry).toList()); + if (DockConfigManager.isInteractionsLocked()) { + return lines; + } + if (failureReason == null) { lines.add(Component.literal("Right-click: pin")); } else { @@ -484,17 +556,157 @@ public final class BagTabOverlay { } } - private record RenderedTab(BagEntry entry, int x, int y, boolean selected, boolean pinned) { + private static DockLayout getDockLayout(AbstractContainerScreen screen, Player player, int bagCount) { + DockConfigManager.DockSettings dockSettings = DockConfigManager.getEffectiveSettings(getScreenKey(screen)); + float scale = dockSettings.scalePercent() / 100.0F; + int tabWidth = Math.max(16, Math.round(BASE_TAB_WIDTH * scale)); + int tabHeight = Math.max(16, Math.round(BASE_TAB_HEIGHT * scale)); + int controlWidth = Math.max(14, Math.round(BASE_DOCK_WIDTH * scale)); + int controlHeight = Math.max(16, Math.round(BASE_DOCK_HEIGHT * scale)); + int leftBound = getInventoryLeftBound(screen, player); + int rightBound = getInventoryRightBound(screen, player); + int topBound = getInventoryTopBound(screen, player); + int bottomBound = getInventoryBottomBound(screen, player); + int controlStripWidth = controlWidth; + int firstTabX; + int tabY; + + switch (dockSettings.dockSide()) { + case TOP -> { + firstTabX = leftBound + TAB_X_OFFSET + dockSettings.xOffset() + controlStripWidth; + tabY = topBound - tabHeight + dockSettings.yOffset(); + } + case LEFT -> { + firstTabX = leftBound - (bagCount * tabWidth) + dockSettings.xOffset() + controlStripWidth - controlStripWidth; + tabY = bottomBound - tabHeight + dockSettings.yOffset(); + } + case RIGHT -> { + firstTabX = rightBound + dockSettings.xOffset() + controlStripWidth; + tabY = bottomBound - tabHeight + dockSettings.yOffset(); + } + case FLOATING -> { + int totalWidth = (bagCount * tabWidth) + controlStripWidth; + int controlX = (screen.width / 2) - (totalWidth / 2) + dockSettings.xOffset(); + firstTabX = controlX + controlStripWidth; + tabY = (screen.height / 2) + dockSettings.yOffset(); + } + case BOTTOM -> { + firstTabX = leftBound + TAB_X_OFFSET + dockSettings.xOffset() + controlStripWidth; + tabY = bottomBound + TAB_Y_OFFSET + dockSettings.yOffset() + 8; + } + default -> { + firstTabX = leftBound + TAB_X_OFFSET + dockSettings.xOffset() + controlStripWidth; + tabY = bottomBound + TAB_Y_OFFSET + dockSettings.yOffset() + 8; + } + } + + return new DockLayout(firstTabX, tabY, tabWidth, tabHeight, controlWidth, controlHeight, scale); + } + + private static DockControl getDockControl(DockLayout layout) { + int y = layout.tabY() + Math.max(0, (layout.tabHeight() - layout.controlHeight()) / 2); + int x = layout.firstTabX() - layout.controlWidth() - 2; + return new DockControl(x, y, layout.controlWidth(), layout.controlHeight(), layout.scale()); + } + + private static void renderDockControl(GuiGraphics guiGraphics, DockControl control, int mouseX, int mouseY) { + DockIcon hoveredIcon = control.hoveredIcon(mouseX, mouseY); + blitScaled( + guiGraphics, + DOCK_TEXTURE, + control.x(), + control.y(), + control.width(), + control.height(), + 0, + 0, + BASE_DOCK_WIDTH, + BASE_DOCK_HEIGHT, + BASE_DOCK_WIDTH, + BASE_DOCK_HEIGHT + ); + + if (hoveredIcon != null) { + if (hoveredIcon == DockIcon.CONFIG) { + guiGraphics.fill(control.x() + 2, control.y() + 1, control.x() + control.width() - 2, control.y() + (control.height() / 2), 0x22FFFFFF); + } else { + guiGraphics.fill(control.x() + 2, control.y() + (control.height() / 2), control.x() + control.width() - 2, control.y() + control.height() - 3, 0x22FFFFFF); + } + } + + int iconSize = Math.max(6, Math.min(Math.round(8 * control.scale()), control.width() - 4)); + int iconX = control.x() + (control.width() - iconSize) / 2; + int topSectionHeight = control.height() / 2; + int bottomSectionHeight = control.height() - topSectionHeight; + int gearY = control.y() + Math.max(1, (topSectionHeight - iconSize) / 2); + int lockY = control.y() + topSectionHeight + Math.max(1, (bottomSectionHeight - iconSize) / 2); + blitScaled(guiGraphics, GEAR_ICON_TEXTURE, iconX, gearY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + blitScaled(guiGraphics, DockConfigManager.isInteractionsLocked() ? UNLOCK_ICON_TEXTURE : LOCK_ICON_TEXTURE, iconX, lockY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + } + + private static Component getDockTooltip(DockIcon icon) { + return switch (icon) { + case CONFIG -> BagTabs.translation("dock.open"); + case LOCK -> DockConfigManager.isInteractionsLocked() ? BagTabs.translation("dock.unlock") : BagTabs.translation("dock.lock"); + }; + } + + private static void blitScaled( + GuiGraphics guiGraphics, + ResourceLocation texture, + int x, + int y, + int width, + int height, + int u, + int v, + int regionWidth, + int regionHeight, + int textureWidth, + int textureHeight + ) { + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(x, y, 0.0F); + guiGraphics.pose().scale(width / (float) regionWidth, height / (float) regionHeight, 1.0F); + guiGraphics.blit(texture, 0, 0, u, v, regionWidth, regionHeight, textureWidth, textureHeight); + guiGraphics.pose().popPose(); + } + + private static int getInventoryTopBound(AbstractContainerScreen screen, Player player) { + return screen.getGuiTop() + getPlayerInventorySlots(screen, player).stream() + .mapToInt(slot -> slot.y) + .min() + .orElse(84); + } + + private static int getInventoryBottomBound(AbstractContainerScreen screen, Player player) { + return screen.getGuiTop() + getPlayerInventorySlots(screen, player).stream() + .mapToInt(slot -> slot.y + 16) + .max() + .orElse(screen.getYSize()); + } + + private static int getMaxTabX(AbstractContainerScreen screen, Player player, DockLayout layout) { + int guiRight = screen.getGuiLeft() + screen.getXSize(); + int inventoryRight = getInventoryRightBound(screen, player); + return Math.min(guiRight, inventoryRight) - layout.tabWidth(); + } + + private static String getScreenKey(Screen screen) { + return screen.getClass().getName(); + } + + private record RenderedTab(BagEntry entry, int x, int y, boolean selected, boolean pinned, int itemX, int itemY, int width, int height) { private boolean isHovered(double mouseX, double mouseY) { - return mouseX >= this.x && mouseX < this.x + TAB_WIDTH && mouseY >= this.y && mouseY < this.y + TAB_HEIGHT; + return mouseX >= this.x && mouseX < this.x + this.width && mouseY >= this.y && mouseY < this.y + this.height; } private double centerX() { - return this.x + (TAB_WIDTH / 2.0D); + return this.x + (this.width / 2.0D); } private RenderedTab withPosition(int newX, int newY) { - return new RenderedTab(this.entry, newX, newY, this.selected, this.pinned); + return new RenderedTab(this.entry, newX, newY, this.selected, this.pinned, newX + (this.itemX - this.x), newY + (this.itemY - this.y), this.width, this.height); } } @@ -503,4 +715,22 @@ public final class BagTabOverlay { private record DragState(int draggedSlot, double grabOffsetX, List previewOrder) { } + + private record DockLayout(int firstTabX, int tabY, int tabWidth, int tabHeight, int controlWidth, int controlHeight, float scale) { + } + + private record DockControl(int x, int y, int width, int height, float scale) { + private DockIcon hoveredIcon(double mouseX, double mouseY) { + if (mouseX < this.x || mouseX >= this.x + this.width || mouseY < this.y || mouseY >= this.y + this.height) { + return null; + } + int split = this.y + (this.height / 2); + return mouseY < split ? DockIcon.CONFIG : DockIcon.LOCK; + } + } + + private enum DockIcon { + CONFIG, + LOCK, + } } diff --git a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java new file mode 100644 index 0000000..4c179d3 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java @@ -0,0 +1,235 @@ +package com.trunksbomb.bagtabs.client; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.trunksbomb.bagtabs.BagTabs; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ServerData; + +public final class DockConfigManager { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private static SessionSettings cachedSettings = new SessionSettings(); + private static Path cachedPath; + + private DockConfigManager() { + } + + public static DockSettings getEffectiveSettings(String screenKey) { + SessionSettings settings = getSessionSettings(); + DockSettings override = settings.screenOverrides.get(screenKey); + return override == null ? settings.globalDefault : override; + } + + public static DockSettings getEditableSettings(String screenKey, boolean editOverride) { + SessionSettings settings = getSessionSettings(); + if (!editOverride) { + return settings.globalDefault; + } + + return settings.screenOverrides.getOrDefault(screenKey, settings.globalDefault.copy()); + } + + public static boolean hasOverride(String screenKey) { + return getSessionSettings().screenOverrides.containsKey(screenKey); + } + + public static void setEditableSettings(String screenKey, boolean editOverride, DockSettings dockSettings) { + SessionSettings settings = getSessionSettings(); + if (editOverride) { + settings.screenOverrides.put(screenKey, dockSettings.copy()); + } else { + settings.globalDefault = dockSettings.copy(); + } + save(); + } + + public static void clearOverride(String screenKey) { + SessionSettings settings = getSessionSettings(); + settings.screenOverrides.remove(screenKey); + save(); + } + + public static boolean isInteractionsLocked() { + return getSessionSettings().interactionsLocked; + } + + public static void setInteractionsLocked(boolean locked) { + SessionSettings settings = getSessionSettings(); + settings.interactionsLocked = locked; + save(); + } + + public static void toggleInteractionsLocked() { + setInteractionsLocked(!isInteractionsLocked()); + } + + public static int getRememberedPage(String screenKey) { + return getSessionSettings().rememberedPages.getOrDefault(screenKey, 0); + } + + public static void setRememberedPage(String screenKey, int page) { + SessionSettings settings = getSessionSettings(); + settings.rememberedPages.put(screenKey, Math.max(0, page)); + save(); + } + + private static SessionSettings getSessionSettings() { + Path path = getConfigPath(); + if (!Objects.equals(path, cachedPath)) { + cachedPath = path; + cachedSettings = load(path); + } + return cachedSettings; + } + + private static SessionSettings load(Path path) { + if (!Files.exists(path)) { + return new SessionSettings(); + } + + try (Reader reader = Files.newBufferedReader(path)) { + SessionSettings settings = GSON.fromJson(reader, SessionSettings.class); + return settings == null ? new SessionSettings() : settings.normalize(); + } catch (IOException exception) { + BagTabs.LOGGER.warn("Failed to load dock config", exception); + return new SessionSettings(); + } + } + + private static void save() { + if (cachedPath == null) { + cachedPath = getConfigPath(); + } + + try { + Files.createDirectories(cachedPath.getParent()); + try (Writer writer = Files.newBufferedWriter(cachedPath)) { + GSON.toJson(cachedSettings, writer); + } + } catch (IOException exception) { + BagTabs.LOGGER.warn("Failed to save dock config", exception); + } + } + + private static Path getConfigPath() { + Minecraft minecraft = Minecraft.getInstance(); + String playerKey = minecraft.player != null ? minecraft.player.getUUID().toString() : "unknown-player"; + String worldKey = "default"; + if (minecraft.hasSingleplayerServer() && minecraft.getSingleplayerServer() != null) { + worldKey = "singleplayer-" + sanitize(minecraft.getSingleplayerServer().getWorldData().getLevelName()); + } else { + ServerData currentServer = minecraft.getCurrentServer(); + if (currentServer != null) { + worldKey = "server-" + sanitize(currentServer.ip); + } + } + + return minecraft.gameDirectory.toPath() + .resolve("config") + .resolve("bagtabs") + .resolve(playerKey) + .resolve(worldKey + ".json"); + } + + private static String sanitize(String value) { + return value == null ? "unknown" : value.replaceAll("[^a-zA-Z0-9._-]", "_"); + } + + public enum DockSide { + BOTTOM, + TOP, + LEFT, + RIGHT, + FLOATING; + + public DockSide next() { + DockSide[] values = values(); + return values[(ordinal() + 1) % values.length]; + } + } + + public static final class DockSettings { + private DockSide dockSide = DockSide.BOTTOM; + private int xOffset = 0; + private int yOffset = 0; + private int scalePercent = 100; + + public DockSide dockSide() { + return dockSide; + } + + public int xOffset() { + return xOffset; + } + + public int yOffset() { + return yOffset; + } + + public int scalePercent() { + return scalePercent; + } + + public DockSettings withDockSide(DockSide nextDockSide) { + DockSettings copy = copy(); + copy.dockSide = nextDockSide; + return copy; + } + + public DockSettings withXOffset(int nextXOffset) { + DockSettings copy = copy(); + copy.xOffset = nextXOffset; + return copy; + } + + public DockSettings withYOffset(int nextYOffset) { + DockSettings copy = copy(); + copy.yOffset = nextYOffset; + return copy; + } + + public DockSettings withScalePercent(int nextScalePercent) { + DockSettings copy = copy(); + copy.scalePercent = Math.max(75, Math.min(150, nextScalePercent)); + return copy; + } + + public DockSettings copy() { + DockSettings copy = new DockSettings(); + copy.dockSide = this.dockSide; + copy.xOffset = this.xOffset; + copy.yOffset = this.yOffset; + copy.scalePercent = this.scalePercent; + return copy; + } + } + + private static final class SessionSettings { + private boolean interactionsLocked = false; + private DockSettings globalDefault = new DockSettings(); + private Map screenOverrides = new HashMap<>(); + private Map rememberedPages = new HashMap<>(); + + private SessionSettings normalize() { + if (globalDefault == null) { + globalDefault = new DockSettings(); + } + if (screenOverrides == null) { + screenOverrides = new HashMap<>(); + } + if (rememberedPages == null) { + rememberedPages = new HashMap<>(); + } + return this; + } + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java new file mode 100644 index 0000000..224f4f7 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java @@ -0,0 +1,128 @@ +package com.trunksbomb.bagtabs.client; + +import com.trunksbomb.bagtabs.BagTabs; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class DockConfigScreen extends Screen { + private final Screen parent; + private final String screenKey; + private boolean editOverride; + private DockConfigManager.DockSettings dockSettings; + + private Button targetButton; + private Button sideButton; + private Button xMinusButton; + private Button xPlusButton; + private Button yMinusButton; + private Button yPlusButton; + private Button scaleMinusButton; + private Button scalePlusButton; + private Button resetButton; + + public DockConfigScreen(Screen parent) { + super(BagTabs.translation("dock.title")); + this.parent = parent; + this.screenKey = parent.getClass().getName(); + this.editOverride = DockConfigManager.hasOverride(this.screenKey); + this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, this.editOverride).copy(); + } + + @Override + protected void init() { + int centerX = this.width / 2; + int top = this.height / 2 - 70; + + this.targetButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> { + saveCurrent(); + this.editOverride = !this.editOverride; + this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, this.editOverride).copy(); + syncLabels(); + }).bounds(centerX - 90, top, 180, 20).build()); + + this.sideButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> { + this.dockSettings = this.dockSettings.withDockSide(this.dockSettings.dockSide().next()); + saveCurrent(); + syncLabels(); + }).bounds(centerX - 90, top + 26, 180, 20).build()); + + this.xMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustX(-4)) + .bounds(centerX - 90, top + 56, 20, 20).build()); + this.xPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustX(4)) + .bounds(centerX + 70, top + 56, 20, 20).build()); + + this.yMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustY(-4)) + .bounds(centerX - 90, top + 82, 20, 20).build()); + this.yPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustY(4)) + .bounds(centerX + 70, top + 82, 20, 20).build()); + + this.scaleMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustScale(-5)) + .bounds(centerX - 90, top + 108, 20, 20).build()); + this.scalePlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustScale(5)) + .bounds(centerX + 70, top + 108, 20, 20).build()); + + this.resetButton = this.addRenderableWidget(Button.builder(BagTabs.translation("dock.reset"), button -> { + DockConfigManager.clearOverride(this.screenKey); + this.editOverride = false; + this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, false).copy(); + syncLabels(); + }).bounds(centerX - 90, top + 138, 180, 20).build()); + + this.addRenderableWidget(Button.builder(BagTabs.translation("dock.done"), button -> onClose()) + .bounds(centerX - 90, top + 164, 180, 20).build()); + + syncLabels(); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + super.render(guiGraphics, mouseX, mouseY, partialTick); + + int centerX = this.width / 2; + int top = this.height / 2 - 70; + guiGraphics.drawCenteredString(this.font, this.title, centerX, top - 18, 0xFFFFFF); + guiGraphics.drawCenteredString(this.font, Component.literal("X Offset: " + this.dockSettings.xOffset()), centerX, top + 62, 0xFFFFFF); + guiGraphics.drawCenteredString(this.font, Component.literal("Y Offset: " + this.dockSettings.yOffset()), centerX, top + 88, 0xFFFFFF); + guiGraphics.drawCenteredString(this.font, Component.literal("Tab Size: " + this.dockSettings.scalePercent() + "%"), centerX, top + 114, 0xFFFFFF); + } + + @Override + public void onClose() { + saveCurrent(); + Minecraft.getInstance().setScreen(this.parent); + } + + private void adjustX(int delta) { + this.dockSettings = this.dockSettings.withXOffset(this.dockSettings.xOffset() + delta); + saveCurrent(); + syncLabels(); + } + + private void adjustY(int delta) { + this.dockSettings = this.dockSettings.withYOffset(this.dockSettings.yOffset() + delta); + saveCurrent(); + syncLabels(); + } + + private void adjustScale(int delta) { + this.dockSettings = this.dockSettings.withScalePercent(this.dockSettings.scalePercent() + delta); + saveCurrent(); + syncLabels(); + } + + private void syncLabels() { + this.targetButton.setMessage(this.editOverride + ? BagTabs.translation("dock.target.override") + : BagTabs.translation("dock.target.global")); + this.sideButton.setMessage(BagTabs.translation("dock.side." + this.dockSettings.dockSide().name().toLowerCase())); + this.resetButton.active = this.editOverride || DockConfigManager.hasOverride(this.screenKey); + } + + private void saveCurrent() { + DockConfigManager.setEditableSettings(this.screenKey, this.editOverride, this.dockSettings); + } +} diff --git a/src/main/resources/assets/bagtabs/lang/en_us.json b/src/main/resources/assets/bagtabs/lang/en_us.json index 52e17fc..02fc7e7 100644 --- a/src/main/resources/assets/bagtabs/lang/en_us.json +++ b/src/main/resources/assets/bagtabs/lang/en_us.json @@ -7,5 +7,21 @@ "bagtabs.tooltip.click_to_open": "Open from your inventory tabs", "bagtabs.gui.bag_namer.name": "New Name", "bagtabs.gui.bag_namer.placeholder": "Leave blank to clear", - "bagtabs.gui.bag_namer.rename": "Rename" + "bagtabs.gui.bag_namer.rename": "Rename", + "bagtabs.dock.title": "Tab Dock Settings", + "bagtabs.dock.done": "Done", + "bagtabs.dock.reset": "Reset Screen Override", + "bagtabs.dock.target.global": "Editing: Global Default", + "bagtabs.dock.target.override": "Editing: This Screen Override", + "bagtabs.dock.x_offset": "X Offset", + "bagtabs.dock.y_offset": "Y Offset", + "bagtabs.dock.scale": "Tab Size", + "bagtabs.dock.open": "Open dock settings", + "bagtabs.dock.lock": "Lock tab interactions", + "bagtabs.dock.unlock": "Unlock tab interactions", + "bagtabs.dock.side.bottom": "Dock: Bottom", + "bagtabs.dock.side.top": "Dock: Top", + "bagtabs.dock.side.left": "Dock: Left", + "bagtabs.dock.side.right": "Dock: Right", + "bagtabs.dock.side.floating": "Dock: Floating" } diff --git a/src/main/resources/assets/bagtabs/textures/gui/bag_tabs_base.png b/src/main/resources/assets/bagtabs/textures/gui/bag_tabs_base.png index 68d02de2041887974370d4fd40fb7695bda52a23..4e5a1b52d408877561a6e2e10edd12af0362a0cc 100644 GIT binary patch literal 426 zcmeAS@N?(olHy`uVBq!ia0vp^IzTMO!3-pOcK7`NQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`IRkt`T#p?)_Vw#mAWy&Zm>`g1E(!7r{{Nrh`YB$ZS_aMnkH}&M2EIce z%qXhi)c_QfEOCt}3C>R|DNig)We7;j%q!9Ja}7~2)icyHH0BQ?S^&C^qh zfdj~4WsqWIWncudynt95$_6=EgOM35&IDu|GBPoM+zUjgF)84cZSmMHts`gv7+y9A20k iEM4{gX1IL$dQa)4yv(M2cOU41O!9Q~b6Mw<&;$U3vRwB7 delta 179 zcmZ3*{E%^iBnKM<1HFMs73&K;B8wRq_zr_G z1P*`t?|x4|&u`%sXA@O5?}JQhHy!`6VpXN{SC{hh(wk-zopr E0Ouf5t^fc4 diff --git a/src/main/resources/assets/bagtabs/textures/gui/bag_tabs_dock.png b/src/main/resources/assets/bagtabs/textures/gui/bag_tabs_dock.png new file mode 100644 index 0000000000000000000000000000000000000000..d864a1e2d59be432c7f89f0684ee85db5683693e GIT binary patch literal 429 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9W!3-pSHQE#dDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(ehe`~f~8u0Z<#|NqC19h)|78jv0S;3`NvYe|q_Fi`wI!|!c;!9W4d0*^?b zfqVx*m@(^GCnr!)vcxr_Bsf2>N!K-4Mt|LI1`X<$jHPX2&AKcIJ2DvES>{ogTVeW zMurzaC&FlSTNxODk`ve&Sb%B`jEoH!7eLGf*~z*9Viw4wAixAPnF*{i$kGDHg6c9f zFaXJ36aAI$yoJ{t$kg?8aSYK2etW_Y=r946gHg}^=er&1)V~@j9{M)=;|u$poDZjY gys&<~`He|A1NTFw9_0g*Kq187>FVdQ&MBb@0JYg*PXGV_ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bagtabs/textures/gui/bag_tabs_overlay.png b/src/main/resources/assets/bagtabs/textures/gui/bag_tabs_overlay.png index d71787b28be1e332f464c4aa95ceda8ec776b7fa..79254ad1d48c1922f29c0077d6c020ac783d22c7 100644 GIT binary patch literal 452 zcmeAS@N?(olHy`uVBq!ia0vp^IzTMO!3HFEr`DYUQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4uLSEsD@VqP*AeOHKHUqKdq!Zu_%=xATcwqM9 zpvg>Nl|hykKo(S&p@9KNHs)>OFU}XMb_1COo-U3d5$3LGx z5)+k#%0HK)+>VBj?7Lzt9Lc{ zd;Ct@*RMGl1UJg;P4Ag_`q$#M_uPN7U7fzAQh%p`Ca0V7+fa}@JYD@<);T3K0RVau Ba|!?e delta 162 zcmX@Y{FrfqXZ>LY2EN0L$MaXDFfcGwc)B=-M7VD~AI)_@fyedWn=7$9?@r5#=cv<| z$^2cPM@8w;j;D1sPf-f?eD&1#z$-`I^l2)4d!zaF*S%z5GM kU-ECHbFNiv-?d$MXL2?76|<$TKu0imy85}Sb4q9e0MLF#%>V!Z diff --git a/src/main/resources/assets/bagtabs/textures/gui/dock_gear.png b/src/main/resources/assets/bagtabs/textures/gui/dock_gear.png new file mode 100644 index 0000000000000000000000000000000000000000..3de1d2bc81d157e769c973042f5eca3e3b7307ec GIT binary patch literal 592 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCim11AIbU-Q4_U&t6zj(Uh7}02J%tU0wmCSWANZg8u^n!|!c;!9W4d0*}aI z1_r)EAj~ML;ne^Xlq_+LC<)F_D=AMbN@WO0%*-p%^K%VRFx4~EGc@et5IYA{v?Ddb zGtJXei-7~kVP%kFWMyCkvb=y;8p;MaU4xMsEY1XE8!|F62mpvg>Nl|hykKo(S& zp@9KNHs)>OFU}XMb_1CgJY5_^Ec~ZVj^txe@y!82k{>?I@Rth zP@JtJmh|pv!^B1_4YtVB<|(?Nq4PQ0n3RHp!b>d=Feo%|?qyDrU(+sk)a-*0vz1_ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bagtabs/textures/gui/dock_lock.png b/src/main/resources/assets/bagtabs/textures/gui/dock_lock.png new file mode 100644 index 0000000000000000000000000000000000000000..95e291192ceb60a54da4c962c2badef1f8e58e3d GIT binary patch literal 470 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G|s0G|+7AiaP8{`&g*|Ns9xI5@oSH2nT38z?Buua^a+I7)*2f`L*Xz;Lg5 zZzNERv%n*=n1O-s5C}7hYIrpO1tm*dBT9nv(@M${i&7Z^5;OBk^!!{y6ioFD^$ZQW zIK<8Y741lk@J#dc)MDTOa#$Ip7+Dz@fh;c|mWHxH-q2uV28%NR*@lcv41z#93Wzh? zS-|2sKsE?`Dr01L0rV}5Mz@uL0Vp|voq+|Y*1*WvfN=rDT#%it3m|4q1F}JY31~7C zSY?o<1&{^RWoTdkl8t$r_>1$!s@*`Qm#2$kh(>U0Um)KB1rBHH30424^%VlI=u|&k zT&bz?i!n^9kjqA>l5?U#s_X6P4oYX=|C%vTY>C3kSA9zvWq2Q~FT8srPV>rqHT{zd VSiW34G#L~(44$rjF6*2UngFkEa?k(( literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bagtabs/textures/gui/dock_unlock.png b/src/main/resources/assets/bagtabs/textures/gui/dock_unlock.png new file mode 100644 index 0000000000000000000000000000000000000000..a145510bfd89fb02c3f8c7dda3811b4664ecb210 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G|s0G|+7ApQUU|NZ;-*VorOI5@oSH2nT38z@+;ZzchxI7)*2f`JM^fZ<;A z-bkPrXMsm#F#`kNArNL1)$nQn3QCr^MwA5SrKPh# zafqD*D%z16;hE;?sl~tnIesvJDxT7zBZI6cA^& zvw+2OfNT)>RL02g0_a;9jczLg15k1TI|B<)t$~rT0pkLQxga}P7eLIK24sT(6VPNP zu*x7y3m^-s%h138BpdTK@fYWdRl9-AKu;IP5RKs0{y@G13LMVdUgiI#^{32S`1{81 zj3-Nzj+8cdedL<4^Y@C)Y4T3ZN6tu_?T(nqB$1UetwX`trfEr})i$;QlZO4r(vN@h cO8&d8kU3V5C2vvMf<%x-p00i_>zopr04HB}Z~y=R literal 0 HcmV?d00001