diff --git a/.gitignore b/.gitignore index 87240257..1d6848fd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ changelog.html \.classpath \.project \.settings/ -*.launch +*.launch \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f8fbabf3..2ef4b8cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ generate_javadocs_jar = false # Mod Information # HIGHLY RECOMMEND complying with SemVer for mod_version: https://semver.org/ -mod_version = 4.29.15 +mod_version = 4.30.0 root_package = mezz mod_id = jei mod_name = Had Enough Items diff --git a/src/api/java/mezz/jei/api/ICollapsibleGroupRegistry.java b/src/api/java/mezz/jei/api/ICollapsibleGroupRegistry.java new file mode 100644 index 00000000..6674768d --- /dev/null +++ b/src/api/java/mezz/jei/api/ICollapsibleGroupRegistry.java @@ -0,0 +1,110 @@ +package mezz.jei.api; + +import mezz.jei.api.ingredients.IIngredientRegistry; +import mezz.jei.api.recipe.IIngredientType; + +import java.util.function.Predicate; + +/** + * Registry for mods to define collapsible groups in the HEI ingredient list. + * They can be toggled on/off by the user but cannot be edited or deleted. + * + * Obtain an instance via {@link IModPlugin#registerCollapsibleGroups(ICollapsibleGroupRegistry)}. + * + * EXAMPLE 1 — Group with filtered items: + * + * @Override + * public void registerCollapsibleGroups(ICollapsibleGroupRegistry registry) { + * registry.newGroup("matteroverdrive:colored_floor_tile", "tile.decorative.floor_tile.name") + * .addAny(VanillaTypes.ITEM, + * stack -> Block.getBlockFromItem(stack.getItem()) == MatterOverdrive.BLOCKS.decorative_floor_tile); + * } + * + * + * EXAMPLE 2 — Group mixing exact items, fluids, and all of a type: + * + * @Override + * public void registerCollapsibleGroups(ICollapsibleGroupRegistry registry) { + * registry.newGroup("mymod:my_group", "group.mymod.my_group") + * .add(new ItemStack(MyMod.Items.PICKAXE)) // add one exact item + * .add(new ItemStack(MyMod.Blocks.MY_BLOCK, 1, 1)) // add a specific block variant (meta=1) + * .add(FluidRegistry.getFluidStack("water", 1000)) // add an exact fluid + * .addAllOf(MyMod.CUSTOM_INGREDIENT_TYPE); // add every ingredient of a custom type + * } + * + * + * EXAMPLE 3 — Multiple groups from one plugin: + * + * @Override + * public void registerCollapsibleGroups(ICollapsibleGroupRegistry registry) { + * registry.newGroup("mymod:ores", "group.mymod.ores") + * .addAny(VanillaTypes.ITEM, stack -> isOre(stack)); + * + * registry.newGroup("mymod:gems", "group.mymod.gems") + * .addAny(VanillaTypes.ITEM, stack -> isGem(stack)); + * } + * + * + * @since HEI 4.30.0 + */ +public interface ICollapsibleGroupRegistry { + + /** + * Creates (or retrieves) a mod collapsible group builder. + * + * Multiple calls with the same {@code id} return a builder for the same logical group. + * + * @param id Unique group ID, should be namespaced with your mod ID. + * @param langKey Unlocalized translation key for the group name (e.g. {@code "tile.mymod.name"}). + */ + CollapsibleGroupBuilder newGroup(String id, String langKey); + + interface CollapsibleGroupBuilder { + /** + * Add one exact ingredient to the group. + * + * The backend resolves this ingredient to its unique id and matches by id. + * Works with ItemStacks, FluidStacks, or any registered ingredient type. + * + * @param ingredient the ingredient to add (e.g. new ItemStack(Items.APPLE)) + * @return this builder for chaining + */ + CollapsibleGroupBuilder add(Object ingredient); + + /** + * Add multiple exact ingredients to the group. Equivalent to calling {@link #add(Object)} + * for each element in order. + * + * @param ingredients varargs list of ingredients to add + * @return this builder for chaining + */ + CollapsibleGroupBuilder add(Object... ingredients); + + /** + * Add every ingredient of the given type(s) to the group — no filtering applied. + * + * Use this for custom ingredient types where you want to collapse all registered + * instances into one group. Third-party ingredient types registered via + * {@link IIngredientRegistry} are supported. + * + *

Note: passing {@code VanillaTypes.ITEM} or {@code VanillaTypes.FLUID} will match + * every item or fluid in the game. Prefer {@link #addAny} with a predicate + * when you only want a subset. + * + * @param types the ingredient type(s) whose every instance should be included + * @return this builder for chaining + */ + CollapsibleGroupBuilder addAllOf(IIngredientType... types); + + /** + * Add any ingredient of a given type that matches the provided predicate filter. + * + * Use this when you need to match items by condition (e.g. "all tools", "all ores", etc). + * + * @param type The ingredient type (e.g. {@code VanillaTypes.ITEM}). + * @param filter Predicate receiving a fully-typed {@code V} — no casting needed. + * @return this builder for chaining + */ + CollapsibleGroupBuilder addAny(IIngredientType type, Predicate filter); + } +} diff --git a/src/api/java/mezz/jei/api/IModPlugin.java b/src/api/java/mezz/jei/api/IModPlugin.java index 8e93e240..b9b193e0 100644 --- a/src/api/java/mezz/jei/api/IModPlugin.java +++ b/src/api/java/mezz/jei/api/IModPlugin.java @@ -48,6 +48,22 @@ default void registerCategories(IRecipeCategoryRegistration registry) { } + /** + * Register collapsible ingredient groups provided by this mod. + * These appear in the "Manage Groups" screen tagged as "Mod" and can be toggled + * by the user but are not editable or deletable. + *

+ * Use {@link ICollapsibleGroupRegistry#newGroup(String, String)} to create a builder + * and call its methods to define the group's members. See {@link ICollapsibleGroupRegistry} + * for full usage examples. + * + * @param registry the registry used to create collapsible group builders + * @since HEI 4.30.0 + */ + default void registerCollapsibleGroups(ICollapsibleGroupRegistry registry) { + + } + /** * Register this mod plugin with the mod registry. */ diff --git a/src/main/java/mezz/jei/Internal.java b/src/main/java/mezz/jei/Internal.java index ee87b787..d63f0449 100644 --- a/src/main/java/mezz/jei/Internal.java +++ b/src/main/java/mezz/jei/Internal.java @@ -5,6 +5,7 @@ import mezz.jei.bookmarks.BookmarkList; import mezz.jei.color.ColorNamer; import mezz.jei.gui.GuiEventHandler; +import mezz.jei.ingredients.CollapsedStackRegistry; import mezz.jei.ingredients.IngredientFilter; import mezz.jei.ingredients.IngredientRegistry; import mezz.jei.input.InputHandler; @@ -40,6 +41,8 @@ public final class Internal { private static InputHandler inputHandler; @Nullable private static BookmarkList bookmarkList; + @Nullable + private static CollapsedStackRegistry collapsedStackRegistry; private Internal() { @@ -151,4 +154,16 @@ public static BookmarkList getBookmarkList() { Preconditions.checkState(bookmarkList != null, "Bookmark List has not been created yet."); return bookmarkList; } + + public static CollapsedStackRegistry getCollapsedStackRegistry() { + if (collapsedStackRegistry == null) { + collapsedStackRegistry = CollapsedStackRegistry.getInstance(); + } + return collapsedStackRegistry; + } + + public static void setCollapsedStackRegistry(CollapsedStackRegistry registry) { + Internal.collapsedStackRegistry = registry; + CollapsedStackRegistry.setInstance(registry); + } } diff --git a/src/main/java/mezz/jei/config/Config.java b/src/main/java/mezz/jei/config/Config.java index de12b7b0..31f1b9d7 100644 --- a/src/main/java/mezz/jei/config/Config.java +++ b/src/main/java/mezz/jei/config/Config.java @@ -16,6 +16,7 @@ import mezz.jei.startup.ForgeModIdHelper; import mezz.jei.startup.IModIdHelper; import mezz.jei.util.GiveMode; +import mezz.jei.util.CollapsedClickAction; import mezz.jei.util.Log; import mezz.jei.util.Translator; import net.minecraft.init.Items; @@ -39,6 +40,7 @@ import java.io.IOException; import java.util.*; import java.util.List; +import java.util.ArrayList; public final class Config { private static final String configKeyPrefix = "config.jei"; @@ -49,6 +51,7 @@ public final class Config { public static final String CATEGORY_RENDERING = "rendering"; public static final String CATEGORY_MISC = "misc"; public static final String CATEGORY_CATEGORY = "category"; + public static final String CATEGORY_COLLAPSIBLE = "collapsible"; public static final String defaultModNameFormatFriendly = "blue italic"; public static final int smallestNumColumns = 4; @@ -65,6 +68,8 @@ public final class Config { @Nullable private static LocalizedConfiguration searchColorsConfig; @Nullable + private static CustomGroupsConfig customGroupsConfig; + @Nullable private static File bookmarkFile; @Nullable private static File favoriteFile; @@ -89,6 +94,39 @@ public static boolean isOverlayEnabled() { KeyBindings.toggleOverlay.getKeyCode() == 0; // if there is no key binding to enable it, don't allow the overlay to be disabled } + public static boolean isCollapsibleGroupsEnabled() { + return values.collapsibleGroupsEnabled; + } + + public static boolean isCollapseOnClose() { + return values.collapseOnClose; + } + + public static CollapsedClickAction getCollapsedClickAction() { + return values.collapsedClickAction; + } + + @Nullable + public static CustomGroupsConfig getCustomGroupsConfig() { + return customGroupsConfig; + } + + public static Set getDisabledGroups() { + return values.disabledGroups; + } + + public static void saveDisabledGroups(Set disabledGroups) { + values.disabledGroups.clear(); + values.disabledGroups.addAll(disabledGroups); + if (config != null) { + Property property = config.get(CATEGORY_COLLAPSIBLE, "disabledGroups", new String[]{}); + property.set(disabledGroups.toArray(new String[0])); + if (config.hasChanged()) { + config.save(); + } + } + } + public static void toggleOverlayEnabled() { values.overlayEnabled = !values.overlayEnabled; @@ -412,6 +450,9 @@ public static void preInit(FMLPreInitializationEvent event) { itemBlacklistConfig = new LocalizedConfiguration(configKeyPrefix, itemBlacklistConfigFile, "0.1.0"); searchColorsConfig = new LocalizedConfiguration(configKeyPrefix, searchColorsConfigFile, "0.1.0"); + customGroupsConfig = new CustomGroupsConfig(jeiConfigurationDir); + customGroupsConfig.load(); + syncConfig(); syncItemBlacklistConfig(); syncSearchColorsConfig(); @@ -448,6 +489,10 @@ private static boolean syncConfig() { config.addCategory(CATEGORY_SEARCH); config.addCategory(CATEGORY_ADVANCED); config.addCategory(CATEGORY_MISC); + config.addCategory(CATEGORY_COLLAPSIBLE); + // Override collapsible category lang keys from config.jei.* to config.hei.* + config.setCategoryLanguageKey(CATEGORY_COLLAPSIBLE, "config.hei.collapsible"); + config.setCategoryComment(CATEGORY_COLLAPSIBLE, Translator.translateToLocal("config.hei.collapsible.comment")); ConfigCategory modeCategory = config.getCategory("mode"); if (modeCategory != null) { @@ -525,7 +570,13 @@ private static boolean syncConfig() { values.tooltipShowRecipeBy = config.getBoolean(CATEGORY_MISC, "tooltipShowRecipeBy", defaultValues.tooltipShowRecipeBy); - values.showHiddenIngredientsInCreative = config.getBoolean(CATEGORY_MISC, "showHiddenIngredientsInCreative", defaultValues.showHiddenIngredientsInCreative); + { + boolean prev = values.showHiddenIngredientsInCreative; + values.showHiddenIngredientsInCreative = config.getBoolean(CATEGORY_MISC, "showHiddenIngredientsInCreative", defaultValues.showHiddenIngredientsInCreative); + if (prev != values.showHiddenIngredientsInCreative) { + needsReload = true; + } + } values.skipShowingProgressBar = config.getBoolean(CATEGORY_MISC, "skipShowingProgressBar", defaultValues.skipShowingProgressBar); @@ -533,6 +584,54 @@ private static boolean syncConfig() { values.hideBottomLeftCornerBookmarkButton = config.getBoolean(CATEGORY_MISC, "hideBottomLeftCornerBookmarkButton", defaultValues.hideBottomLeftCornerBookmarkButton); + { + boolean prev = values.collapsibleGroupsEnabled; + values.collapsibleGroupsEnabled = config.getBoolean(CATEGORY_COLLAPSIBLE, "collapsibleGroupsEnabled", defaultValues.collapsibleGroupsEnabled); + if (prev != values.collapsibleGroupsEnabled) { + needsReload = true; + } + } + + values.collapseOnClose = config.getBoolean(CATEGORY_COLLAPSIBLE, "collapseOnClose", defaultValues.collapseOnClose); + + values.collapsedClickAction = config.getEnum("collapsedClickAction", CATEGORY_COLLAPSIBLE, defaultValues.collapsedClickAction, CollapsedClickAction.values()); + + // Override property lang keys and comments from config.jei.collapsible.* to config.hei.collapsible.* + { + String heiPrefix = "config.hei.collapsible."; + + Property collapsibleGroupsEnabledProp = config.get(CATEGORY_COLLAPSIBLE, "collapsibleGroupsEnabled", defaultValues.collapsibleGroupsEnabled); + collapsibleGroupsEnabledProp.setLanguageKey(heiPrefix + "collapsibleGroupsEnabled"); + collapsibleGroupsEnabledProp.setComment(Translator.translateToLocal(heiPrefix + "collapsibleGroupsEnabled.comment")); + + Property collapseOnCloseProp = config.get(CATEGORY_COLLAPSIBLE, "collapseOnClose", defaultValues.collapseOnClose); + collapseOnCloseProp.setLanguageKey(heiPrefix + "collapseOnClose"); + collapseOnCloseProp.setComment(Translator.translateToLocal(heiPrefix + "collapseOnClose.comment")); + + Property collapsedClickActionProp = config.get(CATEGORY_COLLAPSIBLE, "collapsedClickAction", defaultValues.collapsedClickAction.name()); + collapsedClickActionProp.setLanguageKey(heiPrefix + "collapsedClickAction"); + String defaultLocalized = Translator.translateToLocal("config.jei.default"); + String validLocalized = Translator.translateToLocal("config.jei.valid"); + collapsedClickActionProp.setComment(Translator.translateToLocal(heiPrefix + "collapsedClickAction.comment") + + "\n[" + defaultLocalized + ": " + defaultValues.collapsedClickAction.name().toLowerCase(Locale.ENGLISH) + "]" + + "\n[" + validLocalized + ": " + Arrays.toString(collapsedClickActionProp.getValidValues()) + ']'); + } + + // Explicit property order so the GUI shows collapsibleGroupsEnabled first, then collapseOnClose. + List collapsibleOrder = new ArrayList<>(); + collapsibleOrder.add("collapsibleGroupsEnabled"); + collapsibleOrder.add("collapseOnClose"); + collapsibleOrder.add("collapsedClickAction"); + config.setCategoryPropertyOrder(CATEGORY_COLLAPSIBLE, collapsibleOrder); + + { + String[] disabledGroupsArray = config.getStringList("disabledGroups", CATEGORY_COLLAPSIBLE, new String[]{}); + Property disabledProp = config.get(CATEGORY_COLLAPSIBLE, "disabledGroups", new String[]{}); + disabledProp.setShowInGui(false); + values.disabledGroups.clear(); + Collections.addAll(values.disabledGroups, disabledGroupsArray); + } + { Property property = config.get(CATEGORY_ADVANCED, "debugModeEnabled", defaultValues.debugModeEnabled); property.setShowInGui(false); diff --git a/src/main/java/mezz/jei/config/ConfigValues.java b/src/main/java/mezz/jei/config/ConfigValues.java index ab88d7a4..50fced31 100644 --- a/src/main/java/mezz/jei/config/ConfigValues.java +++ b/src/main/java/mezz/jei/config/ConfigValues.java @@ -1,11 +1,14 @@ package mezz.jei.config; +import mezz.jei.util.CollapsedClickAction; import mezz.jei.util.GiveMode; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class ConfigValues { // advanced @@ -53,4 +56,10 @@ public class ConfigValues { // category public List categoryUidOrder = new ArrayList<>(); + + // collapsible groups + public boolean collapsibleGroupsEnabled = true; + public boolean collapseOnClose = false; + public CollapsedClickAction collapsedClickAction = CollapsedClickAction.OPEN_GROUP; + public Set disabledGroups = new HashSet<>(); } diff --git a/src/main/java/mezz/jei/config/CustomGroupsConfig.java b/src/main/java/mezz/jei/config/CustomGroupsConfig.java new file mode 100644 index 00000000..5b81f930 --- /dev/null +++ b/src/main/java/mezz/jei/config/CustomGroupsConfig.java @@ -0,0 +1,109 @@ +package mezz.jei.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import mezz.jei.util.Log; + +import javax.annotation.Nullable; +import java.io.*; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages custom collapsible groups persistence as JSON. + * File: config/jei/customCollapsibleGroups.json + */ +public class CustomGroupsConfig { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Type GROUP_LIST_TYPE = new TypeToken>() {}.getType(); + + private final File configFile; + private Map customGroups = new LinkedHashMap<>(); + + public CustomGroupsConfig(File configDir) { + this.configFile = new File(configDir, "customCollapsibleGroups.json"); + } + + public void load() { + customGroups = new LinkedHashMap<>(); + if (!configFile.exists()) { + return; + } + try (Reader reader = new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8)) { + List loaded = GSON.fromJson(reader, GROUP_LIST_TYPE); + if (loaded != null) { + for (CustomGroup group : loaded) { + customGroups.put(group.id, group); + } + } + } catch (Exception e) { + Log.get().error("Failed to load custom collapsible groups from {}", configFile, e); + customGroups = new LinkedHashMap<>(); + } + } + + public void save() { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(configFile), StandardCharsets.UTF_8)) { + GSON.toJson(new ArrayList<>(customGroups.values()), GROUP_LIST_TYPE, writer); + } catch (Exception e) { + Log.get().error("Failed to save custom collapsible groups to {}", configFile, e); + } + } + + public Collection getCustomGroups() { + return Collections.unmodifiableCollection(customGroups.values()); + } + + @Nullable + public CustomGroup getGroup(String id) { + return customGroups.get(id); + } + + public void addGroup(CustomGroup group) { + customGroups.put(group.id, group); + save(); + } + + public void removeGroup(String id) { + customGroups.remove(id); + save(); + } + + public void updateGroup(CustomGroup updated) { + customGroups.put(updated.id, updated); + save(); + } + + /** + * A user-defined collapsible group stored as JSON. + * Items are identified by their unique identifier string from StackHelper. + */ + public static class CustomGroup { + public String id; + public String displayName; + public List itemUids; + + public CustomGroup() { + this.id = ""; + this.displayName = ""; + this.itemUids = new ArrayList<>(); + } + + public CustomGroup(String id, String displayName, List itemUids) { + this.id = id; + this.displayName = displayName; + this.itemUids = new ArrayList<>(itemUids); + } + + public CustomGroup copy() { + return new CustomGroup(id, displayName, new ArrayList<>(itemUids)); + } + } +} diff --git a/src/main/java/mezz/jei/config/JEIModConfigGui.java b/src/main/java/mezz/jei/config/JEIModConfigGui.java index f65dd89f..d87e3c47 100644 --- a/src/main/java/mezz/jei/config/JEIModConfigGui.java +++ b/src/main/java/mezz/jei/config/JEIModConfigGui.java @@ -1,11 +1,15 @@ package mezz.jei.config; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; import net.minecraftforge.fml.client.FMLClientHandler; import net.minecraftforge.fml.client.GuiModList; +import net.minecraftforge.fml.client.config.ConfigGuiType; import net.minecraftforge.fml.client.config.GuiConfig; +import net.minecraftforge.fml.client.config.GuiConfigEntries; import net.minecraftforge.fml.client.config.IConfigElement; import net.minecraftforge.common.config.ConfigCategory; import net.minecraftforge.common.config.ConfigElement; @@ -15,9 +19,12 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.inventory.GuiInventory; +import net.minecraft.client.resources.I18n; import net.minecraft.network.NetworkManager; +import net.minecraftforge.fml.client.config.GuiButtonExt; import mezz.jei.JustEnoughItems; +import mezz.jei.gui.overlay.collapsible.GuiCollapsibleGroups; import mezz.jei.gui.recipes.RecipesGui; import mezz.jei.network.packets.PacketRequestCheatPermission; import mezz.jei.util.Translator; @@ -49,19 +56,46 @@ private static GuiScreen getParent(GuiScreen parent) { return parent; } + private static void addCollapsibleElements(List configElements, LocalizedConfiguration config) { + // Show collapsibleGroupsEnabled inline (aligned like other boolean properties) + ConfigCategory catCollapsible = config.getCategory(Config.CATEGORY_COLLAPSIBLE); + for (IConfigElement element : new ConfigElement(catCollapsible).getChildElements()) { + if (element.showInGui()) { + configElements.add(element); + } + } + // "Manage Groups" navigation entry — opens GuiCollapsibleGroups in the same visual row style + configElements.add(new ManageGroupsConfigElement()); + } + private static List getConfigElements() { List configElements = new ArrayList<>(); + LocalizedConfiguration config = Config.getConfig(); + if (Minecraft.getMinecraft().world != null) { Configuration worldConfig = Config.getWorldConfig(); if (worldConfig != null) { NetworkManager networkManager = FMLClientHandler.instance().getClientToServerNetworkManager(); ConfigCategory categoryWorldConfig = worldConfig.getCategory(ServerInfo.getWorldUid(networkManager)); - configElements.addAll(new ConfigElement(categoryWorldConfig).getChildElements()); + List worldElements = new ConfigElement(categoryWorldConfig).getChildElements(); + + // Find the "Hide Ingredients Mode" entry and insert Collapsible Groups submenu immediately after it + int insertAt = worldElements.size(); + for (int i = 0; i < worldElements.size(); i++) { + if ("config.jei.mode.editEnabled".equals(worldElements.get(i).getLanguageKey())) { + insertAt = i + 1; + break; + } + } + configElements.addAll(worldElements.subList(0, insertAt)); + if (config != null) { + addCollapsibleElements(configElements, config); + } + configElements.addAll(worldElements.subList(insertAt, worldElements.size())); } } - LocalizedConfiguration config = Config.getConfig(); if (config != null) { ConfigCategory categoryAdvanced = config.getCategory(Config.CATEGORY_ADVANCED); configElements.addAll(new ConfigElement(categoryAdvanced).getChildElements()); @@ -72,6 +106,11 @@ private static List getConfigElements() { ConfigCategory categoryMisc = config.getCategory(Config.CATEGORY_MISC); configElements.addAll(new ConfigElement(categoryMisc).getChildElements()); + // If we never had a world config section (world == null), show Collapsible Groups here instead + if (Minecraft.getMinecraft().world == null) { + addCollapsibleElements(configElements, config); + } + ConfigCategory categorySearch = config.getCategory(Config.CATEGORY_SEARCH); configElements.add(new ConfigElement(categorySearch)); } @@ -97,4 +136,66 @@ protected void actionPerformed(GuiButton button) { JustEnoughItems.getProxy().sendPacketToServer(new PacketRequestCheatPermission()); } } + + // ------------------------------------------------------------------------- + // "Manage Groups" config list entry + // ------------------------------------------------------------------------- + + /** + * A CategoryEntry that opens GuiCollapsibleGroups instead of a standard GuiConfig subcategory. + * Constructor signature must match (GuiConfig, GuiConfigEntries, IConfigElement). + */ + public static class ManageGroupsEntry extends GuiConfigEntries.ButtonEntry { + public ManageGroupsEntry(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement configElement) { + super(owningScreen, owningEntryList, configElement, + new GuiButtonExt(0, owningEntryList.controlX, 0, owningEntryList.controlWidth, 18, + I18n.format("hei.gui.collapsible.title"))); + } + + @Override public void updateValueButtonText() {} + @Override public void valueButtonPressed(int slotIndex) { + this.mc.displayGuiScreen(new GuiCollapsibleGroups(this.owningScreen)); + } + @Override public boolean isDefault() { return true; } + @Override public void setToDefault() {} + @Override public boolean isChanged() { return false; } + @Override public void undoChanges() {} + @Override public boolean saveConfigElement() { return false; } + @Override public Object getCurrentValue() { return ""; } + @Override public Object[] getCurrentValues() { return new Object[]{ "" }; } + } + + /** + * A minimal IConfigElement that represents a category-type navigation entry + * pointing to GuiCollapsibleGroups via ManageGroupsEntry. + */ + public static class ManageGroupsConfigElement implements IConfigElement { + @Override public boolean isProperty() { return false; } + @Override public Class getConfigEntryClass() { return ManageGroupsEntry.class; } + @Override public Class getArrayEntryClass() { return null; } + @Override public String getName() { return "manageGroups"; } + @Override public String getQualifiedName() { return "manageGroups"; } + @Override public String getLanguageKey() { return "hei.gui.collapsible.title"; } + @Override public String getComment() { return ""; } + @Override public List getChildElements() { return Collections.emptyList(); } + @Override public ConfigGuiType getType() { return ConfigGuiType.CONFIG_CATEGORY; } + @Override public boolean isList() { return false; } + @Override public boolean isListLengthFixed() { return false; } + @Override public int getMaxListLength() { return -1; } + @Override public boolean isDefault() { return true; } + @Override public Object getDefault() { return null; } + @Override public Object[] getDefaults() { return null; } + @Override public void setToDefault() {} + @Override public boolean requiresWorldRestart() { return false; } + @Override public boolean showInGui() { return true; } + @Override public boolean requiresMcRestart() { return false; } + @Override public Object get() { return null; } + @Override public Object[] getList() { return null; } + @Override public void set(Object value) {} + @Override public void set(Object[] aVal) {} + @Override public String[] getValidValues() { return null; } + @Override public Object getMinValue() { return null; } + @Override public Object getMaxValue() { return null; } + @Override public Pattern getValidationPattern() { return null; } + } } diff --git a/src/main/java/mezz/jei/gui/GuiEventHandler.java b/src/main/java/mezz/jei/gui/GuiEventHandler.java index c4ecafd4..81208cae 100644 --- a/src/main/java/mezz/jei/gui/GuiEventHandler.java +++ b/src/main/java/mezz/jei/gui/GuiEventHandler.java @@ -1,7 +1,9 @@ package mezz.jei.gui; +import mezz.jei.Internal; import mezz.jei.config.Config; import mezz.jei.config.OverlayToggleEvent; +import mezz.jei.ingredients.CollapsedStackRegistry; import mezz.jei.gui.ghost.GhostIngredientDragManager; import mezz.jei.gui.overlay.IngredientListOverlay; import mezz.jei.gui.overlay.bookmarks.LeftAreaDispatcher; @@ -56,10 +58,18 @@ public void onGuiInit(GuiScreenEvent.InitGuiEvent.Post event) { @SubscribeEvent public void onGuiOpen(GuiOpenEvent event) { + boolean wasDisplayed = ingredientListOverlay.isListDisplayed(); GuiScreen gui = event.getGui(); ingredientListOverlay.updateScreen(gui, false); leftAreaDispatcher.updateScreen(gui, false); ghostIngredientDragManager.updateScreen(gui, false); + if (wasDisplayed && !ingredientListOverlay.isListDisplayed() && Config.isCollapseOnClose() + && Internal.hasIngredientFilter()) { + CollapsedStackRegistry registry = Internal.getCollapsedStackRegistry(); + registry.getEntries().forEach(e -> e.setExpanded(false)); + registry.getCustomEntries().forEach(e -> e.setExpanded(false)); + Internal.getIngredientFilter().notifyCollapsedStateChanged(); + } } @SubscribeEvent diff --git a/src/main/java/mezz/jei/gui/overlay/ConfigButton.java b/src/main/java/mezz/jei/gui/overlay/ConfigButton.java index 70aa2f24..d397df2d 100644 --- a/src/main/java/mezz/jei/gui/overlay/ConfigButton.java +++ b/src/main/java/mezz/jei/gui/overlay/ConfigButton.java @@ -1,5 +1,6 @@ package mezz.jei.gui.overlay; +import java.util.Collection; import java.util.List; import net.minecraft.client.Minecraft; @@ -14,6 +15,8 @@ import mezz.jei.config.KeyBindings; import mezz.jei.gui.GuiHelper; import mezz.jei.gui.elements.GuiIconToggleButton; +import mezz.jei.ingredients.CollapsedStack; +import mezz.jei.ingredients.CollapsedStackRegistry; import mezz.jei.util.Translator; import org.lwjgl.input.Keyboard; @@ -33,6 +36,9 @@ private ConfigButton(IDrawable disabledIcon, IDrawable enabledIcon, IngredientLi @Override protected void getTooltips(List tooltip) { tooltip.add(Translator.translateToLocal("jei.tooltip.config")); + if (Config.isOverlayEnabled() && Config.isCollapsibleGroupsEnabled()) { + tooltip.add(TextFormatting.GOLD + Translator.translateToLocal("hei.tooltip.config.expandCollapseAll")); + } if (!Config.isOverlayEnabled()) { tooltip.add(TextFormatting.GOLD + Translator.translateToLocal("jei.tooltip.ingredient.list.disabled")); tooltip.add(TextFormatting.GOLD + Translator.translateToLocalFormatted("jei.tooltip.ingredient.list.disabled.how.to.fix", KeyBindings.toggleOverlay.getDisplayName())); @@ -59,7 +65,18 @@ protected boolean isIconToggledOn() { @Override protected boolean onMouseClicked(int mouseX, int mouseY) { if (Config.isOverlayEnabled()) { - if (Keyboard.getEventKeyState() && (Keyboard.getEventKey() == Keyboard.KEY_LCONTROL || Keyboard.getEventKey() == Keyboard.KEY_RCONTROL)) { + if (GuiScreen.isAltKeyDown() && Config.isCollapsibleGroupsEnabled() + && Internal.hasIngredientFilter()) { + CollapsedStackRegistry registry = Internal.getCollapsedStackRegistry(); + Collection entries = registry.getEntries(); + List customEntries = registry.getCustomEntries(); + boolean allExpanded = entries.stream().allMatch(CollapsedStack::isExpanded) + && customEntries.stream().allMatch(CollapsedStack::isExpanded); + boolean targetExpanded = !allExpanded; + entries.forEach(e -> e.setExpanded(targetExpanded)); + customEntries.forEach(e -> e.setExpanded(targetExpanded)); + Internal.getIngredientFilter().notifyCollapsedStateChanged(); + } else if (Keyboard.getEventKeyState() && (Keyboard.getEventKey() == Keyboard.KEY_LCONTROL || Keyboard.getEventKey() == Keyboard.KEY_RCONTROL)) { Config.toggleCheatItemsEnabled(); } else { Minecraft minecraft = Minecraft.getMinecraft(); diff --git a/src/main/java/mezz/jei/gui/overlay/IIngredientGridSource.java b/src/main/java/mezz/jei/gui/overlay/IIngredientGridSource.java index f99a4b54..a613c045 100644 --- a/src/main/java/mezz/jei/gui/overlay/IIngredientGridSource.java +++ b/src/main/java/mezz/jei/gui/overlay/IIngredientGridSource.java @@ -7,6 +7,22 @@ public interface IIngredientGridSource { List getIngredientList(); + /** + * Returns a list of display items for the grid, which may include + * CollapsedStack objects alongside IIngredientListElement objects + * when collapsible groups are enabled. + */ + default List getCollapsedIngredientList() { + return getIngredientList(); + } + + /** + * Returns the total number of displayed ingredients (counting collapsed groups as 1 each). + */ + default int collapsedSize() { + return size(); + } + int size(); void addListener(Listener listener); diff --git a/src/main/java/mezz/jei/gui/overlay/IngredientGrid.java b/src/main/java/mezz/jei/gui/overlay/IngredientGrid.java index a8a300e8..537f191b 100644 --- a/src/main/java/mezz/jei/gui/overlay/IngredientGrid.java +++ b/src/main/java/mezz/jei/gui/overlay/IngredientGrid.java @@ -6,21 +6,26 @@ import mezz.jei.gui.TooltipRenderer; import mezz.jei.gui.ingredients.GuiItemStackGroup; import mezz.jei.gui.ingredients.IIngredientListElement; +import mezz.jei.ingredients.CollapsedStack; +import mezz.jei.ingredients.IngredientFilter; import mezz.jei.input.ClickedIngredient; import mezz.jei.input.IClickedIngredient; import mezz.jei.input.IShowsRecipeFocuses; import mezz.jei.input.MouseHelper; import mezz.jei.network.packets.PacketDeletePlayerItem; import mezz.jei.network.packets.PacketJei; +import mezz.jei.render.CollapsedStackRenderer; import mezz.jei.render.IngredientListBatchRenderer; import mezz.jei.render.IngredientListSlot; import mezz.jei.render.IngredientRenderer; import mezz.jei.runtime.JeiRuntime; import mezz.jei.util.GiveMode; +import mezz.jei.util.CollapsedClickAction; import mezz.jei.util.MathUtil; import mezz.jei.util.Translator; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; @@ -108,11 +113,17 @@ public void draw(Minecraft minecraft, int mouseX, int mouseY) { GlStateManager.disableBlend(); guiIngredientSlots.render(minecraft); + guiIngredientSlots.renderExpandedGroupOutlines(); if (!shouldDeleteItemOnClick(minecraft, mouseX, mouseY) && isMouseOver(mouseX, mouseY)) { - IngredientRenderer hovered = guiIngredientSlots.getHovered(mouseX, mouseY); - if (hovered != null) { - hovered.drawHighlight(); + CollapsedStackRenderer collapsedHovered = guiIngredientSlots.getHoveredCollapsed(mouseX, mouseY); + if (collapsedHovered != null) { + collapsedHovered.drawHighlight(); + } else { + IngredientRenderer hovered = guiIngredientSlots.getHovered(mouseX, mouseY); + if (hovered != null) { + hovered.drawHighlight(); + } } } @@ -125,9 +136,21 @@ public void drawTooltips(Minecraft minecraft, int mouseX, int mouseY) { String deleteItem = Translator.translateToLocal("jei.tooltip.delete.item"); TooltipRenderer.drawHoveringText(minecraft, deleteItem, mouseX, mouseY); } else { - IngredientRenderer hovered = guiIngredientSlots.getHovered(mouseX, mouseY); - if (hovered != null) { - hovered.drawTooltip(minecraft, mouseX, mouseY); + CollapsedStackRenderer collapsedHovered = guiIngredientSlots.getHoveredCollapsed(mouseX, mouseY); + if (collapsedHovered != null) { + collapsedHovered.drawTooltip(minecraft, mouseX, mouseY); + } else { + IngredientRenderer hovered = guiIngredientSlots.getHovered(mouseX, mouseY); + if (hovered != null) { + CollapsedStack expandedGroup = guiIngredientSlots.getExpandedCollapsedGroupAt(mouseX, mouseY); + if (expandedGroup != null) { + String hint = net.minecraft.util.text.TextFormatting.YELLOW + + mezz.jei.util.Translator.translateToLocal("hei.tooltip.collapsed.collapse"); + hovered.drawTooltip(minecraft, mouseX, mouseY, java.util.Collections.singletonList(hint)); + } else { + hovered.drawTooltip(minecraft, mouseX, mouseY); + } + } } } } @@ -168,6 +191,30 @@ public boolean isMouseOver(int mouseX, int mouseY) { public boolean handleMouseClicked(int mouseX, int mouseY) { if (isMouseOver(mouseX, mouseY)) { + boolean firstItemMode = Config.getCollapsedClickAction() == CollapsedClickAction.FIRST_ITEM; + boolean altDown = GuiScreen.isAltKeyDown(); + // OPEN_GROUP: plain click expands a collapsed icon; alt+click falls through (first item). + // FIRST_ITEM: alt+click expands a collapsed icon; plain click falls through (first item). + boolean expandKeyDown = firstItemMode ? altDown : !altDown; + if (expandKeyDown) { + CollapsedStackRenderer collapsedHovered = guiIngredientSlots.getHoveredCollapsed(mouseX, mouseY); + // A group with only 1 visible item should act as a plain ingredient click, + // not expand/collapse — the single item is already trivially "shown". + if (collapsedHovered != null && collapsedHovered.getCollapsedStack().size() > 1) { + collapsedHovered.getCollapsedStack().toggleExpanded(); + Internal.getIngredientFilter().notifyCollapsedStateChanged(); + return true; + } + } + // Alt+Click on any item inside an expanded group always collapses it. + if (altDown) { + CollapsedStack expandedHovered = guiIngredientSlots.getExpandedCollapsedGroupAt(mouseX, mouseY); + if (expandedHovered != null) { + expandedHovered.toggleExpanded(); + Internal.getIngredientFilter().notifyCollapsedStateChanged(); + return true; + } + } Minecraft minecraft = Minecraft.getMinecraft(); if (shouldDeleteItemOnClick(minecraft, mouseX, mouseY)) { EntityPlayerSP player = minecraft.player; diff --git a/src/main/java/mezz/jei/gui/overlay/IngredientGridWithNavigation.java b/src/main/java/mezz/jei/gui/overlay/IngredientGridWithNavigation.java index 87ff9b26..5c8830b7 100644 --- a/src/main/java/mezz/jei/gui/overlay/IngredientGridWithNavigation.java +++ b/src/main/java/mezz/jei/gui/overlay/IngredientGridWithNavigation.java @@ -51,11 +51,11 @@ public void updateLayout(boolean resetToFirstPage) { if (resetToFirstPage) { firstItemIndex = 0; } - List ingredientList = ingredientSource.getIngredientList(); - if (firstItemIndex >= ingredientList.size()) { + List collapsedList = ingredientSource.getCollapsedIngredientList(); + if (firstItemIndex >= ingredientSource.collapsedSize()) { firstItemIndex = 0; } - this.ingredientGrid.guiIngredientSlots.set(firstItemIndex, ingredientList); + this.ingredientGrid.guiIngredientSlots.setCollapsed(firstItemIndex, collapsedList); this.navigation.updatePageState(); } @@ -200,7 +200,7 @@ public List getVisibleElements() { private class IngredientGridPaged implements IPaged { @Override public boolean nextPage() { - final int itemsCount = ingredientSource.size(); + final int itemsCount = ingredientSource.collapsedSize(); if (itemsCount > 0) { firstItemIndex += ingredientGrid.size(); if (firstItemIndex >= itemsCount) { @@ -223,7 +223,7 @@ public boolean previousPage() { updateLayout(false); return false; } - final int itemsCount = ingredientSource.size(); + final int itemsCount = ingredientSource.collapsedSize(); int pageNum = firstItemIndex / itemsPerPage; if (pageNum == 0) { @@ -245,19 +245,19 @@ public boolean previousPage() { public boolean hasNext() { // true if there is more than one page because this wraps around int itemsPerPage = ingredientGrid.size(); - return itemsPerPage > 0 && ingredientSource.size() > itemsPerPage; + return itemsPerPage > 0 && ingredientSource.collapsedSize() > itemsPerPage; } @Override public boolean hasPrevious() { // true if there is more than one page because this wraps around int itemsPerPage = ingredientGrid.size(); - return itemsPerPage > 0 && ingredientSource.size() > itemsPerPage; + return itemsPerPage > 0 && ingredientSource.collapsedSize() > itemsPerPage; } @Override public int getPageCount() { - final int itemCount = ingredientSource.size(); + final int itemCount = ingredientSource.collapsedSize(); final int stacksPerPage = ingredientGrid.size(); if (stacksPerPage == 0) { return 1; diff --git a/src/main/java/mezz/jei/gui/overlay/IngredientListOverlay.java b/src/main/java/mezz/jei/gui/overlay/IngredientListOverlay.java index 93137d75..2621764a 100644 --- a/src/main/java/mezz/jei/gui/overlay/IngredientListOverlay.java +++ b/src/main/java/mezz/jei/gui/overlay/IngredientListOverlay.java @@ -59,6 +59,7 @@ public IngredientListOverlay(IngredientFilter ingredientFilter, IngredientRegist this.contents = new IngredientGridWithNavigation(ingredientFilter, guiScreenHelper, GridAlignment.LEFT); ingredientFilter.addListener(() -> onSetFilterText(Config.getFilterText())); + ingredientFilter.addCollapsedStateListener(() -> this.contents.updateLayout(false)); this.searchField = new GuiTextFieldFilter(0, ingredientFilter); this.configButton = ConfigButton.create(this); this.ghostIngredientDragManager = dragManager; diff --git a/src/main/java/mezz/jei/gui/overlay/collapsible/GuiCollapsibleGroups.java b/src/main/java/mezz/jei/gui/overlay/collapsible/GuiCollapsibleGroups.java new file mode 100644 index 00000000..63391206 --- /dev/null +++ b/src/main/java/mezz/jei/gui/overlay/collapsible/GuiCollapsibleGroups.java @@ -0,0 +1,667 @@ +package mezz.jei.gui.overlay.collapsible; + +import mezz.jei.Internal; +import mezz.jei.api.ingredients.IIngredientRenderer; +import mezz.jei.config.Config; +import mezz.jei.config.CustomGroupsConfig; +import mezz.jei.gui.ingredients.IIngredientListElement; +import mezz.jei.ingredients.CollapsedStack; +import mezz.jei.ingredients.CollapsedStack.GroupSource; +import mezz.jei.ingredients.CollapsedStackRegistry; +import mezz.jei.ingredients.IngredientFilter; +import mezz.jei.util.Translator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.util.ITooltipFlag; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fml.client.config.GuiUtils; +import org.lwjgl.input.Mouse; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.*; + +/** + * Management screen for collapsible groups. + * Shows all default and custom groups with toggle/configure/delete controls. + */ +public class GuiCollapsibleGroups extends GuiScreen { + + private static final int CARD_HEIGHT = 84; + private static final int CARD_PADDING = 4; + private static final int PREVIEW_SIZE = 16; + private static final int PREVIEW_COLS = 8; + private static final int PREVIEW_ROWS = 3; + // Maximum items fetched per card for the scrollable preview (20 scrollable rows) + private static final int PREVIEW_FETCH_MAX = PREVIEW_COLS * 20; + + // Dynamic layout — recomputed on each initGui() call so the screen adapts to GUI scale + private int cardsPerCol; + private int cardsPerPage; + private int layoutContentTop; + private int layoutContentWidth; + private int layoutContentLeft; + private int layoutColGap; + private int layoutColWidth; + + private static final int BTN_BACK = 0; + private static final int BTN_NEW = 1; + private static final int BTN_PREV_PAGE = 2; + private static final int BTN_NEXT_PAGE = 3; + private static final int BTN_TOGGLE_BASE = 100; + private static final int BTN_CONFIGURE_BASE = 200; + private static final int BTN_DELETE_BASE = 300; + private static final int BTN_DELETE_CONFIRM_BASE = 400; + private static final int BTN_DELETE_CANCEL_BASE = 500; + + private final GuiScreen parentScreen; + private final List cardEntries = new ArrayList<>(); + private int currentPage = 0; + private int totalPages = 1; + /** Index into {@code cardEntries} of the custom group awaiting delete confirmation, or -1. */ + private int pendingDeleteIdx = -1; + @Nullable private IIngredientListElement tooltipElement = null; + + // Drag-to-scroll state for card preview boxes + private int dragCardAbsIdx = -1; + private int dragStartMouseY; + private int dragStartRow; + + public GuiCollapsibleGroups(GuiScreen parentScreen) { + this.parentScreen = parentScreen; + } + + private void computeLayout() { + layoutContentTop = 30; + layoutColGap = 4; + // Use up to 90% of screen width, capped at 700 so wide monitors still look reasonable + layoutContentWidth = Math.min(this.width - 20, Math.max(300, (int) (this.width * 0.9))); + layoutContentLeft = (this.width - layoutContentWidth) / 2; + layoutColWidth = (layoutContentWidth - layoutColGap) / 2; + // Reserve: header (30) + header buttons (24) + nav row (28) + bottom margin (6) + int availableForCards = this.height - layoutContentTop - 28 - 6; + cardsPerCol = Math.max(1, availableForCards / (CARD_HEIGHT + CARD_PADDING)); + cardsPerPage = 2 * cardsPerCol; + } + + @Override + public void initGui() { + super.initGui(); + this.buttonList.clear(); + computeLayout(); + + // Back button + this.buttonList.add(new GuiButton(BTN_BACK, layoutContentLeft, 4, 60, 20, + Translator.translateToLocal("hei.gui.collapsible.back"))); + + // New Group button + this.buttonList.add(new GuiButton(BTN_NEW, layoutContentLeft + layoutContentWidth - 62, 4, 60, 20, + Translator.translateToLocal("hei.gui.collapsible.newGroup"))); + + rebuildCards(); + rebuildPageButtons(); + } + + private void rebuildCards() { + cardEntries.clear(); + + CollapsedStackRegistry registry = Internal.getCollapsedStackRegistry(); + + // Custom groups come first (like REI) + addCardsMergedById(registry.getCustomEntries(), GroupSource.CUSTOM, registry.getDisabledGroups()); + + // Mod-registered groups (same ID can be registered multiple times for different ingredient types) + addCardsMergedById(registry.getModEntries(), GroupSource.MOD, registry.getDisabledGroups()); + + // Default groups + addCardsMergedById(registry.getEntries(), GroupSource.DEFAULT, registry.getDisabledGroups()); + + totalPages = Math.max(1, (cardEntries.size() + cardsPerPage - 1) / cardsPerPage); + if (currentPage >= totalPages) { + currentPage = totalPages - 1; + } + } + + private void addCardsMergedById(Collection entries, GroupSource source, Set disabledGroups) { + Map> groupedById = new LinkedHashMap<>(); + Map displayNamesById = new HashMap<>(); + + for (CollapsedStack entry : entries) { + groupedById.computeIfAbsent(entry.getId(), k -> new ArrayList<>()).add(entry); + displayNamesById.putIfAbsent(entry.getId(), entry.getDisplayName()); + } + + for (Map.Entry> groupedEntry : groupedById.entrySet()) { + String id = groupedEntry.getKey(); + List groupedStacks = groupedEntry.getValue(); + List> previewItems = getPreviewItems(groupedStacks); + int itemCount = getMatchedItemCount(groupedStacks); + cardEntries.add(new GroupCardEntry(id, displayNamesById.get(id), source, + !disabledGroups.contains(id), previewItems, itemCount)); + } + } + + private void rebuildPageButtons() { + // Remove old card-specific and page buttons + buttonList.removeIf(b -> b.id >= BTN_PREV_PAGE); + + int startIdx = currentPage * cardsPerPage; + int endIdx = Math.min(startIdx + cardsPerPage, cardEntries.size()); + + for (int i = startIdx; i < endIdx; i++) { + int localIdx = i - startIdx; + int col = localIdx / cardsPerCol; // 0 = left, 1 = right + int row = localIdx % cardsPerCol; + int cardX = layoutContentLeft + col * (layoutColWidth + layoutColGap); + int cardY = layoutContentTop + row * (CARD_HEIGHT + CARD_PADDING); + GroupCardEntry card = cardEntries.get(i); + + int btnX = cardX + layoutColWidth - 56; + int btnY = cardY + 4; + + // Toggle button + String toggleLabel = card.enabled + ? Translator.translateToLocal("hei.gui.collapsible.enabled") + : Translator.translateToLocal("hei.gui.collapsible.disabled"); + this.buttonList.add(new GuiButton(BTN_TOGGLE_BASE + i, btnX, btnY, 52, 20, toggleLabel)); + + if (card.source == GroupSource.CUSTOM) { + if (i == pendingDeleteIdx) { + // Confirm row: ✔ Yes / ✗ No + this.buttonList.add(new GuiButton(BTN_DELETE_CONFIRM_BASE + i, btnX, btnY + 22, 24, 20, + "\u2714")); // ✔ checkmark + this.buttonList.add(new GuiButton(BTN_DELETE_CANCEL_BASE + i, btnX + 26, btnY + 22, 26, 20, + "\u2716")); // ✗ cancel + } else { + // Configure button + this.buttonList.add(new GuiButton(BTN_CONFIGURE_BASE + i, btnX, btnY + 22, 24, 20, + "\u270E")); // pencil unicode + // Delete button + this.buttonList.add(new GuiButton(BTN_DELETE_BASE + i, btnX + 26, btnY + 22, 26, 20, + "\u2716")); // cross unicode + } + } + } + + // Page navigation + if (totalPages > 1) { + int navY = layoutContentTop + cardsPerCol * (CARD_HEIGHT + CARD_PADDING) + 4; + this.buttonList.add(new GuiButton(BTN_PREV_PAGE, layoutContentLeft, navY, 40, 20, "<")); + this.buttonList.add(new GuiButton(BTN_NEXT_PAGE, layoutContentLeft + layoutContentWidth - 40, navY, 40, 20, ">")); + } + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + if (button.id == BTN_BACK) { + this.mc.displayGuiScreen(parentScreen); + return; + } + if (button.id == BTN_NEW) { + String newId = "custom:" + UUID.randomUUID().toString().substring(0, 8); + CustomGroupsConfig customGroupsConfig = Config.getCustomGroupsConfig(); + if (customGroupsConfig != null) { + CustomGroupsConfig.CustomGroup newGroup = new CustomGroupsConfig.CustomGroup(newId, "New Group", new ArrayList<>()); + this.mc.displayGuiScreen(new GuiCustomGroupEditor(this, newGroup)); + } + return; + } + if (button.id == BTN_PREV_PAGE) { + currentPage = Math.max(0, currentPage - 1); + pendingDeleteIdx = -1; + rebuildPageButtons(); + return; + } + if (button.id == BTN_NEXT_PAGE) { + currentPage = Math.min(totalPages - 1, currentPage + 1); + pendingDeleteIdx = -1; + rebuildPageButtons(); + return; + } + + // Toggle + if (button.id >= BTN_TOGGLE_BASE && button.id < BTN_CONFIGURE_BASE) { + int idx = button.id - BTN_TOGGLE_BASE; + if (idx >= 0 && idx < cardEntries.size()) { + GroupCardEntry card = cardEntries.get(idx); + card.enabled = !card.enabled; + + CollapsedStackRegistry registry = Internal.getCollapsedStackRegistry(); + Set disabled = new HashSet<>(registry.getDisabledGroups()); + if (card.enabled) { + disabled.remove(card.id); + } else { + disabled.add(card.id); + } + registry.setDisabledGroups(disabled); + Config.saveDisabledGroups(disabled); + + if (Internal.hasIngredientFilter()) { + IngredientFilter filter = Internal.getIngredientFilter(); + filter.invalidateCache(); + filter.getIngredientList(Config.getFilterText()); + filter.notifyListenersOfChange(); + } + + rebuildPageButtons(); + } + return; + } + + // Configure + if (button.id >= BTN_CONFIGURE_BASE && button.id < BTN_DELETE_BASE) { + int idx = button.id - BTN_CONFIGURE_BASE; + if (idx >= 0 && idx < cardEntries.size()) { + GroupCardEntry card = cardEntries.get(idx); + if (card.source == GroupSource.CUSTOM) { + CustomGroupsConfig customGroupsConfig = Config.getCustomGroupsConfig(); + if (customGroupsConfig != null) { + CustomGroupsConfig.CustomGroup group = customGroupsConfig.getGroup(card.id); + if (group != null) { + this.mc.displayGuiScreen(new GuiCustomGroupEditor(this, group)); + return; + } + } + } + } + return; + } + + // Delete — first click: arm confirmation + if (button.id >= BTN_DELETE_BASE && button.id < BTN_DELETE_CONFIRM_BASE) { + int idx = button.id - BTN_DELETE_BASE; + if (idx >= 0 && idx < cardEntries.size() && cardEntries.get(idx).source == GroupSource.CUSTOM) { + pendingDeleteIdx = idx; + rebuildPageButtons(); + } + return; + } + + // Delete confirmed — execute the actual removal + if (button.id >= BTN_DELETE_CONFIRM_BASE && button.id < BTN_DELETE_CANCEL_BASE) { + int idx = button.id - BTN_DELETE_CONFIRM_BASE; + pendingDeleteIdx = -1; + if (idx >= 0 && idx < cardEntries.size()) { + GroupCardEntry card = cardEntries.get(idx); + if (card.source == GroupSource.CUSTOM) { + CustomGroupsConfig customGroupsConfig = Config.getCustomGroupsConfig(); + if (customGroupsConfig != null) { + customGroupsConfig.removeGroup(card.id); + Internal.getCollapsedStackRegistry().recollectCustomEntries(); + + if (Internal.hasIngredientFilter()) { + IngredientFilter filter = Internal.getIngredientFilter(); + filter.invalidateCache(); + filter.getIngredientList(Config.getFilterText()); + filter.notifyListenersOfChange(); + } + + rebuildCards(); + rebuildPageButtons(); + } + } + } + return; + } + + // Delete cancelled + if (button.id >= BTN_DELETE_CANCEL_BASE) { + pendingDeleteIdx = -1; + rebuildPageButtons(); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.drawDefaultBackground(); + + // Title + String title = Translator.translateToLocal("hei.gui.collapsible.title"); + this.drawCenteredString(this.fontRenderer, title, this.width / 2, 10, 0xFFFFFF); + + int startIdx = currentPage * cardsPerPage; + int endIdx = Math.min(startIdx + cardsPerPage, cardEntries.size()); + + tooltipElement = null; + for (int i = startIdx; i < endIdx; i++) { + int localIdx = i - startIdx; + int col = localIdx / cardsPerCol; + int row = localIdx % cardsPerCol; + int cardX = layoutContentLeft + col * (layoutColWidth + layoutColGap); + int cardY = layoutContentTop + row * (CARD_HEIGHT + CARD_PADDING); + GroupCardEntry card = cardEntries.get(i); + drawCard(card, cardX, cardY, layoutColWidth, mouseX, mouseY); + + // Draw "Delete?" label to the left of the ✔/✗ confirm buttons + if (i == pendingDeleteIdx && card.source == GroupSource.CUSTOM) { + int btnX = cardX + layoutColWidth - 56; + int btnY = cardY + 4; + String confirmLabel = Translator.translateToLocal("hei.gui.collapsible.confirmDelete"); + int labelX = btnX - this.fontRenderer.getStringWidth(confirmLabel) - 3; + this.fontRenderer.drawStringWithShadow(confirmLabel, labelX, btnY + 27, 0xFFFF4444); + } + } + + // Page counter + if (totalPages > 1) { + int navY = layoutContentTop + cardsPerCol * (CARD_HEIGHT + CARD_PADDING) + 4; + String pageText = (currentPage + 1) + "/" + totalPages; + this.drawCenteredString(this.fontRenderer, pageText, this.width / 2, navY + 6, 0xAAAAAA); + } + + // Empty state + if (cardEntries.isEmpty()) { + this.drawCenteredString(this.fontRenderer, "No collapsible groups defined", this.width / 2, layoutContentTop + 20, 0x888888); + } + + super.drawScreen(mouseX, mouseY, partialTicks); + + if (tooltipElement != null) { + renderIngredientTooltip(tooltipElement, mouseX, mouseY); + } + } + + private void drawCard(GroupCardEntry card, int x, int y, int width, int mouseX, int mouseY) { + // Card background + int bgColor; + switch (card.source) { + case CUSTOM: bgColor = 0x40336699; break; + case MOD: bgColor = 0x40553366; break; + default: bgColor = 0x40444444; break; + } + drawRect(x, y, x + width, y + CARD_HEIGHT, bgColor); + + // Border + int borderColor = card.enabled ? 0xFF558855 : 0xFF885555; + drawHorizontalLine(x, x + width - 1, y, borderColor); + drawHorizontalLine(x, x + width - 1, y + CARD_HEIGHT - 1, borderColor); + drawVerticalLine(x, y, y + CARD_HEIGHT - 1, borderColor); + drawVerticalLine(x + width - 1, y, y + CARD_HEIGHT - 1, borderColor); + + // Group name + String sourceLabel; + String sourceColor; + switch (card.source) { + case CUSTOM: + sourceColor = "\u00A7e"; + sourceLabel = Translator.translateToLocal("hei.gui.collapsible.customGroup"); + break; + case MOD: + sourceColor = "\u00A7d"; + sourceLabel = Translator.translateToLocal("hei.gui.collapsible.modGroup"); + break; + default: + sourceColor = "\u00A77"; + sourceLabel = Translator.translateToLocal("hei.gui.collapsible.defaultGroup"); + break; + } + String namePrefix = sourceColor + "[" + sourceLabel + "] \u00A7r"; + this.fontRenderer.drawStringWithShadow(namePrefix + card.displayName, x + 4, y + 4, 0xFFFFFF); + + // Item count + String countText = String.format(Translator.translateToLocal("hei.gui.collapsible.itemCount"), card.itemCount); + this.fontRenderer.drawStringWithShadow(countText, x + 4, y + 16, 0xAAAAAA); + + // Scrollable preview grid + int slotSize = PREVIEW_SIZE + 2; + int previewX = x + 4; + int previewY = y + 28; + int totalRows = (card.previewItems.size() + PREVIEW_COLS - 1) / PREVIEW_COLS; + int maxScrollRow = Math.max(0, totalRows - PREVIEW_ROWS); + card.previewScrollRow = Math.max(0, Math.min(card.previewScrollRow, maxScrollRow)); + int scrollRow = card.previewScrollRow; + + int firstItem = scrollRow * PREVIEW_COLS; + int lastItem = Math.min(card.previewItems.size(), (scrollRow + PREVIEW_ROWS) * PREVIEW_COLS); + + RenderHelper.enableGUIStandardItemLighting(); + GlStateManager.enableDepth(); + for (int i = firstItem; i < lastItem; i++) { + IIngredientListElement element = card.previewItems.get(i); + int visibleRow = (i / PREVIEW_COLS) - scrollRow; + int col = i % PREVIEW_COLS; + int itemX = previewX + col * slotSize; + int itemY = previewY + visibleRow * slotSize; + renderIngredient(element, itemX + 1, itemY + 1); + if (mouseX >= itemX && mouseX < itemX + PREVIEW_SIZE + && mouseY >= itemY && mouseY < itemY + PREVIEW_SIZE) { + tooltipElement = element; + } + } + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + + // Scrollbar indicator + if (maxScrollRow > 0) { + int sbX = previewX + PREVIEW_COLS * slotSize + 2; + int sbY = previewY; + int sbH = PREVIEW_ROWS * slotSize; + drawRect(sbX, sbY, sbX + 3, sbY + sbH, 0x55FFFFFF); + int thumbH = Math.max(4, sbH * PREVIEW_ROWS / totalRows); + int thumbY = sbY + (sbH - thumbH) * scrollRow / maxScrollRow; + drawRect(sbX, thumbY, sbX + 3, thumbY + thumbH, 0xCCFFFFFF); + } + } + + @Override + public void handleMouseInput() throws IOException { + super.handleMouseInput(); + int scrollDelta = Mouse.getEventDWheel(); + if (scrollDelta != 0) { + int mouseX = Mouse.getEventX() * this.width / this.mc.displayWidth; + int mouseY = this.height - Mouse.getEventY() * this.height / this.mc.displayHeight - 1; + + // Check if the cursor is inside any visible card's preview box + boolean handledByCard = false; + int slotSize = PREVIEW_SIZE + 2; + int startIdx = currentPage * cardsPerPage; + int endIdx = Math.min(startIdx + cardsPerPage, cardEntries.size()); + for (int i = startIdx; i < endIdx; i++) { + int localIdx = i - startIdx; + int col = localIdx / cardsPerCol; + int row = localIdx % cardsPerCol; + int cardX = layoutContentLeft + col * (layoutColWidth + layoutColGap); + int cardY = layoutContentTop + row * (CARD_HEIGHT + CARD_PADDING); + int previewX = cardX + 4; + int previewY = cardY + 28; + int previewW = PREVIEW_COLS * slotSize; + int previewH = PREVIEW_ROWS * slotSize; + if (mouseX >= previewX && mouseX < previewX + previewW + && mouseY >= previewY && mouseY < previewY + previewH) { + GroupCardEntry card = cardEntries.get(i); + int totalRows = (card.previewItems.size() + PREVIEW_COLS - 1) / PREVIEW_COLS; + int maxScrollRow = Math.max(0, totalRows - PREVIEW_ROWS); + if (maxScrollRow > 0) { + if (scrollDelta < 0) { + card.previewScrollRow = Math.min(card.previewScrollRow + 1, maxScrollRow); + } else { + card.previewScrollRow = Math.max(card.previewScrollRow - 1, 0); + } + handledByCard = true; + } + break; + } + } + + if (!handledByCard) { + if (scrollDelta < 0 && currentPage < totalPages - 1) { + currentPage++; + rebuildPageButtons(); + } else if (scrollDelta > 0 && currentPage > 0) { + currentPage--; + rebuildPageButtons(); + } + } + } + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + super.mouseClicked(mouseX, mouseY, mouseButton); + if (mouseButton == 0) { + dragCardAbsIdx = -1; + int slotSize = PREVIEW_SIZE + 2; + int startIdx = currentPage * cardsPerPage; + int endIdx = Math.min(startIdx + cardsPerPage, cardEntries.size()); + for (int i = startIdx; i < endIdx; i++) { + int localIdx = i - startIdx; + int col = localIdx / cardsPerCol; + int row = localIdx % cardsPerCol; + int cardX = layoutContentLeft + col * (layoutColWidth + layoutColGap); + int cardY = layoutContentTop + row * (CARD_HEIGHT + CARD_PADDING); + int previewX = cardX + 4; + int previewY = cardY + 28; + int previewW = PREVIEW_COLS * slotSize; + int previewH = PREVIEW_ROWS * slotSize; + if (mouseX >= previewX && mouseX < previewX + previewW + && mouseY >= previewY && mouseY < previewY + previewH) { + dragCardAbsIdx = i; + dragStartMouseY = mouseY; + dragStartRow = cardEntries.get(i).previewScrollRow; + break; + } + } + } + } + + @Override + protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { + super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); + if (clickedMouseButton == 0 && dragCardAbsIdx >= 0 && dragCardAbsIdx < cardEntries.size()) { + int slotSize = PREVIEW_SIZE + 2; + GroupCardEntry card = cardEntries.get(dragCardAbsIdx); + int totalRows = (card.previewItems.size() + PREVIEW_COLS - 1) / PREVIEW_COLS; + int maxScrollRow = Math.max(0, totalRows - PREVIEW_ROWS); + // Dragging up (negative deltaY) scrolls down through items + int rowDelta = (dragStartMouseY - mouseY) / slotSize; + card.previewScrollRow = Math.max(0, Math.min(dragStartRow + rowDelta, maxScrollRow)); + } + } + + @Override + protected void mouseReleased(int mouseX, int mouseY, int state) { + super.mouseReleased(mouseX, mouseY, state); + if (state == 0) { + dragCardAbsIdx = -1; + } + } + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + if (keyCode == 1) { // ESC + this.mc.displayGuiScreen(parentScreen); + return; + } + super.keyTyped(typedChar, keyCode); + } + + /** + * Get up to PREVIEW_FETCH_MAX preview elements for a collapsible entry, + * returning the raw IIngredientListElement so each type renders via its own renderer. + */ + private List> getPreviewItems(List entries) { + List> items = new ArrayList<>(); + if (!Internal.hasIngredientFilter()) { + return items; + } + IngredientFilter filter = Internal.getIngredientFilter(); + @SuppressWarnings({"unchecked", "rawtypes"}) + List> ingredientList = (List>) (List) filter.getIngredientList(""); + for (IIngredientListElement element : ingredientList) { + if (matchesAny(entries, element)) { + items.add(element); + if (items.size() >= PREVIEW_FETCH_MAX) { + break; + } + } + } + return items; + } + + @SuppressWarnings("unchecked") + private void renderIngredient(IIngredientListElement element, int x, int y) { + try { + IIngredientRenderer renderer = element.getIngredientRenderer(); + renderer.render(this.mc, x, y, element.getIngredient()); + } catch (Exception ignored) { + } + } + + @SuppressWarnings("unchecked") + private void renderIngredientTooltip(IIngredientListElement element, int mouseX, int mouseY) { + try { + T ingredient = element.getIngredient(); + if (ingredient instanceof ItemStack) { + renderToolTip((ItemStack) ingredient, mouseX, mouseY); + return; + } + IIngredientRenderer renderer = element.getIngredientRenderer(); + List tooltip = renderer.getTooltip(this.mc, ingredient, + this.mc.gameSettings.advancedItemTooltips + ? ITooltipFlag.TooltipFlags.ADVANCED + : ITooltipFlag.TooltipFlags.NORMAL); + if (!tooltip.isEmpty()) { + drawHoveringText(tooltip, mouseX, mouseY, renderer.getFontRenderer(this.mc, ingredient)); + } + } catch (Exception ignored) { + } + } + + /** + * Count matched items for display. + */ + private int getMatchedItemCount(List entries) { + if (!Internal.hasIngredientFilter()) { + return 0; + } + IngredientFilter filter = Internal.getIngredientFilter(); + @SuppressWarnings({"unchecked", "rawtypes"}) + List> ingredientList = (List>) (List) filter.getIngredientList(""); + int count = 0; + for (IIngredientListElement element : ingredientList) { + if (matchesAny(entries, element)) { + count++; + } + } + return count; + } + + private static boolean matchesAny(List entries, IIngredientListElement element) { + for (CollapsedStack entry : entries) { + if (entry.matches(element)) { + return true; + } + } + return false; + } + + /** + * Called when returning from the editor screen to refresh the card list. + */ + public void onEditorClosed() { + rebuildCards(); + rebuildPageButtons(); + } + + private static class GroupCardEntry { + final String id; + final String displayName; + final GroupSource source; + boolean enabled; + final List> previewItems; + final int itemCount; + int previewScrollRow = 0; + + GroupCardEntry(String id, String displayName, GroupSource source, boolean enabled, List> previewItems, int itemCount) { + this.id = id; + this.displayName = displayName; + this.source = source; + this.enabled = enabled; + this.previewItems = previewItems; + this.itemCount = itemCount; + } + } +} diff --git a/src/main/java/mezz/jei/gui/overlay/collapsible/GuiCustomGroupEditor.java b/src/main/java/mezz/jei/gui/overlay/collapsible/GuiCustomGroupEditor.java new file mode 100644 index 00000000..4e5d4dc4 --- /dev/null +++ b/src/main/java/mezz/jei/gui/overlay/collapsible/GuiCustomGroupEditor.java @@ -0,0 +1,947 @@ +package mezz.jei.gui.overlay.collapsible; + +import mezz.jei.Internal; +import mezz.jei.api.ingredients.IIngredientRenderer; +import mezz.jei.config.Config; +import mezz.jei.config.CustomGroupsConfig; +import mezz.jei.gui.ingredients.IIngredientListElement; +import mezz.jei.ingredients.IngredientFilter; +import mezz.jei.startup.StackHelper; +import mezz.jei.util.Translator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiTextField; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.util.ITooltipFlag; +import net.minecraft.item.ItemStack; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.fml.client.config.GuiUtils; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.*; + +/** + * Editor screen for a custom collapsible group. + * Left panel: searchable item grid for selection. + * Right panel: preview of selected items. + */ +public class GuiCustomGroupEditor extends GuiScreen { + + private static final int ITEM_SIZE = 18; + private static final int GRID_PADDING = 2; + + private static final int BTN_SAVE = 0; + private static final int BTN_CANCEL = 1; + private static final int BTN_PREV_PAGE = 2; + private static final int BTN_NEXT_PAGE = 3; + private static final int BTN_PREV_SEL_PAGE = 4; + private static final int BTN_NEXT_SEL_PAGE = 5; + + private final GuiCollapsibleGroups parentScreen; + private final CustomGroupsConfig.CustomGroup group; + private final Set selectedUids = new LinkedHashSet<>(); + + // Persisted across editor instances, reopening restores last search and page. + private static String savedSearchText = ""; + private static int savedFirstItemIndex = 0; + + @Nullable + private GuiTextField nameField; + @Nullable + private GuiTextField searchField; + + // Left grid (all items) + private List filteredItems = Collections.emptyList(); + private int leftCols; + private int leftRows; + private int leftGridX; + private int leftGridY; + private int leftPage = 0; + private int leftTotalPages = 1; + private int leftItemsPerPage; + + // Right grid (selected items) + private int rightCols; + private int rightRows; + private int rightGridX; + private int rightGridY; + private int rightPage = 0; + private int rightTotalPages = 1; + private int rightItemsPerPage; + + // Cached selected elements for the right panel (any ingredient type) + private List> selectedStacks = new ArrayList<>(); + // Maps each right-panel element to its stored UID (exact or wildcard ending in ":*") + private final Map, String> selectedStackToStoredUid = new HashMap<>(); + + // Index of OTHER custom groups — used to tint items that already belong to a different group. + // Built once in initGui(); key = exact UID, value = list of display names. + private Map> otherGroupExactUids = new HashMap<>(); + // [prefix, displayName] pairs for wildcard entries in other groups + private List otherGroupWildcardPrefixes = new ArrayList<>(); + + // Drag-select state + private boolean isDragging = false; + private boolean dragAdding = false; + /** True when the current drag was started with Ctrl held — uses wildcard UIDs. */ + private boolean dragWildcard = false; + @Nullable private String lastDraggedUid = null; + + public GuiCustomGroupEditor(GuiCollapsibleGroups parentScreen, CustomGroupsConfig.CustomGroup group) { + this.parentScreen = parentScreen; + this.group = group; + this.selectedUids.addAll(group.itemUids); + } + + @Override + public void initGui() { + super.initGui(); + Keyboard.enableRepeatEvents(true); + this.buttonList.clear(); + + int topBarHeight = 48; + int panelDivider = (int) (this.width * 0.65); + + // Name field + nameField = new GuiTextField(10, this.fontRenderer, 62, 6, panelDivider - 70, 16); + nameField.setMaxStringLength(40); + nameField.setText(group.displayName != null ? group.displayName : ""); + + // Search field — restore saved text so the user's last search carries over + searchField = new GuiTextField(11, this.fontRenderer, 4, topBarHeight - 18, panelDivider - 12, 14); + searchField.setMaxStringLength(128); + searchField.setText(savedSearchText); + + // Save & Cancel buttons + this.buttonList.add(new GuiButton(BTN_SAVE, panelDivider + 4, 4, 50, 20, + Translator.translateToLocal("hei.gui.collapsible.editor.save"))); + this.buttonList.add(new GuiButton(BTN_CANCEL, panelDivider + 58, 4, 50, 20, + Translator.translateToLocal("hei.gui.collapsible.back"))); + + // Calculate left grid layout + int leftWidth = panelDivider - 8; + int leftHeight = this.height - topBarHeight - 26; // room for page nav + leftCols = Math.max(1, leftWidth / ITEM_SIZE); + leftRows = Math.max(1, leftHeight / ITEM_SIZE); + leftGridX = (panelDivider - 4 - leftCols * ITEM_SIZE) / 2; + leftGridY = topBarHeight; + leftItemsPerPage = leftCols * leftRows; + + // Calculate right grid layout + int rightWidth = this.width - panelDivider - 8; + int rightHeight = this.height - topBarHeight - 26; + rightCols = Math.max(1, rightWidth / ITEM_SIZE); + rightRows = Math.max(1, rightHeight / ITEM_SIZE); + rightGridX = panelDivider + 4; + rightGridY = topBarHeight; + rightItemsPerPage = rightCols * rightRows; + + // Page nav buttons for left grid + int leftNavY = this.height - 22; + this.buttonList.add(new GuiButton(BTN_PREV_PAGE, 4, leftNavY, 30, 20, "<")); + this.buttonList.add(new GuiButton(BTN_NEXT_PAGE, panelDivider - 34, leftNavY, 30, 20, ">")); + + // Page nav buttons for right grid + this.buttonList.add(new GuiButton(BTN_PREV_SEL_PAGE, panelDivider + 4, leftNavY, 30, 20, "<")); + this.buttonList.add(new GuiButton(BTN_NEXT_SEL_PAGE, this.width - 34, leftNavY, 30, 20, ">")); + + updateFilteredItems(); + leftPage = Math.max(0, Math.min(savedFirstItemIndex / leftItemsPerPage, leftTotalPages - 1)); + updateSelectedStacks(); + buildOtherGroupIndex(); + } + + @Override + public void onGuiClosed() { + super.onGuiClosed(); + Keyboard.enableRepeatEvents(false); + // Persist the current search text and the absolute index of the first visible item. + savedSearchText = (searchField != null) ? searchField.getText() : ""; + savedFirstItemIndex = leftPage * leftItemsPerPage; + } + + /** + * Returns a unique string identifier for any ingredient type, or null if unavailable. + * Uses StackHelper for ItemStacks, and the generic IngredientRegistry helper for everything else + * (e.g. FluidStack via FluidStackHelper.getUniqueId). + */ + @Nullable + private static String getIngredientUid(Object ingredient) { + if (ingredient instanceof ItemStack) { + ItemStack stack = (ItemStack) ingredient; + if (stack.isEmpty()) return null; + try { + return Internal.getStackHelper().getUniqueIdentifierForStack(stack); + } catch (Exception e) { + return null; + } + } + try { + @SuppressWarnings("unchecked") + mezz.jei.api.ingredients.IIngredientHelper helper = + (mezz.jei.api.ingredients.IIngredientHelper) + Internal.getIngredientRegistry().getIngredientHelper(ingredient); + return helper.getUniqueId(ingredient); + } catch (Exception e) { + return null; + } + } + + /** + * Returns a wildcard UID for ItemStacks by stripping metadata/subtype info, then appending ":*" + * as a storage marker to distinguish it from exact UIDs. + * Non-ItemStack ingredients have no metadata variants, so their normal UID is returned. + */ + @Nullable + private static String getIngredientWildcardUid(Object ingredient) { + if (ingredient instanceof ItemStack) { + ItemStack stack = (ItemStack) ingredient; + if (stack.isEmpty()) return null; + try { + String prefix = Internal.getStackHelper().getUniqueIdentifierForStack(stack, StackHelper.UidMode.WILDCARD); + return prefix + ":*"; + } catch (Exception e) { + return null; + } + } + // Non-ItemStack types have no metadata variants — fall back to exact UID + return getIngredientUid(ingredient); + } + + /** + * Returns true if the given normal UID is covered by any selected entry — + * either an exact match, or a wildcard prefix match (stored as "prefix:*"). + */ + private boolean isUidSelected(String normalUid) { + if (selectedUids.contains(normalUid)) return true; + return findCoveringWildcard(normalUid) != null; + } + + /** + * Returns the wildcard stored UID (ending in ":*") that covers the given normal UID, + * or null if no wildcard covers it. + */ + @Nullable + /** Builds a reverse index of all other custom groups for fast membership lookup. */ + private void buildOtherGroupIndex() { + otherGroupExactUids.clear(); + otherGroupWildcardPrefixes.clear(); + CustomGroupsConfig cfg = Config.getCustomGroupsConfig(); + if (cfg == null) return; + for (CustomGroupsConfig.CustomGroup g : cfg.getCustomGroups()) { + if (g.id.equals(group.id) || g.itemUids == null) continue; + String name = (g.displayName != null && !g.displayName.isEmpty()) ? g.displayName : g.id; + for (String uid : g.itemUids) { + if (uid.endsWith(":*")) { + otherGroupWildcardPrefixes.add(new String[]{uid.substring(0, uid.length() - 2), name}); + } else { + otherGroupExactUids.computeIfAbsent(uid, k -> new ArrayList<>()).add(name); + } + } + } + } + + /** Returns the names of other custom groups that contain the given normal UID, or an empty list. */ + private List getOtherGroupNames(String normalUid) { + List names = new ArrayList<>(otherGroupExactUids.getOrDefault(normalUid, Collections.emptyList())); + for (String[] entry : otherGroupWildcardPrefixes) { + if (normalUid.equals(entry[0]) || normalUid.startsWith(entry[0] + ":")) { + names.add(entry[1]); + } + } + return names; + } + + private String findCoveringWildcard(String normalUid) { + for (String stored : selectedUids) { + if (stored.endsWith(":*")) { + String prefix = stored.substring(0, stored.length() - 2); + if (normalUid.equals(prefix) || normalUid.startsWith(prefix + ":")) return prefix + ":*"; + } + } + return null; + } + + /** + * Collects all exact UIDs from the full ingredient list that share the given wildcard prefix. + * Used for auto-promote checks and wildcard decomposition. + */ + private List getSiblingUids(String wildcardUid) { + if (!wildcardUid.endsWith(":*") || !Internal.hasIngredientFilter()) return Collections.emptyList(); + String prefix = wildcardUid.substring(0, wildcardUid.length() - 2); + List all = Internal.getIngredientFilter().getIngredientList(""); + List result = new ArrayList<>(); + for (IIngredientListElement elem : all) { + String uid = getIngredientUid(elem.getIngredient()); + if (uid != null && (uid.equals(prefix) || uid.startsWith(prefix + ":"))) { + result.add(uid); + } + } + return result; + } + + /** + * Decomposes a wildcard entry into individual exact entries, excluding one item + * (the one the user just clicked to remove). + */ + private void decomposeWildcard(String wildcardUid, @Nullable String excludeUid) { + selectedUids.remove(wildcardUid); + for (String sibling : getSiblingUids(wildcardUid)) { + if (!sibling.equals(excludeUid)) { + selectedUids.add(sibling); + } + } + updateSelectedStacks(); + } + + /** + * After adding an exact UID, checks if every meta variant of that item is now individually + * selected. If so, replaces them all with a single wildcard entry. + */ + private void maybePromoteToWildcard(Object ingredient, String addedUid) { + String wildcardUid = getIngredientWildcardUid(ingredient); + if (wildcardUid == null || !wildcardUid.endsWith(":*")) return; + List siblings = getSiblingUids(wildcardUid); + if (siblings.isEmpty()) return; + for (String sibling : siblings) { + if (!selectedUids.contains(sibling)) return; + } + // All variants are individually selected — promote to a single wildcard entry + selectedUids.removeAll(siblings); + selectedUids.add(wildcardUid); + updateSelectedStacks(); + } + + /** Returns a mutable list of tooltip lines for any ingredient element. */ + @SuppressWarnings("unchecked") + private List getIngredientTooltipLines(IIngredientListElement element) { + try { + T ingredient = element.getIngredient(); + ITooltipFlag flag = mc.gameSettings.advancedItemTooltips + ? ITooltipFlag.TooltipFlags.ADVANCED + : ITooltipFlag.TooltipFlags.NORMAL; + if (ingredient instanceof ItemStack) { + return ((ItemStack) ingredient).getTooltip(mc.player, flag); + } + IIngredientRenderer renderer = element.getIngredientRenderer(); + return new ArrayList<>(renderer.getTooltip(mc, ingredient, flag)); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + private void renderIngredientTooltip(IIngredientListElement element, int mouseX, int mouseY) { + List lines = getIngredientTooltipLines(element); + if (!lines.isEmpty()) { + drawHoveringText(lines, mouseX, mouseY, mc.fontRenderer); + } + } + + private void updateFilteredItems() { if (!Internal.hasIngredientFilter()) { + filteredItems = Collections.emptyList(); + return; + } + IngredientFilter filter = Internal.getIngredientFilter(); + String search = (searchField != null) ? searchField.getText() : ""; + filteredItems = filter.getIngredientList(search); + leftTotalPages = Math.max(1, (filteredItems.size() + leftItemsPerPage - 1) / leftItemsPerPage); + if (leftPage >= leftTotalPages) { + leftPage = leftTotalPages - 1; + } + } + + private void updateSelectedStacks() { + selectedStacks.clear(); + selectedStackToStoredUid.clear(); + if (!Internal.hasIngredientFilter()) { + return; + } + IngredientFilter filter = Internal.getIngredientFilter(); + List all = filter.getIngredientList(""); + + // Precompute wildcard prefix → stored UID for O(n) matching + Map wildcardPrefixToStored = new LinkedHashMap<>(); + for (String uid : selectedUids) { + if (uid.endsWith(":*")) { + wildcardPrefixToStored.put(uid.substring(0, uid.length() - 2), uid); + } + } + + for (IIngredientListElement element : all) { + String uid = getIngredientUid(element.getIngredient()); + if (uid == null) continue; + if (selectedUids.contains(uid)) { + // Exact match + selectedStacks.add(element); + selectedStackToStoredUid.put(element, uid); + } else if (!wildcardPrefixToStored.isEmpty()) { + // Wildcard prefix match — show ALL matching elements + for (Map.Entry entry : wildcardPrefixToStored.entrySet()) { + String prefix = entry.getKey(); + String storedUid = entry.getValue(); + if (uid.equals(prefix) || uid.startsWith(prefix + ":")) { + selectedStacks.add(element); + selectedStackToStoredUid.put(element, storedUid); + break; + } + } + } + } + + rightTotalPages = Math.max(1, (selectedStacks.size() + rightItemsPerPage - 1) / rightItemsPerPage); + if (rightPage >= rightTotalPages) { + rightPage = rightTotalPages - 1; + } + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + switch (button.id) { + case BTN_SAVE: + saveAndClose(); + break; + case BTN_CANCEL: + this.mc.displayGuiScreen(parentScreen); + break; + case BTN_PREV_PAGE: + leftPage = Math.max(0, leftPage - 1); + break; + case BTN_NEXT_PAGE: + leftPage = Math.min(leftTotalPages - 1, leftPage + 1); + break; + case BTN_PREV_SEL_PAGE: + rightPage = Math.max(0, rightPage - 1); + break; + case BTN_NEXT_SEL_PAGE: + rightPage = Math.min(rightTotalPages - 1, rightPage + 1); + break; + } + } + + private void saveAndClose() { + if (nameField != null) { + group.displayName = nameField.getText(); + } + group.itemUids = new ArrayList<>(selectedUids); + + CustomGroupsConfig customGroupsConfig = Config.getCustomGroupsConfig(); + if (customGroupsConfig != null) { + customGroupsConfig.updateGroup(group); + Internal.getCollapsedStackRegistry().recollectCustomEntries(); + if (Internal.hasIngredientFilter()) { + IngredientFilter filter = Internal.getIngredientFilter(); + // Invalidate the filter cache so the next call to getIngredientList + // rebuilds collapsedListCached with the new custom entries. + filter.invalidateCache(); + filter.getIngredientList(Config.getFilterText()); + filter.notifyListenersOfChange(); + } + } + parentScreen.onEditorClosed(); + this.mc.displayGuiScreen(parentScreen); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.drawDefaultBackground(); + + int panelDivider = (int) (this.width * 0.65); + + // Name label + this.fontRenderer.drawStringWithShadow( + Translator.translateToLocal("hei.gui.collapsible.editor.name") + ":", + 4, 10, 0xFFFFFF); + if (nameField != null) { + nameField.drawTextBox(); + } + + // Search field + if (searchField != null) { + searchField.drawTextBox(); + } + + // Divider line + drawVerticalLine(panelDivider, 0, this.height, 0xFF555555); + + // "Selected" header on right panel + String selHeader = Translator.translateToLocal("hei.gui.collapsible.editor.title"); + this.fontRenderer.drawStringWithShadow(selHeader, panelDivider + 4, 26, 0xCCCCCC); + String selCount = String.format(Translator.translateToLocal("hei.gui.collapsible.editor.selected"), selectedUids.size()); + this.fontRenderer.drawStringWithShadow(selCount, panelDivider + 4, 36, 0x888888); + + // Draw left grid (all items) + drawLeftGrid(mouseX, mouseY); + + // Draw right grid (selected items) + drawRightGrid(mouseX, mouseY); + + // Left page counter + if (leftTotalPages > 1) { + int navY = this.height - 18; + String pageText = (leftPage + 1) + "/" + leftTotalPages; + int centerX = (4 + panelDivider) / 2; + this.drawCenteredString(this.fontRenderer, pageText, centerX, navY, 0xAAAAAA); + } + + // Right page counter + if (rightTotalPages > 1) { + int navY = this.height - 18; + String pageText = (rightPage + 1) + "/" + rightTotalPages; + int centerX = (panelDivider + 4 + this.width) / 2; + this.drawCenteredString(this.fontRenderer, pageText, centerX, navY, 0xAAAAAA); + } + + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw tooltips last (after buttons) + drawLeftGridTooltips(mouseX, mouseY); + drawRightGridTooltips(mouseX, mouseY); + } + + private void drawLeftGrid(int mouseX, int mouseY) { + if (filteredItems.isEmpty()) { + return; + } + int startIdx = leftPage * leftItemsPerPage; + + RenderHelper.enableGUIStandardItemLighting(); + GlStateManager.enableDepth(); + + for (int i = 0; i < leftItemsPerPage && (startIdx + i) < filteredItems.size(); i++) { + IIngredientListElement element = filteredItems.get(startIdx + i); + int col = i % leftCols; + int row = i / leftCols; + int x = leftGridX + col * ITEM_SIZE; + int y = leftGridY + row * ITEM_SIZE; + + Object ingredient = element.getIngredient(); + renderIngredient(element, x + 1, y + 1); + + // Orange tint if this item belongs to another custom group + String uid = getIngredientUid(ingredient); + if (uid != null && !getOtherGroupNames(uid).isEmpty()) { + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + GlStateManager.colorMask(true, true, true, false); + GuiUtils.drawGradientRect(0, x, y, x + ITEM_SIZE, y + ITEM_SIZE, 0x40FF8800, 0x40FF8800); + GlStateManager.colorMask(true, true, true, true); + GlStateManager.enableDepth(); + RenderHelper.enableGUIStandardItemLighting(); + } + + // Green overlay if selected (exact or wildcard) + boolean selected = uid != null && isUidSelected(uid); + if (selected) { + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + GlStateManager.colorMask(true, true, true, false); + GuiUtils.drawGradientRect(0, x, y, x + ITEM_SIZE, y + ITEM_SIZE, 0x4000FF00, 0x4000FF00); + GlStateManager.colorMask(true, true, true, true); + // "*" badge for wildcard-matched items (covered by a stored ":*" uid, not exact) + if (uid != null && !selectedUids.contains(uid)) { + fontRenderer.drawStringWithShadow("*", x + 1, y + 1, 0xFFAA00); + } + GlStateManager.enableDepth(); + RenderHelper.enableGUIStandardItemLighting(); + } + + // Highlight on hover + if (mouseX >= x && mouseX < x + ITEM_SIZE && mouseY >= y && mouseY < y + ITEM_SIZE) { + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + GlStateManager.colorMask(true, true, true, false); + GuiUtils.drawGradientRect(0, x, y, x + ITEM_SIZE, y + ITEM_SIZE, 0x80FFFFFF, 0x80FFFFFF); + GlStateManager.colorMask(true, true, true, true); + GlStateManager.enableDepth(); + RenderHelper.enableGUIStandardItemLighting(); + } + } + + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + } + + private void drawRightGrid(int mouseX, int mouseY) { + if (selectedStacks.isEmpty()) { + return; + } + int startIdx = rightPage * rightItemsPerPage; + + RenderHelper.enableGUIStandardItemLighting(); + GlStateManager.enableDepth(); + + for (int i = 0; i < rightItemsPerPage && (startIdx + i) < selectedStacks.size(); i++) { + IIngredientListElement element = selectedStacks.get(startIdx + i); + int col = i % rightCols; + int row = i / rightCols; + int x = rightGridX + col * ITEM_SIZE; + int y = rightGridY + row * ITEM_SIZE; + + renderIngredient(element, x + 1, y + 1); + + // "*" badge if this is a wildcard representative entry + String storedUid = selectedStackToStoredUid.get(element); + if (storedUid != null && storedUid.endsWith(":*")) { + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + fontRenderer.drawStringWithShadow("*", x + 1, y + 1, 0xFFAA00); + GlStateManager.enableDepth(); + RenderHelper.enableGUIStandardItemLighting(); + } + + // Highlight on hover + if (mouseX >= x && mouseX < x + ITEM_SIZE && mouseY >= y && mouseY < y + ITEM_SIZE) { + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + GlStateManager.colorMask(true, true, true, false); + GuiUtils.drawGradientRect(0, x, y, x + ITEM_SIZE, y + ITEM_SIZE, 0x80FF8888, 0x80FF8888); + GlStateManager.colorMask(true, true, true, true); + GlStateManager.enableDepth(); + RenderHelper.enableGUIStandardItemLighting(); + } + } + + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableDepth(); + } + + @SuppressWarnings("unchecked") + private void renderIngredient(IIngredientListElement element, int x, int y) { + try { + IIngredientRenderer renderer = element.getIngredientRenderer(); + T ingredient = element.getIngredient(); + renderer.render(this.mc, x, y, ingredient); + } catch (Exception ignored) { + } + } + + private void drawLeftGridTooltips(int mouseX, int mouseY) { + if (filteredItems.isEmpty()) { + return; + } + int startIdx = leftPage * leftItemsPerPage; + for (int i = 0; i < leftItemsPerPage && (startIdx + i) < filteredItems.size(); i++) { + int col = i % leftCols; + int row = i / leftCols; + int x = leftGridX + col * ITEM_SIZE; + int y = leftGridY + row * ITEM_SIZE; + if (mouseX >= x && mouseX < x + ITEM_SIZE && mouseY >= y && mouseY < y + ITEM_SIZE) { + IIngredientListElement element = filteredItems.get(startIdx + i); + List lines = getIngredientTooltipLines(element); + if (element.getIngredient() instanceof ItemStack) { + String uid3 = getIngredientUid(element.getIngredient()); + boolean alreadySelected = uid3 != null && isUidSelected(uid3); + if (alreadySelected) { + lines.add(TextFormatting.GOLD + "Ctrl+Click: Remove all variants"); + } else { + lines.add(TextFormatting.GOLD + "Ctrl+Click: Select all variants (Wildcard)"); + } + } + String uid2 = getIngredientUid(element.getIngredient()); + if (uid2 != null) { + List otherGroups = getOtherGroupNames(uid2); + if (!otherGroups.isEmpty()) { + lines.add(TextFormatting.GOLD + "In group" + (otherGroups.size() > 1 ? "s" : "") + ": " + + String.join(", ", otherGroups)); + } + } + if (!lines.isEmpty()) { + drawHoveringText(lines, mouseX, mouseY, mc.fontRenderer); + } + return; + } + } + } + + private void drawRightGridTooltips(int mouseX, int mouseY) { + if (selectedStacks.isEmpty()) { + return; + } + int startIdx = rightPage * rightItemsPerPage; + for (int i = 0; i < rightItemsPerPage && (startIdx + i) < selectedStacks.size(); i++) { + int col = i % rightCols; + int row = i / rightCols; + int x = rightGridX + col * ITEM_SIZE; + int y = rightGridY + row * ITEM_SIZE; + if (mouseX >= x && mouseX < x + ITEM_SIZE && mouseY >= y && mouseY < y + ITEM_SIZE) { + IIngredientListElement element = selectedStacks.get(startIdx + i); + String storedUid = selectedStackToStoredUid.get(element); + List lines = getIngredientTooltipLines(element); + if (storedUid != null && storedUid.endsWith(":*")) { + lines.add(TextFormatting.GOLD + "Wildcard (*)"); + } + if (!lines.isEmpty()) { + drawHoveringText(lines, mouseX, mouseY, mc.fontRenderer); + } + return; + } + } + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + if (nameField != null) { + nameField.mouseClicked(mouseX, mouseY, mouseButton); + } + if (searchField != null) { + boolean wasSearchFocused = searchField.isFocused(); + searchField.mouseClicked(mouseX, mouseY, mouseButton); + // Right-click clears search + if (searchField.isFocused() && mouseButton == 1) { + searchField.setText(""); + leftPage = 0; + updateFilteredItems(); + } + } + + // Left grid click: toggle item selection and start drag + // Ctrl+Click: adds a wildcard UID, or removes entire family if item is already selected + // Normal click: adds/removes exact UID, with auto-promote and wildcard-decompose support + if (mouseButton == 0 && !filteredItems.isEmpty()) { + int startIdx = leftPage * leftItemsPerPage; + for (int i = 0; i < leftItemsPerPage && (startIdx + i) < filteredItems.size(); i++) { + int col = i % leftCols; + int row = i / leftCols; + int x = leftGridX + col * ITEM_SIZE; + int y = leftGridY + row * ITEM_SIZE; + if (mouseX >= x && mouseX < x + ITEM_SIZE && mouseY >= y && mouseY < y + ITEM_SIZE) { + IIngredientListElement element = filteredItems.get(startIdx + i); + boolean ctrl = isCtrlKeyDown(); + String exactUid = getIngredientUid(element.getIngredient()); + if (exactUid == null) return; + + if (ctrl) { + if (isUidSelected(exactUid)) { + // Ctrl+Click on a selected item → remove the entire family (all meta variants) + String familyWildcard = getIngredientWildcardUid(element.getIngredient()); + if (familyWildcard != null && familyWildcard.endsWith(":*")) { + String prefix = familyWildcard.substring(0, familyWildcard.length() - 2); + selectedUids.remove(familyWildcard); + selectedUids.removeIf(existing -> + existing.equals(prefix) || existing.startsWith(prefix + ":")); + } else { + selectedUids.remove(exactUid); + } + dragAdding = false; + dragWildcard = true; + isDragging = true; + lastDraggedUid = exactUid; + updateSelectedStacks(); + } else { + // Ctrl+Click on unselected item → add wildcard, remove any exact UIDs it already covers + String wildcardUid = getIngredientWildcardUid(element.getIngredient()); + if (wildcardUid == null) wildcardUid = exactUid; + if (wildcardUid.endsWith(":*")) { + String prefix = wildcardUid.substring(0, wildcardUid.length() - 2); + selectedUids.removeIf(existing -> + existing.equals(prefix) || existing.startsWith(prefix + ":")); + } + dragAdding = true; + dragWildcard = true; + isDragging = true; + lastDraggedUid = wildcardUid; + selectedUids.add(wildcardUid); + updateSelectedStacks(); + } + } else { + // Normal click + if (!selectedUids.contains(exactUid)) { + // Item is covered by a wildcard → decompose to exact entries minus the clicked one + String coveringWildcard = findCoveringWildcard(exactUid); + if (coveringWildcard != null) { + decomposeWildcard(coveringWildcard, exactUid); + dragAdding = false; + dragWildcard = false; + isDragging = true; + lastDraggedUid = exactUid; + return; + } + } + boolean adding = !selectedUids.contains(exactUid); + dragAdding = adding; + dragWildcard = false; + isDragging = true; + lastDraggedUid = exactUid; + toggleSelectionByUid(exactUid); + // Auto-promote: if all meta variants are now individually selected, consolidate to a wildcard + if (adding) { + maybePromoteToWildcard(element.getIngredient(), exactUid); + } + } + return; + } + } + } + + // Right grid click: remove from selection. + // Ctrl+Click: wildcard removal — removes the entire family (wildcard entry + all exact siblings). + // Normal click on a wildcard entry: decomposes it, removing only the clicked item. + // Normal click on an exact entry: removes just that item. + if (mouseButton == 0 && !selectedStacks.isEmpty()) { + int startIdx = rightPage * rightItemsPerPage; + for (int i = 0; i < rightItemsPerPage && (startIdx + i) < selectedStacks.size(); i++) { + int col = i % rightCols; + int row = i / rightCols; + int x = rightGridX + col * ITEM_SIZE; + int y = rightGridY + row * ITEM_SIZE; + if (mouseX >= x && mouseX < x + ITEM_SIZE && mouseY >= y && mouseY < y + ITEM_SIZE) { + IIngredientListElement element = selectedStacks.get(startIdx + i); + String storedUid = selectedStackToStoredUid.get(element); + if (storedUid != null) { + boolean ctrl = isCtrlKeyDown(); + if (ctrl) { + // Ctrl+Click — remove all variants regardless of exact/wildcard storage + String familyWildcard = getIngredientWildcardUid(element.getIngredient()); + if (familyWildcard != null && familyWildcard.endsWith(":*")) { + String prefix = familyWildcard.substring(0, familyWildcard.length() - 2); + selectedUids.remove(familyWildcard); + selectedUids.removeIf(existing -> + existing.equals(prefix) || existing.startsWith(prefix + ":")); + updateSelectedStacks(); + } else { + removeSelectionByUid(storedUid); + } + } else if (storedUid.endsWith(":*")) { + // Normal click on a wildcard entry — decompose, removing only this item + String exactUid = getIngredientUid(element.getIngredient()); + decomposeWildcard(storedUid, exactUid); + } else { + removeSelectionByUid(storedUid); + } + } + return; + } + } + } + + super.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + protected void mouseReleased(int mouseX, int mouseY, int state) { + super.mouseReleased(mouseX, mouseY, state); + isDragging = false; + dragWildcard = false; + lastDraggedUid = null; + } + + @Override + protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { + super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); + // Wildcard selections are single-click only — no drag expansion + if (!isDragging || clickedMouseButton != 0 || filteredItems.isEmpty() || dragWildcard) return; + int startIdx = leftPage * leftItemsPerPage; + for (int i = 0; i < leftItemsPerPage && (startIdx + i) < filteredItems.size(); i++) { + int col = i % leftCols; + int row = i / leftCols; + int x = leftGridX + col * ITEM_SIZE; + int y = leftGridY + row * ITEM_SIZE; + if (mouseX >= x && mouseX < x + ITEM_SIZE && mouseY >= y && mouseY < y + ITEM_SIZE) { + IIngredientListElement elem = filteredItems.get(startIdx + i); + String uid = getIngredientUid(elem.getIngredient()); + if (uid != null && !uid.equals(lastDraggedUid)) { + lastDraggedUid = uid; + if (dragAdding) { + // Skip items already covered by an exact or wildcard selection + if (!isUidSelected(uid) && selectedUids.add(uid)) { + updateSelectedStacks(); + // Auto-promote: if all variants of this item are selected, consolidate to a wildcard + maybePromoteToWildcard(elem.getIngredient(), uid); + } + } else { + if (selectedUids.remove(uid)) updateSelectedStacks(); + } + } + return; + } + } + } + + private void toggleSelectionByUid(String uid) { + if (selectedUids.contains(uid)) { + selectedUids.remove(uid); + } else { + selectedUids.add(uid); + } + updateSelectedStacks(); + } + + private void removeSelectionByUid(String uid) { + selectedUids.remove(uid); + updateSelectedStacks(); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + if (nameField != null && nameField.isFocused()) { + nameField.textboxKeyTyped(typedChar, keyCode); + return; + } + if (searchField != null && searchField.isFocused()) { + String before = searchField.getText(); + searchField.textboxKeyTyped(typedChar, keyCode); + if (!searchField.getText().equals(before)) { + leftPage = 0; + } + updateFilteredItems(); + return; + } + // Ctrl+F focuses the search field + if (keyCode == Keyboard.KEY_F && isCtrlKeyDown()) { + if (searchField != null) { + searchField.setFocused(true); + if (nameField != null) { + nameField.setFocused(false); + } + } + return; + } + if (keyCode == Keyboard.KEY_ESCAPE) { + this.mc.displayGuiScreen(parentScreen); + return; + } + super.keyTyped(typedChar, keyCode); + } + + @Override + public void handleMouseInput() throws IOException { + super.handleMouseInput(); + int scrollDelta = Mouse.getEventDWheel(); + if (scrollDelta != 0) { + int mouseX = Mouse.getEventX() * this.width / this.mc.displayWidth; + int panelDivider = (int) (this.width * 0.65); + + if (mouseX < panelDivider) { + // Scroll left grid + if (scrollDelta < 0 && leftPage < leftTotalPages - 1) { + leftPage++; + } else if (scrollDelta > 0 && leftPage > 0) { + leftPage--; + } + } else { + // Scroll right grid + if (scrollDelta < 0 && rightPage < rightTotalPages - 1) { + rightPage++; + } else if (scrollDelta > 0 && rightPage > 0) { + rightPage--; + } + } + } + } + + @Override + public void updateScreen() { + super.updateScreen(); + if (nameField != null) { + nameField.updateCursorCounter(); + } + if (searchField != null) { + searchField.updateCursorCounter(); + } + } +} diff --git a/src/main/java/mezz/jei/ingredients/CollapsedStack.java b/src/main/java/mezz/jei/ingredients/CollapsedStack.java new file mode 100644 index 00000000..df9c0bee --- /dev/null +++ b/src/main/java/mezz/jei/ingredients/CollapsedStack.java @@ -0,0 +1,266 @@ +package mezz.jei.ingredients; + +import mezz.jei.Internal; +import mezz.jei.api.ingredients.IIngredientHelper; +import mezz.jei.api.ingredients.IIngredientRenderer; +import mezz.jei.api.recipe.IIngredientType; +import mezz.jei.gui.ingredients.IIngredientListElement; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Represents a collapsible ingredient group — both the group definition (id, display name, + * matcher, expanded state) and the runtime list of matched ingredients from the current filter. + *

+ * Registered as an {@link IIngredientType} so that addons which introspect grid items + * always find a valid type. Recipe lookups are delegated to the first ingredient via + * {@link mezz.jei.api.ingredients.IIngredientHelper#translateFocus}. + *

+ */ +public class CollapsedStack implements IIngredientListElement { + // Registered as IIngredientType for addon compatibility — addons expect every grid item to have a type + public static final IIngredientType TYPE = () -> CollapsedStack.class; + + /** Identifies who registered this group. */ + public enum GroupSource { + DEFAULT, + MOD, + CUSTOM + } + + private final String id; + private final String displayName; + private final GroupSource source; + /** Matches against the raw ingredient object (any type). */ + private final Predicate matcher; + /** + * Optional fast-path matcher that receives a pre-computed UID string instead of the raw + * ingredient. Set on custom groups by {@link CollapsedStackRegistry} so that + * {@link mezz.jei.ingredients.IngredientFilter#collapse} can skip calling + * {@code getUniqueIdentifierForStack()} N×M times per filter cycle. + */ + @Nullable private Predicate uidMatcher; + private boolean expanded; + private boolean visible = true; + private final List> ingredients; + + /** + * Primary constructor — matcher receives the raw ingredient object. + */ + public CollapsedStack(String id, String displayName, Predicate matcher) { + this(id, displayName, matcher, GroupSource.DEFAULT); + } + + /** + * Constructor with explicit group source. + */ + public CollapsedStack(String id, String displayName, Predicate matcher, GroupSource source) { + this.id = id; + this.displayName = displayName; + this.matcher = matcher; + this.source = source; + this.expanded = false; + this.ingredients = new ArrayList<>(); + } + + /** + * Convenience factory for groups that only care about ItemStack ingredients. + * Non-ItemStack ingredients automatically return false. + */ + public static CollapsedStack ofItemStack(String id, String displayName, Predicate stackMatcher) { + return new CollapsedStack(id, displayName, + ingredient -> ingredient instanceof ItemStack && stackMatcher.test((ItemStack) ingredient)); + } + + /** + * Convenience factory for ItemStack groups with an explicit source. + */ + public static CollapsedStack ofItemStack(String id, String displayName, Predicate stackMatcher, GroupSource source) { + return new CollapsedStack(id, displayName, + ingredient -> ingredient instanceof ItemStack && stackMatcher.test((ItemStack) ingredient), source); + } + + // --- Group definition --- + + public String getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + public GroupSource getSource() { + return source; + } + + public boolean isExpanded() { + return expanded; + } + + public void setExpanded(boolean expanded) { + this.expanded = expanded; + } + + public void toggleExpanded() { + this.expanded = !this.expanded; + } + + /** + * Tests whether the given element matches this collapsible group. + * The raw ingredient (any type) is passed to the matcher. + * Empty ItemStacks are always rejected. + */ + public boolean matches(IIngredientListElement element) { + Object ingredient = element.getIngredient(); + if (ingredient instanceof ItemStack && ((ItemStack) ingredient).isEmpty()) { + return false; + } + return matcher.test(ingredient); + } + + /** Returns the UID-based matcher, or {@code null} if this group uses a raw-ingredient predicate. */ + @Nullable + public Predicate getUidMatcher() { + return uidMatcher; + } + + public void setUidMatcher(Predicate uidMatcher) { + this.uidMatcher = uidMatcher; + } + + /** + * Computes a unique identifier string for any ingredient type. + * Returns {@code null} if the ingredient is empty or an error occurs. + * Used by {@code collapse()} to precompute UIDs once per element. + */ + @Nullable + public static String computeIngredientUid(Object ingredient) { + if (ingredient instanceof ItemStack) { + ItemStack stack = (ItemStack) ingredient; + if (stack.isEmpty()) return null; + try { + return Internal.getStackHelper().getUniqueIdentifierForStack(stack); + } catch (Exception e) { + return null; + } + } + try { + @SuppressWarnings("unchecked") + IIngredientHelper helper = (IIngredientHelper) + Internal.getIngredientRegistry().getIngredientHelper(ingredient); + return helper.getUniqueId(ingredient); + } catch (Exception e) { + return null; + } + } + + // --- Runtime ingredient list (transient per filter cycle) --- + + public List> getIngredients() { + return ingredients; + } + + public void addIngredient(IIngredientListElement element) { + ingredients.add(element); + } + + /** Clears the transient ingredient list for reuse across filter recalculations. */ + public void clearIngredients() { + ingredients.clear(); + } + + public int size() { + return ingredients.size(); + } + + public boolean isEmpty() { + return ingredients.isEmpty(); + } + + // --- IIngredientListElement implementation --- + + @Override + public CollapsedStack getIngredient() { + return this; + } + + @Override + public int getOrderIndex() { + return ingredients.isEmpty() ? 0 : ingredients.get(0).getOrderIndex(); + } + + @SuppressWarnings("unchecked") + @Override + public IIngredientHelper getIngredientHelper() { + return CollapsedStackIngredientHelper.INSTANCE; + } + + @SuppressWarnings("unchecked") + @Override + public IIngredientRenderer getIngredientRenderer() { + return mezz.jei.render.CollapsedStackRenderer.INSTANCE; + } + + @Override + public String getModNameForSorting() { + return ingredients.isEmpty() ? "" : ingredients.get(0).getModNameForSorting(); + } + + @Override + public Set getModNameStrings() { + return ingredients.isEmpty() ? Collections.emptySet() : ingredients.get(0).getModNameStrings(); + } + + @Override + public List getTooltipStrings() { + return ingredients.isEmpty() ? Collections.emptyList() : ingredients.get(0).getTooltipStrings(); + } + + @Override + public Collection getOreDictStrings() { + return Collections.emptyList(); + } + + @Override + public Collection getCreativeTabsStrings() { + return Collections.emptyList(); + } + + @Override + public Collection getColorStrings() { + return ingredients.isEmpty() ? Collections.emptyList() : ingredients.get(0).getColorStrings(); + } + + @Override + public String getResourceId() { + return "collapsedstack:" + id; + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public void setVisible(boolean visible) { + this.visible = visible; + } + + @Override + public int getGroupIndex() { + return 0; + } + + @Override + public boolean startsNewRow() { + return false; + } +} diff --git a/src/main/java/mezz/jei/ingredients/CollapsedStackIngredientHelper.java b/src/main/java/mezz/jei/ingredients/CollapsedStackIngredientHelper.java new file mode 100644 index 00000000..0416142b --- /dev/null +++ b/src/main/java/mezz/jei/ingredients/CollapsedStackIngredientHelper.java @@ -0,0 +1,138 @@ +package mezz.jei.ingredients; + +import mezz.jei.Internal; +import mezz.jei.api.ingredients.IIngredientHelper; +import mezz.jei.api.recipe.IFocus; +import mezz.jei.gui.ingredients.IIngredientListElement; +import net.minecraft.item.ItemStack; + +import javax.annotation.Nullable; +import java.awt.*; +import java.util.Collection; +import java.util.Collections; + +/** + * IIngredientHelper for the CollapsedStack ingredient type. + * Follows the BookmarkIngredientHelper delegation pattern — most methods + * delegate to the first ingredient's helper for addon compatibility. + */ +public class CollapsedStackIngredientHelper implements IIngredientHelper { + public static final CollapsedStackIngredientHelper INSTANCE = new CollapsedStackIngredientHelper(); + + @Nullable + @Override + public CollapsedStack getMatch(Iterable ingredients, CollapsedStack ingredientToMatch) { + for (CollapsedStack cs : ingredients) { + if (cs.getId().equals(ingredientToMatch.getId())) { + return cs; + } + } + return null; + } + + @Override + public String getDisplayName(CollapsedStack ingredient) { + return ingredient.getDisplayName(); + } + + @Override + public String getUniqueId(CollapsedStack ingredient) { + // Prefixed to avoid collisions with other ingredient type UIDs + return "collapsedstack:" + ingredient.getId(); + } + + @Override + public String getWildcardId(CollapsedStack ingredient) { + return getUniqueId(ingredient); + } + + @Override + public String getModId(CollapsedStack ingredient) { + return getFirstIngredientHelper(ingredient).getModId(getFirstIngredient(ingredient)); + } + + @Override + public String getDisplayModId(CollapsedStack ingredient) { + return getFirstIngredientHelper(ingredient).getDisplayModId(getFirstIngredient(ingredient)); + } + + @Override + public Iterable getColors(CollapsedStack ingredient) { + return getFirstIngredientHelper(ingredient).getColors(getFirstIngredient(ingredient)); + } + + @Override + public String getResourceId(CollapsedStack ingredient) { + return "collapsedstack:" + ingredient.getId(); + } + + @Override + public ItemStack getCheatItemStack(CollapsedStack ingredient) { + // Delegate cheat-give to the first ingredient in the group + return getFirstIngredientHelper(ingredient).getCheatItemStack(getFirstIngredient(ingredient)); + } + + @Override + public CollapsedStack copyIngredient(CollapsedStack ingredient) { + // CollapsedStack instances are transient and shared via the registry per filter cycle; + // returning the same instance is safe for current usage patterns. + return ingredient; + } + + @Override + public boolean isValidIngredient(CollapsedStack ingredient) { + return !ingredient.isEmpty(); + } + + @Override + public boolean isIngredientOnServer(CollapsedStack ingredient) { + return true; + } + + @Override + public Collection getOreDictNames(CollapsedStack ingredient) { + return Collections.emptyList(); + } + + @Override + public Collection getCreativeTabNames(CollapsedStack ingredient) { + return Collections.emptyList(); + } + + @Override + public String getErrorInfo(@Nullable CollapsedStack ingredient) { + if (ingredient == null) { + return "CollapsedStack is null"; + } + return "CollapsedStack[" + ingredient.getId() + ", " + ingredient.size() + " items]"; + } + + // Delegate recipe lookups to the representative item for addon compatibility + @Override + public IFocus translateFocus(IFocus focus, IFocusFactory focusFactory) { + CollapsedStack cs = focus.getValue(); + if (cs != null && !cs.isEmpty()) { + Object firstIngredient = cs.getIngredients().get(0).getIngredient(); + return focusFactory.createFocus(focus.getMode(), firstIngredient); + } + return focus; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static IIngredientHelper getFirstIngredientHelper(CollapsedStack ingredient) { + if (ingredient.isEmpty()) { + // Fallback: return ItemStack helper as a safe default + return Internal.getIngredientRegistry().getIngredientHelper(ItemStack.class); + } + Object first = ingredient.getIngredients().get(0).getIngredient(); + return Internal.getIngredientRegistry().getIngredientHelper(first); + } + + @SuppressWarnings("rawtypes") + private static Object getFirstIngredient(CollapsedStack ingredient) { + if (ingredient.isEmpty()) { + return ItemStack.EMPTY; + } + return ingredient.getIngredients().get(0).getIngredient(); + } +} diff --git a/src/main/java/mezz/jei/ingredients/CollapsedStackRegistry.java b/src/main/java/mezz/jei/ingredients/CollapsedStackRegistry.java new file mode 100644 index 00000000..9c67689d --- /dev/null +++ b/src/main/java/mezz/jei/ingredients/CollapsedStackRegistry.java @@ -0,0 +1,178 @@ +package mezz.jei.ingredients; + +import mezz.jei.config.Config; +import mezz.jei.config.CustomGroupsConfig; +import mezz.jei.gui.ingredients.IIngredientListElement; +import mezz.jei.startup.StackHelper; +import mezz.jei.util.Log; +import net.minecraft.item.ItemStack; +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.Predicate; + +/** + * Registry for CollapsedStack group definitions. + * Groups are checked in registration order; first match wins. + */ +public class CollapsedStackRegistry { + @Nullable + private static CollapsedStackRegistry instance; + + private final LinkedHashMap entries = new LinkedHashMap<>(); + private final List modEntries = new ArrayList<>(); + private final List customEntries = new ArrayList<>(); + private final Set disabledGroups = new HashSet<>(); + + public static CollapsedStackRegistry getInstance() { + if (instance == null) { + instance = new CollapsedStackRegistry(); + } + return instance; + } + + public static void setInstance(@Nullable CollapsedStackRegistry registry) { + instance = registry; + } + + /** + * Register a collapsible group whose membership is determined by an ItemStack predicate. + * Non-ItemStack ingredients are automatically excluded. + * + * @param id unique identifier for the group + * @param displayName localized display name + * @param matcher predicate that returns true for ItemStacks belonging to this group + */ + public void group(String id, String displayName, Predicate matcher) { + entries.put(id, CollapsedStack.ofItemStack(id, displayName, matcher)); + } + + /** + * Register a collapsible group whose membership is determined by a predicate on the + * raw ingredient object. Use this when the ingredients are not ItemStacks + * (e.g. EnchantmentData for enchanted books). + * + * @param id unique identifier for the group + * @param displayName localized display name + * @param matcher predicate on the raw ingredient object + */ + public void groupForType(String id, String displayName, Predicate matcher) { + entries.put(id, new CollapsedStack(id, displayName, matcher)); + } + + public Collection getEntries() { + return entries.values(); + } + + @Nullable + public CollapsedStack getEntry(String id) { + return entries.get(id); + } + + public void clear() { + entries.clear(); + modEntries.clear(); + } + + public Set getDisabledGroups() { + return disabledGroups; + } + + public void setDisabledGroups(Collection disabled) { + this.disabledGroups.clear(); + this.disabledGroups.addAll(disabled); + } + + public boolean isGroupEnabled(String id) { + return !disabledGroups.contains(id); + } + + public List getCustomEntries() { + return customEntries; + } + + public List getModEntries() { + return modEntries; + } + + /** + * Register a mod-provided collapsible group. + * The matcher may combine exact ingredient matches, type-wide matches, and custom predicates. + */ + public CollapsedStack addModGroup(String id, String displayName, Predicate matcher) { + CollapsedStack group = new CollapsedStack(id, displayName, matcher, CollapsedStack.GroupSource.MOD); + modEntries.add(group); + return group; + } + + /** + * Load custom collapsible groups from the JSON config. + * Creates CollapsedStack objects that match items by their unique identifier. + */ + public void loadCustomGroups() { + customEntries.clear(); + CustomGroupsConfig customGroupsConfig = Config.getCustomGroupsConfig(); + if (customGroupsConfig == null) { + return; + } + for (CustomGroupsConfig.CustomGroup group : customGroupsConfig.getCustomGroups()) { + if (group.id == null || group.id.isEmpty() || group.itemUids == null) { + continue; + } + // Split stored UIDs into exact matches and wildcard prefixes (stored as "prefix:*") + Set exactUids = new HashSet<>(); + Set wildcardPrefixes = new HashSet<>(); + for (String uid : group.itemUids) { + if (uid.endsWith(":*")) { + wildcardPrefixes.add(uid.substring(0, uid.length() - 2)); + } else { + exactUids.add(uid); + } + } + String displayName = group.displayName != null ? group.displayName : group.id; + + // UID-based fast-path predicate: O(K) hash-set lookups (K = number of ':' segments + // in the UID, typically 2–3) instead of O(W) iteration over all wildcard prefixes. + // Used by IngredientFilter.collapse() after it has pre-computed each element's UID once. + final Predicate uidPredicate = uid -> { + if (exactUids.contains(uid)) return true; + if (!wildcardPrefixes.isEmpty()) { + // Check if the UID itself is a wildcard prefix (uid.equals(prefix)) + if (wildcardPrefixes.contains(uid)) return true; + // Walk colon boundaries and check each prefix substring against the set + int idx = 0; + while ((idx = uid.indexOf(':', idx)) >= 0) { + if (wildcardPrefixes.contains(uid.substring(0, idx))) return true; + idx++; + } + } + return false; + }; + + // Ingredient-level matcher (fallback for call sites that don't pre-compute UIDs, + // e.g. withGroupNameMatches). Delegates UID computation to CollapsedStack.computeIngredientUid + // and then uses the same uidPredicate to avoid duplicating the matching logic. + CollapsedStack cs = new CollapsedStack(group.id, displayName, ingredient -> { + String uid = CollapsedStack.computeIngredientUid(ingredient); + return uid != null && uidPredicate.test(uid); + }); + cs.setUidMatcher(uidPredicate); + customEntries.add(cs); + } + Log.get().debug("Loaded {} custom collapsible groups", customEntries.size()); + } + + /** + * Reload custom entries from config. Called after saving changes. + */ + public void recollectCustomEntries() { + loadCustomGroups(); + } + + /** + * Sync disabled group state from Config values. + */ + public void syncDisabledGroups() { + this.disabledGroups.clear(); + this.disabledGroups.addAll(Config.getDisabledGroups()); + } +} diff --git a/src/main/java/mezz/jei/ingredients/IngredientFilter.java b/src/main/java/mezz/jei/ingredients/IngredientFilter.java index b806bed6..814926ea 100644 --- a/src/main/java/mezz/jei/ingredients/IngredientFilter.java +++ b/src/main/java/mezz/jei/ingredients/IngredientFilter.java @@ -3,6 +3,7 @@ import javax.annotation.Nullable; import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -31,11 +32,30 @@ public class IngredientFilter implements IIngredientFilter, IIngredientGridSourc public static boolean rebuild = false; private final List listeners = new ArrayList<>(); + private final List collapsedStateListeners = new ArrayList<>(); private IngredientBlacklistInternal blacklist; private IElementSearch elementSearch; private List ingredientListCached = Collections.emptyList(); + private List collapsedListCached = Collections.emptyList(); @Nullable private String filterCached; + /** + * Cached sorted list of all currently-visible ingredients — the result of a full + * suffix-tree traversal + sort. This does NOT change when the search-bar text changes, + * only when ingredients are added/removed or their visibility changes. Caching it here + * avoids the expensive {@code elementSearch.getAllIngredients()} traversal on every + * keystroke (once in {@link #getIngredientListUncached} for empty filter and once more + * in {@link #withGroupNameMatches} for every non-empty filter). + */ + @Nullable private List> allVisibleIngredientsCache = null; + /** + * Precomputed mapping from each visible element (by identity) to the list of + * {@link CollapsedStack} groups it belongs to. Built once from + * {@link #getAllVisibleIngredients()} and ALL registered groups; reused across + * keystrokes. Invalidated alongside {@code allVisibleIngredientsCache} when + * ingredients or groups change. + */ + @Nullable private IdentityHashMap, List> groupMembershipCache = null; private boolean afterBlock = false; @Nullable private List delegatedActions; @@ -54,13 +74,13 @@ public void logStatistics() { public void addIngredients(NonNullList ingredients) { ingredients.sort(IngredientListElementComparator.INSTANCE); this.elementSearch.addAll(ingredients); - this.filterCached = null; + invalidateCache(); } public void addIngredient(IIngredientListElement element) { updateHiddenState(element); this.elementSearch.add(element); - this.filterCached = null; + invalidateCache(); } public void delegateAfterBlock(Runnable runnable) { @@ -95,6 +115,75 @@ public void block() { public void invalidateCache() { this.filterCached = null; + this.allVisibleIngredientsCache = null; + this.groupMembershipCache = null; + } + + /** + * Returns a cached, sorted list of every currently-visible ingredient. + * The cache is invalidated whenever {@link #invalidateCache()} is called (ingredient + * additions, visibility changes, mode changes), but NOT on search-text changes — the + * full ingredient set is independent of the search bar content. + */ + private List> getAllVisibleIngredients() { + if (allVisibleIngredientsCache == null) { + allVisibleIngredientsCache = this.elementSearch.getAllIngredients().stream() + .filter(IIngredientListElement::isVisible) + .sorted(IngredientListElementComparator.INSTANCE) + .collect(Collectors.toList()); + } + return allVisibleIngredientsCache; + } + + /** + * Returns a cached identity-map from each visible element to the list of + * {@link CollapsedStack} groups it matches. Elements with no group match are + * absent from the map. The cache is built once from the full visible-ingredient + * list and ALL registered groups (both built-in and custom), so the expensive + * matcher / UID computation happens only once — not on every keystroke. + *

+ * {@link #collapse} then filters this by the currently-active (enabled) entries + * and uses O(1) map lookups per element instead of re-running matchers. + */ + private IdentityHashMap, List> getGroupMembership() { + if (groupMembershipCache == null) { + groupMembershipCache = new IdentityHashMap<>(); + if (!Config.isCollapsibleGroupsEnabled()) return groupMembershipCache; + + CollapsedStackRegistry registry = CollapsedStackRegistry.getInstance(); + List allEntries = new ArrayList<>(); + allEntries.addAll(registry.getEntries()); + allEntries.addAll(registry.getModEntries()); + allEntries.addAll(registry.getCustomEntries()); + if (allEntries.isEmpty()) return groupMembershipCache; + + boolean hasUidEntries = false; + for (CollapsedStack entry : allEntries) { + if (entry.getUidMatcher() != null) { + hasUidEntries = true; + break; + } + } + + for (IIngredientListElement element : getAllVisibleIngredients()) { + String uid = hasUidEntries ? CollapsedStack.computeIngredientUid(element.getIngredient()) : null; + List matched = null; + for (CollapsedStack entry : allEntries) { + Predicate uidMatcher = entry.getUidMatcher(); + boolean isMatch = (uidMatcher != null && uid != null) + ? uidMatcher.test(uid) + : entry.matches(element); + if (isMatch) { + if (matched == null) matched = new ArrayList<>(2); + matched.add(entry); + } + } + if (matched != null) { + groupMembershipCache.put(element, matched); + } + } + } + return groupMembershipCache; } public List> findMatchingElements(IIngredientListElement element) { @@ -138,8 +227,21 @@ public void modesChanged() { @SubscribeEvent public void onEditModeToggleEvent(EditModeToggleEvent event) { - this.filterCached = null; + invalidateCache(); updateHidden(); + + // In Hide Ingredients Mode the user cannot Alt+Click to expand/collapse groups, + // so expand all groups when entering edit mode and collapse them on exit. + boolean editMode = event.isEditModeEnabled(); + CollapsedStackRegistry registry = mezz.jei.Internal.getCollapsedStackRegistry(); + for (CollapsedStack entry : registry.getEntries()) { + entry.setExpanded(editMode); + } + for (CollapsedStack entry : registry.getCustomEntries()) { + entry.setExpanded(editMode); + } + this.collapsedListCached = Collections.emptyList(); + notifyCollapsedStateChanged(); } public void updateHidden() { @@ -156,7 +258,7 @@ public void updateHiddenState(IIngredientListElement element) { (Config.isEditModeEnabled() || !Config.isIngredientOnConfigBlacklist(ingredient, ingredientHelper)); if (element.isVisible() != visible) { element.setVisible(visible); - this.filterCached = null; + invalidateCache(); } } @@ -169,12 +271,37 @@ public List getIngredientList(String filterText) { filterText = Translator.toLowercaseWithLocale(filterText); if (!filterText.equals(filterCached)) { List> ingredientList = getIngredientListUncached(filterText); + if (!filterText.isEmpty() && Config.isCollapsibleGroupsEnabled()) { + ingredientList = withGroupNameMatches(ingredientList, filterText); + } ingredientListCached = Collections.unmodifiableList(ingredientList); + collapsedListCached = collapse(ingredientListCached); filterCached = filterText; } return ingredientListCached; } + @Override + public List getCollapsedIngredientList() { + getIngredientList(); // ensure cache is populated + return collapsedListCached; + } + + @Override + public int collapsedSize() { + List collapsed = getCollapsedIngredientList(); + int count = 0; + for (IIngredientListElement obj : collapsed) { + if (obj instanceof CollapsedStack) { + CollapsedStack cs = (CollapsedStack) obj; + count += cs.isExpanded() ? cs.size() : 1; + } else { + count++; + } + } + return count; + } + @Override public ImmutableList getFilteredIngredients() { return getFilteredIngredients(Config.getFilterText()); @@ -205,20 +332,14 @@ public void setFilterText(String filterText) { private List> getIngredientListUncached(String filterText) { if (filterText.isEmpty()) { - return this.elementSearch.getAllIngredients().stream() - .filter(IIngredientListElement::isVisible) - .sorted(IngredientListElementComparator.INSTANCE) - .collect(Collectors.toList()); + return new ArrayList<>(getAllVisibleIngredients()); } List tokens = Arrays.stream(filterText.split("\\|")) .map(SearchToken::parseSearchToken) .filter(s -> !s.search.isEmpty()) .collect(Collectors.toList()); if (tokens.isEmpty()) { - return this.elementSearch.getAllIngredients().stream() - .filter(IIngredientListElement::isVisible) - .sorted(IngredientListElementComparator.INSTANCE) - .collect(Collectors.toList()); + return new ArrayList<>(getAllVisibleIngredients()); } return tokens.stream() .map(token -> token.getSearchResults(this.elementSearch)) @@ -228,6 +349,141 @@ private List> getIngredientListUncached(String filterT .collect(Collectors.toList()); } + /** + * Augments a filtered ingredient list with any ingredients that belong to a group whose + * display name contains the filter text, but that weren't returned by the normal token + * search. This lets users find groups by name in the main search bar. + */ + @SuppressWarnings("unchecked") + private List> withGroupNameMatches( + List> baseList, String filterText) { + CollapsedStackRegistry registry = CollapsedStackRegistry.getInstance(); + List matchingGroups = new ArrayList<>(); + for (CollapsedStack entry : registry.getEntries()) { + if (Translator.toLowercaseWithLocale(entry.getDisplayName()).contains(filterText)) { + matchingGroups.add(entry); + } + } + for (CollapsedStack entry : registry.getModEntries()) { + if (Translator.toLowercaseWithLocale(entry.getDisplayName()).contains(filterText)) { + matchingGroups.add(entry); + } + } + for (CollapsedStack entry : registry.getCustomEntries()) { + if (Translator.toLowercaseWithLocale(entry.getDisplayName()).contains(filterText)) { + matchingGroups.add(entry); + } + } + if (matchingGroups.isEmpty()) { + return baseList; + } + // Use identity comparison so dedup works regardless of equals() implementation. + Set> seen = Collections.newSetFromMap(new IdentityHashMap<>()); + seen.addAll(baseList); + Set matchingGroupSet = Collections.newSetFromMap(new IdentityHashMap<>()); + matchingGroupSet.addAll(matchingGroups); + List> result = new ArrayList<>(baseList); + // Use the precomputed membership cache instead of re-running matchers. + IdentityHashMap, List> membership = getGroupMembership(); + for (IIngredientListElement element : getAllVisibleIngredients()) { + if (seen.contains(element)) { + continue; + } + List groups = membership.get(element); + if (groups != null) { + for (CollapsedStack entry : groups) { + if (matchingGroupSet.contains(entry)) { + result.add(element); + seen.add(element); + break; + } + } + } + } + result.sort(IngredientListElementComparator.INSTANCE); + return result; + } + + /** + * Converts a flat filtered ingredient list into a mixed list containing + * both individual IIngredientListElement objects and CollapsedStack groups. + * Each ingredient is assigned to the first matching CollapsedStack group (first match wins). + * If collapsible groups are disabled, returns the original list cast to List<Object>. + */ + private List collapse(List ingredientList) { + if (!Config.isCollapsibleGroupsEnabled()) { + return new ArrayList<>(ingredientList); + } + CollapsedStackRegistry registry = CollapsedStackRegistry.getInstance(); + Collection entries = registry.getEntries(); + List modEntries = registry.getModEntries(); + List customEntries = registry.getCustomEntries(); + if (entries.isEmpty() && modEntries.isEmpty() && customEntries.isEmpty()) { + return new ArrayList<>(ingredientList); + } + + // Build the list of active entries (not disabled) + List activeEntries = new ArrayList<>(); + for (CollapsedStack entry : entries) { + if (registry.isGroupEnabled(entry.getId())) { + activeEntries.add(entry); + } + } + for (CollapsedStack entry : modEntries) { + if (registry.isGroupEnabled(entry.getId())) { + activeEntries.add(entry); + } + } + for (CollapsedStack entry : customEntries) { + if (registry.isGroupEnabled(entry.getId())) { + activeEntries.add(entry); + } + } + if (activeEntries.isEmpty()) { + return new ArrayList<>(ingredientList); + } + + // CollapsedStack serves as both group definition and runtime container; + // clear transient ingredients before repopulating from the current filter. + for (CollapsedStack entry : activeEntries) { + entry.clearIngredients(); + } + + // Use the precomputed group-membership cache for O(1) per-element lookups. + // The cache maps each visible element to ALL groups it belongs to (built once), + // so we only need to intersect with the active set here — no matchers, no UID + // computation on the per-keystroke path. + Set activeSet = Collections.newSetFromMap(new IdentityHashMap<>()); + activeSet.addAll(activeEntries); + IdentityHashMap, List> membership = getGroupMembership(); + + List result = new ArrayList<>(ingredientList.size()); + Set addedToResult = Collections.newSetFromMap(new IdentityHashMap<>()); + + for (IIngredientListElement element : ingredientList) { + List groups = membership.get(element); + boolean matched = false; + if (groups != null) { + for (CollapsedStack entry : groups) { + if (activeSet.contains(entry)) { + if (addedToResult.add(entry)) { + result.add(entry); + } + entry.addIngredient(element); + matched = true; + } + } + } + if (!matched) { + result.add(element); + } + } + + // Remove empty collapsed stacks (shouldn't happen, but be safe) + result.removeIf(obj -> obj instanceof CollapsedStack && ((CollapsedStack) obj).isEmpty()); + return result; + } + /** * Scans up and down the element list to find wildcard matches that touch the given element. */ @@ -284,11 +540,22 @@ public void addListener(IIngredientGridSource.Listener listener) { listeners.add(listener); } + public void addCollapsedStateListener(Runnable listener) { + collapsedStateListeners.add(listener); + } + + public void notifyCollapsedStateChanged() { + // Do NOT null filterCached here. Creates client lag spikes. + for (Runnable listener : collapsedStateListeners) { + listener.run(); + } + } + public void replaceBlacklist(IngredientBlacklistInternal blacklist) { this.blacklist = blacklist; } - private void notifyListenersOfChange() { + public void notifyListenersOfChange() { for (IIngredientGridSource.Listener listener : listeners) { listener.onChange(); } diff --git a/src/main/java/mezz/jei/ingredients/IngredientListElement.java b/src/main/java/mezz/jei/ingredients/IngredientListElement.java index 13a87d53..bce7083c 100644 --- a/src/main/java/mezz/jei/ingredients/IngredientListElement.java +++ b/src/main/java/mezz/jei/ingredients/IngredientListElement.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableSet; import it.unimi.dsi.fastutil.objects.ObjectArraySet; +import mezz.jei.Internal; import mezz.jei.api.ingredients.IIngredientHelper; import mezz.jei.api.ingredients.IIngredientRenderer; import mezz.jei.bookmarks.BookmarkItem; diff --git a/src/main/java/mezz/jei/plugins/jei/JEIInternalPlugin.java b/src/main/java/mezz/jei/plugins/jei/JEIInternalPlugin.java index 7fe00822..3c346062 100644 --- a/src/main/java/mezz/jei/plugins/jei/JEIInternalPlugin.java +++ b/src/main/java/mezz/jei/plugins/jei/JEIInternalPlugin.java @@ -9,6 +9,9 @@ import mezz.jei.bookmarks.BookmarkIngredientHelper; import mezz.jei.bookmarks.BookmarkItem; import mezz.jei.bookmarks.BookmarkItemRender; +import mezz.jei.ingredients.CollapsedStack; +import mezz.jei.ingredients.CollapsedStackIngredientHelper; +import mezz.jei.render.CollapsedStackRenderer; import net.minecraftforge.fml.common.registry.ForgeRegistries; import net.minecraftforge.fluids.Fluid; import net.minecraftforge.fluids.FluidRegistry; @@ -60,6 +63,10 @@ public void registerIngredients(IModIngredientRegistration ingredientRegistratio BookmarkIngredientHelper bookmarkIngredientHelper = new BookmarkIngredientHelper(); BookmarkItemRender bookmarkItemRender = new BookmarkItemRender(); ingredientRegistration.register(BookmarkItem.TYPE, Collections.emptyList(), bookmarkIngredientHelper, bookmarkItemRender); + + // Register CollapsedStack as ingredient type — addons that introspect grid items require a registered type + CollapsedStackIngredientHelper csHelper = new CollapsedStackIngredientHelper(); + ingredientRegistration.register(CollapsedStack.TYPE, Collections.emptyList(), csHelper, CollapsedStackRenderer.INSTANCE); } @Override diff --git a/src/main/java/mezz/jei/plugins/vanilla/ingredients/enchant/EnchantDataHelper.java b/src/main/java/mezz/jei/plugins/vanilla/ingredients/enchant/EnchantDataHelper.java index 1300d069..a1c0b8db 100644 --- a/src/main/java/mezz/jei/plugins/vanilla/ingredients/enchant/EnchantDataHelper.java +++ b/src/main/java/mezz/jei/plugins/vanilla/ingredients/enchant/EnchantDataHelper.java @@ -4,6 +4,7 @@ import java.awt.Color; import net.minecraft.enchantment.EnchantmentData; +import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.util.ResourceLocation; diff --git a/src/main/java/mezz/jei/render/CollapsedStackRenderer.java b/src/main/java/mezz/jei/render/CollapsedStackRenderer.java new file mode 100644 index 00000000..c61954b9 --- /dev/null +++ b/src/main/java/mezz/jei/render/CollapsedStackRenderer.java @@ -0,0 +1,316 @@ +package mezz.jei.render; + +import mezz.jei.api.ingredients.IIngredientRenderer; +import mezz.jei.config.Config; +import mezz.jei.gui.ingredients.IIngredientListElement; +import mezz.jei.ingredients.CollapsedStack; +import mezz.jei.input.ClickedIngredient; +import mezz.jei.util.CollapsedClickAction; +import mezz.jei.util.Translator; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.util.ITooltipFlag; +import net.minecraft.item.ItemStack; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.fml.client.config.GuiUtils; + +import javax.annotation.Nullable; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Renders a collapsed group as a single ingredient list slot. + * Shows the first item with a count badge indicating total group size, + * plus a semi-transparent background to distinguish it from normal items. + */ +public class CollapsedStackRenderer implements IIngredientRenderer { + private static final int COLLAPSED_BG_COLOR = 0x33FFFFFF; + private static final int COLLAPSED_BORDER_COLOR = 0x55AAAAFF; + + /** Singleton registered with the ingredient type system — {@code collapsedStack} is null. */ + public static final CollapsedStackRenderer INSTANCE = new CollapsedStackRenderer(null); + + private final CollapsedStack collapsedStack; + private Rectangle area = new Rectangle(0, 0, 16, 16); + private int padding; + + public CollapsedStackRenderer(CollapsedStack collapsedStack) { + this.collapsedStack = collapsedStack; + } + + public void setArea(Rectangle area) { + this.area = area; + } + + public void setPadding(int padding) { + this.padding = padding; + } + + public CollapsedStack getCollapsedStack() { + return collapsedStack; + } + + public Rectangle getArea() { + return area; + } + + /** Grid overlay render — uses this instance's stack and area+padding. */ + public void render(Minecraft minecraft) { + if (collapsedStack == null || collapsedStack.isEmpty()) { + return; + } + renderAt(minecraft, collapsedStack, area.x + padding, area.y + padding); + } + + /** + * Stateless render at an arbitrary position — shared by the instance render and + * the {@link IIngredientRenderer} contract. + * For groups with 2+ items, mimics REI's stacked-card icon: each item is rendered at + * 0.75× scale (12 px), offset 4 px so both stay entirely within the 16×16 slot area. + * Back item (upper-right): screen origin (x+4, y+0), occupies (x+4..x+16, y..y+12) + * Front item (lower-left) : screen origin (x+0, y+4), occupies (x..x+12, y+4..y+16) + * Count badge is drawn at 0.75× scale in orange in the bottom-right corner. + */ + private static void renderAt(Minecraft minecraft, CollapsedStack ingredient, int x, int y) { + List> ingredients = ingredient.getIngredients(); + if (ingredients.isEmpty()) { + return; + } + + // Draw background tint to visually distinguish collapsed groups + GuiScreen.drawRect(x, y, x + 16, y + 16, COLLAPSED_BG_COLOR); + + if (ingredients.size() == 1) { + // Single item: render at full size + renderElementAt(minecraft, ingredients.get(0), x, y, 1.0f); + } else { + // 0.75 scale → 12 px icon. + // Back (upper-right): origin at (x+4, y+0) → occupies x+4..x+16, y..y+12 + // Front (lower-left) : origin at (x+0, y+4) → occupies x..x+12, y+4..y+16 + RenderItem renderItem = minecraft.getRenderItem(); + renderElementAt(minecraft, ingredients.get(1), x + 4, y + 0, 0.75f); // back + // Elevate zLevel so the front item's depth values are naturally in front of the + // back item's geometry. Using GL_LEQUAL (normal) keeps the front item's own + // internal face culling intact — GL_ALWAYS would break tile-entity models. + float prevZLevel = renderItem.zLevel; + renderItem.zLevel += 100; + renderElementAt(minecraft, ingredients.get(0), x + 0, y + 4, 0.75f); // front + renderItem.zLevel = prevZLevel; + } + + // Count badge: 0.75× scale, orange, right-aligned at the bottom of the slot + int count = ingredient.size(); + if (count > 1) { + FontRenderer fontRenderer = minecraft.fontRenderer; + String countStr = String.valueOf(count); + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + GlStateManager.disableBlend(); + final float badgeScale = 0.75f; + // Convert desired screen position to scaled-coordinate space. + // Screen right edge: x+16 → scaled coord (x+16)/badgeScale + // Screen top of text: y+10 → scaled coord (y+10)/badgeScale + int textWidth = fontRenderer.getStringWidth(countStr); + int scaledRight = (int) ((x + 16) / badgeScale); + int scaledTop = (int) ((y + 10) / badgeScale); + GlStateManager.pushMatrix(); + GlStateManager.scale(badgeScale, badgeScale, 1.0f); + fontRenderer.drawStringWithShadow(countStr, scaledRight - textWidth, scaledTop, 0xFFAA00); + GlStateManager.popMatrix(); + GlStateManager.enableDepth(); + } + + drawCollapsedBorder(x, y); + } + + /** + * Renders one ingredient at (x, y) at the given scale using the GL matrix stack. + * Delegates to renderItemAndEffectIntoGUI so all item types (2D, 3D, built-in) render correctly. + */ + private static void renderElementAt(Minecraft minecraft, IIngredientListElement element, int x, int y, float scale) { + Object ingredient = element.getIngredient(); + try { + GlStateManager.pushMatrix(); + GlStateManager.translate(x, y, 0); + GlStateManager.scale(scale, scale, scale); + if (ingredient instanceof ItemStack) { + minecraft.getRenderItem().renderItemAndEffectIntoGUI((ItemStack) ingredient, 0, 0); + } else { + renderIngredient(minecraft, 0, 0, element); + } + GlStateManager.popMatrix(); + } catch (RuntimeException | LinkageError ignored) { + GlStateManager.popMatrix(); + } + } + + private static void drawCollapsedBorder(int x, int y) { + // Small triangle indicator in the top-left corner to show it's collapsible + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + GuiScreen.drawRect(x, y, x + 4, y + 1, COLLAPSED_BORDER_COLOR); + GuiScreen.drawRect(x, y, x + 1, y + 4, COLLAPSED_BORDER_COLOR); + GlStateManager.enableDepth(); + } + + // --- IIngredientRenderer implementation --- + // INSTANCE (null stack) is registered with the ingredient type system. + + @Override + public void render(Minecraft minecraft, int xPosition, int yPosition, @Nullable CollapsedStack ingredient) { + if (ingredient == null || ingredient.isEmpty()) { + return; + } + renderAt(minecraft, ingredient, xPosition, yPosition); + } + + @Override + public List getTooltip(Minecraft minecraft, CollapsedStack ingredient, ITooltipFlag tooltipFlag) { + List tooltip = new ArrayList<>(); + tooltip.add(TextFormatting.GOLD + ingredient.getDisplayName() + + TextFormatting.GRAY + " (" + ingredient.size() + " items)"); + return tooltip; + } + + public void drawHighlight() { + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + GlStateManager.colorMask(true, true, true, false); + GuiUtils.drawGradientRect(0, area.x, area.y, area.x + area.width, area.y + area.height, 0x80FFFFFF, 0x80FFFFFF); + GlStateManager.colorMask(true, true, true, true); + GlStateManager.enableDepth(); + } + + public void drawTooltip(Minecraft minecraft, int mouseX, int mouseY) { + List> ingredients = collapsedStack.getIngredients(); + if (ingredients.isEmpty()) return; + + // Single-item group (e.g. search filtered to one result): show the item's native tooltip + if (ingredients.size() == 1) { + new IngredientRenderer<>(ingredients.get(0)).drawTooltip(minecraft, mouseX, mouseY); + return; + } + + FontRenderer font = minecraft.fontRenderer; + final int COLS = 8; + final int SLOT = 18; // 16px icon + 1px padding each side + final int MAX_VISIBLE = COLS * 2 + 7; // 23 = rows of 8, 8, 7 + + int total = ingredients.size(); + int shown = Math.min(total, MAX_VISIBLE); + int overflow = total - shown; + int numRows = shown <= COLS ? 1 : shown <= COLS * 2 ? 2 : 3; + int gridCols = numRows > 1 ? COLS : shown; + int gridW = gridCols * SLOT; + int gridH = numRows * SLOT; + + String header = TextFormatting.GOLD + collapsedStack.getDisplayName() + + TextFormatting.GRAY + " (" + total + " items)"; + // In OPEN_GROUP mode, alt+click uses first item; show that as the hint. + // In FIRST_ITEM mode, alt+click expands; show that instead. + String hint = TextFormatting.YELLOW + Translator.translateToLocal( + Config.getCollapsedClickAction() == CollapsedClickAction.OPEN_GROUP + ? "hei.tooltip.collapsed.expand.firstItem" + : "hei.tooltip.collapsed.expand"); + + int tw = Math.max(Math.max(font.getStringWidth(header), font.getStringWidth(hint)), gridW); + int th = 12 + gridH + 10; + + ScaledResolution sr = new ScaledResolution(minecraft); + int tx = mouseX + 12; + if (tx + tw + 6 > sr.getScaledWidth()) tx = mouseX - 16 - tw; + int ty = mouseY - 12; + if (ty + th + 4 > sr.getScaledHeight()) ty = sr.getScaledHeight() - th - 4; + if (ty < 4) ty = 4; + + GlStateManager.disableRescaleNormal(); + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + + // Draw tooltip background (MC-style dark purple box with gradient border) + final int z = 300; + int bg = 0xF0100010, bs = 0x505000FF, be = (bs & 0xFEFEFE) >> 1 | (bs & 0xFF000000); + GuiUtils.drawGradientRect(z, tx-3, ty-4, tx+tw+3, ty-3, bg, bg); + GuiUtils.drawGradientRect(z, tx-3, ty+th+3, tx+tw+3, ty+th+4, bg, bg); + GuiUtils.drawGradientRect(z, tx-3, ty-3, tx+tw+3, ty+th+3, bg, bg); + GuiUtils.drawGradientRect(z, tx-4, ty-3, tx-3, ty+th+3, bg, bg); + GuiUtils.drawGradientRect(z, tx+tw+3, ty-3, tx+tw+4, ty+th+3, bg, bg); + GuiUtils.drawGradientRect(z, tx-3, ty-2, tx-2, ty+th+2, bs, be); + GuiUtils.drawGradientRect(z, tx+tw+2, ty-2, tx+tw+3, ty+th+2, bs, be); + GuiUtils.drawGradientRect(z, tx-3, ty-3, tx+tw+3, ty-2, bs, bs); + GuiUtils.drawGradientRect(z, tx-3, ty+th+2, tx+tw+3, ty+th+3, be, be); + + // Title + font.drawStringWithShadow(header, tx, ty, -1); + + // Item icon grid + int itemsY = ty + 12; + GlStateManager.pushMatrix(); + GlStateManager.translate(0.0f, 0.0f, 300.0f); + RenderHelper.enableGUIStandardItemLighting(); + GlStateManager.enableDepth(); + RenderItem renderItem = minecraft.getRenderItem(); + for (int i = 0; i < shown; i++) { + IIngredientListElement element = ingredients.get(i); + int ix = tx + (i % COLS) * SLOT + 1; + int iy = itemsY + (i / COLS) * SLOT + 1; + Object ing = element.getIngredient(); + if (ing instanceof ItemStack) { + renderItem.renderItemAndEffectIntoGUI((ItemStack) ing, ix, iy); + } else { + try { renderIngredient(minecraft, ix, iy, element); } + catch (RuntimeException | LinkageError ignored) {} + } + } + RenderHelper.disableStandardItemLighting(); + GlStateManager.popMatrix(); + + // "+N" overflow indicator in 8th slot of row 3 (only when there are hidden items) + GlStateManager.disableDepth(); + GlStateManager.disableLighting(); + if (overflow > 0) { + String overStr = "+" + overflow; + int ox = tx + 7 * SLOT + 2; + int oy = itemsY + 2 * SLOT + (SLOT - 8) / 2 + 1; + font.drawStringWithShadow(overStr, ox, oy, 0xAAAAAA); + } + font.drawStringWithShadow(hint, tx, itemsY + gridH + 2, -1); + + GlStateManager.enableLighting(); + GlStateManager.enableDepth(); + RenderHelper.enableStandardItemLighting(); + GlStateManager.enableRescaleNormal(); + } + + /** + * Returns the CollapsedStack as the clicked ingredient — registered as IIngredientType + * for addon compatibility. Recipe lookups are delegated via translateFocus on the helper. + */ + @Nullable + public ClickedIngredient getClickedIngredient() { + List> ingredients = collapsedStack.getIngredients(); + if (ingredients.isEmpty()) { + return null; + } + // Return CollapsedStack directly — it is a registered IIngredientType + return ClickedIngredient.create(collapsedStack, area); + } + + public boolean isMouseOver(int mouseX, int mouseY) { + return area.contains(mouseX, mouseY); + } + + @SuppressWarnings("unchecked") + private static void renderIngredient(Minecraft minecraft, int x, int y, IIngredientListElement element) { + IIngredientRenderer renderer = element.getIngredientRenderer(); + T ingredient = element.getIngredient(); + renderer.render(minecraft, x, y, ingredient); + } +} diff --git a/src/main/java/mezz/jei/render/IngredientListBatchRenderer.java b/src/main/java/mezz/jei/render/IngredientListBatchRenderer.java index a11eec0e..498dfa7b 100644 --- a/src/main/java/mezz/jei/render/IngredientListBatchRenderer.java +++ b/src/main/java/mezz/jei/render/IngredientListBatchRenderer.java @@ -5,6 +5,7 @@ import mezz.jei.api.ingredients.ISlowRenderItem; import mezz.jei.config.Config; import mezz.jei.gui.ingredients.IIngredientListElement; +import mezz.jei.ingredients.CollapsedStack; import mezz.jei.input.ClickedIngredient; import mezz.jei.util.ErrorUtil; import mezz.jei.util.Log; @@ -19,9 +20,15 @@ import net.minecraft.item.ItemStack; import org.lwjgl.opengl.GL11; +import net.minecraft.client.gui.Gui; + import javax.annotation.Nullable; +import java.awt.Rectangle; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static mezz.jei.gui.overlay.IngredientGrid.INGREDIENT_HEIGHT; @@ -33,6 +40,11 @@ public class IngredientListBatchRenderer { protected final List renderItems2d = new ArrayList<>(); protected final List renderItems3d = new ArrayList<>(); protected final List renderOther = new ArrayList<>(); + protected final List renderCollapsed = new ArrayList<>(); + protected final Map collapsedStackIndexed = new HashMap<>(); + protected final Map, CollapsedStack> expandedElementToGroup = new HashMap<>(); + // Per-group list of individual slot rectangles (used for per-slot fill + edge-detection border). + protected final Map> expandedGroupSlots = new HashMap<>(); @Nullable private Framebuffer framebuffer = null; @@ -59,6 +71,10 @@ public void clear() { renderItems2d.clear(); renderItems3d.clear(); renderOther.clear(); + renderCollapsed.clear(); + collapsedStackIndexed.clear(); + expandedElementToGroup.clear(); + expandedGroupSlots.clear(); size = 0; maxSize = 0; @@ -83,6 +99,10 @@ public void set(final int startIndex, List ingredientLis renderItems2d.clear(); renderItems3d.clear(); renderOther.clear(); + renderCollapsed.clear(); + collapsedStackIndexed.clear(); + expandedElementToGroup.clear(); + expandedGroupSlots.clear(); maxSize = 0; size = 0; @@ -114,6 +134,90 @@ public void set(final int startIndex, List ingredientLis invalidateBuffer(); } + /** + * Sets the grid contents from a collapsed ingredient list (mixed IIngredientListElement and CollapsedStack objects). + * Collapsed groups are rendered as a single slot; expanded groups have their items rendered individually. + */ + public void setCollapsed(final int startIndex, List collapsedList) { + renderItems2d.clear(); + renderItems3d.clear(); + renderOther.clear(); + renderCollapsed.clear(); + collapsedStackIndexed.clear(); + expandedElementToGroup.clear(); + expandedGroupSlots.clear(); + maxSize = 0; + size = 0; + + for (List row : slots) { + for (IngredientListSlot slot : row) { + slot.clear(); + } + } + + // Flatten the ENTIRE collapsed list into display items first, then slice at startIndex. + // This ensures expanded groups don't break pagination — firstItemIndex is an index into + // the flattened view, which matches what collapsedSize() now returns. + List displayItems = new ArrayList<>(); + Map itemToCollapsed = new HashMap<>(); + for (IIngredientListElement obj : collapsedList) { + if (obj instanceof CollapsedStack) { + CollapsedStack collapsed = (CollapsedStack) obj; + if (collapsed.isExpanded()) { + // Expanded: add each ingredient individually, track which belong to this group + for (IIngredientListElement element : collapsed.getIngredients()) { + displayItems.add(element); + itemToCollapsed.put(element, collapsed); + } + } else { + // Collapsed: add the CollapsedStack itself as a single display item + displayItems.add(collapsed); + } + } else { + displayItems.add(obj); + } + } + + int i = startIndex; + int slotIndex = 0; + for (List row : slots) { + maxSize += (int) row.stream().filter(IngredientListSlot::isFree).count(); + for (int column = 0; column < row.size(); column++) { + if (i >= displayItems.size()) { + break; + } + IngredientListSlot ingredientListSlot = row.get(column); + if (ingredientListSlot.isBlocked()) { + slotIndex++; + continue; + } + IIngredientListElement displayItem = displayItems.get(i); + if (displayItem instanceof CollapsedStack) { + CollapsedStack collapsed = (CollapsedStack) displayItem; + CollapsedStackRenderer renderer = new CollapsedStackRenderer(collapsed); + renderer.setArea(ingredientListSlot.getArea()); + renderer.setPadding(1); + renderCollapsed.add(renderer); + collapsedStackIndexed.put(slotIndex, collapsed); + } else { + set(ingredientListSlot, displayItem); + CollapsedStack parentCollapsed = itemToCollapsed.get(displayItem); + if (parentCollapsed != null) { + collapsedStackIndexed.put(slotIndex, parentCollapsed); + expandedElementToGroup.put(displayItem, parentCollapsed); + expandedGroupSlots.computeIfAbsent(parentCollapsed, k -> new ArrayList<>()) + .add(new Rectangle(ingredientListSlot.getArea())); + } + } + size++; + i++; + slotIndex++; + } + } + + invalidateBuffer(); + } + /** * Returns the maximum number of ingredients that can be displayed, if none of them ended rows early. * @return the maximum number of ingredients. @@ -143,7 +247,7 @@ protected void set(IngredientListSlot ingredientListSlot, IIngredientListEle } if (!bakedModel.isBuiltInRenderer() && !(itemStack.getItem() instanceof ISlowRenderItem)) { - ItemStackFastRenderer renderer = new ItemStackFastRenderer(itemStackElement); + ItemStackFastRenderer renderer = new ItemStackFastRenderer(itemStackElement, bakedModel); ingredientListSlot.setIngredientRenderer(renderer); if (bakedModel.isGui3d()) { renderItems3d.add(renderer); @@ -191,6 +295,11 @@ public void moveSlotsToFit(int maxWidth) { @Nullable public ClickedIngredient getIngredientUnderMouse(int mouseX, int mouseY) { + // Check collapsed renderers first + CollapsedStackRenderer collapsedHovered = getHoveredCollapsed(mouseX, mouseY); + if (collapsedHovered != null) { + return collapsedHovered.getClickedIngredient(); + } IngredientRenderer hovered = getHovered(mouseX, mouseY); if (hovered != null) { IIngredientListElement element = hovered.getElement(); @@ -208,6 +317,64 @@ public IngredientRenderer getHovered(int mouseX, int mouseY) { return null; } + @Nullable + public CollapsedStack getExpandedCollapsedGroupAt(int mouseX, int mouseY) { + IngredientRenderer hovered = getHovered(mouseX, mouseY); + if (hovered == null) return null; + return expandedElementToGroup.get(hovered.getElement()); + } + + public void renderExpandedGroupOutlines() { + if (expandedGroupSlots.isEmpty()) return; + GlStateManager.disableLighting(); + GlStateManager.enableBlend(); + GlStateManager.tryBlendFuncSeparate( + GlStateManager.SourceFactor.SRC_ALPHA, + GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, + GlStateManager.SourceFactor.ONE, + GlStateManager.DestFactor.ZERO + ); + int bgColor = 0x33555555; // subtle smoke background + int borderColor = 0xCC888888; // medium smoke border + for (List slots : expandedGroupSlots.values()) { + // Build a fast lookup set keyed by "x,y" to detect adjacent group slots. + java.util.Set keys = new java.util.HashSet<>(); + for (Rectangle r : slots) keys.add(r.x + "," + r.y); + for (Rectangle r : slots) { + // Subtle background fill over each slot + Gui.drawRect(r.x, r.y, r.x + r.width, r.y + r.height, bgColor); + // Draw only the edges that are NOT shared with another group slot + if (!keys.contains(r.x + "," + (r.y - INGREDIENT_HEIGHT))) { + Gui.drawRect(r.x, r.y, r.x + r.width, r.y + 1, borderColor); // top + } + if (!keys.contains(r.x + "," + (r.y + INGREDIENT_HEIGHT))) { + Gui.drawRect(r.x, r.y + r.height - 1, r.x + r.width, r.y + r.height, borderColor); // bottom + } + if (!keys.contains((r.x - INGREDIENT_WIDTH) + "," + r.y)) { + Gui.drawRect(r.x, r.y, r.x + 1, r.y + r.height, borderColor); // left + } + if (!keys.contains((r.x + INGREDIENT_WIDTH) + "," + r.y)) { + Gui.drawRect(r.x + r.width - 1, r.y, r.x + r.width, r.y + r.height, borderColor); // right + } + } + } + GlStateManager.disableBlend(); + } + + @Nullable + public CollapsedStackRenderer getHoveredCollapsed(int mouseX, int mouseY) { + for (CollapsedStackRenderer renderer : renderCollapsed) { + if (renderer.isMouseOver(mouseX, mouseY)) { + return renderer; + } + } + return null; + } + + public Map getCollapsedStackIndexed() { + return collapsedStackIndexed; + } + public void render(Minecraft minecraft) { if (allowBuffering && !Config.isEditModeEnabled() && Config.bufferIngredientRenders() && OpenGlHelper.isFramebufferEnabled()) { if (framebuffer == null) { @@ -310,6 +477,14 @@ protected void renderImpl(Minecraft minecraft) { slot.renderSlow(); } + // collapsed group rendering — lighting enabled once for all groups; each renderer + // assumes it is on and does not toggle it per item. + RenderHelper.enableGUIStandardItemLighting(); + GlStateManager.enableDepth(); + for (CollapsedStackRenderer collapsed : renderCollapsed) { + collapsed.render(minecraft); + } + RenderHelper.disableStandardItemLighting(); } diff --git a/src/main/java/mezz/jei/render/IngredientRenderer.java b/src/main/java/mezz/jei/render/IngredientRenderer.java index 84653b23..f3377363 100644 --- a/src/main/java/mezz/jei/render/IngredientRenderer.java +++ b/src/main/java/mezz/jei/render/IngredientRenderer.java @@ -85,9 +85,17 @@ public void drawHighlight() { } public void drawTooltip(Minecraft minecraft, int mouseX, int mouseY) { + drawTooltip(minecraft, mouseX, mouseY, null); + } + + public void drawTooltip(Minecraft minecraft, int mouseX, int mouseY, List extraLines) { T ingredient = element.getIngredient(); IIngredientRenderer ingredientRenderer = element.getIngredientRenderer(); List tooltip = getTooltip(minecraft, element); + if (extraLines != null && !extraLines.isEmpty()) { + tooltip = new ArrayList<>(tooltip); + tooltip.addAll(extraLines); + } FontRenderer fontRenderer = ingredientRenderer.getFontRenderer(minecraft, ingredient); IIngredientHelper ingredientHelper = element.getIngredientHelper(); diff --git a/src/main/java/mezz/jei/render/ItemStackFastRenderer.java b/src/main/java/mezz/jei/render/ItemStackFastRenderer.java index e3910113..e0f3f3c0 100644 --- a/src/main/java/mezz/jei/render/ItemStackFastRenderer.java +++ b/src/main/java/mezz/jei/render/ItemStackFastRenderer.java @@ -6,7 +6,6 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.ItemModelMesher; import net.minecraft.client.renderer.RenderItem; import net.minecraft.client.renderer.block.model.IBakedModel; import net.minecraft.client.renderer.block.model.ItemCameraTransforms; @@ -23,8 +22,12 @@ public class ItemStackFastRenderer extends IngredientRenderer { private static final ResourceLocation RES_ITEM_GLINT = new ResourceLocation("textures/misc/enchanted_item_glint.png"); - public ItemStackFastRenderer(IIngredientListElement itemStackElement) { + // Pre-computed at list-population time so getBakedModel() is a free field read every frame. + private final IBakedModel cachedModel; + + public ItemStackFastRenderer(IIngredientListElement itemStackElement, IBakedModel model) { super(itemStackElement); + this.cachedModel = model; } public void renderItemAndEffectIntoGUI() { @@ -36,10 +39,7 @@ public void renderItemAndEffectIntoGUI() { } private IBakedModel getBakedModel() { - ItemModelMesher itemModelMesher = Minecraft.getMinecraft().getRenderItem().getItemModelMesher(); - ItemStack itemStack = element.getIngredient(); - IBakedModel bakedModel = itemModelMesher.getItemModel(itemStack); - return bakedModel.getOverrides().handleItemState(bakedModel, itemStack, null, null); + return cachedModel; } private void uncheckedRenderItemAndEffectIntoGUI() { diff --git a/src/main/java/mezz/jei/startup/JeiStarter.java b/src/main/java/mezz/jei/startup/JeiStarter.java index 2107bade..4f822a2b 100644 --- a/src/main/java/mezz/jei/startup/JeiStarter.java +++ b/src/main/java/mezz/jei/startup/JeiStarter.java @@ -2,6 +2,7 @@ import mezz.jei.Internal; import mezz.jei.Tags; +import mezz.jei.api.ICollapsibleGroupRegistry; import mezz.jei.api.IJeiRuntime; import mezz.jei.api.IModPlugin; import mezz.jei.api.gui.IAdvancedGuiHandler; @@ -12,6 +13,7 @@ import mezz.jei.bookmarks.BookmarkList; import mezz.jei.config.Config; import mezz.jei.gui.GuiEventHandler; +import mezz.jei.ingredients.CollapsedStackRegistry; import mezz.jei.gui.GuiHelper; import mezz.jei.gui.GuiScreenHelper; import mezz.jei.gui.ghost.GhostIngredientDragManager; @@ -21,6 +23,7 @@ import mezz.jei.gui.recipes.RecipesGui; import mezz.jei.gui.textures.Textures; import mezz.jei.ingredients.IngredientBlacklistInternal; +import mezz.jei.util.Translator; import mezz.jei.ingredients.IngredientFilter; import mezz.jei.ingredients.IngredientListElementFactory; import mezz.jei.ingredients.IngredientRegistry; @@ -33,11 +36,17 @@ import mezz.jei.util.ErrorUtil; import mezz.jei.util.Log; import mezz.jei.util.LoggedTimer; +import net.minecraft.init.Items; +import net.minecraft.item.ItemMonsterPlacer; import net.minecraftforge.fml.common.ProgressManager; import java.util.Iterator; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; public class JeiStarter { private boolean started; @@ -97,6 +106,15 @@ public void load(List plugins, Textures textures, boolean recipesOnl timer.stop(); } + registerDefaultCollapsibleGroups(); + registerModCollapsibleGroups(plugins); + + { + CollapsedStackRegistry registry = Internal.getCollapsedStackRegistry(); + registry.loadCustomGroups(); + registry.syncDisabledGroups(); + } + BookmarkList bookmarkList = new BookmarkList(ingredientRegistry); Internal.setBookmarkList(bookmarkList); @@ -336,4 +354,148 @@ private static void sendRuntime(List plugins, IJeiRuntime jeiRuntime } } + private static void registerDefaultCollapsibleGroups() { + CollapsedStackRegistry registry = Internal.getCollapsedStackRegistry(); + // Enchanted books in JEI are stored as EnchantmentData (VanillaTypes.ENCHANT), + // not as ItemStacks — IngredientRegistry strips them from the ItemStack list. + // Match all EnchantmentData directly; every EnchantmentData IS an enchanted book. + registry.groupForType("enchanted_books", "Enchanted Books", + ingredient -> ingredient instanceof net.minecraft.enchantment.EnchantmentData); + registry.group("potions", "Potions", + stack -> stack.getItem() == Items.POTIONITEM); + registry.group("splash_potions", "Splash Potions", + stack -> stack.getItem() == Items.SPLASH_POTION); + registry.group("lingering_potions", "Lingering Potions", + stack -> stack.getItem() == Items.LINGERING_POTION); + registry.group("tipped_arrows", "Tipped Arrows", + stack -> stack.getItem() == Items.TIPPED_ARROW); + registry.group("spawn_eggs", "Spawn Eggs", + stack -> stack.getItem() instanceof ItemMonsterPlacer); + } + + private static void registerModCollapsibleGroups(List plugins) { + CollapsedStackRegistry registry = Internal.getCollapsedStackRegistry(); + Map groupsById = new HashMap<>(); + + ICollapsibleGroupRegistry apiRegistry = new ICollapsibleGroupRegistry() { + @Override + public CollapsibleGroupBuilder newGroup(String id, String langKey) { + final ModGroupBuilderState state = groupsById.computeIfAbsent(id, key -> { + ModGroupBuilderState created = new ModGroupBuilderState(); + // No uidMatcher yet — installed after all plugins finish registering. + // This avoids IngredientFilter using an empty uid fast-path that would + // short-circuit addAny/addAllOf predicates and cause them to never match. + created.registeredStack = registry.addModGroup(id, Translator.translateToLocal(langKey), created::matches); + return created; + }); + + return new CollapsibleGroupBuilder() { + @Override + public CollapsibleGroupBuilder add(Object ingredient) { + if (ingredient != null) { + String uid = mezz.jei.ingredients.CollapsedStack.computeIngredientUid(ingredient); + if (uid != null) { + state.exactUids.add(uid); + } + } + return this; + } + + @Override + public CollapsibleGroupBuilder add(Object... ingredients) { + if (ingredients != null) { + for (Object ingredient : ingredients) { + add(ingredient); + } + } + return this; + } + + @Override + public CollapsibleGroupBuilder addAllOf(mezz.jei.api.recipe.IIngredientType... types) { + if (types != null) { + for (mezz.jei.api.recipe.IIngredientType type : types) { + if (type != null) { + state.allOfTypes.add(type.getIngredientClass()); + } + } + } + return this; + } + + @Override + public CollapsibleGroupBuilder addAny(mezz.jei.api.recipe.IIngredientType type, Predicate filter) { + if (type != null && filter != null) { + Class ingredientClass = type.getIngredientClass(); + state.typedPredicates.add(ingredient -> { + if (!ingredientClass.isInstance(ingredient)) { + return false; + } + return filter.test(ingredientClass.cast(ingredient)); + }); + } + return this; + } + }; + } + }; + + if (Config.skipShowingProgressBar()) { + for (IModPlugin plugin : plugins) { + try { + plugin.registerCollapsibleGroups(apiRegistry); + } catch (RuntimeException | LinkageError e) { + Log.get().error("Failed to register collapsible groups for plugin: {}", plugin.getClass(), e); + } + } + } else { + ProgressManager.ProgressBar bar = ProgressManager.push("Registering collapsible groups", plugins.size()); + for (IModPlugin plugin : plugins) { + try { + bar.step(plugin.getClass().getName()); + plugin.registerCollapsibleGroups(apiRegistry); + } catch (RuntimeException | LinkageError e) { + Log.get().error("Failed to register collapsible groups for plugin: {}", plugin.getClass(), e); + } + } + ProgressManager.pop(bar); + } + + for (ModGroupBuilderState state : groupsById.values()) { + if (!state.exactUids.isEmpty() && state.registeredStack != null) { + state.registeredStack.setUidMatcher(state::matchesUid); + } + } + } + + private static class ModGroupBuilderState { + final Set exactUids = new HashSet<>(); + final Set> allOfTypes = new HashSet<>(); + final List> typedPredicates = new java.util.ArrayList<>(); + /** The CollapsedStack registered in the registry — uid matcher installed post-loop. */ + mezz.jei.ingredients.CollapsedStack registeredStack; + + boolean matchesUid(String uid) { + return exactUids.contains(uid); + } + + boolean matches(Object ingredient) { + String uid = mezz.jei.ingredients.CollapsedStack.computeIngredientUid(ingredient); + if (uid != null && exactUids.contains(uid)) { + return true; + } + for (Class ingredientClass : allOfTypes) { + if (ingredientClass.isInstance(ingredient)) { + return true; + } + } + for (Predicate predicate : typedPredicates) { + if (predicate.test(ingredient)) { + return true; + } + } + return false; + } + } + } diff --git a/src/main/java/mezz/jei/util/CollapsedClickAction.java b/src/main/java/mezz/jei/util/CollapsedClickAction.java new file mode 100644 index 00000000..b3263647 --- /dev/null +++ b/src/main/java/mezz/jei/util/CollapsedClickAction.java @@ -0,0 +1,12 @@ +package mezz.jei.util; + +/** + * Controls what a plain left-click on a collapsed group slot does. + * + * OPEN_GROUP — click opens/expands the group; Alt+Click applies the action to the first item. + * FIRST_ITEM — click applies the action to the first item in the group; Alt+Click expands/collapses. + */ +public enum CollapsedClickAction { + OPEN_GROUP, + FIRST_ITEM +} diff --git a/src/main/resources/assets/jei/lang/ar_sa.lang b/src/main/resources/assets/jei/lang/ar_sa.lang index 364057dd..a6c49b40 100644 --- a/src/main/resources/assets/jei/lang/ar_sa.lang +++ b/src/main/resources/assets/jei/lang/ar_sa.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=إخفاء زر الإشارة المرجعية في الزاوية السفلية اليسرى -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=ما إذا كان سيتم إخفاء زر الإشارة المرجعية في الزاوية السفلية اليسرى \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=ما إذا كان سيتم إخفاء زر الإشارة المرجعية في الزاوية السفلية اليسرى + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+انقر للتوسيع +hei.tooltip.collapsed.expand.firstItem=Alt+انقر للعنصر الأول +hei.tooltip.collapsed.collapse=Alt+انقر للطي +hei.tooltip.config.expandCollapseAll=Alt+النقر لتبديل المجموعات + +config.hei.collapsible=المجموعات القابلة للطي +config.hei.collapsible.comment=خيارات لطي مجموعات المكونات في خانة واحدة في قائمة المكونات. +config.hei.collapsible.collapsibleGroupsEnabled=تفعيل المجموعات القابلة للطي +config.hei.collapsible.collapsibleGroupsEnabled.comment=عند التفعيل، يتم طي مجموعات المكونات ذات الصلة (الجرعات، الكتب المسحورة، إلخ) في خانة واحدة. Alt+انقر على مجموعة لتوسيعها أو طيها. +config.hei.collapsible.collapseOnClose=الطي عند الإغلاق +config.hei.collapsible.collapseOnClose.comment=طي المجموعات المفتوحة حالياً عند إغلاق HEI. +config.hei.collapsible.collapsedClickAction=إجراء النقر الافتراضي +config.hei.collapsible.collapsedClickAction.comment=يبدل إجراء النقرة الواحدة، يصبح الخيار الآخر هو إجراء Alt+النقر. فتح المجموعة يفتح المجموعة. العنصر الأول يطبق الإجراء على أول عنصر في المجموعة. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=فتح المجموعة +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=العنصر الأول + +# Group Management GUI +hei.gui.collapsible.title=إدارة المجموعات +hei.gui.collapsible.back=رجوع +hei.gui.collapsible.newGroup=مجموعة جديدة +hei.gui.collapsible.title.tooltip=تفعيل/تعطيل المجموعات المدمجة وإنشاء مجموعات مخصصة. +hei.gui.collapsible.enabled=مفعّل +hei.gui.collapsible.disabled=معطّل +hei.gui.collapsible.customGroup=مخصص +hei.gui.collapsible.defaultGroup=افتراضي +hei.gui.collapsible.itemCount=%d عناصر +hei.gui.collapsible.editor.title=العناصر المحددة +hei.gui.collapsible.editor.name=الاسم +hei.gui.collapsible.editor.save=حفظ +hei.gui.collapsible.editor.selected=%d محدد \ No newline at end of file diff --git a/src/main/resources/assets/jei/lang/bg_bg.lang b/src/main/resources/assets/jei/lang/bg_bg.lang index 3e679329..fe8cf4a4 100644 --- a/src/main/resources/assets/jei/lang/bg_bg.lang +++ b/src/main/resources/assets/jei/lang/bg_bg.lang @@ -120,3 +120,35 @@ description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Cir config.jei.misc.hideBottomLeftCornerBookmarkButton=Скрий бутона за отметка в долния ляв ъгъл config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Дали да се скрие бутона за отметка в долния ляв ъгъл +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Клик за разгъване +hei.tooltip.collapsed.expand.firstItem=Alt+Клик за първия предмет +hei.tooltip.collapsed.collapse=Alt+Клик за свиване +hei.tooltip.config.expandCollapseAll=Alt+Клик за превключване на групи + +config.hei.collapsible=Свиваеми групи +config.hei.collapsible.comment=Опции за свиване на групи от съставки в един слот в списъка със съставки. +config.hei.collapsible.collapsibleGroupsEnabled=Активиране на свиваеми групи +config.hei.collapsible.collapsibleGroupsEnabled.comment=Когато е активирано, групи от свързани съставки (отвари, омагьосани книги и др.) се свиват в един слот. Alt+Клик върху група, за да я разгънете или свиете. +config.hei.collapsible.collapseOnClose=Свиване при затваряне +config.hei.collapsible.collapseOnClose.comment=Свиване на текущо отворените групи при затваряне на HEI. +config.hei.collapsible.collapsedClickAction=Действие при клик по подразбиране +config.hei.collapsible.collapsedClickAction.comment=Разменя действието при единично кликване, другата опция става действието при Alt+Клик. Отвори групата отваря групата. Първият предмет прилага действието към първия предмет в групата. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Отвори групата +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Първият предмет + +# Group Management GUI +hei.gui.collapsible.title=Управление на групи +hei.gui.collapsible.back=Назад +hei.gui.collapsible.newGroup=Нова група +hei.gui.collapsible.title.tooltip=Активиране/деактивиране на вградени групи и създаване на потребителски групи. +hei.gui.collapsible.enabled=Активирано +hei.gui.collapsible.disabled=Деактивирано +hei.gui.collapsible.customGroup=Потребителска +hei.gui.collapsible.defaultGroup=По подразбиране +hei.gui.collapsible.itemCount=%d предмети +hei.gui.collapsible.editor.title=Избрани предмети +hei.gui.collapsible.editor.name=Название +hei.gui.collapsible.editor.save=Запазване +hei.gui.collapsible.editor.selected=%d избрани + diff --git a/src/main/resources/assets/jei/lang/cs_cz.lang b/src/main/resources/assets/jei/lang/cs_cz.lang index 1a4c9c3c..129fa18a 100644 --- a/src/main/resources/assets/jei/lang/cs_cz.lang +++ b/src/main/resources/assets/jei/lang/cs_cz.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Kliknutí na dveře změní jejich status a jejich description.jei.wooden.door.3=Dřevěné dveře mohou být otevřeny/zavřeny pomocí Redstone Obvodů. config.jei.misc.hideBottomLeftCornerBookmarkButton=Skrýt tlačítko záložky v levém dolním rohu -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Zda skrýt tlačítko záložky v levém dolním rohu \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Zda skrýt tlačítko záložky v levém dolním rohu + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Klik pro rozbalení +hei.tooltip.collapsed.expand.firstItem=Alt+Klik pro první předmět +hei.tooltip.collapsed.collapse=Alt+Klik pro sbalení +hei.tooltip.config.expandCollapseAll=Alt+Klik pro přepínat skupiny + +config.hei.collapsible=Sbalitelné skupiny +config.hei.collapsible.comment=Možnosti pro sbalení skupin ingrediencí do jednoho slotu v seznamu ingrediencí. +config.hei.collapsible.collapsibleGroupsEnabled=Povolit sbalitelné skupiny +config.hei.collapsible.collapsibleGroupsEnabled.comment=Pokud je povoleno, skupiny souvisejících ingrediencí (lektvary, očarované knihy atd.) jsou sbaleny do jednoho slotu. Alt+Klik na skupinu pro rozbalení nebo sbalení. +config.hei.collapsible.collapseOnClose=Sbalit při zavření +config.hei.collapsible.collapseOnClose.comment=Sbalí aktuálně otevřené skupiny při zavření HEI. +config.hei.collapsible.collapsedClickAction=Výchozí akce kliknutí +config.hei.collapsible.collapsedClickAction.comment=Zamění akci jednoduchého kliknutí, druhá možnost se stane akcí Alt+Klik. Otevřít skupinu otevře skupinu. První předmět aplikuje akci na první předmět ve skupině. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Otevřít skupinu +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=První předmět + +# Group Management GUI +hei.gui.collapsible.title=Správa skupin +hei.gui.collapsible.back=Zpět +hei.gui.collapsible.newGroup=Nová skupina +hei.gui.collapsible.title.tooltip=Povolení/zakázání vestavěných skupin a vytváření uživatelských skupin. +hei.gui.collapsible.enabled=Povoleno +hei.gui.collapsible.disabled=Zakázáno +hei.gui.collapsible.customGroup=Vlastní +hei.gui.collapsible.defaultGroup=Výchozí +hei.gui.collapsible.itemCount=%d předmětů +hei.gui.collapsible.editor.title=Vybrané předměty +hei.gui.collapsible.editor.name=Název +hei.gui.collapsible.editor.save=Uložit +hei.gui.collapsible.editor.selected=%d vybráno diff --git a/src/main/resources/assets/jei/lang/de_de.lang b/src/main/resources/assets/jei/lang/de_de.lang index f8dbc342..ce904480 100644 --- a/src/main/resources/assets/jei/lang/de_de.lang +++ b/src/main/resources/assets/jei/lang/de_de.lang @@ -151,4 +151,36 @@ config.jei.interface.defaultFluidContainerItem=Standard-Flüssigkeitsbehälter config.jei.interface.defaultFluidContainerItem.comment=Voreingestelltes Item, der zum Auffüllen von Flüssigkeiten verwendet wird, wenn im Menü auf eine Flüssigkeitszutat geklickt wird. config.jei.misc.hideBottomLeftCornerBookmarkButton=Lesezeichen-Button in der unteren linken Ecke ausblenden -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Ob der Lesezeichen-Button in der unteren linken Ecke ausgeblendet werden soll \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Ob der Lesezeichen-Button in der unteren linken Ecke ausgeblendet werden soll + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Klick zum Aufklappen +hei.tooltip.collapsed.expand.firstItem=Alt+Klick für erstes Element +hei.tooltip.collapsed.collapse=Alt+Klick zum Zuklappen +hei.tooltip.config.expandCollapseAll=Alt+Klick zum Gruppen umschalten + +config.hei.collapsible=Einklappbare Gruppen +config.hei.collapsible.comment=Optionen zum Einklappen von Zutatengruppen in einen einzelnen Slot in der Zutatenliste. +config.hei.collapsible.collapsibleGroupsEnabled=Einklappbare Gruppen aktivieren +config.hei.collapsible.collapsibleGroupsEnabled.comment=Wenn aktiviert, werden Gruppen verwandter Zutaten (Tränke, verzauberte Bücher usw.) in einen einzelnen Slot zusammengeklappt. Alt+Klick auf eine Gruppe zum Auf- oder Zuklappen. +config.hei.collapsible.collapseOnClose=Beim Schließen einklappen +config.hei.collapsible.collapseOnClose.comment=Aktuell geöffnete Gruppen beim Schließen von HEI einklappen. +config.hei.collapsible.collapsedClickAction=Standard-Klickaktion +config.hei.collapsible.collapsedClickAction.comment=Wechselt die Einzelklick-Aktion, die andere Option wird zur Alt+Klick-Aktion. Gruppe öffnen öffnet die Gruppe. Erstes Element wendet die Aktion auf das erste Element der Gruppe an. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Gruppe öffnen +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Erstes Element + +# Group Management GUI +hei.gui.collapsible.title=Gruppen verwalten +hei.gui.collapsible.back=Zurück +hei.gui.collapsible.newGroup=Neue Gruppe +hei.gui.collapsible.title.tooltip=Integrierte Gruppen aktivieren/deaktivieren und benutzerdefinierte Gruppen erstellen. +hei.gui.collapsible.enabled=Aktiviert +hei.gui.collapsible.disabled=Deaktiviert +hei.gui.collapsible.customGroup=Benutzerdefiniert +hei.gui.collapsible.defaultGroup=Standard +hei.gui.collapsible.itemCount=%d Elemente +hei.gui.collapsible.editor.title=Ausgewählte Elemente +hei.gui.collapsible.editor.name=Name +hei.gui.collapsible.editor.save=Speichern +hei.gui.collapsible.editor.selected=%d ausgewählt diff --git a/src/main/resources/assets/jei/lang/el_gr.lang b/src/main/resources/assets/jei/lang/el_gr.lang index 31aca95a..b0bde75e 100644 --- a/src/main/resources/assets/jei/lang/el_gr.lang +++ b/src/main/resources/assets/jei/lang/el_gr.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Απόκρυψη Κουμπιού Σελιδοδείκτη στην Κάτω Αριστερή Γωνία -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Αν θα αποκρύπτεται το κουμπί σελιδοδείκτη στην κάτω αριστερή γωνία \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Αν θα αποκρύπτεται το κουμπί σελιδοδείκτη στην κάτω αριστερή γωνία + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Κλικ για ανάπτυξη +hei.tooltip.collapsed.expand.firstItem=Alt+Κλικ για πρώτο αντικείμενο +hei.tooltip.collapsed.collapse=Alt+Κλικ για σύμπτυξη +hei.tooltip.config.expandCollapseAll=Alt+Κλικ Εναλλαγή Ομάδων + +config.hei.collapsible=Πτυσσόμενες Ομάδες +config.hei.collapsible.comment=Επιλογές για σύμπτυξη ομάδων συστατικών σε μία θέση στη λίστα συστατικών. +config.hei.collapsible.collapsibleGroupsEnabled=Ενεργοποίηση πτυσσόμενων ομάδων +config.hei.collapsible.collapsibleGroupsEnabled.comment=Όταν ενεργοποιηθεί, οι ομάδες σχετικών συστατικών (φίλτρα, μαγεμένα βιβλία κ.λπ.) συμπτύσσονται σε μία θέση. Alt+Κλικ σε μια ομάδα για ανάπτυξη ή σύμπτυξη. +config.hei.collapsible.collapseOnClose=Σύμπτυξη κατά το κλείσιμο +config.hei.collapsible.collapseOnClose.comment=Σύμπτυξη των τρεχόντων ανοιχτών ομάδων όταν κλείνει το HEI. +config.hei.collapsible.collapsedClickAction=Προεπιλεγμένη ενέργεια κλικ +config.hei.collapsible.collapsedClickAction.comment=Εναλλάσσει την ενέργεια μονού κλικ, η άλλη επιλογή γίνεται η ενέργεια Alt+Κλικ. Άνοιγμα Ομάδας ανοίγει την ομάδα. Πρώτο Αντικείμενο εφαρμόζει την ενέργεια στο πρώτο αντικείμενο της ομάδας. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Άνοιγμα Ομάδας +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Πρώτο Αντικείμενο + +# Group Management GUI +hei.gui.collapsible.title=Διαχείριση ομάδων +hei.gui.collapsible.back=Πίσω +hei.gui.collapsible.newGroup=Νέα ομάδα +hei.gui.collapsible.title.tooltip=Ενεργοποίηση/απενεργοποίηση ενσωματωμένων ομάδων και δημιουργία ομάδων χρήστη. +hei.gui.collapsible.enabled=Ενεργοποιημένο +hei.gui.collapsible.disabled=Απενεργοποιημένο +hei.gui.collapsible.customGroup=Προσαρμοσμένη +hei.gui.collapsible.defaultGroup=Προεπιλογή +hei.gui.collapsible.itemCount=%d αντικείμενα +hei.gui.collapsible.editor.title=Επιλεγμένα αντικείμενα +hei.gui.collapsible.editor.name=Όνομα +hei.gui.collapsible.editor.save=Αποθήκευση +hei.gui.collapsible.editor.selected=%d επιλεγμένα diff --git a/src/main/resources/assets/jei/lang/en_au.lang b/src/main/resources/assets/jei/lang/en_au.lang index 5ca7b147..16936896 100644 --- a/src/main/resources/assets/jei/lang/en_au.lang +++ b/src/main/resources/assets/jei/lang/en_au.lang @@ -119,4 +119,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Hide Bottom-Left Corner Bookmark Button -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Whether to hide the bottom-left corner bookmark button \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Whether to hide the bottom-left corner bookmark button + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Click to expand +hei.tooltip.collapsed.expand.firstItem=Alt+Click for first item +hei.tooltip.collapsed.collapse=Alt+Click to collapse +hei.tooltip.config.expandCollapseAll=Alt+Click Toggle Groups + +config.hei.collapsible=Collapsible Groups +config.hei.collapsible.comment=Options for collapsing groups of ingredients into a single slot in the ingredient list. +config.hei.collapsible.collapsibleGroupsEnabled=Enable Collapsible Groups +config.hei.collapsible.collapsibleGroupsEnabled.comment=When enabled, groups of related ingredients (potions, enchanted books, etc.) are collapsed into a single slot. Alt+Click a group to expand or collapse it. +config.hei.collapsible.collapseOnClose=Collapse on Close +config.hei.collapsible.collapseOnClose.comment=Collapse currently opened groups when HEI is closed. +config.hei.collapsible.collapsedClickAction=Default Collapsed Action +config.hei.collapsible.collapsedClickAction.comment=Swaps the single click action, The other option becomes the Alt+Click action. Open Group opens the group. First Item applies the action to the first item in the group. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Open Group +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=First Item + +# Group Management GUI +hei.gui.collapsible.title=Manage Groups +hei.gui.collapsible.back=Back +hei.gui.collapsible.newGroup=New Group +hei.gui.collapsible.title.tooltip=Enable/Disable built in groups and create user defined groups. +hei.gui.collapsible.enabled=Enabled +hei.gui.collapsible.disabled=Disabled +hei.gui.collapsible.customGroup=Custom +hei.gui.collapsible.defaultGroup=Default +hei.gui.collapsible.itemCount=%d items +hei.gui.collapsible.editor.title=Selected Items +hei.gui.collapsible.editor.name=Name +hei.gui.collapsible.editor.save=Save +hei.gui.collapsible.editor.selected=%d selected diff --git a/src/main/resources/assets/jei/lang/en_us.lang b/src/main/resources/assets/jei/lang/en_us.lang index dc47485e..4c6355ca 100644 --- a/src/main/resources/assets/jei/lang/en_us.lang +++ b/src/main/resources/assets/jei/lang/en_us.lang @@ -22,6 +22,12 @@ jei.tooltip.bookmarks.usage.nokey=Add a key binding for HEI bookmarks in your Co jei.tooltip.bookmarks.usage.key=Hover over an ingredient and press "%s" to bookmark it. jei.tooltip.bookmarks.not.enough.space=There is not enough space to display bookmarks here. +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Click to Expand +hei.tooltip.collapsed.expand.firstItem=Alt+Click for First Item +hei.tooltip.collapsed.collapse=Alt+Click to Collapse +hei.tooltip.config.expandCollapseAll=Alt+Click Toggle Groups + # Error Tooltips jei.tooltip.error.recipe.transfer.missing=Missing Items jei.tooltip.error.recipe.transfer.inventory.full=Inventory is too full @@ -182,6 +188,17 @@ config.jei.misc.hideBottomRightCornerConfigButton.comment=Whether to hide the bo config.jei.misc.hideBottomLeftCornerBookmarkButton=Hide Bottom-Left Corner Bookmark Button config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Whether to hide the bottom-left corner bookmark button +config.hei.collapsible=Collapsible Groups +config.hei.collapsible.comment=Options for collapsing groups of ingredients into a single slot in the ingredient list. +config.hei.collapsible.collapsibleGroupsEnabled=Enable Collapsible Groups +config.hei.collapsible.collapsibleGroupsEnabled.comment=When enabled, groups of related ingredients (potions, enchanted books, etc.) are collapsed into a single slot. Alt+Click a group to expand or collapse it. +config.hei.collapsible.collapseOnClose=Collapse on Close +config.hei.collapsible.collapseOnClose.comment=Collapse currently opened groups when HEI is closed. +config.hei.collapsible.collapsedClickAction=Default Collapsed Action +config.hei.collapsible.collapsedClickAction.comment=Swaps the single click action, The other option becomes the Alt+Click action. Open Group opens the group. First Item applies the action to the first item in the group. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Open Group +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=First Item + config.jei.category.categoryUidOrder=Category UID Order config.jei.category.categoryUidOrder.comment=Determines the display order of recipe categories in HEI. Categories listed earlier appear first in the HEI interface. @@ -202,4 +219,20 @@ hei.tooltip.organizer.1=§6CTRL + LEFT CLICK§7: Move Bookmarks Between Groups hei.tooltip.organizer.2=§6UP§r or §6DOWN§7: Move Group Up or Down hei.tooltip.organizer.3=§6CTRL + SCROLL§7: Change Final Output hei.tooltip.organizer.4=§6%s§7: Autocraft + +hei.gui.collapsible.title=Manage Groups +hei.gui.collapsible.back=Back +hei.gui.collapsible.newGroup=New Group +hei.gui.collapsible.title.tooltip=Enable/Disable built in groups and create user defined groups. +hei.gui.collapsible.enabled=Enabled +hei.gui.collapsible.disabled=Disabled +hei.gui.collapsible.customGroup=Custom +hei.gui.collapsible.modGroup=Mod +hei.gui.collapsible.defaultGroup=Default +hei.gui.collapsible.itemCount=%d items +hei.gui.collapsible.editor.title=Selected Items +hei.gui.collapsible.editor.name=Name +hei.gui.collapsible.editor.save=Save +hei.gui.collapsible.editor.selected=%d selected +hei.gui.collapsible.confirmDelete=Delete? hei.tooltip.missing_ingredients=§cMissing Ingredients: \ No newline at end of file diff --git a/src/main/resources/assets/jei/lang/es_es.lang b/src/main/resources/assets/jei/lang/es_es.lang index f5e411e0..6d52595b 100644 --- a/src/main/resources/assets/jei/lang/es_es.lang +++ b/src/main/resources/assets/jei/lang/es_es.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Ocultar botón de marcador en la esquina inferior izquierda -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Si se debe ocultar el botón de marcador en la esquina inferior izquierda \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Si se debe ocultar el botón de marcador en la esquina inferior izquierda + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Clic para expandir +hei.tooltip.collapsed.expand.firstItem=Alt+Clic para primer objeto +hei.tooltip.collapsed.collapse=Alt+Clic para colapsar +hei.tooltip.config.expandCollapseAll=Alt+Clic alternar grupos + +config.hei.collapsible=Grupos colapsables +config.hei.collapsible.comment=Opciones para colapsar grupos de ingredientes en una sola ranura en la lista de ingredientes. +config.hei.collapsible.collapsibleGroupsEnabled=Habilitar grupos colapsables +config.hei.collapsible.collapsibleGroupsEnabled.comment=Cuando está habilitado, los grupos de ingredientes relacionados (pociones, libros encantados, etc.) se colapsan en una sola ranura. Alt+Clic en un grupo para expandirlo o colapsarlo. +config.hei.collapsible.collapseOnClose=Colapsar al cerrar +config.hei.collapsible.collapseOnClose.comment=Colapsa los grupos actualmente abiertos cuando se cierra HEI. +config.hei.collapsible.collapsedClickAction=Acción de clic predeterminada +config.hei.collapsible.collapsedClickAction.comment=Intercambia la acción del clic simple, la otra opción se convierte en la acción de Alt+Clic. Abrir Grupo abre el grupo. Primer Objeto aplica la acción al primer objeto del grupo. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Abrir Grupo +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Primer Objeto + +# Group Management GUI +hei.gui.collapsible.title=Gestionar grupos +hei.gui.collapsible.back=Atrás +hei.gui.collapsible.newGroup=Nuevo grupo +hei.gui.collapsible.title.tooltip=Activar/desactivar grupos integrados y crear grupos definidos por el usuario. +hei.gui.collapsible.enabled=Activado +hei.gui.collapsible.disabled=Desactivado +hei.gui.collapsible.customGroup=Personalizado +hei.gui.collapsible.defaultGroup=Predeterminado +hei.gui.collapsible.itemCount=%d objetos +hei.gui.collapsible.editor.title=Objetos seleccionados +hei.gui.collapsible.editor.name=Nombre +hei.gui.collapsible.editor.save=Guardar +hei.gui.collapsible.editor.selected=%d seleccionados diff --git a/src/main/resources/assets/jei/lang/fi_fi.lang b/src/main/resources/assets/jei/lang/fi_fi.lang index 8ee1f75a..271b09a2 100644 --- a/src/main/resources/assets/jei/lang/fi_fi.lang +++ b/src/main/resources/assets/jei/lang/fi_fi.lang @@ -118,4 +118,35 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Piilota vasemman alan kulman kirjanmerkkipainike -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Piilotetaanko vasemman alan kulman kirjanmerkkipainike \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Piilotetaanko vasemman alan kulman kirjanmerkkipainike + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Klikkaa laajentaaksesi +hei.tooltip.collapsed.expand.firstItem=Alt+Klikkaa ensimmäiselle esineelle +hei.tooltip.collapsed.collapse=Alt+Klikkaa tiivistääksesi +hei.tooltip.config.expandCollapseAll=Alt+Klikkaa vaihda ryhmiä +config.hei.collapsible=Tiivistetyt ryhmät +config.hei.collapsible.comment=Asetukset ainesosaryhmien tiivistämiseen yhdeksi paikaksi ainesosaluettelossa. +config.hei.collapsible.collapsibleGroupsEnabled=Ota tiivistetyt ryhmät käyttöön +config.hei.collapsible.collapsibleGroupsEnabled.comment=Kun käytössä, toisiinsa liittyvien ainesosien ryhmät (juomat, lumotut kirjat jne.) tiivistetään yhteen paikkaan. Alt+Klikkaa ryhmää laajentaaksesi tai tiivistääksesi sen. +config.hei.collapsible.collapseOnClose=Tiivistä suljettaessa +config.hei.collapsible.collapseOnClose.comment=Tiivistetään avoinna olevat ryhmät kun HEI suljetaan. +config.hei.collapsible.collapsedClickAction=Oletusklikkitoiminto +config.hei.collapsible.collapsedClickAction.comment=Vaihtaa yksittäisklikkauksen toiminnon, toinen vaihtoehto tulee Alt+Klikkaus-toiminnoksi. Avaa ryhmä avaa ryhmän. Ensimmäinen esine soveltaa toiminnon ryhmän ensimmäiseen esineeseen. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Avaa ryhmä +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Ensimmäinen esine + +# Group Management GUI +hei.gui.collapsible.title=Hallitse ryhmiä +hei.gui.collapsible.back=Takaisin +hei.gui.collapsible.newGroup=Uusi ryhmä +hei.gui.collapsible.title.tooltip=Sisäänrakennettujen ryhmien käyttöönotto/poistaminen käytöstä ja käyttäjän määrittämien ryhmien luominen. +hei.gui.collapsible.enabled=Käytössä +hei.gui.collapsible.disabled=Ei käytössä +hei.gui.collapsible.customGroup=Mukautettu +hei.gui.collapsible.defaultGroup=Oletus +hei.gui.collapsible.itemCount=%d esinettä +hei.gui.collapsible.editor.title=Valitut esineet +hei.gui.collapsible.editor.name=Nimi +hei.gui.collapsible.editor.save=Tallenna +hei.gui.collapsible.editor.selected=%d valittu diff --git a/src/main/resources/assets/jei/lang/fr_fr.lang b/src/main/resources/assets/jei/lang/fr_fr.lang index 36a931f0..350a7964 100644 --- a/src/main/resources/assets/jei/lang/fr_fr.lang +++ b/src/main/resources/assets/jei/lang/fr_fr.lang @@ -128,4 +128,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Masquer le bouton de marque-page en bas à gauche -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Si le bouton de marque-page en bas à gauche doit être masqué \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Si le bouton de marque-page en bas à gauche doit être masqué + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Clic pour développer +hei.tooltip.collapsed.expand.firstItem=Alt+Clic pour le premier objet +hei.tooltip.collapsed.collapse=Alt+Clic pour réduire +hei.tooltip.config.expandCollapseAll=Alt+Clic basculer les groupes + +config.hei.collapsible=Groupes réductibles +config.hei.collapsible.comment=Options pour réduire les groupes d'ingrédients en un seul emplacement dans la liste des ingrédients. +config.hei.collapsible.collapsibleGroupsEnabled=Activer les groupes réductibles +config.hei.collapsible.collapsibleGroupsEnabled.comment=Lorsqu'activé, les groupes d'ingrédients liés (potions, livres enchantés, etc.) sont réduits en un seul emplacement. Alt+Clic sur un groupe pour le développer ou le réduire. +config.hei.collapsible.collapseOnClose=Réduire à la fermeture +config.hei.collapsible.collapseOnClose.comment=Réduit les groupes actuellement ouverts lorsque HEI est fermé. +config.hei.collapsible.collapsedClickAction=Action de clic par défaut +config.hei.collapsible.collapsedClickAction.comment=Échange l'action du clic simple, l'autre option devient l'action Alt+Clic. Ouvrir le groupe ouvre le groupe. Premier objet applique l'action au premier objet du groupe. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Ouvrir le groupe +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Premier objet + +# Group Management GUI +hei.gui.collapsible.title=Gérer les groupes +hei.gui.collapsible.back=Retour +hei.gui.collapsible.newGroup=Nouveau groupe +hei.gui.collapsible.title.tooltip=Activer/désactiver les groupes intégrés et créer des groupes définis par l'utilisateur. +hei.gui.collapsible.enabled=Activé +hei.gui.collapsible.disabled=Désactivé +hei.gui.collapsible.customGroup=Personnalisé +hei.gui.collapsible.defaultGroup=Par défaut +hei.gui.collapsible.itemCount=%d objets +hei.gui.collapsible.editor.title=Objets sélectionnés +hei.gui.collapsible.editor.name=Nom +hei.gui.collapsible.editor.save=Enregistrer +hei.gui.collapsible.editor.selected=%d sélectionnés diff --git a/src/main/resources/assets/jei/lang/he_il.lang b/src/main/resources/assets/jei/lang/he_il.lang index 60b27c0f..76ab104a 100644 --- a/src/main/resources/assets/jei/lang/he_il.lang +++ b/src/main/resources/assets/jei/lang/he_il.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=הסתר כפתור סימנייה בפינה השמאלית התחתונה -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=האם להסתיר את כפתור הסימנייה בפינה השמאלית התחתונה \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=האם להסתיר את כפתור הסימנייה בפינה השמאלית התחתונה + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+לחיצה כדי להרחיב +hei.tooltip.collapsed.expand.firstItem=Alt+לחיצה לפריט הראשון +hei.tooltip.collapsed.collapse=Alt+לחיצה כדי לכווץ +hei.tooltip.config.expandCollapseAll=Alt+לחיצה להחלפת קבוצות + +config.hei.collapsible=קבוצות מתכווצות +config.hei.collapsible.comment=אפשרויות לכיווץ קבוצות רכיבים לתא יחיד ברשימת הרכיבים. +config.hei.collapsible.collapsibleGroupsEnabled=הפעל קבוצות מתכווצות +config.hei.collapsible.collapsibleGroupsEnabled.comment=כאשר מופעל, קבוצות של רכיבים קשורים (שיקויים, ספרים מכושפים וכו') מכווצות לתא יחיד. Alt+לחיצה על קבוצה כדי להרחיב או לכווץ אותה. +config.hei.collapsible.collapseOnClose=כיווץ בסגירה +config.hei.collapsible.collapseOnClose.comment=כיווץ קבוצות פתוחות כאשר HEI נסגר. +config.hei.collapsible.collapsedClickAction=פעולת לחיצה ברירת מחדל +config.hei.collapsible.collapsedClickAction.comment=מחליף את פעולת הלחיצה הבודדת, האפשרות השנייה הופכת לפעולת Alt+לחיצה. פתח קבוצה פותח את הקבוצה. פריט ראשון מחיל את הפעולה על הפריט הראשון בקבוצה. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=פתח קבוצה +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=פריט ראשון + +# Group Management GUI +hei.gui.collapsible.title=ניהול קבוצות +hei.gui.collapsible.back=חזרה +hei.gui.collapsible.newGroup=קבוצה חדשה +hei.gui.collapsible.title.tooltip=הפעלה/השבתה של קבוצות מובנות ויצירת קבוצות מותאמות אישית. +hei.gui.collapsible.enabled=מופעל +hei.gui.collapsible.disabled=מושבת +hei.gui.collapsible.customGroup=מותאם אישית +hei.gui.collapsible.defaultGroup=ברירת מחדל +hei.gui.collapsible.itemCount=%d פריטים +hei.gui.collapsible.editor.title=פריטים נבחרים +hei.gui.collapsible.editor.name=שם +hei.gui.collapsible.editor.save=שמירה +hei.gui.collapsible.editor.selected=%d נבחרו \ No newline at end of file diff --git a/src/main/resources/assets/jei/lang/it_it.lang b/src/main/resources/assets/jei/lang/it_it.lang index 24fd4fc1..bc6a8efb 100644 --- a/src/main/resources/assets/jei/lang/it_it.lang +++ b/src/main/resources/assets/jei/lang/it_it.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Nascondi pulsante segnalibro nell'angolo in basso a sinistra -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Se nascondere il pulsante segnalibro nell'angolo in basso a sinistra \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Se nascondere il pulsante segnalibro nell'angolo in basso a sinistra + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Clic per espandere +hei.tooltip.collapsed.expand.firstItem=Alt+Clic per il primo oggetto +hei.tooltip.collapsed.collapse=Alt+Clic per comprimere +hei.tooltip.config.expandCollapseAll=Alt+Clic attiva/disattiva gruppi + +config.hei.collapsible=Gruppi comprimibili +config.hei.collapsible.comment=Opzioni per comprimere gruppi di ingredienti in un singolo slot nell'elenco degli ingredienti. +config.hei.collapsible.collapsibleGroupsEnabled=Abilita gruppi comprimibili +config.hei.collapsible.collapsibleGroupsEnabled.comment=Quando abilitato, i gruppi di ingredienti correlati (pozioni, libri incantati, ecc.) vengono compressi in un singolo slot. Alt+Clic su un gruppo per espanderlo o comprimerlo. +config.hei.collapsible.collapseOnClose=Comprimi alla chiusura +config.hei.collapsible.collapseOnClose.comment=Comprime i gruppi attualmente aperti quando HEI viene chiuso. +config.hei.collapsible.collapsedClickAction=Azione clic predefinita +config.hei.collapsible.collapsedClickAction.comment=Scambia l'azione del clic singolo, l'altra opzione diventa l'azione Alt+Clic. Apri Gruppo apre il gruppo. Primo Oggetto applica l'azione al primo oggetto del gruppo. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Apri Gruppo +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Primo Oggetto + +# Group Management GUI +hei.gui.collapsible.title=Gestisci gruppi +hei.gui.collapsible.back=Indietro +hei.gui.collapsible.newGroup=Nuovo gruppo +hei.gui.collapsible.title.tooltip=Abilita/disabilita i gruppi integrati e crea gruppi definiti dall'utente. +hei.gui.collapsible.enabled=Abilitato +hei.gui.collapsible.disabled=Disabilitato +hei.gui.collapsible.customGroup=Personalizzato +hei.gui.collapsible.defaultGroup=Predefinito +hei.gui.collapsible.itemCount=%d oggetti +hei.gui.collapsible.editor.title=Oggetti selezionati +hei.gui.collapsible.editor.name=Nome +hei.gui.collapsible.editor.save=Salva +hei.gui.collapsible.editor.selected=%d selezionati \ No newline at end of file diff --git a/src/main/resources/assets/jei/lang/ja_jp.lang b/src/main/resources/assets/jei/lang/ja_jp.lang index a5ffd9b0..ba1c1f97 100644 --- a/src/main/resources/assets/jei/lang/ja_jp.lang +++ b/src/main/resources/assets/jei/lang/ja_jp.lang @@ -128,4 +128,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=左下隅のブックマークボタンを非表示にする -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=左下隅のブックマークボタンを非表示にするかどうか \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=左下隅のブックマークボタンを非表示にするかどうか + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+クリックで展開 +hei.tooltip.collapsed.expand.firstItem=Alt+クリックで最初のアイテム +hei.tooltip.collapsed.collapse=Alt+クリックで折りたたむ +hei.tooltip.config.expandCollapseAll=Alt+クリックでグループ展開切替 + +config.hei.collapsible=折りたたみグループ +config.hei.collapsible.comment=アイテムリスト内でアイテムグループをひとつのスロットに折りたたむオプション。 +config.hei.collapsible.collapsibleGroupsEnabled=折りたたみグループを有効にする +config.hei.collapsible.collapsibleGroupsEnabled.comment=有効にすると、関連するアイテムのグループ(ポーション、エンチャント本など)がひとつのスロットに折りたたまれます。Alt+クリックでグループを展開または折りたたみます。 +config.hei.collapsible.collapseOnClose=閉じる時に折りたたむ +config.hei.collapsible.collapseOnClose.comment=HEIを閉じた時に現在開いているグループを折りたたみます。 +config.hei.collapsible.collapsedClickAction=デフォルトのクリック動作 +config.hei.collapsible.collapsedClickAction.comment=シングルクリックの動作を切り替えます。もう一方のオプションがAlt+クリックの動作になります。グループを開くはグループを開きます。最初のアイテムはグループの最初のアイテムにアクションを適用します。 +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=グループを開く +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=最初のアイテム + +# Group Management GUI +hei.gui.collapsible.title=グループ管理 +hei.gui.collapsible.back=戻る +hei.gui.collapsible.newGroup=新しいグループ +hei.gui.collapsible.title.tooltip=組み込みグループを有効/無効にし、ユーザー定義グループを作成します。 +hei.gui.collapsible.enabled=有効 +hei.gui.collapsible.disabled=無効 +hei.gui.collapsible.customGroup=カスタム +hei.gui.collapsible.defaultGroup=デフォルト +hei.gui.collapsible.itemCount=%d アイテム +hei.gui.collapsible.editor.title=選択されたアイテム +hei.gui.collapsible.editor.name=名前 +hei.gui.collapsible.editor.save=保存 +hei.gui.collapsible.editor.selected=%d 選択中 diff --git a/src/main/resources/assets/jei/lang/ko_kr.lang b/src/main/resources/assets/jei/lang/ko_kr.lang index f5809424..038e5909 100644 --- a/src/main/resources/assets/jei/lang/ko_kr.lang +++ b/src/main/resources/assets/jei/lang/ko_kr.lang @@ -165,4 +165,36 @@ config.jei.misc.tooltipShowRecipeBy.comment=툴팁에 레시피가 언제 변경 config.jei.interface.defaultFluidContainerItem.comment=메뉴에서 유체 재료를 클릭할 시 표시할 기본 유체 보관 아이템을 지정합니다, (형식: 아이템 이름@메타데이터) config.jei.misc.hideBottomLeftCornerBookmarkButton=왼쪽 하단 모서리 북마크 버튼 숨기기 -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=왼쪽 하단 모서리 북마크 버튼을 숨길지 여부 \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=왼쪽 하단 모서리 북마크 버튼을 숨길지 여부 + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+클릭하여 펼치기 +hei.tooltip.collapsed.expand.firstItem=Alt+클릭하여 첫 번째 아이템 +hei.tooltip.collapsed.collapse=Alt+클릭하여 접기 +hei.tooltip.config.expandCollapseAll=Alt+클릭으로 그룹 전환 + +config.hei.collapsible=접을 수 있는 그룹 +config.hei.collapsible.comment=재료 목록에서 재료 그룹을 하나의 슬롯으로 접는 옵션입니다. +config.hei.collapsible.collapsibleGroupsEnabled=접을 수 있는 그룹 활성화 +config.hei.collapsible.collapsibleGroupsEnabled.comment=활성화하면 관련 재료 그룹(물약, 마법이 부여된 책 등)이 하나의 슬롯으로 접힙니다. Alt+클릭으로 그룹을 펼치거나 접을 수 있습니다. +config.hei.collapsible.collapseOnClose=닫을 때 접기 +config.hei.collapsible.collapseOnClose.comment=HEI를 닫을 때 현재 열려 있는 그룹을 접습니다. +config.hei.collapsible.collapsedClickAction=기본 클릭 동작 +config.hei.collapsible.collapsedClickAction.comment=단일 클릭 동작을 전환합니다. 다른 옵션이 Alt+클릭 동작이 됩니다. 그룹 열기는 그룹을 엽니다. 첫 번째 아이템은 그룹의 첫 번째 아이템에 동작을 적용합니다. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=그룹 열기 +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=첫 번째 아이템 + +# Group Management GUI +hei.gui.collapsible.title=그룹 관리 +hei.gui.collapsible.back=뒤로 +hei.gui.collapsible.newGroup=새 그룹 +hei.gui.collapsible.title.tooltip=기본 제공 그룹을 활성화/비활성화하고 사용자 정의 그룹을 만듭니다. +hei.gui.collapsible.enabled=활성화됨 +hei.gui.collapsible.disabled=비활성화됨 +hei.gui.collapsible.customGroup=사용자 정의 +hei.gui.collapsible.defaultGroup=기본값 +hei.gui.collapsible.itemCount=%d 아이템 +hei.gui.collapsible.editor.title=선택된 아이템 +hei.gui.collapsible.editor.name=이름 +hei.gui.collapsible.editor.save=저장 +hei.gui.collapsible.editor.selected=%d 선택됨 diff --git a/src/main/resources/assets/jei/lang/lt_lt.lang b/src/main/resources/assets/jei/lang/lt_lt.lang index e1cb177c..e75a5e48 100644 --- a/src/main/resources/assets/jei/lang/lt_lt.lang +++ b/src/main/resources/assets/jei/lang/lt_lt.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Paslėpti žymės mygtuką apatiniame kairiajame kampe -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Ar paslėpti žymės mygtuką apatiniame kairiajame kampe \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Ar paslėpti žymės mygtuką apatiniame kairiajame kampe + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+spustelėkite, kad išskleistumėte +hei.tooltip.collapsed.expand.firstItem=Alt+spustelėkite pirmam daiktui +hei.tooltip.collapsed.collapse=Alt+spustelėkite, kad sutrauktumėte +hei.tooltip.config.expandCollapseAll=Alt+Spustelėkite perjungti grupes + +config.hei.collapsible=Sutraukiamos grupės +config.hei.collapsible.comment=Parinktys ingredientų grupių sutraukimui į vieną vietą ingredientų sąraše. +config.hei.collapsible.collapsibleGroupsEnabled=Įjungti sutraukiamas grupes +config.hei.collapsible.collapsibleGroupsEnabled.comment=Kai įjungta, susijusių ingredientų grupės (eliksyrai, užkeiktos knygos ir kt.) sutraukiamos į vieną vietą. Alt+spustelėkite grupę, kad ją išskleistumėte ar sutrauktumėte. +config.hei.collapsible.collapseOnClose=Sutraukti uždarant +config.hei.collapsible.collapseOnClose.comment=Sutraukti šiuo metu atidarytas grupes kai HEI uždaromas. +config.hei.collapsible.collapsedClickAction=Numatytasis paspaudimo veiksmas +config.hei.collapsible.collapsedClickAction.comment=Pakeičia vieno paspaudimo veiksmą, kita parinktis tampa Alt+spustelėjimo veiksmu. Atidaryti grupę atidaro grupę. Pirmas daiktas taiko veiksmą pirmam grupės daiktui. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Atidaryti grupę +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Pirmas daiktas + +# Group Management GUI +hei.gui.collapsible.title=Valdyti grupes +hei.gui.collapsible.back=Atgal +hei.gui.collapsible.newGroup=Nauja grupė +hei.gui.collapsible.title.tooltip=Įgąlinti/išjungti įtaisytąsias grupes ir kurti naudotojo apibrėžtas grupes. +hei.gui.collapsible.enabled=Įgąlinta +hei.gui.collapsible.disabled=Išjungta +hei.gui.collapsible.customGroup=Pasirinktiniű +hei.gui.collapsible.defaultGroup=Numatytoji +hei.gui.collapsible.itemCount=%d daiktų +hei.gui.collapsible.editor.title=Pasirinkti daiktai +hei.gui.collapsible.editor.name=Pavadinimas +hei.gui.collapsible.editor.save=Išsaugoti +hei.gui.collapsible.editor.selected=%d pasirinkta diff --git a/src/main/resources/assets/jei/lang/nb_no.lang b/src/main/resources/assets/jei/lang/nb_no.lang index 17aa9069..14bca670 100644 --- a/src/main/resources/assets/jei/lang/nb_no.lang +++ b/src/main/resources/assets/jei/lang/nb_no.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Skjul bokmerke-knapp i nedre venstre hjørne -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Om bokmerke-knappen i nedre venstre hjørne skal skjules \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Om bokmerke-knappen i nedre venstre hjørne skal skjules + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Klikk for å utvide +hei.tooltip.collapsed.expand.firstItem=Alt+Klikk for første gjenstand +hei.tooltip.collapsed.collapse=Alt+Klikk for å skjule +hei.tooltip.config.expandCollapseAll=Alt+Klikk for å bytte grupper + +config.hei.collapsible=Sammenleggbare grupper +config.hei.collapsible.comment=Alternativer for å slå sammen grupper av ingredienser til én plass i ingredienslisten. +config.hei.collapsible.collapsibleGroupsEnabled=Aktiver sammenleggbare grupper +config.hei.collapsible.collapsibleGroupsEnabled.comment=Når aktivert, blir grupper av relaterte ingredienser (drikkevarer, forheksede bøker osv.) slått sammen til én plass. Alt+Klikk på en gruppe for å utvide eller skjule den. +config.hei.collapsible.collapseOnClose=Legg sammen ved lukking +config.hei.collapsible.collapseOnClose.comment=Legger sammen åpne grupper når HEI lukkes. +config.hei.collapsible.collapsedClickAction=Standard klikkhandling +config.hei.collapsible.collapsedClickAction.comment=Bytter enkeltklikkhandlingen, det andre alternativet blir Alt+Klikk-handlingen. Åpne gruppe åpner gruppen. Første gjenstand bruker handlingen på den første gjenstanden i gruppen. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Åpne gruppe +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Første gjenstand + +# Group Management GUI +hei.gui.collapsible.title=Administrer grupper +hei.gui.collapsible.back=Tilbake +hei.gui.collapsible.newGroup=Ny gruppe +hei.gui.collapsible.title.tooltip=Aktiver/deaktiver innebygde grupper og opprett brukerdefinerte grupper. +hei.gui.collapsible.enabled=Aktivert +hei.gui.collapsible.disabled=Deaktivert +hei.gui.collapsible.customGroup=Egendefinert +hei.gui.collapsible.defaultGroup=Standard +hei.gui.collapsible.itemCount=%d gjenstander +hei.gui.collapsible.editor.title=Valgte gjenstander +hei.gui.collapsible.editor.name=Navn +hei.gui.collapsible.editor.save=Lagre +hei.gui.collapsible.editor.selected=%d valgt diff --git a/src/main/resources/assets/jei/lang/pl_pl.lang b/src/main/resources/assets/jei/lang/pl_pl.lang index 190cfb94..fa7bb10f 100644 --- a/src/main/resources/assets/jei/lang/pl_pl.lang +++ b/src/main/resources/assets/jei/lang/pl_pl.lang @@ -131,4 +131,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Ukryj przycisk zakładki w lewym dolnym rogu -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Czy ukryć przycisk zakładki w lewym dolnym rogu \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Czy ukryć przycisk zakładki w lewym dolnym rogu + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Klik aby rozwinąć +hei.tooltip.collapsed.expand.firstItem=Alt+Klik dla pierwszego przedmiotu +hei.tooltip.collapsed.collapse=Alt+Klik aby zwinąć +hei.tooltip.config.expandCollapseAll=Alt+Klik aby przełączyć grupy + +config.hei.collapsible=Zwijane grupy +config.hei.collapsible.comment=Opcje zwijania grup składników do jednego slotu na liście składników. +config.hei.collapsible.collapsibleGroupsEnabled=Włącz zwijane grupy +config.hei.collapsible.collapsibleGroupsEnabled.comment=Po włączeniu grupy powiązanych składników (mikstury, zaklęte księgi itp.) są zwijane do jednego slotu. Alt+Klik na grupę, aby ją rozwinąć lub zwinąć. +config.hei.collapsible.collapseOnClose=Zwiń przy zamknięciu +config.hei.collapsible.collapseOnClose.comment=Zwija aktualnie otwarte grupy po zamknięciu HEI. +config.hei.collapsible.collapsedClickAction=Domyślna akcja kliknięcia +config.hei.collapsible.collapsedClickAction.comment=Zamienia akcję pojedynczego kliknięcia, druga opcja staje się akcją Alt+Klik. Otwórz grupę otwiera grupę. Pierwszy przedmiot stosuje akcję do pierwszego przedmiotu w grupie. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Otwórz grupę +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Pierwszy przedmiot + +# Group Management GUI +hei.gui.collapsible.title=Zarządzaj grupami +hei.gui.collapsible.back=Wstecz +hei.gui.collapsible.newGroup=Nowa grupa +hei.gui.collapsible.title.tooltip=Włącz/wyłącz wbudowane grupy i twórz grupy zdefiniowane przez użytkownika. +hei.gui.collapsible.enabled=Włączone +hei.gui.collapsible.disabled=Wyłączone +hei.gui.collapsible.customGroup=Niestandardowe +hei.gui.collapsible.defaultGroup=Domyślne +hei.gui.collapsible.itemCount=%d przedmiotów +hei.gui.collapsible.editor.title=Wybrane przedmioty +hei.gui.collapsible.editor.name=Nazwa +hei.gui.collapsible.editor.save=Zapisz +hei.gui.collapsible.editor.selected=%d wybranych diff --git a/src/main/resources/assets/jei/lang/pt_br.lang b/src/main/resources/assets/jei/lang/pt_br.lang index b376dd2a..43bb1c0d 100644 --- a/src/main/resources/assets/jei/lang/pt_br.lang +++ b/src/main/resources/assets/jei/lang/pt_br.lang @@ -128,4 +128,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Ocultar botão de favorito no canto inferior esquerdo -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Se o botão de favorito no canto inferior esquerdo deve ser ocultado \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Se o botão de favorito no canto inferior esquerdo deve ser ocultado + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Clique para expandir +hei.tooltip.collapsed.expand.firstItem=Alt+Clique para o primeiro item +hei.tooltip.collapsed.collapse=Alt+Clique para recolher +hei.tooltip.config.expandCollapseAll=Alt+Clique para alternar grupos + +config.hei.collapsible=Grupos recolhíveis +config.hei.collapsible.comment=Opções para recolher grupos de ingredientes em um único slot na lista de ingredientes. +config.hei.collapsible.collapsibleGroupsEnabled=Habilitar grupos recolhíveis +config.hei.collapsible.collapsibleGroupsEnabled.comment=Quando habilitado, grupos de ingredientes relacionados (poções, livros encantados, etc.) são recolhidos em um único slot. Alt+Clique em um grupo para expandir ou recolher. +config.hei.collapsible.collapseOnClose=Recolher ao fechar +config.hei.collapsible.collapseOnClose.comment=Recolhe os grupos atualmente abertos quando o HEI é fechado. +config.hei.collapsible.collapsedClickAction=Ação de clique padrão +config.hei.collapsible.collapsedClickAction.comment=Troca a ação do clique simples, a outra opção se torna a ação Alt+Clique. Abrir Grupo abre o grupo. Primeiro Item aplica a ação ao primeiro item do grupo. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Abrir Grupo +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Primeiro Item + +# Group Management GUI +hei.gui.collapsible.title=Gerenciar grupos +hei.gui.collapsible.back=Voltar +hei.gui.collapsible.newGroup=Novo grupo +hei.gui.collapsible.title.tooltip=Ativar/desativar grupos integrados e criar grupos definidos pelo usuário. +hei.gui.collapsible.enabled=Ativado +hei.gui.collapsible.disabled=Desativado +hei.gui.collapsible.customGroup=Personalizado +hei.gui.collapsible.defaultGroup=Padrão +hei.gui.collapsible.itemCount=%d itens +hei.gui.collapsible.editor.title=Itens selecionados +hei.gui.collapsible.editor.name=Nome +hei.gui.collapsible.editor.save=Salvar +hei.gui.collapsible.editor.selected=%d selecionados diff --git a/src/main/resources/assets/jei/lang/ru_ru.lang b/src/main/resources/assets/jei/lang/ru_ru.lang index cc59257e..9cc8f83d 100644 --- a/src/main/resources/assets/jei/lang/ru_ru.lang +++ b/src/main/resources/assets/jei/lang/ru_ru.lang @@ -163,4 +163,36 @@ config.jei.interface.defaultFluidContainerItem=Контейнер жидкост config.jei.interface.defaultFluidContainerItem.comment=Предмет по умолчанию, используемый для заполнения жидкостей при нажатии на жидкий ингредиент в меню (формат: item_name@meta) config.jei.misc.hideBottomLeftCornerBookmarkButton=Скрыть кнопку закладки в левом нижнем углу -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Скрывать ли кнопку закладки в левом нижнем углу \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Скрывать ли кнопку закладки в левом нижнем углу + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Клик для раскрытия +hei.tooltip.collapsed.expand.firstItem=Alt+Клик для первого предмета +hei.tooltip.collapsed.collapse=Alt+Клик для сворачивания +hei.tooltip.config.expandCollapseAll=Alt+Клик переключить группы + +config.hei.collapsible=Сворачиваемые группы +config.hei.collapsible.comment=Настройки сворачивания групп ингредиентов в один слот в списке ингредиентов. +config.hei.collapsible.collapsibleGroupsEnabled=Включить сворачиваемые группы +config.hei.collapsible.collapsibleGroupsEnabled.comment=При включении группы связанных ингредиентов (зелья, зачарованные книги и т.д.) сворачиваются в один слот. Alt+Клик по группе для раскрытия или сворачивания. +config.hei.collapsible.collapseOnClose=Сворачивать при закрытии +config.hei.collapsible.collapseOnClose.comment=Сворачивать открытые группы при закрытии HEI. +config.hei.collapsible.collapsedClickAction=Действие клика по умолчанию +config.hei.collapsible.collapsedClickAction.comment=Меняет действие одиночного клика, другой вариант становится действием Alt+Клик. Открыть группу открывает группу. Первый предмет применяет действие к первому предмету в группе. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Открыть группу +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Первый предмет + +# Group Management GUI +hei.gui.collapsible.title=Управление группами +hei.gui.collapsible.back=Назад +hei.gui.collapsible.newGroup=Новая группа +hei.gui.collapsible.title.tooltip=Включение/отключение встроенных групп и создание пользовательских групп. +hei.gui.collapsible.enabled=Включено +hei.gui.collapsible.disabled=Отключено +hei.gui.collapsible.customGroup=Пользовательская +hei.gui.collapsible.defaultGroup=По умолчанию +hei.gui.collapsible.itemCount=%d предметов +hei.gui.collapsible.editor.title=Выбранные предметы +hei.gui.collapsible.editor.name=Название +hei.gui.collapsible.editor.save=Сохранить +hei.gui.collapsible.editor.selected=%d выбрано diff --git a/src/main/resources/assets/jei/lang/sv_se.lang b/src/main/resources/assets/jei/lang/sv_se.lang index ff836ec5..f199ffe1 100644 --- a/src/main/resources/assets/jei/lang/sv_se.lang +++ b/src/main/resources/assets/jei/lang/sv_se.lang @@ -131,4 +131,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Dölj bokmärkesknapp i nedre vänstra hörnet -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Om bokmärkesknappen i nedre vänstra hörnet ska döljas \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Om bokmärkesknappen i nedre vänstra hörnet ska döljas + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Klicka för att expandera +hei.tooltip.collapsed.expand.firstItem=Alt+Klicka för första föremålet +hei.tooltip.collapsed.collapse=Alt+Klicka för att fälla ihop +hei.tooltip.config.expandCollapseAll=Alt+Klicka för att växla grupper + +config.hei.collapsible=Ihopfällbara grupper +config.hei.collapsible.comment=Alternativ för att fälla ihop grupper av ingredienser till en enda plats i ingredienslistan. +config.hei.collapsible.collapsibleGroupsEnabled=Aktivera ihopfällbara grupper +config.hei.collapsible.collapsibleGroupsEnabled.comment=När aktiverat fälls grupper av relaterade ingredienser (bryggdrycker, förtrollade böcker osv.) ihop till en enda plats. Alt+Klicka på en grupp för att expandera eller fälla ihop den. +config.hei.collapsible.collapseOnClose=Fäll ihop vid stängning +config.hei.collapsible.collapseOnClose.comment=Fäller ihop öppna grupper när HEI stängs. +config.hei.collapsible.collapsedClickAction=Standardklickåtgärd +config.hei.collapsible.collapsedClickAction.comment=Byter enkelklicksåtgärden, det andra alternativet blir Alt+Klick-åtgärden. Öppna grupp öppnar gruppen. Första föremålet tillämpar åtgärden på gruppens första föremål. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Öppna grupp +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Första föremålet + +# Group Management GUI +hei.gui.collapsible.title=Hantera grupper +hei.gui.collapsible.back=Tillbaka +hei.gui.collapsible.newGroup=Ny grupp +hei.gui.collapsible.title.tooltip=Aktivera/inaktivera inbyggda grupper och skapa användardefinierade grupper. +hei.gui.collapsible.enabled=Aktiverad +hei.gui.collapsible.disabled=Inaktiverad +hei.gui.collapsible.customGroup=Anpassad +hei.gui.collapsible.defaultGroup=Standard +hei.gui.collapsible.itemCount=%d föremål +hei.gui.collapsible.editor.title=Valda föremål +hei.gui.collapsible.editor.name=Namn +hei.gui.collapsible.editor.save=Spara +hei.gui.collapsible.editor.selected=%d valda diff --git a/src/main/resources/assets/jei/lang/tr_tr.lang b/src/main/resources/assets/jei/lang/tr_tr.lang index d3d1692b..8c9dc3d7 100644 --- a/src/main/resources/assets/jei/lang/tr_tr.lang +++ b/src/main/resources/assets/jei/lang/tr_tr.lang @@ -118,4 +118,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Sol alt köşedeki yer imi düğmesini gizle -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Sol alt köşedeki yer imi düğmesinin gizlenip gizlenmeyeceği \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Sol alt köşedeki yer imi düğmesinin gizlenip gizlenmeyeceği + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Tıkla genişletmek için +hei.tooltip.collapsed.expand.firstItem=Alt+Tıkla ilk öğe için +hei.tooltip.collapsed.collapse=Alt+Tıkla daraltmak için +hei.tooltip.config.expandCollapseAll=Alt+Tıkla Grupları Dönüştür + +config.hei.collapsible=Daraltılabilir Gruplar +config.hei.collapsible.comment=Malzeme listesinde malzeme gruplarını tek bir yuvaya daraltma seçenekleri. +config.hei.collapsible.collapsibleGroupsEnabled=Daraltılabilir Grupları Etkinleştir +config.hei.collapsible.collapsibleGroupsEnabled.comment=Etkinleştirildiğinde, ilgili malzeme grupları (iksirler, büyülü kitaplar vb.) tek bir yuvaya daraltılır. Alt+Tıkla ile bir grubu genişletin veya daraltın. +config.hei.collapsible.collapseOnClose=Kapatırken Daralt +config.hei.collapsible.collapseOnClose.comment=HEI kapatıldığında açık olan grupları daraltır. +config.hei.collapsible.collapsedClickAction=Varsayılan Tıklama Eylemi +config.hei.collapsible.collapsedClickAction.comment=Tek tıklama eylemini değiştirir, diğer seçenek Alt+Tıklama eylemi olur. Grubu Aç grubu açar. İlk Öğe eylemi gruptaki ilk öğeye uygular. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Grubu Aç +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=İlk Öğe + +# Group Management GUI +hei.gui.collapsible.title=Grupları Yönet +hei.gui.collapsible.back=Geri +hei.gui.collapsible.newGroup=Yeni Grup +hei.gui.collapsible.title.tooltip=Yerleşik grupları etkinleştirin/devre dışı bırakın ve kullanıcı tanımlı gruplar oluşturun. +hei.gui.collapsible.enabled=Etkin +hei.gui.collapsible.disabled=Devre dışı +hei.gui.collapsible.customGroup=Özel +hei.gui.collapsible.defaultGroup=Varsayılan +hei.gui.collapsible.itemCount=%d öğe +hei.gui.collapsible.editor.title=Seçili Öğeler +hei.gui.collapsible.editor.name=Ad +hei.gui.collapsible.editor.save=Kaydet +hei.gui.collapsible.editor.selected=%d seçildi diff --git a/src/main/resources/assets/jei/lang/uk_ua.lang b/src/main/resources/assets/jei/lang/uk_ua.lang index 14c79d16..b7739815 100644 --- a/src/main/resources/assets/jei/lang/uk_ua.lang +++ b/src/main/resources/assets/jei/lang/uk_ua.lang @@ -120,4 +120,36 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=Приховати кнопку закладки в лівому нижньому куті -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Чи приховувати кнопку закладки в лівому нижньому куті \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=Чи приховувати кнопку закладки в лівому нижньому куті + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+Клік для розгортання +hei.tooltip.collapsed.expand.firstItem=Alt+Клік для першого предмета +hei.tooltip.collapsed.collapse=Alt+Клік для згортання +hei.tooltip.config.expandCollapseAll=Alt+Клік перемкнути групи + +config.hei.collapsible=Згортувані групи +config.hei.collapsible.comment=Налаштування згортання груп інгредієнтів в один слот у списку інгредієнтів. +config.hei.collapsible.collapsibleGroupsEnabled=Увімкнути згортувані групи +config.hei.collapsible.collapsibleGroupsEnabled.comment=Коли увімкнено, групи пов'язаних інгредієнтів (зілля, зачаровані книги тощо) згортаються в один слот. Alt+Клік по групі для розгортання або згортання. +config.hei.collapsible.collapseOnClose=Згорнути при закритті +config.hei.collapsible.collapseOnClose.comment=Згортає відкриті групи при закритті HEI. +config.hei.collapsible.collapsedClickAction=Дія кліку за замовчуванням +config.hei.collapsible.collapsedClickAction.comment=Змінює дію одиничного кліку, інший варіант стає дією Alt+Клік. Відкрити групу відкриває групу. Перший предмет застосовує дію до першого предмета в групі. +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=Відкрити групу +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=Перший предмет + +# Group Management GUI +hei.gui.collapsible.title=Управління групами +hei.gui.collapsible.back=Назад +hei.gui.collapsible.newGroup=Нова група +hei.gui.collapsible.title.tooltip=Увімкнення/вимкнення вбудованих груп та створення користувацьких груп. +hei.gui.collapsible.enabled=Увімкнено +hei.gui.collapsible.disabled=Вимкнено +hei.gui.collapsible.customGroup=Користувацька +hei.gui.collapsible.defaultGroup=За замовчуванням +hei.gui.collapsible.itemCount=%d предметів +hei.gui.collapsible.editor.title=Вибрані предмети +hei.gui.collapsible.editor.name=Назва +hei.gui.collapsible.editor.save=Зберегти +hei.gui.collapsible.editor.selected=%d вибрано diff --git a/src/main/resources/assets/jei/lang/zh_cn.lang b/src/main/resources/assets/jei/lang/zh_cn.lang index eeaf67a9..ec026b90 100644 --- a/src/main/resources/assets/jei/lang/zh_cn.lang +++ b/src/main/resources/assets/jei/lang/zh_cn.lang @@ -160,4 +160,38 @@ config.jei.interface.defaultFluidContainerItem=默认流体容器 config.jei.interface.defaultFluidContainerItem.comment=当点击 HEI 界面中的流体时,用来填充流体的默认物品(格式: item_name@meta) config.jei.misc.hideBottomLeftCornerBookmarkButton=隐藏左下角书签按钮 -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=是否隐藏左下角书签按钮 \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=是否隐藏左下角书签按钮 + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+点击展开 +hei.tooltip.collapsed.expand.firstItem=Alt+点击查看第一个物品 +hei.tooltip.collapsed.collapse=Alt+点击收起 +hei.tooltip.config.expandCollapseAll=Alt+点击切换分组 + +config.hei.collapsible=可折叠分组 +config.hei.collapsible.comment=将配料分组折叠为配料列表中的单个格子的相关选项。 +config.hei.collapsible.collapsibleGroupsEnabled=启用可折叠分组 +config.hei.collapsible.collapsibleGroupsEnabled.comment=启用后,相关配料的分组(药水、附魔书等)将折叠为单个格子。Alt+点击分组以展开或折叠。 +config.hei.collapsible.collapseOnClose=关闭时折叠 +config.hei.collapsible.collapseOnClose.comment=关闭HEI时折叠当前已展开的分组。 +config.hei.collapsible.collapsedClickAction=默认点击操作 +config.hei.collapsible.collapsedClickAction.comment=切换单击操作,另一个选项变为Alt+点击操作。打开分组会展开分组。第一个物品会对分组中的第一个物品执行操作。 +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=打开分组 +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=第一个物品 + +# Group Management GUI +hei.gui.collapsible.title=管理分组 +hei.gui.collapsible.back=返回 +hei.gui.collapsible.newGroup=新建分组 +hei.gui.collapsible.title.tooltip=启用/禁用内置分组并创建用户自定义分组。 +hei.gui.collapsible.enabled=已启用 +hei.gui.collapsible.disabled=已禁用 +hei.gui.collapsible.customGroup=自定义 +hei.gui.collapsible.modGroup=Mod +hei.gui.collapsible.defaultGroup=默认 +hei.gui.collapsible.itemCount=%d 个物品 +hei.gui.collapsible.editor.title=已选物品 +hei.gui.collapsible.editor.name=名称 +hei.gui.collapsible.editor.save=保存 +hei.gui.collapsible.editor.selected=%d 已选择 +hei.gui.collapsible.confirmDelete=删除? diff --git a/src/main/resources/assets/jei/lang/zh_tw.lang b/src/main/resources/assets/jei/lang/zh_tw.lang index b7a87f34..2967625c 100644 --- a/src/main/resources/assets/jei/lang/zh_tw.lang +++ b/src/main/resources/assets/jei/lang/zh_tw.lang @@ -128,4 +128,38 @@ description.jei.wooden.door.2=Clicking on a door changes its state from open to description.jei.wooden.door.3=Wooden Doors can be opened/closed via Redstone Circuits. config.jei.misc.hideBottomLeftCornerBookmarkButton=隱藏左下角書籤按鈕 -config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=是否隱藏左下角書籤按鈕 \ No newline at end of file +config.jei.misc.hideBottomLeftCornerBookmarkButton.comment=是否隱藏左下角書籤按鈕 + +# Collapsible Groups +hei.tooltip.collapsed.expand=Alt+點擊展開 +hei.tooltip.collapsed.expand.firstItem=Alt+點擊查看第一個物品 +hei.tooltip.collapsed.collapse=Alt+點擊收合 +hei.tooltip.config.expandCollapseAll=Alt+點擊切換群組 + +config.hei.collapsible=可折疊群組 +config.hei.collapsible.comment=將材料群組折疊為材料列表中的單個格子的相關選項。 +config.hei.collapsible.collapsibleGroupsEnabled=啟用可折疊群組 +config.hei.collapsible.collapsibleGroupsEnabled.comment=啟用後,相關材料的群組(藥水、附魔書等)將折疊為單個格子。Alt+點擊群組以展開或收合。 +config.hei.collapsible.collapseOnClose=關閉時收合 +config.hei.collapsible.collapseOnClose.comment=關閉HEI時收合目前已展開的群組。 +config.hei.collapsible.collapsedClickAction=預設點擊操作 +config.hei.collapsible.collapsedClickAction.comment=切換單擊操作,另一個選項變為Alt+點擊操作。開啟群組會展開群組。第一個物品會對群組中的第一個物品執行操作。 +config.hei.collapsible.collapsedClickAction.OPEN_GROUP=開啟群組 +config.hei.collapsible.collapsedClickAction.FIRST_ITEM=第一個物品 + +# Group Management GUI +hei.gui.collapsible.title=管理群組 +hei.gui.collapsible.back=返回 +hei.gui.collapsible.newGroup=新建群組 +hei.gui.collapsible.title.tooltip=啟用/停用內建群組並建立使用者自訂群組。 +hei.gui.collapsible.enabled=已啟用 +hei.gui.collapsible.disabled=已停用 +hei.gui.collapsible.customGroup=自訂 +hei.gui.collapsible.modGroup=Mod +hei.gui.collapsible.defaultGroup=預設 +hei.gui.collapsible.itemCount=%d 個物品 +hei.gui.collapsible.editor.title=已選物品 +hei.gui.collapsible.editor.name=名稱 +hei.gui.collapsible.editor.save=儲存 +hei.gui.collapsible.editor.selected=%d 已選擇 +hei.gui.collapsible.confirmDelete=刪除?