From 31e3604e9d9f6f402deaa89721bdc9527f5afabd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 17:06:29 +0000 Subject: [PATCH 1/2] Pause sorting in the storage terminal while shift is held While the shift key is held down, ingredients keep the position they had in the storage terminal, so that quantity changes caused by shift-clicking items in or out don't move them around anymore. Ingredients that are new since sorting was paused are appended at the end. As soon as shift is released, the regular sorting is applied again. This can be disabled with the new guiStoragePauseSortingWhileShifting client config option. Closes #154 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01GuGBvU7WNr6XhwVxbGdLmY --- .../integratedterminals/GeneralConfig.java | 2 + ...alStorageTabIngredientComponentClient.java | 105 +++++++++++++++++- .../button/TerminalButtonFilterCrafting.java | 1 + .../button/TerminalButtonSort.java | 1 + 4 files changed, 103 insertions(+), 6 deletions(-) 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..1bdf73eef 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; } @@ -229,11 +237,54 @@ public void resetFilteredIngredientsViews(int channel) { filteredIngredientsViews.remove(channel); } + /** + * Forget the ingredient order that is used to keep ingredient positions stable while sorting is paused. + * + * This should be called when the user explicitly changes the way ingredients are shown, + * as those changes should always be applied immediately. + * + * @param channel A channel id. + */ + public void resetPausedSortingOrder(int channel) { + 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 public void setInstanceFilter(int channel, String filter) { TerminalStorageTabClientSearchFieldUpdateEvent event = new TerminalStorageTabClientSearchFieldUpdateEvent(this, filter); MinecraftForge.EVENT_BUS.post(event); filter = event.getSearchString(); + resetPausedSortingOrder(channel); resetFilteredIngredientsViews(channel); container.getGuiState().setSearch(getTabSettingsName().toString(), channel, filter.toLowerCase(Locale.ENGLISH)); } @@ -295,27 +346,68 @@ 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. + Map previousPositions = new TreeMap<>(matcher); + int position = 0; + for (InstanceWithMetadata instanceWithMetadata : previousView) { + previousPositions.putIfAbsent(matcher.withQuantity(instanceWithMetadata.getInstance(), 1), 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(matcher.withQuantity(instanceWithMetadata.getInstance(), 1), Integer.MAX_VALUE)); + } + + ingredientsView.sort(Comparator + .>comparingInt(positions::get) + .thenComparing(InstanceWithMetadata.createComparator(sorter != null ? sorter : matcher))); + } + protected Stream> transformIngredientsView(Stream> ingredientStream) { return ingredientStream; } @Override public List> getSlots(int channel, int offset, int limit) { + updateSortingPausedState(channel); List> ingredients = getFilteredIngredientsView(channel); int size = ingredients.size(); if (offset >= size) { @@ -355,6 +447,7 @@ public Optional> getSlot(int channel, int in @Override public int getSlotCount(int channel) { + updateSortingPausedState(channel); return getFilteredIngredientsView(channel).size(); } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonFilterCrafting.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonFilterCrafting.java index 68faee5d8..f1ae255c4 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonFilterCrafting.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonFilterCrafting.java @@ -74,6 +74,7 @@ public void onClick(TerminalStorageTabIngredientComponentClient clientTab, data.putInt("active", active.ordinal()); state.setButton(clientTab.getTabSettingsName().toString(), this.buttonName, data); + clientTab.resetPausedSortingOrder(channel); clientTab.resetFilteredIngredientsViews(channel); } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java index 10253cb31..6c52a3cf1 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java @@ -91,6 +91,7 @@ public void onClick(TerminalStorageTabIngredientComponentClient clientTab, state.setButton(clientTab.getTabSettingsName().toString(), this.buttonName, data); updateSorter(); + clientTab.resetPausedSortingOrder(channel); clientTab.resetFilteredIngredientsViews(channel); } From 43c628ed1ddc26e7796ea8ea29915265916cec68 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 05:07:16 +0000 Subject: [PATCH 2/2] Fix crafting options jumping around while sorting is paused Crafting options shared their position with the stored ingredient of the same item, causing them to jump next to that stored ingredient as soon as the view was rebuilt while sorting was paused. Positions are now determined based on the ingredient and its crafting option. Furthermore, the paused sorting order is now reset from within resetFilteredIngredientsViews, and the paused state is checked from within getFilteredIngredientsView. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01GuGBvU7WNr6XhwVxbGdLmY --- ...alStorageTabIngredientComponentClient.java | 47 ++++++++++++------- .../button/TerminalButtonFilterCrafting.java | 1 - .../button/TerminalButtonSort.java | 1 - 3 files changed, 31 insertions(+), 18 deletions(-) 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 1bdf73eef..612f98ad9 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java @@ -234,19 +234,22 @@ public Predicate> getInstanceFilterMetadata() { } public void resetFilteredIngredientsViews(int channel) { - filteredIngredientsViews.remove(channel); + resetFilteredIngredientsViews(channel, true); } /** - * Forget the ingredient order that is used to keep ingredient positions stable while sorting is paused. - * - * This should be called when the user explicitly changes the way ingredients are shown, - * as those changes should always be applied immediately. - * + * 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 resetPausedSortingOrder(int channel) { - lastFilteredIngredientsViews.remove(channel); + public void resetFilteredIngredientsViews(int channel, boolean resetPausedSortingOrder) { + filteredIngredientsViews.remove(channel); + if (resetPausedSortingOrder) { + lastFilteredIngredientsViews.remove(channel); + } } /** @@ -284,7 +287,6 @@ public void setInstanceFilter(int channel, String filter) { TerminalStorageTabClientSearchFieldUpdateEvent event = new TerminalStorageTabClientSearchFieldUpdateEvent(this, filter); MinecraftForge.EVENT_BUS.post(event); filter = event.getSearchString(); - resetPausedSortingOrder(channel); resetFilteredIngredientsViews(channel); container.getGuiState().setSearch(getTabSettingsName().toString(), channel, filter.toLowerCase(Locale.ENGLISH)); } @@ -333,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); @@ -383,17 +386,20 @@ protected void sortByPreviousOrder(List> ingredientsView IIngredientMatcher matcher = this.ingredientComponent.getMatcher(); // Determine the position of all previously shown ingredients, while ignoring their quantities. - Map previousPositions = new TreeMap<>(matcher); + // 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(matcher.withQuantity(instanceWithMetadata.getInstance(), 1), position++); + 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(matcher.withQuantity(instanceWithMetadata.getInstance(), 1), Integer.MAX_VALUE)); + .getOrDefault(withoutQuantity(instanceWithMetadata), Integer.MAX_VALUE)); } ingredientsView.sort(Comparator @@ -401,13 +407,23 @@ protected void sortByPreviousOrder(List> ingredientsView .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; } @Override public List> getSlots(int channel, int offset, int limit) { - updateSortingPausedState(channel); List> ingredients = getFilteredIngredientsView(channel); int size = ingredients.size(); if (offset >= size) { @@ -447,7 +463,6 @@ public Optional> getSlot(int channel, int in @Override public int getSlotCount(int channel) { - updateSortingPausedState(channel); return getFilteredIngredientsView(channel).size(); } @@ -510,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. @@ -556,7 +571,7 @@ public synchronized void addCraftingOptions(int channel, List clientTab, data.putInt("active", active.ordinal()); state.setButton(clientTab.getTabSettingsName().toString(), this.buttonName, data); - clientTab.resetPausedSortingOrder(channel); clientTab.resetFilteredIngredientsViews(channel); } diff --git a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java index 6c52a3cf1..10253cb31 100644 --- a/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java +++ b/src/main/java/org/cyclops/integratedterminals/core/terminalstorage/button/TerminalButtonSort.java @@ -91,7 +91,6 @@ public void onClick(TerminalStorageTabIngredientComponentClient clientTab, state.setButton(clientTab.getTabSettingsName().toString(), this.buttonName, data); updateSorter(); - clientTab.resetPausedSortingOrder(channel); clientTab.resetFilteredIngredientsViews(channel); }