소스 검색

Semi working favorites

shedaniel 5 년 전
부모
커밋
e1a95642af

+ 4 - 4
gradle.properties

@@ -1,9 +1,9 @@
-mod_version=3.2.17
-minecraft_version=1.15-pre6
-yarn_version=1.15-pre6+build.1
+mod_version=3.2.18
+minecraft_version=1.15
+yarn_version=1.15+build.1
 fabricloader_version=0.7.2+build.174
 cloth_events_version=1.0.1-unstable.201911010702
-cloth_config_version=2.4-unstable.201911031154
+cloth_config_version=2.5.1
 modmenu_version=1.8.0+build.16
 fabric_api=0.4.20+build.273-1.15
 autoconfig1u=1.2.4

+ 18 - 18
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java

@@ -141,6 +141,24 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
         return ClientSidePacketRegistry.INSTANCE.canServerReceive(RoughlyEnoughItemsNetwork.CREATE_ITEMS_PACKET) && ClientSidePacketRegistry.INSTANCE.canServerReceive(RoughlyEnoughItemsNetwork.DELETE_ITEMS_PACKET);
     }
     
+    @Internal
+    @Deprecated
+    public static void syncRecipes(AtomicLong lastSync) {
+        if (lastSync != null) {
+            if (lastSync.get() > 0 && System.currentTimeMillis() - lastSync.get() <= 5000) {
+                RoughlyEnoughItemsCore.LOGGER.warn("[REI] Suppressing Sync Recipes!");
+                return;
+            }
+            lastSync.set(System.currentTimeMillis());
+        }
+        RecipeManager recipeManager = MinecraftClient.getInstance().getNetworkHandler().getRecipeManager();
+        if (ConfigManager.getInstance().getConfig().doesRegisterRecipesInAnotherThread()) {
+            CompletableFuture.runAsync(() -> ((RecipeHelperImpl) RecipeHelper.getInstance()).recipesLoaded(recipeManager), SYNC_RECIPES);
+        } else {
+            ((RecipeHelperImpl) RecipeHelper.getInstance()).recipesLoaded(recipeManager);
+        }
+    }
+    
     @SuppressWarnings("deprecation")
     @Override
     public void onInitializeClient() {
@@ -221,24 +239,6 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
         }
     }
     
-    @Internal
-    @Deprecated
-    public static void syncRecipes(AtomicLong lastSync) {
-        if (lastSync != null) {
-            if (lastSync.get() > 0 && System.currentTimeMillis() - lastSync.get() <= 5000) {
-                RoughlyEnoughItemsCore.LOGGER.warn("[REI] Suppressing Sync Recipes!");
-                return;
-            }
-            lastSync.set(System.currentTimeMillis());
-        }
-        RecipeManager recipeManager = MinecraftClient.getInstance().getNetworkHandler().getRecipeManager();
-        if (ConfigManager.getInstance().getConfig().doesRegisterRecipesInAnotherThread()) {
-            CompletableFuture.runAsync(() -> ((RecipeHelperImpl) RecipeHelper.getInstance()).recipesLoaded(recipeManager), SYNC_RECIPES);
-        } else {
-            ((RecipeHelperImpl) RecipeHelper.getInstance()).recipesLoaded(recipeManager);
-        }
-    }
-    
     @SuppressWarnings("deprecation")
     private void registerClothEvents() {
         final Identifier recipeButtonTex = new Identifier("textures/gui/recipe_button.png");

+ 2 - 0
src/main/java/me/shedaniel/rei/api/ClientHelper.java

@@ -132,6 +132,8 @@ public interface ClientHelper {
      */
     String getModFromIdentifier(Identifier identifier);
     
+    FabricKeyBinding[] getREIKeyBindings();
+    
     /**
      * @return the recipe keybind, defaulted R
      */

+ 3 - 1
src/main/java/me/shedaniel/rei/api/ConfigManager.java

@@ -8,7 +8,7 @@ package me.shedaniel.rei.api;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import net.minecraft.client.gui.screen.Screen;
 
-import java.io.IOException;
+import java.util.List;
 
 public interface ConfigManager {
     
@@ -17,6 +17,8 @@ public interface ConfigManager {
         return RoughlyEnoughItemsCore.getConfigManager();
     }
     
+    List<EntryStack> getFavorites();
+    
     /**
      * Saves the config.
      */

+ 10 - 0
src/main/java/me/shedaniel/rei/api/ConfigObject.java

@@ -9,6 +9,7 @@ import me.shedaniel.rei.gui.config.ItemCheatingMode;
 import me.shedaniel.rei.gui.config.ItemListOrdering;
 import me.shedaniel.rei.gui.config.RecipeScreenType;
 import me.shedaniel.rei.gui.config.SearchFieldLocation;
+import net.minecraft.client.util.InputUtil;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -81,6 +82,15 @@ public interface ConfigObject {
     
     boolean doesSnapToRows();
     
+    boolean isFavoritesEnabled();
+    
+    InputUtil.KeyCode getFavoriteKeybind();
+    
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.FIELD})
+    public @interface AddInFrontKeyCode {
+    }
+    
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.FIELD})
     public @interface DontApplyFieldName {

+ 52 - 0
src/main/java/me/shedaniel/rei/api/EntryStack.java

@@ -5,6 +5,8 @@
 
 package me.shedaniel.rei.api;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.gui.widget.QueuedTooltip;
 import me.shedaniel.rei.impl.EmptyEntryStack;
@@ -15,7 +17,10 @@ import net.minecraft.fluid.Fluid;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemConvertible;
 import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.StringNbtReader;
 import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.Registry;
 
 import javax.annotation.Nullable;
 import java.util.Collections;
@@ -47,6 +52,53 @@ public interface EntryStack {
         return new ItemEntryStack(new ItemStack(item));
     }
     
+    static EntryStack readFromJson(JsonElement jsonElement) {
+        try {
+            JsonObject obj = jsonElement.getAsJsonObject();
+            switch (obj.get("type").getAsString()) {
+                case "stack":
+                    return EntryStack.create(ItemStack.fromTag(StringNbtReader.parse(obj.get("nbt").getAsString())));
+                case "fluid":
+                    return EntryStack.create(Registry.FLUID.get(Identifier.tryParse(obj.get("id").getAsString())));
+                case "empty":
+                    return EntryStack.empty();
+                default:
+                    throw new IllegalArgumentException("Invalid Entry Type!");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return EntryStack.empty();
+        }
+    }
+    
+    @Nullable
+    default JsonElement toJson() {
+        try {
+            switch (getType()) {
+                case ITEM:
+                    JsonObject obj1 = new JsonObject();
+                    obj1.addProperty("type", "stack");
+                    obj1.addProperty("nbt", getItemStack().toTag(new CompoundTag()).toString());
+                    return obj1;
+                case FLUID:
+                    JsonObject obj2 = new JsonObject();
+                    obj2.addProperty("type", "fluid");
+                    obj2.addProperty("id", getIdentifier().get().toString());
+                    return obj2;
+                case RENDER:
+                case EMPTY:
+                    JsonObject obj3 = new JsonObject();
+                    obj3.addProperty("type", "empty");
+                    return obj3;
+                default:
+                    throw new IllegalArgumentException("Invalid Entry Type!");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+    
     Optional<Identifier> getIdentifier();
     
     EntryStack.Type getType();

+ 8 - 8
src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -23,7 +23,6 @@ import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.screen.ingame.AbstractContainerScreen;
 import net.minecraft.client.render.DiffuseLighting;
-import net.minecraft.client.render.DiffuseLighting;
 import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexConsumerProvider;
 import net.minecraft.client.resource.language.I18n;
@@ -89,12 +88,12 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
         return ENTRY_LIST_WIDGET;
     }
     
-    public void init() {
-        init(false);
+    public void init(boolean useless) {
+        init();
     }
     
     @SuppressWarnings("deprecation")
-    public void init(boolean setPage) {
+    public void init() {
         this.shouldReInit = false;
         //Update Variables
         this.children().clear();
@@ -103,9 +102,10 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
         DisplayHelper.DisplayBoundsHandler boundsHandler = DisplayHelper.getInstance().getResponsibleBoundsHandler(MinecraftClient.getInstance().currentScreen.getClass());
         this.rectangle = ConfigManager.getInstance().getConfig().isLeftHandSidePanel() ? boundsHandler.getLeftBounds(MinecraftClient.getInstance().currentScreen) : boundsHandler.getRightBounds(MinecraftClient.getInstance().currentScreen);
         widgets.add(ENTRY_LIST_WIDGET);
-        if (ScreenHelper.getSearchField() == null)
+        ENTRY_LIST_WIDGET.updateArea(boundsHandler, ScreenHelper.getSearchField() == null ? "" : null);
+        if (ScreenHelper.getSearchField() == null) {
             ScreenHelper.setSearchField(new OverlaySearchField(0, 0, 0, 0));
-        ENTRY_LIST_WIDGET.updateArea(boundsHandler, ScreenHelper.getSearchField().getText());
+        }
         ScreenHelper.getSearchField().getBounds().setBounds(getTextFieldArea());
         this.widgets.add(ScreenHelper.getSearchField());
         ScreenHelper.getSearchField().setChangedListener(s -> {
@@ -414,11 +414,11 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     public void render(int mouseX, int mouseY, float delta) {
         List<ItemStack> currentStacks = ClientHelper.getInstance().getInventoryItemsTypes();
         if (shouldReInit)
-            init(true);
+            init();
         else {
             for (DisplayHelper.DisplayBoundsHandler<?> handler : DisplayHelper.getInstance().getSortedBoundsHandlers(minecraft.currentScreen.getClass())) {
                 if (handler != null && handler.shouldRecalculateArea(!ConfigManager.getInstance().getConfig().isLeftHandSidePanel(), rectangle)) {
-                    init(true);
+                    init();
                     break;
                 }
             }

+ 152 - 26
src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java

@@ -14,6 +14,7 @@ import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.config.ItemCheatingMode;
 import me.shedaniel.rei.gui.config.ItemListOrdering;
 import me.shedaniel.rei.impl.ScreenHelper;
@@ -24,6 +25,8 @@ import net.minecraft.client.render.BufferBuilder;
 import net.minecraft.client.render.DiffuseLighting;
 import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.InputUtil;
 import net.minecraft.item.ItemGroup;
 import net.minecraft.util.ActionResult;
 import net.minecraft.util.Identifier;
@@ -31,10 +34,7 @@ import net.minecraft.util.math.MathHelper;
 import org.apache.commons.lang3.StringUtils;
 
 import javax.annotation.Nullable;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Locale;
+import java.util.*;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -60,13 +60,26 @@ public class EntryListWidget extends WidgetWithBounds {
     protected int blockedCount;
     private Rectangle bounds, innerBounds;
     private List<EntryStack> allStacks = null;
+    private List<EntryStack> favorites = null;
     private List<EntryListEntry> entries = Collections.emptyList();
     @SuppressWarnings("deprecation")
     private List<SearchArgument.SearchArguments> lastSearchArguments = Collections.emptyList();
     private boolean draggingScrollBar = false;
     
+    protected final int getSlotsHeightNumberForFavorites() {
+        if (favorites.isEmpty()) return 0;
+        return MathHelper.ceil(2 + favorites.size() / (innerBounds.width / 18f));
+    }
+    
+    protected final int getScrollNumberForFavorites() {
+        if (favorites.isEmpty()) return 0;
+        return (innerBounds.width / 18) * getSlotsHeightNumberForFavorites();
+    }
+    
     protected final int getMaxScrollPosition() {
-        return MathHelper.ceil((allStacks.size() + blockedCount) / (innerBounds.width / 18f)) * 18;
+        if (favorites.isEmpty())
+            return MathHelper.ceil((allStacks.size() + blockedCount) / (innerBounds.width / 18f)) * 18;
+        return MathHelper.ceil((allStacks.size() + blockedCount + getScrollNumberForFavorites()) / (innerBounds.width / 18f)) * 18 - 12;
     }
     
     protected final int getMaxScroll() {
@@ -139,20 +152,48 @@ public class EntryListWidget extends WidgetWithBounds {
         if (ConfigManager.getInstance().getConfig().isEntryListWidgetScrolled()) {
             for (EntryListEntry entry : entries) entry.clearStacks();
             ScissorsHandler.INSTANCE.scissor(bounds);
-            int skip = Math.max(0, MathHelper.floor(scroll / 18f));
+            int sizeForFavorites = getSlotsHeightNumberForFavorites();
+            int skip = Math.max(0, MathHelper.floor(scroll / 18f) - sizeForFavorites);
             int nextIndex = skip * innerBounds.width / 18;
             int i = nextIndex;
             blockedCount = 0;
+            if (sizeForFavorites > 0) {
+                drawString(font, I18n.translate("text.rei.favorites"), innerBounds.x + 2, (int) (innerBounds.y + 6 - scroll), -1);
+                nextIndex += innerBounds.width / 18;
+                for (int i1 = 0; i1 < favorites.size(); i1++) {
+                    EntryStack stack = favorites.get(i1);
+                    back1:
+                    while (true) {
+                        EntryListEntry entry = entries.get(nextIndex);
+                        entry.getBounds().y = (int) (entry.backupY - scroll);
+                        if (entry.getBounds().y > bounds.getMaxY())
+                            break back1;
+                        if (notSteppingOnExclusionZones(entry.getBounds().x, entry.getBounds().y, innerBounds)) {
+                            entry.entry(stack);
+                            entry.isFavorites = true;
+                            entry.render(mouseX, mouseY, delta);
+                            nextIndex++;
+                            break;
+                        } else {
+                            blockedCount++;
+                            nextIndex++;
+                        }
+                    }
+                }
+                nextIndex += innerBounds.width / -18 + getScrollNumberForFavorites() - favorites.size();
+            }
+            int offset = sizeForFavorites > 0 ? -12 : 0;
             back:
             for (; i < allStacks.size(); i++) {
                 EntryStack stack = allStacks.get(i);
                 while (true) {
                     EntryListEntry entry = entries.get(nextIndex);
-                    entry.getBounds().y = (int) (entry.backupY - scroll);
+                    entry.getBounds().y = (int) (entry.backupY - scroll + offset);
                     if (entry.getBounds().y > bounds.getMaxY())
                         break back;
                     if (notSteppingOnExclusionZones(entry.getBounds().x, entry.getBounds().y, innerBounds)) {
                         entry.entry(stack);
+                        entry.isFavorites = false;
                         entry.render(mouseX, mouseY, delta);
                         nextIndex++;
                         break;
@@ -166,7 +207,8 @@ public class EntryListWidget extends WidgetWithBounds {
             ScissorsHandler.INSTANCE.removeLastScissor();
             renderScrollbar();
         } else {
-            for (Widget widget : entries) {
+            for (EntryListEntry widget : entries) {
+                widget.isFavorites = false;
                 widget.render(mouseX, mouseY, delta);
             }
         }
@@ -272,7 +314,7 @@ public class EntryListWidget extends WidgetWithBounds {
         this.bounds = boundsHandler.getItemListArea(ScreenHelper.getLastOverlay().getBounds());
         if (searchTerm != null)
             updateSearch(searchTerm);
-        else if (allStacks == null)
+        else if (allStacks == null || favorites == null)
             updateSearch("");
         else updateEntriesPosition();
     }
@@ -302,7 +344,8 @@ public class EntryListWidget extends WidgetWithBounds {
             page = 0;
             int width = innerBounds.width / 18;
             int pageHeight = innerBounds.height / 18;
-            int slotsToPrepare = allStacks.size();
+            int sizeForFavorites = getScrollNumberForFavorites();
+            int slotsToPrepare = allStacks.size() + sizeForFavorites;
             int currentX = 0;
             int currentY = 0;
             List<EntryListEntry> entries = Lists.newLinkedList();
@@ -350,22 +393,42 @@ public class EntryListWidget extends WidgetWithBounds {
     @SuppressWarnings("deprecation")
     public void updateSearch(String searchTerm) {
         lastSearchArguments = processSearchTerm(searchTerm);
-        List<EntryStack> list = Lists.newLinkedList();
-        boolean checkCraftable = ConfigManager.getInstance().isCraftableOnlyEnabled() && !ScreenHelper.inventoryStacks.isEmpty();
-        List<EntryStack> workingItems = checkCraftable ? RecipeHelper.getInstance().findCraftableEntriesByItems(CollectionUtils.map(ScreenHelper.inventoryStacks, EntryStack::create)) : null;
-        for (EntryStack stack : EntryRegistry.getInstance().getStacksList()) {
-            if (lastSearchArguments.isEmpty() || canSearchTermsBeAppliedTo(stack, lastSearchArguments)) {
-                if (workingItems != null && CollectionUtils.findFirstOrNullEquals(workingItems, stack) == null)
-                    continue;
-                list.add(stack.copy().setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE)
-                        .setting(EntryStack.Settings.Item.RENDER_OVERLAY, RENDER_EXTRA_CONFIG));
+        {
+            List<EntryStack> list = Lists.newLinkedList();
+            boolean checkCraftable = ConfigManager.getInstance().isCraftableOnlyEnabled() && !ScreenHelper.inventoryStacks.isEmpty();
+            List<EntryStack> workingItems = checkCraftable ? RecipeHelper.getInstance().findCraftableEntriesByItems(CollectionUtils.map(ScreenHelper.inventoryStacks, EntryStack::create)) : null;
+            for (EntryStack stack : EntryRegistry.getInstance().getStacksList()) {
+                if (lastSearchArguments.isEmpty() || canSearchTermsBeAppliedTo(stack, lastSearchArguments)) {
+                    if (workingItems != null && CollectionUtils.findFirstOrNullEquals(workingItems, stack) == null)
+                        continue;
+                    list.add(stack.copy().setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE)
+                            .setting(EntryStack.Settings.Item.RENDER_OVERLAY, RENDER_EXTRA_CONFIG));
+                }
             }
+            ItemListOrdering ordering = ConfigManager.getInstance().getConfig().getItemListOrdering();
+            if (ordering == ItemListOrdering.name) list.sort(ENTRY_NAME_COMPARER);
+            if (ordering == ItemListOrdering.item_groups) list.sort(ENTRY_GROUP_COMPARER);
+            if (!ConfigManager.getInstance().getConfig().isItemListAscending()) Collections.reverse(list);
+            allStacks = list;
         }
-        ItemListOrdering ordering = ConfigManager.getInstance().getConfig().getItemListOrdering();
-        if (ordering == ItemListOrdering.name) list.sort(ENTRY_NAME_COMPARER);
-        if (ordering == ItemListOrdering.item_groups) list.sort(ENTRY_GROUP_COMPARER);
-        if (!ConfigManager.getInstance().getConfig().isItemListAscending()) Collections.reverse(list);
-        allStacks = list;
+        if (ConfigManager.getInstance().getConfig().isFavoritesEnabled()) {
+            List<EntryStack> list = Lists.newLinkedList();
+            boolean checkCraftable = ConfigManager.getInstance().isCraftableOnlyEnabled() && !ScreenHelper.inventoryStacks.isEmpty();
+            List<EntryStack> workingItems = checkCraftable ? RecipeHelper.getInstance().findCraftableEntriesByItems(CollectionUtils.map(ScreenHelper.inventoryStacks, EntryStack::create)) : null;
+            for (EntryStack stack : ConfigManager.getInstance().getFavorites()) {
+                if (lastSearchArguments.isEmpty() || canSearchTermsBeAppliedTo(stack, lastSearchArguments)) {
+                    if (workingItems != null && CollectionUtils.findFirstOrNullEquals(workingItems, stack) == null)
+                        continue;
+                    list.add(stack.copy().setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE)
+                            .setting(EntryStack.Settings.Item.RENDER_OVERLAY, RENDER_EXTRA_CONFIG));
+                }
+            }
+            ItemListOrdering ordering = ConfigManager.getInstance().getConfig().getItemListOrdering();
+            if (ordering == ItemListOrdering.name) list.sort(ENTRY_NAME_COMPARER);
+            if (ordering == ItemListOrdering.item_groups) list.sort(ENTRY_GROUP_COMPARER);
+            if (!ConfigManager.getInstance().getConfig().isItemListAscending()) Collections.reverse(list);
+            favorites = list;
+        } else favorites = Collections.emptyList();
         updateEntriesPosition();
     }
     
@@ -475,6 +538,7 @@ public class EntryListWidget extends WidgetWithBounds {
     
     private class EntryListEntry extends EntryWidget {
         private int backupY;
+        private boolean isFavorites;
         
         private EntryListEntry(int x, int y) {
             super(x, y);
@@ -491,10 +555,72 @@ public class EntryListWidget extends WidgetWithBounds {
             if (getCurrentEntry().getType() != EntryStack.Type.EMPTY) super.drawHighlighted(mouseX, mouseY, delta);
         }
         
+        private String getLocalizedName(InputUtil.KeyCode value) {
+            String string_1 = value.getName();
+            int int_1 = value.getKeyCode();
+            String string_2 = null;
+            switch (value.getCategory()) {
+                case KEYSYM:
+                    string_2 = InputUtil.getKeycodeName(int_1);
+                    break;
+                case SCANCODE:
+                    string_2 = InputUtil.getScancodeName(int_1);
+                    break;
+                case MOUSE:
+                    String string_3 = I18n.translate(string_1, new Object[0]);
+                    string_2 = Objects.equals(string_3, string_1) ? I18n.translate(InputUtil.Type.MOUSE.getName(), new Object[]{int_1 + 1}) : string_3;
+            }
+            
+            return string_2 == null ? I18n.translate(string_1, new Object[0]) : string_2;
+        }
+        
         @Override
         protected void queueTooltip(int mouseX, int mouseY, float delta) {
-            if (!ClientHelper.getInstance().isCheating() || minecraft.player.inventory.getCursorStack().isEmpty())
-                super.queueTooltip(mouseX, mouseY, delta);
+            if (!ClientHelper.getInstance().isCheating() || minecraft.player.inventory.getCursorStack().isEmpty()) {
+                QueuedTooltip tooltip = getCurrentTooltip(mouseX, mouseY);
+                if (tooltip != null) {
+                    // TODO Finalize favorites
+//                    if (ConfigManager.getInstance().getConfig().isFavoritesEnabled()) {
+//                        String name = getLocalizedName(ConfigManager.getInstance().getConfig().getFavoriteKeybind());
+//                        if (!isFavorites)
+//                            tooltip.getText().addAll(Arrays.asList(I18n.translate("text.rei.favorites_tooltip", name).split("\n")));
+//                        else
+//                            tooltip.getText().addAll(Arrays.asList(I18n.translate("text.rei.remove_favorites_tooltip", name).split("\n")));
+//                    }
+                    ScreenHelper.getLastOverlay().addTooltip(tooltip);
+                }
+            }
+        }
+        
+        @Override
+        public boolean keyPressed(int int_1, int int_2, int int_3) {
+            if (!interactable)
+                return false;
+            if (containsMouse(PointHelper.fromMouse()) && !getCurrentEntry().isEmpty()) {
+                InputUtil.KeyCode keyCode = ConfigManager.getInstance().getConfig().getFavoriteKeybind();
+                if (int_1 == InputUtil.UNKNOWN_KEYCODE.getKeyCode()) {
+                    if (keyCode.getCategory() == InputUtil.Type.SCANCODE && keyCode.getKeyCode() == int_2) {
+                        if (!isFavorites) {
+                            ConfigManager.getInstance().getFavorites().add(getCurrentEntry().copy());
+                            ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText());
+                        } else {
+                            ConfigManager.getInstance().getFavorites().remove(getCurrentEntry());
+                            ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText());
+                        }
+                        return true;
+                    }
+                } else if (keyCode.getCategory() == InputUtil.Type.KEYSYM && keyCode.getKeyCode() == int_1) {
+                    if (!isFavorites) {
+                        ConfigManager.getInstance().getFavorites().add(getCurrentEntry().copy());
+                        ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText());
+                    } else {
+                        ConfigManager.getInstance().getFavorites().remove(getCurrentEntry());
+                        ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText());
+                    }
+                    return true;
+                }
+            }
+            return super.keyPressed(int_1, int_2, int_3);
         }
         
         @Override

+ 1 - 1
src/main/java/me/shedaniel/rei/gui/widget/QueuedTooltip.java

@@ -22,7 +22,7 @@ public class QueuedTooltip {
     
     private QueuedTooltip(Point location, List<String> text) {
         this.location = location;
-        this.text = Collections.unmodifiableList(text);
+        this.text = Lists.newArrayList(text);
     }
     
     public static QueuedTooltip create(Point location, List<String> text) {

+ 8 - 0
src/main/java/me/shedaniel/rei/impl/ClientHelperImpl.java

@@ -54,6 +54,7 @@ public class ClientHelperImpl implements ClientHelper, ClientModInitializer {
     private final Identifier nextPageKeybind = new Identifier("roughlyenoughitems", "next_page");
     private final Identifier focusSearchFieldKeybind = new Identifier("roughlyenoughitems", "focus_search");
     private final Identifier copyRecipeIdentifierKeybind = new Identifier("roughlyenoughitems", "copy_recipe_id");
+    private final Identifier favoriteEntryKeybind = new Identifier("roughlyenoughitems", "favorite_entry");
     private final Map<String, String> modNameCache = Maps.newHashMap();
     public FabricKeyBinding recipe, usage, hide, previousPage, nextPage, focusSearchField, copyRecipeIdentifier;
     
@@ -73,6 +74,13 @@ public class ClientHelperImpl implements ClientHelper, ClientModInitializer {
         return Formatting.BLUE.toString() + Formatting.ITALIC.toString() + mod;
     }
     
+    @Override
+    public FabricKeyBinding[] getREIKeyBindings() {
+        return new FabricKeyBinding[]{
+                recipe, usage, hide, previousPage, nextPage, focusSearchField, copyRecipeIdentifier
+        };
+    }
+    
     @Override
     public FabricKeyBinding getRecipeKeyBinding() {
         return recipe;

+ 61 - 4
src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java

@@ -5,26 +5,35 @@
 
 package me.shedaniel.rei.impl;
 
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
 import me.sargunvohra.mcmods.autoconfig1u.AutoConfig;
 import me.sargunvohra.mcmods.autoconfig1u.gui.ConfigScreenProvider;
 import me.sargunvohra.mcmods.autoconfig1u.gui.registry.GuiRegistry;
 import me.sargunvohra.mcmods.autoconfig1u.serializer.JanksonConfigSerializer;
+import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.Jankson;
+import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.JsonPrimitive;
 import me.shedaniel.cloth.hooks.ScreenHooks;
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
+import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
-import me.shedaniel.rei.api.ConfigManager;
-import me.shedaniel.rei.api.ConfigObject;
-import me.shedaniel.rei.api.RecipeHelper;
+import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.api.annotations.Internal;
 import me.shedaniel.rei.gui.ConfigReloadingScreen;
+import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.credits.CreditsScreen;
 import me.shedaniel.rei.gui.widget.ReloadConfigButtonWidget;
+import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.widget.AbstractPressableButtonWidget;
 import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.InputUtil;
 import net.minecraft.text.LiteralText;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -36,10 +45,18 @@ import static me.sargunvohra.mcmods.autoconfig1u.util.Utils.setUnsafely;
 public class ConfigManagerImpl implements ConfigManager {
     
     private boolean craftableOnly;
+    private List<EntryStack> favorites = new ArrayList<>();
     
     public ConfigManagerImpl() {
         this.craftableOnly = false;
-        AutoConfig.register(ConfigObjectImpl.class, JanksonConfigSerializer::new);
+        AutoConfig.register(ConfigObjectImpl.class, (definition, configClass) -> {
+            return new JanksonConfigSerializer<ConfigObjectImpl>(definition, configClass, Jankson.builder()
+                    .registerPrimitiveTypeAdapter(InputUtil.KeyCode.class, it -> {
+                        return it instanceof String ? InputUtil.fromName((String) it) : null;
+                    })
+                    .registerSerializer(InputUtil.KeyCode.class, (it, marshaller) -> new JsonPrimitive(it.getName()))
+                    .build());
+        });
         GuiRegistry guiRegistry = AutoConfig.getGuiRegistry(ConfigObjectImpl.class);
         //noinspection rawtypes
         guiRegistry.registerAnnotationProvider((i13n, field, config, defaults, guiProvider) -> Collections.singletonList(
@@ -48,11 +65,48 @@ public class ConfigManagerImpl implements ConfigManager {
                         .setSaveConsumer(newValue -> setUnsafely(field, config, newValue))
                         .build()
         ), field -> field.getType().isEnum(), ConfigObject.UseEnumSelectorInstead.class);
+        loadFavoredEntries();
+        guiRegistry.registerAnnotationProvider((i13n, field, config, defaults, guiProvider) -> {
+            @SuppressWarnings("rawtypes") List<AbstractConfigListEntry> entries = new ArrayList<>();
+            for (FabricKeyBinding binding : ClientHelper.getInstance().getREIKeyBindings()) {
+                entries.add(ConfigEntryBuilder.create().fillKeybindingField(I18n.translate(binding.getId()) + ":", binding).build());
+            }
+            KeyCodeEntry entry = ConfigEntryBuilder.create().startKeyCodeField(i13n, getUnsafely(field, config, null))
+                    .setDefaultValue(() -> getUnsafely(field, defaults))
+                    .setSaveConsumer(newValue -> setUnsafely(field, config, newValue))
+                    .build();
+            entry.setAllowMouse(false);
+            entries.add(entry);
+            return entries;
+        }, field -> field.getType() == InputUtil.KeyCode.class, ConfigObject.AddInFrontKeyCode.class);
+        loadFavoredEntries();
         RoughlyEnoughItemsCore.LOGGER.info("[REI] Config is loaded.");
     }
     
+    @Override
+    public List<EntryStack> getFavorites() {
+        return favorites;
+    }
+    
+    public void loadFavoredEntries() {
+        favorites.clear();
+        Gson gson = new GsonBuilder().create();
+        for (String entry : ((ConfigObjectImpl) getConfig()).general.favorites) {
+            EntryStack stack = EntryStack.readFromJson(gson.fromJson(entry, JsonElement.class));
+            if (!stack.isEmpty()) favorites.add(stack);
+        }
+        saveConfig();
+    }
+    
     @Override
     public void saveConfig() {
+        Gson gson = new GsonBuilder().create();
+        ConfigObjectImpl object = (ConfigObjectImpl) getConfig();
+        object.general.favorites.clear();
+        for (EntryStack stack : favorites) {
+            JsonElement element = stack.toJson();
+            if (element != null) object.general.favorites.add(gson.toJson(element));
+        }
         ((me.sargunvohra.mcmods.autoconfig1u.ConfigManager<ConfigObjectImpl>) AutoConfig.getConfigHolder(ConfigObjectImpl.class)).save();
     }
     
@@ -103,6 +157,9 @@ public class ConfigManagerImpl implements ConfigManager {
                             MinecraftClient.getInstance().openScreen(new CreditsScreen(screen));
                         }
                     });
+                }).setSavingRunnable(() -> {
+                    saveConfig();
+                    ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText());
                 }).build();
             });
             return provider.get();

+ 20 - 4
src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java

@@ -12,6 +12,10 @@ import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.Comment;
 import me.shedaniel.rei.api.ConfigObject;
 import me.shedaniel.rei.api.annotations.Internal;
 import me.shedaniel.rei.gui.config.*;
+import net.minecraft.client.util.InputUtil;
+
+import java.util.ArrayList;
+import java.util.List;
 
 @Deprecated
 @Internal
@@ -21,18 +25,15 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     @ConfigEntry.Category("!general")
     @ConfigEntry.Gui.TransitiveObject
     @DontApplyFieldName
-    private General general = new General();
-    
+    public General general = new General();
     @ConfigEntry.Category("appearance")
     @ConfigEntry.Gui.TransitiveObject
     @DontApplyFieldName
     private Appearance appearance = new Appearance();
-    
     @ConfigEntry.Category("modules")
     @ConfigEntry.Gui.TransitiveObject
     @DontApplyFieldName
     private Modules modules = new Modules();
-    
     @ConfigEntry.Category("technical")
     @ConfigEntry.Gui.TransitiveObject
     @DontApplyFieldName
@@ -198,12 +199,27 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         return appearance.snapToRows;
     }
     
+    @Override
+    public boolean isFavoritesEnabled() {
+        return general.favoritesEnabled;
+    }
+    
+    @Override
+    public InputUtil.KeyCode getFavoriteKeybind() {
+        return general.favoriteKeybind;
+    }
+    
     public static class General {
+        @ConfigEntry.Gui.Excluded
+        public List<String> favorites = new ArrayList<>();
         @Comment("Declares whether cheating mode is on.")
         private boolean cheating = false;
         @Comment("Declares whether REI is visible.")
         @ConfigEntry.Gui.Excluded
         private boolean overlayVisible = true;
+        private boolean favoritesEnabled = true;
+        @AddInFrontKeyCode
+        private InputUtil.KeyCode favoriteKeybind = InputUtil.Type.KEYSYM.createFromCode(65);
     }
     
     public static class Appearance {

+ 1 - 1
src/main/java/me/shedaniel/rei/impl/ScreenHelper.java

@@ -85,7 +85,7 @@ public class ScreenHelper implements ClientModInitializer {
     public static ContainerScreenOverlay getLastOverlay(boolean reset, boolean setPage) {
         if (overlay == null || reset) {
             overlay = new ContainerScreenOverlay();
-            overlay.init(setPage);
+            overlay.init();
         }
         return overlay;
     }

+ 13 - 0
src/main/java/me/shedaniel/rei/utils/ExecutorUtil.java

@@ -0,0 +1,13 @@
+package me.shedaniel.rei.utils;
+
+import java.util.function.Supplier;
+
+@Deprecated
+public class ExecutorUtil {
+    private ExecutorUtil() {
+    }
+    
+    public static void execute(Supplier<Runnable> runnableSupplier) {
+        runnableSupplier.get().run();
+    }
+}

+ 5 - 0
src/main/resources/assets/roughlyenoughitems/lang/en_us.json

@@ -77,6 +77,9 @@
   "text.rei.next_page": "Next Page",
   "text.rei.back": "Back",
   "text.rei.select": "Select",
+  "text.rei.favorites": "Favorites",
+  "text.rei.favorites_tooltip": " \n§7Press %s to add this to favorites.",
+  "text.rei.remove_favorites_tooltip": " \n§7Press %s to remove this from favorites.",
   "text.rei.working_station": "Working Station",
   "text.rei.recipe_id": "\n%sRecipe Id: %s",
   "text.rei.recipe_screen_type.selection": "Recipe Screen Type Selection",
@@ -92,6 +95,8 @@
   "config.roughlyenoughitems.modules": "Modules",
   "config.roughlyenoughitems.technical": "Technical",
   "config.roughlyenoughitems.cheating": "Cheating:",
+  "config.roughlyenoughitems.favoriteKeybind": "Favorite Entry:",
+  "config.roughlyenoughitems.favoritesEnabled": "Favorites Enabled:",
   "config.roughlyenoughitems.clickableRecipeArrows": "Clickable Recipe Arrows:",
   "config.roughlyenoughitems.clickableRecipeArrows.boolean.true": "Enabled",
   "config.roughlyenoughitems.clickableRecipeArrows.boolean.false": "Disabled",