diff --git a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java index d114ca6b8..7a20fd913 100644 --- a/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java +++ b/src/main/java/org/cyclops/integratedterminals/GeneralConfig.java @@ -92,6 +92,8 @@ public class GeneralConfig extends DummyConfig { public static int guiStorageScaleMaxColumns = 32; @ConfigurableProperty(category = "general", comment = "If the crafting grid should always be shown centrally, and not be responsive based on screen size.", isCommandable = true, configLocation = ModConfig.Type.CLIENT) public static boolean guiStorageForceCraftingGridCenter = false; + @ConfigurableProperty(category = "general", comment = "If the automatic re-sorting of the storage terminal contents should be paused while the shift key is held down.", isCommandable = true, configLocation = ModConfig.Type.CLIENT) + public static boolean guiStoragePauseSortingWhileShifting = true; public GeneralConfig() { super(IntegratedTerminals._instance, "general"); diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java index b69f4ea73..612f98ad9 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java @@ -39,6 +39,7 @@ import org.cyclops.cyclopscore.ingredient.collection.diff.IngredientCollectionDiffHelpers; import org.cyclops.integrateddynamics.api.ingredient.IIngredientComponentStorageObservable; import org.cyclops.integrateddynamics.api.network.IPositionedAddonsNetwork; +import org.cyclops.integratedterminals.GeneralConfig; import org.cyclops.integratedterminals.IntegratedTerminals; import org.cyclops.integratedterminals.api.ingredient.IIngredientComponentTerminalStorageHandler; import org.cyclops.integratedterminals.api.ingredient.IIngredientInstanceSorter; @@ -68,11 +69,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -100,6 +104,7 @@ public class TerminalStorageTabIngredientComponentClient private final Int2ObjectMap> ingredientsUnsortedViews; private final Int2ObjectMap>> filteredIngredientsViews; + private final Int2ObjectMap>> lastFilteredIngredientsViews; private final Int2ObjectMap>> craftingOptions; private final Int2LongMap maxQuantities; @@ -110,6 +115,7 @@ public class TerminalStorageTabIngredientComponentClient private int activeSlotQuantity; private int activeChannel; private int lastChangeId; + private boolean sortingPaused; @SubscribeEvent public static void onToolTip(ItemTooltipEvent event) { @@ -144,6 +150,7 @@ public TerminalStorageTabIngredientComponentClient(ContainerTerminalStorageBase this.ingredientsUnsortedViews = new Int2ObjectOpenHashMap<>(); this.filteredIngredientsViews = new Int2ObjectOpenHashMap<>(); + this.lastFilteredIngredientsViews = new Int2ObjectOpenHashMap<>(); this.craftingOptions = new Int2ObjectOpenHashMap<>(); this.maxQuantities = new Int2LongOpenHashMap(); @@ -153,6 +160,7 @@ public TerminalStorageTabIngredientComponentClient(ContainerTerminalStorageBase resetActiveSlot(); this.lastChangeId = 0; + this.sortingPaused = false; } @@ -226,7 +234,52 @@ public Predicate> getInstanceFilterMetadata() { } public void resetFilteredIngredientsViews(int channel) { + resetFilteredIngredientsViews(channel, true); + } + + /** + * Reset the filtered ingredients views of the given channel. + * @param channel A channel id. + * @param resetPausedSortingOrder If the ingredient order that is used to keep ingredient positions stable + * while sorting is paused should be forgotten as well. + * This should only be false for changes that are not caused by the user, + * as user-triggered changes should always be applied immediately. + */ + public void resetFilteredIngredientsViews(int channel, boolean resetPausedSortingOrder) { filteredIngredientsViews.remove(channel); + if (resetPausedSortingOrder) { + lastFilteredIngredientsViews.remove(channel); + } + } + + /** + * @return If the automatic re-sorting of ingredients is currently paused. + */ + public boolean isSortingPaused() { + return GeneralConfig.guiStoragePauseSortingWhileShifting && MinecraftHelpers.isShifted(); + } + + /** + * Check if sorting has been paused or resumed since the last call, + * and re-sort all ingredient views when sorting has been resumed. + * @param channel A channel id. + */ + protected void updateSortingPausedState(int channel) { + boolean sortingPaused = isSortingPaused(); + if (this.sortingPaused != sortingPaused) { + // Update the field before doing anything else, so that re-entrant calls become no-ops. + this.sortingPaused = sortingPaused; + if (!sortingPaused) { + // Remember the selected instance, as re-sorting might change its position. + Optional lastInstance = getSlotInstance(channel, this.activeSlotId); + + // Enforce a re-sorting of all views + this.filteredIngredientsViews.clear(); + this.lastFilteredIngredientsViews.clear(); + + updateActiveInstance(lastInstance, channel); + } + } } @Override @@ -282,6 +335,7 @@ public Collection getUniqueCraftingOptionOutputs(ITerminalCraftingOption c } protected List> getFilteredIngredientsView(int channel) { + updateSortingPausedState(channel); List> ingredientsView = filteredIngredientsViews.get(channel); if (ingredientsView == null) { ingredientsView = createUnfilteredIngredientsView(channel); @@ -295,21 +349,75 @@ protected List> getFilteredIngredientsView(int channel) // Sort Comparator sorter = getInstanceSorter(); - if (sorter != null) { - try { + List> pausedOrder = this.sortingPaused + ? lastFilteredIngredientsViews.get(channel) : null; + try { + if (pausedOrder != null) { + // Sorting is paused, so keep the positions of the previously shown ingredients + sortByPreviousOrder(ingredientsView, pausedOrder, sorter); + } else if (sorter != null) { ingredientsView.sort(InstanceWithMetadata.createComparator(sorter)); - } catch (IllegalArgumentException e) { - // We deliberately ignore comparison violations - // If this would cause issues, we'll need to do a deep-copy of all ingredients, which will impact performance - // See https://github.com/CyclopsMC/IntegratedTerminals/issues/119 } + } catch (IllegalArgumentException e) { + // We deliberately ignore comparison violations + // If this would cause issues, we'll need to do a deep-copy of all ingredients, which will impact performance + // See https://github.com/CyclopsMC/IntegratedTerminals/issues/119 } filteredIngredientsViews.put(channel, ingredientsView); + lastFilteredIngredientsViews.put(channel, ingredientsView); } return ingredientsView; } + /** + * Sort the given ingredients view based on the order of a previously shown ingredients view. + * + * Ingredients that were present in the previous view keep their position, independent of their quantity, + * while new ingredients are appended at the end. + * + * @param ingredientsView The ingredients view to sort in-place. + * @param previousView A previously shown ingredients view. + * @param sorter An optional sorter that is used for ordering the new ingredients. + */ + protected void sortByPreviousOrder(List> ingredientsView, + List> previousView, + @Nullable Comparator sorter) { + IIngredientMatcher matcher = this.ingredientComponent.getMatcher(); + + // Determine the position of all previously shown ingredients, while ignoring their quantities. + // Crafting options are taken into account as well, + // as an ingredient can be shown both as a stored ingredient and as a crafting option. + Map, Integer> previousPositions = new TreeMap<>( + InstanceWithMetadata.createComparator(matcher)); + int position = 0; + for (InstanceWithMetadata instanceWithMetadata : previousView) { + previousPositions.putIfAbsent(withoutQuantity(instanceWithMetadata), position++); + } + + // Assign the previous positions to the current ingredients, new ingredients are placed at the end. + Map, Integer> positions = new IdentityHashMap<>(); + for (InstanceWithMetadata instanceWithMetadata : ingredientsView) { + positions.put(instanceWithMetadata, previousPositions + .getOrDefault(withoutQuantity(instanceWithMetadata), Integer.MAX_VALUE)); + } + + ingredientsView.sort(Comparator + .>comparingInt(positions::get) + .thenComparing(InstanceWithMetadata.createComparator(sorter != null ? sorter : matcher))); + } + + /** + * Create a copy of the given ingredient with a fixed quantity, + * so that it can be used as a quantity-independent key. + * @param instanceWithMetadata An ingredient with metadata. + * @return A quantity-independent copy. + */ + protected InstanceWithMetadata withoutQuantity(InstanceWithMetadata instanceWithMetadata) { + return new InstanceWithMetadata<>(this.ingredientComponent.getMatcher() + .withQuantity(instanceWithMetadata.getInstance(), 1), instanceWithMetadata.getCraftingOption()); + } + protected Stream> transformIngredientsView(Stream> ingredientStream) { return ingredientStream; } @@ -417,7 +525,7 @@ public synchronized void onChange(int channel, IIngredientComponentStorageObserv IngredientCollectionDiffHelpers.applyDiff(ingredientComponent, diff, rawPersistedIngredients); // Persist changes - resetFilteredIngredientsViews(channel); + resetFilteredIngredientsViews(channel, false); // Update the active instance by searching for its new position in the slots // If this becomes a performance bottleneck, we could search _around_ the previous position. @@ -463,7 +571,7 @@ public synchronized void addCraftingOptions(int channel, List