mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-10-25 20:22:22 +00:00
Add shader setting search functionality and center selection (#1897)
This commit is contained in:
1 parent
5b704d1bc2
commit
f07d96316a
15 files changed
+648
-61
No files matched your search
@@ -11,6 +11,7 @@ import net.coderbot.iris.gui.screen.ShaderPackScreen;
|
||||
import net.coderbot.iris.shaderpack.ShaderPack;
|
||||
import net.coderbot.iris.shaderpack.option.menu.OptionMenuContainer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiTextField;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -24,6 +25,12 @@ public class ShaderPackOptionList extends IrisGuiSlot {
|
||||
private OptionMenuContainer container;
|
||||
private final List<BaseEntry> entries = new ArrayList<>();
|
||||
|
||||
@Getter
|
||||
private boolean searchModeActive = false;
|
||||
private String typedSearchQuery = "";
|
||||
private int reservedLeftWidth = 48; // Pixels reserved on the left of the header row for the search/clear button + gap.
|
||||
private GuiTextField searchBox = null;
|
||||
|
||||
public ShaderPackOptionList(ShaderPackScreen screen, NavigationController navigation, ShaderPack pack, Minecraft client, int width, int height, int top, int bottom, int left, int right) {
|
||||
super(client, width, height, top, bottom, 24);
|
||||
this.navigation = navigation;
|
||||
@@ -36,6 +43,74 @@ public class ShaderPackOptionList extends IrisGuiSlot {
|
||||
this.container = pack.getMenuContainer();
|
||||
}
|
||||
|
||||
|
||||
/** Applies {@code query} to the option container and rebuilds the list rows. */
|
||||
public void updateSearchQuery(String query) {
|
||||
typedSearchQuery = query != null ? query : "";
|
||||
if (this.container != null) {
|
||||
this.container.setSearchQuery(query);
|
||||
}
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
/** Resets all search state and clears the active filter without rebuilding entries. */
|
||||
public void disableSearchMode() {
|
||||
searchModeActive = false;
|
||||
typedSearchQuery = "";
|
||||
searchBox = null;
|
||||
if (this.container != null) {
|
||||
this.container.setSearchQuery(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void disableSearchModeAndRebuild() {
|
||||
disableSearchMode();
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
public void enableSearchModeAndRebuild() {
|
||||
searchModeActive = true;
|
||||
this.rebuild(); // First rebuild so that the button width is the current one
|
||||
int listWidth = getListWidth();
|
||||
int entryX = this.width / 2 - listWidth / 2;
|
||||
int boxX = entryX + reservedLeftWidth + 3;
|
||||
int boxY = this.top + 5; // got figured out by trial and error and works, don't question
|
||||
int boxW = Math.max(40, listWidth - reservedLeftWidth - 5);
|
||||
this.searchBox = new GuiTextField(Minecraft.getMinecraft().fontRenderer, boxX, boxY, boxW, 14);
|
||||
this.searchBox.setMaxStringLength(64);
|
||||
this.searchBox.setFocused(true);
|
||||
}
|
||||
|
||||
/** True when the navigation stack is non-empty (a sub-screen is open). */
|
||||
public boolean isOnSubScreen() {
|
||||
return navigation != null && navigation.getCurrentScreen() != null;
|
||||
}
|
||||
|
||||
public void setReservedLeftWidth(int w) {
|
||||
reservedLeftWidth = Math.max(0, w);
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes a keystroke into the live search text field, then propagates any text change
|
||||
* to the option container and rebuilds the filtered list.
|
||||
*/
|
||||
public void handleSearchKeyTyped(char typedChar, int keyCode) {
|
||||
if (searchBox == null || !searchModeActive) return;
|
||||
searchBox.textboxKeyTyped(typedChar, keyCode);
|
||||
String newText = searchBox.getText();
|
||||
if (!newText.equals(typedSearchQuery)) {
|
||||
updateSearchQuery(newText);
|
||||
}
|
||||
}
|
||||
|
||||
/** Draws the search text box overlay above the header row */
|
||||
public void drawSearchBox() {
|
||||
if (searchBox == null || !searchModeActive) return;
|
||||
// Hide when the header has scrolled out of view
|
||||
if (this.amountScrolled > 0.5f) return;
|
||||
searchBox.drawTextBox();
|
||||
}
|
||||
|
||||
public void rebuild() {
|
||||
this.entries.clear();
|
||||
this.amountScrolled = 0;
|
||||
@@ -56,7 +131,7 @@ public class ShaderPackOptionList extends IrisGuiSlot {
|
||||
}
|
||||
|
||||
public void addHeader(String text, boolean backButton) {
|
||||
this.addEntry(new HeaderEntry(this.screen, this.navigation, text, backButton));
|
||||
this.addEntry(new HeaderEntry(this.screen, this.navigation, text, backButton, this));
|
||||
}
|
||||
|
||||
public void addWidgets(int columns, List<AbstractElementWidget<?>> elements) {
|
||||
@@ -86,6 +161,15 @@ public class ShaderPackOptionList extends IrisGuiSlot {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(int mouseX, int mouseY, int mouseButton) {
|
||||
// Pass the click to the search box first so it can update its focus state
|
||||
if (searchModeActive && searchBox != null && amountScrolled <= 0.5f) {
|
||||
searchBox.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean elementClicked(int index, boolean doubleClick, int mouseX, int mouseY, int mouseButton) {
|
||||
final BaseEntry entry = this.entries.get(index);
|
||||
|
||||
@@ -14,6 +14,7 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import net.minecraft.util.MathHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
@@ -142,10 +143,15 @@ public class ShaderPackSelectionList extends IrisGuiSlot {
|
||||
topButtonRow.allowEnableShadersButton = !names.isEmpty();
|
||||
|
||||
int index = 0;
|
||||
String selectedName = Iris.getIrisConfig().getShaderPackName().orElse(null);
|
||||
int targetSelectionIndex = -1;
|
||||
|
||||
for (String name : names) {
|
||||
this.addPackEntry(index, name);
|
||||
if (name.equals(selectedName)) {
|
||||
targetSelectionIndex = index;
|
||||
}
|
||||
index++;
|
||||
addPackEntry(index, name);
|
||||
}
|
||||
|
||||
// Try not to gaslight users in case they swap back and forth from lwjgl2 and lwjgl3
|
||||
@@ -153,6 +159,29 @@ public class ShaderPackSelectionList extends IrisGuiSlot {
|
||||
? "pack.iris.list.label"
|
||||
: "pack.iris.list.label.lwjgl2";
|
||||
addLabelEntries(EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format(footerKey));
|
||||
|
||||
if (targetSelectionIndex != -1) {
|
||||
scrollToIndex(targetSelectionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the list to center the entry at {@code targetIndex} in the viewport.
|
||||
*/
|
||||
public void scrollToIndex(int targetIndex) {
|
||||
if (targetIndex < 0 || targetIndex >= this.getSize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.elementClicked(targetIndex, false, 0, 0);
|
||||
this.selectedElement = targetIndex;
|
||||
|
||||
int viewportCenterY = (this.bottom - this.top) / 2;
|
||||
int itemCenterPos = (targetIndex * this.slotHeight) + this.headerPadding + (this.slotHeight / 2);
|
||||
float targetScroll = (float) (itemCenterPos - viewportCenterY);
|
||||
float maxScroll = (float) this.func_148135_f();
|
||||
|
||||
this.amountScrolled = MathHelper.clamp_float(targetScroll, 0.0F, maxScroll);
|
||||
}
|
||||
|
||||
public void addPackEntry(int index, String name) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package net.coderbot.iris.gui.element.shaderoptions;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import net.coderbot.iris.Iris;
|
||||
import net.coderbot.iris.gui.GuiUtil;
|
||||
import net.coderbot.iris.gui.NavigationController;
|
||||
import net.coderbot.iris.gui.element.IrisElementRow;
|
||||
import net.coderbot.iris.gui.element.ShaderPackOptionList;
|
||||
import net.coderbot.iris.gui.screen.ShaderPackScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
@@ -16,58 +18,83 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class HeaderEntry extends BaseEntry {
|
||||
|
||||
public static final String BACK_BUTTON_TEXT = EnumChatFormatting.ITALIC + "< " + I18n.format("options.iris.back");
|
||||
public static final String RESET_BUTTON_TEXT_INACTIVE = EnumChatFormatting.GRAY + I18n.format("options.iris.reset");
|
||||
public static final String RESET_BUTTON_TEXT_ACTIVE = EnumChatFormatting.YELLOW + I18n.format("options.iris.reset");
|
||||
|
||||
public static final String RESET_HOLD_SHIFT_TOOLTIP = EnumChatFormatting.BOLD + I18n.format("options.iris.reset.tooltip.holdShift");
|
||||
public static final String RESET_TOOLTIP = EnumChatFormatting.RED + I18n.format("options.iris.reset.tooltip");
|
||||
public static final String IMPORT_TOOLTIP = I18n.format("options.iris.importSettings.tooltip");
|
||||
// .withStyle(style -> style.withColor(TextColor.fromRgb(0x4da6ff)));
|
||||
public static final String EXPORT_TOOLTIP = I18n.format("options.iris.exportSettings.tooltip");
|
||||
// .withStyle(style -> style.withColor(TextColor.fromRgb(0xfc7d3d)));
|
||||
private static String backButtonText() { return EnumChatFormatting.ITALIC + "< " + I18n.format("options.iris.back"); }
|
||||
private static String resetButtonInactive() { return EnumChatFormatting.GRAY + I18n.format("options.iris.reset"); }
|
||||
private static String resetButtonActive() { return EnumChatFormatting.YELLOW + I18n.format("options.iris.reset"); }
|
||||
private static String resetHoldShiftTooltip() { return EnumChatFormatting.BOLD + I18n.format("options.iris.reset.tooltip.holdShift"); }
|
||||
private static String resetTooltip() { return EnumChatFormatting.RED + I18n.format("options.iris.reset.tooltip"); }
|
||||
private static String importTooltip() { return I18n.format("options.iris.importSettings.tooltip"); }
|
||||
private static String exportTooltip() { return I18n.format("options.iris.exportSettings.tooltip"); }
|
||||
private static String searchButtonText() { return I18n.format("options.iris.search.button"); }
|
||||
private static String clearButtonText() { return I18n.format("options.iris.clear.button"); }
|
||||
private static String searchTooltip() { return I18n.format("options.iris.search.tooltip"); }
|
||||
private static String clearTooltip() { return I18n.format("options.iris.clear.tooltip"); }
|
||||
|
||||
private static final int MIN_SIDE_BUTTON_WIDTH = 42;
|
||||
private static final int BUTTON_HEIGHT = 16;
|
||||
private static final int SEARCH_BOX_GAP = 3;
|
||||
|
||||
private final ShaderPackScreen screen;
|
||||
private final ShaderPackOptionList optionList;
|
||||
private final @Nullable IrisElementRow backButton;
|
||||
/** Width of the left-side button, used to clip the title text away from it. */
|
||||
private final int leftButtonWidth;
|
||||
private final IrisElementRow utilityButtons = new IrisElementRow();
|
||||
private final IrisElementRow.TextButtonElement resetButton;
|
||||
private final IrisElementRow.IconButtonElement importButton;
|
||||
private final IrisElementRow.IconButtonElement exportButton;
|
||||
private final String text;
|
||||
|
||||
public HeaderEntry(ShaderPackScreen screen, NavigationController navigation, String text, boolean hasBackButton) {
|
||||
/** Non-null only on the main-screen header (search/clear toggle). */
|
||||
private final @Nullable IrisElementRow.TextButtonElement searchToggleButton;
|
||||
|
||||
public HeaderEntry(ShaderPackScreen screen, NavigationController navigation, String text, boolean hasBackButton, ShaderPackOptionList optionList) {
|
||||
super(navigation);
|
||||
|
||||
this.optionList = optionList;
|
||||
this.screen = screen;
|
||||
this.text = text;
|
||||
|
||||
IrisElementRow.TextButtonElement toggleRef = null;
|
||||
|
||||
if (hasBackButton) {
|
||||
this.backButton = new IrisElementRow().add(new IrisElementRow.TextButtonElement(BACK_BUTTON_TEXT, this::backButtonClicked),
|
||||
Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(BACK_BUTTON_TEXT) + 8));
|
||||
// Sub-screen: navigating here while search is active → force-disable so the search box disappears
|
||||
if (optionList.isSearchModeActive()) {
|
||||
optionList.disableSearchMode();
|
||||
}
|
||||
String backText = backButtonText();
|
||||
int w = Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(backText) + 8);
|
||||
this.backButton = new IrisElementRow().add(new IrisElementRow.TextButtonElement(backText, this::backButtonClicked), w);
|
||||
this.leftButtonWidth = w;
|
||||
} else {
|
||||
this.backButton = null;
|
||||
// Main screen: left button is the search/clear toggle
|
||||
String buttonText = optionList.isSearchModeActive() ? clearButtonText() : searchButtonText();
|
||||
IrisElementRow.TextButtonElement searchBtn = new IrisElementRow.TextButtonElement(buttonText, this::searchButtonClicked);
|
||||
int w = Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(buttonText) + 8);
|
||||
this.backButton = new IrisElementRow().add(searchBtn, w);
|
||||
toggleRef = searchBtn;
|
||||
this.leftButtonWidth = w;
|
||||
optionList.setReservedLeftWidth(w + SEARCH_BOX_GAP);
|
||||
}
|
||||
|
||||
this.resetButton = new IrisElementRow.TextButtonElement(RESET_BUTTON_TEXT_INACTIVE, this::resetButtonClicked);
|
||||
this.searchToggleButton = toggleRef;
|
||||
|
||||
this.resetButton = new IrisElementRow.TextButtonElement(resetButtonInactive(), this::resetButtonClicked);
|
||||
this.importButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.IMPORT, GuiUtil.Icon.IMPORT_COLORED, this::importSettingsButtonClicked);
|
||||
this.exportButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.EXPORT, GuiUtil.Icon.EXPORT_COLORED, this::exportSettingsButtonClicked);
|
||||
|
||||
this.utilityButtons.add(this.importButton, 15).add(this.exportButton, 15)
|
||||
.add(this.resetButton, Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth((RESET_BUTTON_TEXT_INACTIVE) + 8)));
|
||||
|
||||
this.screen = screen;
|
||||
this.text = text;
|
||||
.add(this.resetButton, Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(resetButtonInactive()) + 8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int slotWidth, int slotHeight, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) {
|
||||
Gui.drawRect(x - 3, (y + slotHeight) - 3, x + slotWidth, (y + slotHeight) - 2, 0x66BEBEBE);
|
||||
// Gui.drawRect leaves GL color set to the rect's color; reset to white so button textures aren't tinted.
|
||||
GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
final int tickDelta = 0;
|
||||
final FontRenderer font = Minecraft.getMinecraft().fontRenderer;
|
||||
|
||||
GuiUtil.drawScrollingText(font, text, x + (int) (slotWidth * 0.5), x + 5,
|
||||
((x + slotWidth) - 10) - this.utilityButtons.getWidth(), y + 5, y + 15, 0xFFFFFF);
|
||||
|
||||
GuiUtil.bindIrisWidgetsTexture();
|
||||
|
||||
// Draw back button if present
|
||||
@@ -75,26 +102,42 @@ public class HeaderEntry extends BaseEntry {
|
||||
backButton.drawScreen(x, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver);
|
||||
}
|
||||
|
||||
if (optionList.isSearchModeActive() && searchToggleButton != null) {
|
||||
if (searchToggleButton.isHovered()) {
|
||||
ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, clearTooltip(), mouseX, mouseY - 16));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GuiUtil.drawScrollingText(font, text, x + (int) (slotWidth * 0.5),
|
||||
x + leftButtonWidth + 5,
|
||||
((x + slotWidth) - 10) - this.utilityButtons.getWidth(),
|
||||
y + 5, y + 15, 0xFFFFFF);
|
||||
|
||||
final boolean shiftDown = GuiScreen.isShiftKeyDown();
|
||||
|
||||
// Set the appearance of the reset button
|
||||
this.resetButton.disabled = !shiftDown;
|
||||
this.resetButton.text = shiftDown ? RESET_BUTTON_TEXT_ACTIVE : RESET_BUTTON_TEXT_INACTIVE;
|
||||
this.resetButton.text = shiftDown ? resetButtonActive() : resetButtonInactive();
|
||||
|
||||
// Draw the utility buttons
|
||||
this.utilityButtons.renderRightAligned((x + slotWidth) - 3, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver);
|
||||
|
||||
// Draw the reset button's tooltip
|
||||
if (this.resetButton.isHovered()) {
|
||||
final String tooltip = shiftDown ? RESET_TOOLTIP : RESET_HOLD_SHIFT_TOOLTIP;
|
||||
final String tooltip = shiftDown ? resetTooltip() : resetHoldShiftTooltip();
|
||||
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, tooltip);
|
||||
}
|
||||
// Draw the import/export button tooltips
|
||||
if (this.importButton.isHovered()) {
|
||||
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, IMPORT_TOOLTIP);
|
||||
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, importTooltip());
|
||||
}
|
||||
if (this.exportButton.isHovered()) {
|
||||
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, EXPORT_TOOLTIP);
|
||||
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, exportTooltip());
|
||||
}
|
||||
// Search-button tooltip
|
||||
if (searchToggleButton != null && searchToggleButton.isHovered()) {
|
||||
ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, searchTooltip(), mouseX, mouseY - 16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +147,9 @@ public class HeaderEntry extends BaseEntry {
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(int mouseX, int mouseY, int button) {
|
||||
if (optionList.isSearchModeActive()) {
|
||||
return backButton != null && backButton.mouseClicked(mouseX, mouseY, button);
|
||||
}
|
||||
final boolean backButtonResult = backButton != null && backButton.mouseClicked(mouseX, mouseY, button);
|
||||
final boolean utilButtonResult = utilityButtons.mouseClicked(mouseX, mouseY, button);
|
||||
|
||||
@@ -122,6 +168,16 @@ public class HeaderEntry extends BaseEntry {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean searchButtonClicked(IrisElementRow.TextButtonElement button) {
|
||||
GuiUtil.playButtonClickSound();
|
||||
if (optionList.isSearchModeActive()) {
|
||||
optionList.disableSearchModeAndRebuild();
|
||||
} else {
|
||||
optionList.enableSearchModeAndRebuild();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean resetButtonClicked(IrisElementRow.TextButtonElement button) {
|
||||
if (GuiScreen.isShiftKeyDown()) {
|
||||
Iris.resetShaderPackOptionsOnNextReload();
|
||||
|
||||
@@ -10,9 +10,10 @@ import net.minecraft.client.resources.I18n;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class TopButtonRowEntry extends BaseEntry {
|
||||
private static final String NONE_PRESENT_LABEL = I18n.format("options.iris.shaders.nonePresent"); //.withStyle(ChatFormatting.GRAY);
|
||||
private static final String SHADERS_DISABLED_LABEL = I18n.format("options.iris.shaders.disabled");
|
||||
private static final String SHADERS_ENABLED_LABEL = I18n.format("options.iris.shaders.enabled");
|
||||
|
||||
private static String nonePresentLabel() { return I18n.format("options.iris.shaders.nonePresent"); }
|
||||
private static String shadersDisabledLabel() { return I18n.format("options.iris.shaders.disabled"); }
|
||||
private static String shadersEnabledLabel() { return I18n.format("options.iris.shaders.enabled"); }
|
||||
|
||||
private final ShaderPackSelectionList shaderPackSelectionList;
|
||||
private final IrisElementRow buttons = new IrisElementRow();
|
||||
@@ -54,7 +55,7 @@ public class TopButtonRowEntry extends BaseEntry {
|
||||
}
|
||||
|
||||
private String getEnableDisableLabel() {
|
||||
return this.allowEnableShadersButton ? this.shadersEnabled ? SHADERS_ENABLED_LABEL : SHADERS_DISABLED_LABEL : NONE_PRESENT_LABEL;
|
||||
return this.allowEnableShadersButton ? this.shadersEnabled ? shadersEnabledLabel() : shadersDisabledLabel() : nonePresentLabel();
|
||||
}
|
||||
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
|
||||
@@ -4,16 +4,19 @@ import net.coderbot.iris.gui.GuiUtil;
|
||||
import net.coderbot.iris.gui.NavigationController;
|
||||
import net.coderbot.iris.gui.screen.ShaderPackScreen;
|
||||
import net.coderbot.iris.shaderpack.option.menu.OptionMenuElement;
|
||||
import net.coderbot.iris.shaderpack.option.menu.OptionMenuOptionElement;
|
||||
import net.coderbot.iris.shaderpack.option.menu.ShaderSearchEngine;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class BaseOptionElementWidget<T extends OptionMenuElement> extends CommentedElementWidget<T> {
|
||||
protected static final String SET_TO_DEFAULT = I18n.format("options.iris.setToDefault");
|
||||
protected static String setToDefault() { return I18n.format("options.iris.setToDefault"); }
|
||||
protected static final String DIVIDER = ": ";
|
||||
|
||||
protected String unmodifiedLabel;
|
||||
@@ -101,7 +104,7 @@ public abstract class BaseOptionElementWidget<T extends OptionMenuElement> exten
|
||||
|
||||
protected final void tryRenderTooltip(int mouseX, int mouseY, boolean hovered) {
|
||||
if (GuiScreen.isShiftKeyDown()) {
|
||||
renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered);
|
||||
renderTooltip(setToDefault(), mouseX, mouseY, hovered);
|
||||
} else if (this.isLabelTrimmed && !this.screen.isDisplayingComment()) {
|
||||
renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered);
|
||||
}
|
||||
@@ -145,13 +148,39 @@ public abstract class BaseOptionElementWidget<T extends OptionMenuElement> exten
|
||||
|
||||
@Override
|
||||
public Optional<String> getCommentBody() {
|
||||
final String key = getCommentKey();
|
||||
if (key == null) {
|
||||
return Optional.empty();
|
||||
Optional<String> base = Optional.ofNullable(getCommentKey()).map(I18n::format);
|
||||
|
||||
if (this.screen == null || !this.screen.isOptionMenuSearchActive()) {
|
||||
return base;
|
||||
}
|
||||
final String translated = GuiUtil.translateLenient(key);
|
||||
// Don't show comment boxes without a translation, otherwise you get something like option.{commentname}.comment
|
||||
return translated.equals(key) ? Optional.empty() : Optional.of(translated);
|
||||
|
||||
String pathLabel = buildTranslatedPath();
|
||||
if (pathLabel == null) {
|
||||
return base;
|
||||
}
|
||||
|
||||
String breadcrumb = EnumChatFormatting.BOLD.toString() + EnumChatFormatting.ITALIC + pathLabel + EnumChatFormatting.RESET;
|
||||
if (base.isPresent()) {
|
||||
return Optional.of(breadcrumb + ". " + base.get());
|
||||
}
|
||||
return Optional.of(breadcrumb);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String buildTranslatedPath() {
|
||||
if (!(this.element instanceof OptionMenuOptionElement)) return null;
|
||||
OptionMenuOptionElement optEl = (OptionMenuOptionElement) this.element;
|
||||
String rawPath = optEl.container.getOptionPath(optEl.optionId);
|
||||
String[] segments = rawPath.split("/");
|
||||
StringBuilder display = new StringBuilder();
|
||||
for (String segment : segments) {
|
||||
if ("root".equals(segment)) continue;
|
||||
String translated = ShaderSearchEngine.getDisplaySettingsName(segment).replaceAll("\\s+>", "");
|
||||
String label = translated.isEmpty() ? segment : translated;
|
||||
if (display.length() > 0) display.append(" > ");
|
||||
display.append(label);
|
||||
}
|
||||
return display.length() > 0 ? display.toString() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,10 +9,9 @@ import net.coderbot.iris.shaderpack.option.menu.OptionMenuBooleanOptionElement;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
|
||||
public class BooleanElementWidget extends BaseOptionElementWidget<OptionMenuBooleanOptionElement> {
|
||||
private static final String TEXT_TRUE = I18n.format("label.iris.true");
|
||||
private static final String TEXT_FALSE = I18n.format("label.iris.false");
|
||||
private static final String TEXT_TRUE_DEFAULT = I18n.format("label.iris.true");
|
||||
private static final String TEXT_FALSE_DEFAULT = I18n.format("label.iris.false");
|
||||
|
||||
private static String textTrue() { return I18n.format("label.iris.true"); }
|
||||
private static String textFalse() { return I18n.format("label.iris.false"); }
|
||||
|
||||
private final BooleanOption option;
|
||||
|
||||
@@ -63,11 +62,7 @@ public class BooleanElementWidget extends BaseOptionElementWidget<OptionMenuBool
|
||||
// Compatibility Mode to "On" when not needed. Now we use white for
|
||||
// default to avoid giving a positive or negative connotation to a
|
||||
// default value.
|
||||
if (this.value == this.defaultValue) {
|
||||
return this.value ? TEXT_TRUE_DEFAULT : TEXT_FALSE_DEFAULT;
|
||||
}
|
||||
|
||||
return this.value ? TEXT_TRUE : TEXT_FALSE;
|
||||
return this.value ? textTrue() : textFalse();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,8 +16,9 @@ import net.minecraft.client.resources.I18n;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProfileElement> {
|
||||
private static final String PROFILE_LABEL = I18n.format("options.iris.profile");
|
||||
private static final String PROFILE_CUSTOM = I18n.format("options.iris.profile.custom");
|
||||
|
||||
private static String profileLabel() { return I18n.format("options.iris.profile"); }
|
||||
private static String profileCustom() { return I18n.format("options.iris.profile.custom"); }
|
||||
|
||||
private Profile next;
|
||||
private Profile previous;
|
||||
@@ -30,7 +31,7 @@ public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProf
|
||||
@Override
|
||||
public void init(ShaderPackScreen screen, NavigationController navigation) {
|
||||
super.init(screen, navigation);
|
||||
this.setLabel(PROFILE_LABEL);
|
||||
this.setLabel(profileLabel());
|
||||
boolean secondProfileSet = ProfileElementTracker.isSecondProfileSet(this.element);
|
||||
|
||||
final ProfileSet profiles = this.element.profiles;
|
||||
@@ -44,12 +45,12 @@ public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProf
|
||||
final Optional<String> profileName = result.current.map(p -> p.name);
|
||||
|
||||
String translationKey = "profile" + (secondProfileSet ? "2." : ".") + profileName.orElse("custom");
|
||||
this.profileLabel = profileName.map(name -> GuiUtil.translateOrDefault(name, translationKey)).orElse(PROFILE_CUSTOM);
|
||||
this.profileLabel = profileName.map(name -> GuiUtil.translateOrDefault(name, translationKey)).orElse(profileCustom());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) {
|
||||
this.updateRenderParams(width, width - (Minecraft.getMinecraft().fontRenderer.getStringWidth(PROFILE_LABEL) + 16));
|
||||
this.updateRenderParams(width, width - (Minecraft.getMinecraft().fontRenderer.getStringWidth(profileLabel()) + 16));
|
||||
|
||||
this.renderOptionWithValue(x, y, width, height, hovered);
|
||||
}
|
||||
@@ -61,12 +62,12 @@ public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProf
|
||||
|
||||
@Override
|
||||
protected int getValueColor() {
|
||||
return PROFILE_CUSTOM.equals(this.profileLabel) ? 0xFFFF55 : 0xFFFFFF;
|
||||
return profileCustom().equals(this.profileLabel) ? 0xFFFF55 : 0xFFFFFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getCommentTitle() {
|
||||
return Optional.of(PROFILE_LABEL);
|
||||
return Optional.of(profileLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,7 +28,7 @@ public class SliderElementWidget extends StringElementWidget {
|
||||
}
|
||||
|
||||
if (GuiScreen.isShiftKeyDown()) {
|
||||
renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered);
|
||||
renderTooltip(setToDefault(), mouseX, mouseY, hovered);
|
||||
} else if (!this.screen.isDisplayingComment()) {
|
||||
renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered);
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
|
||||
*/
|
||||
public static final Set<Runnable> TOP_LAYER_RENDER_QUEUE = new HashSet<>();
|
||||
|
||||
private static final String SELECT_TITLE = EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.select.title");
|
||||
private static final String CONFIGURE_TITLE = EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.configure.title");
|
||||
private static String selectTitle() { return EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.select.title"); }
|
||||
private static String configureTitle() { return EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.configure.title"); }
|
||||
private static final int COMMENT_PANEL_WIDTH = 314;
|
||||
|
||||
private final GuiScreen parent;
|
||||
@@ -104,6 +104,11 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
|
||||
|
||||
refreshForChangedPack();
|
||||
}
|
||||
/** Returns true when the option menu is open and the user has an active search query. */
|
||||
public boolean isOptionMenuSearchActive() {
|
||||
return this.optionMenuOpen && this.shaderOptionList != null && this.shaderOptionList.isSearchModeActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float delta) {
|
||||
GLStateManager.glPushAttrib(GL11.GL_COLOR_BUFFER_BIT);
|
||||
@@ -124,6 +129,7 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
|
||||
if (!this.guiHidden) {
|
||||
if (optionMenuOpen && this.shaderOptionList != null) {
|
||||
this.shaderOptionList.drawScreen(mouseX, mouseY, delta);
|
||||
this.shaderOptionList.drawSearchBox();
|
||||
} else {
|
||||
this.shaderPackList.drawScreen(mouseX, mouseY, delta);
|
||||
}
|
||||
@@ -144,9 +150,9 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
|
||||
drawCenteredString(this.fontRendererObj, notificationDialog, (int) (this.width * 0.5), 21, 0xFFFFFF);
|
||||
} else {
|
||||
if (optionMenuOpen) {
|
||||
drawCenteredString(this.fontRendererObj, CONFIGURE_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF);
|
||||
drawCenteredString(this.fontRendererObj, configureTitle(), (int) (this.width * 0.5), 21, 0xFFFFFF);
|
||||
} else {
|
||||
drawCenteredString(this.fontRendererObj, SELECT_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF);
|
||||
drawCenteredString(this.fontRendererObj, selectTitle(), (int) (this.width * 0.5), 21, 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +213,10 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
|
||||
if (Iris.getCurrentPack().isPresent() && this.navigation != null) {
|
||||
final ShaderPack currentPack = Iris.getCurrentPack().get();
|
||||
|
||||
if (currentPack.getMenuContainer() != null) {
|
||||
currentPack.getMenuContainer().setSearchQuery(null);
|
||||
}
|
||||
|
||||
this.shaderOptionList = new ShaderPackOptionList(this, this.navigation, currentPack, this.mc, this.width, this.height, 32, this.height - 58, 0, this.width);
|
||||
this.navigation.setActiveOptionList(this.shaderOptionList);
|
||||
|
||||
@@ -364,6 +374,31 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
|
||||
|
||||
@Override
|
||||
protected void keyTyped(char typedChar, int keyCode) {
|
||||
// Route all input to the search box while search mode is active
|
||||
if (optionMenuOpen && this.shaderOptionList != null && this.shaderOptionList.isSearchModeActive()) {
|
||||
if (keyCode == Keyboard.KEY_ESCAPE) {
|
||||
this.shaderOptionList.disableSearchModeAndRebuild();
|
||||
return;
|
||||
}
|
||||
// Ctrl+F while search is open: toggle it closed
|
||||
if (keyCode == Keyboard.KEY_F && GuiScreen.isCtrlKeyDown()) {
|
||||
GuiUtil.playButtonClickSound();
|
||||
this.shaderOptionList.disableSearchModeAndRebuild();
|
||||
return;
|
||||
}
|
||||
this.shaderOptionList.handleSearchKeyTyped(typedChar, keyCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+F: open search on the main option screen
|
||||
if (optionMenuOpen && this.shaderOptionList != null
|
||||
&& keyCode == Keyboard.KEY_F && GuiScreen.isCtrlKeyDown()) {
|
||||
if (!this.shaderOptionList.isOnSubScreen()) {
|
||||
GuiUtil.playButtonClickSound();
|
||||
this.shaderOptionList.enableSearchModeAndRebuild();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (GuiScreen.isCtrlKeyDown() && keyCode == Keyboard.KEY_D) {
|
||||
this.mc.displayGuiScreen(new GuiYesNo((result, id) -> {
|
||||
Iris.setDebug(result);
|
||||
@@ -496,6 +531,10 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
|
||||
}
|
||||
|
||||
public void onClose() {
|
||||
if (this.shaderOptionList != null) {
|
||||
this.shaderOptionList.disableSearchModeAndRebuild();
|
||||
}
|
||||
|
||||
if (!dropChanges) {
|
||||
applyChanges();
|
||||
} else {
|
||||
|
||||
@@ -10,9 +10,12 @@ import net.coderbot.iris.shaderpack.option.ShaderPackOptions;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class OptionMenuContainer {
|
||||
public final OptionMenuElementScreen mainScreen;
|
||||
@@ -24,6 +27,8 @@ public class OptionMenuContainer {
|
||||
private final Map<List<OptionMenuElement>, Integer> unusedOptionDumpQueue = new HashMap<>(); // Used by screens with "*" element
|
||||
@Getter private final ProfileSet profiles;
|
||||
@Getter private final ProfileSet profiles2;
|
||||
private final List<OptionMenuElement> originalMainElements = new ArrayList<>(); // Saved snapshot of mainScreen.elements before any search filter is applied.
|
||||
private final Map<String, String> cachedOptionPaths = new HashMap<>(); // Full "root/SCREEN1/SCREEN2" path for every option ID, built once after construction
|
||||
|
||||
public OptionMenuContainer(ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions, ProfileSet profiles) {
|
||||
this.profiles = profiles;
|
||||
@@ -69,6 +74,10 @@ public class OptionMenuContainer {
|
||||
|
||||
entry.getKey().addAll(entry.getValue(), elementsToInsert);
|
||||
}
|
||||
|
||||
// Save the original layout and build the path cache once the full tree is constructed
|
||||
this.originalMainElements.addAll(this.mainScreen.elements);
|
||||
generateAllPaths();
|
||||
}
|
||||
|
||||
// Screens will call this when they contain a "*" element, so that the list of
|
||||
@@ -85,4 +94,81 @@ public class OptionMenuContainer {
|
||||
|
||||
unusedOptions.remove(optionId);
|
||||
}
|
||||
|
||||
private void generateAllPaths() {
|
||||
cachedOptionPaths.clear();
|
||||
traverseScreen(this.mainScreen, "root", new HashSet<String>());
|
||||
}
|
||||
|
||||
private void traverseScreen(OptionMenuElementScreen screen, String currentPath, Set<String> visited) {
|
||||
if (screen == null) return;
|
||||
for (OptionMenuElement element : screen.elements) {
|
||||
if (element instanceof OptionMenuOptionElement optEl) {
|
||||
if (optEl.optionId != null) {
|
||||
cachedOptionPaths.putIfAbsent(optEl.optionId, currentPath);
|
||||
}
|
||||
} else if (element instanceof OptionMenuLinkElement link) {
|
||||
if (link.targetScreenId != null) {
|
||||
String targetId = link.targetScreenId;
|
||||
if (visited.add(targetId)) {
|
||||
OptionMenuElementScreen next = this.subScreens.get(targetId);
|
||||
if (next != null) {
|
||||
traverseScreen(next, currentPath + "/" + targetId, visited);
|
||||
}
|
||||
visited.remove(targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getOptionPath(String optionId) {
|
||||
if (optionId == null) return "root";
|
||||
return cachedOptionPaths.getOrDefault(optionId, "root");
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters and re-orders the main-screen options to match {@code query}, or restores the
|
||||
* original layout when {@code query} is null or blank.
|
||||
*/
|
||||
public void setSearchQuery(String query) {
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
restoreOriginalLayout();
|
||||
return;
|
||||
}
|
||||
|
||||
String normalizedQuery = query.toLowerCase(Locale.ROOT).trim();
|
||||
|
||||
List<OptionMenuOptionElement> flatOptions = ShaderSearchEngine.getAllOptionsFlattened(usedOptionElements);
|
||||
List<ShaderSearchEngine.ScoredOptionElement> scored = new ArrayList<>();
|
||||
|
||||
for (OptionMenuOptionElement el : flatOptions) {
|
||||
int score = ShaderSearchEngine.computeMatchTier(el.optionId, normalizedQuery);
|
||||
if (score > 0) {
|
||||
scored.add(new ShaderSearchEngine.ScoredOptionElement(
|
||||
el,
|
||||
ShaderSearchEngine.getReadableTranslatedName(el.optionId),
|
||||
ShaderSearchEngine.getReadableDefaultName(el.optionId),
|
||||
getOptionPath(el.optionId),
|
||||
score,
|
||||
normalizedQuery
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(scored);
|
||||
applyFilteredLayout(scored);
|
||||
}
|
||||
|
||||
private void applyFilteredLayout(List<ShaderSearchEngine.ScoredOptionElement> sortedElements) {
|
||||
this.mainScreen.elements.clear();
|
||||
for (ShaderSearchEngine.ScoredOptionElement scored : sortedElements) {
|
||||
this.mainScreen.elements.add(scored.getElement());
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreOriginalLayout() {
|
||||
this.mainScreen.elements.clear();
|
||||
this.mainScreen.elements.addAll(this.originalMainElements);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package net.coderbot.iris.shaderpack.option.menu;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.coderbot.iris.Iris;
|
||||
import net.coderbot.iris.shaderpack.ShaderPack;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utility class for searching and scoring shader option elements based on a query string.
|
||||
* <br>By SpacEagle17
|
||||
*/
|
||||
public class ShaderSearchEngine {
|
||||
private static final String WHOLE_WORD_REGEX = "(?<=^|[^a-zA-Z0-9])%s(?=$|[^a-zA-Z0-9])";
|
||||
private static final String STARTS_WITH_REGEX = "(?<=^|[^a-zA-Z0-9])%s";
|
||||
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§.");
|
||||
|
||||
/**
|
||||
* Bitmask of bits from readable-name (translated + default) comparisons.
|
||||
* Used by the comparator to separate name-match quality from rawId/comment noise.
|
||||
*/
|
||||
private static final int READABLE_NAME_BITS =
|
||||
(1 << 13) | (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 3) | (1 << 2);
|
||||
|
||||
|
||||
private static String strippedLanguageString(String key) {
|
||||
String translated = I18n.format(key);
|
||||
if (translated.equals(key)) return "";
|
||||
return COLOR_CODE_PATTERN.matcher(translated).replaceAll("");
|
||||
}
|
||||
|
||||
/** Translated display name for a sub-screen ID (looks up {@code "screen.<screenId>"}). */
|
||||
public static String getDisplaySettingsName(String screenId) {
|
||||
return strippedLanguageString("screen." + screenId);
|
||||
}
|
||||
|
||||
/** Lowercase Minecraft-locale translation of an option's human-readable name. */
|
||||
public static String getReadableTranslatedName(String optionId) {
|
||||
return strippedLanguageString("option." + optionId).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowercase default (en_us from the shader pack's lang folder) translation.
|
||||
* Returns {@code ""} when the current pack has no en_us lang file or the key is missing.
|
||||
*/
|
||||
public static String getReadableDefaultName(String optionId) {
|
||||
try {
|
||||
Optional<ShaderPack> packOpt = Iris.getCurrentPack();
|
||||
if (packOpt.isEmpty()) return "";
|
||||
Map<String, String> translations = packOpt.get().getLanguageMap().getTranslations("en_us");
|
||||
if (translations == null) return "";
|
||||
String value = translations.get("option." + optionId);
|
||||
if (value == null) return "";
|
||||
return COLOR_CODE_PATTERN.matcher(value).replaceAll("").toLowerCase(Locale.ROOT);
|
||||
} catch (Throwable t) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isOnlyAscii(String s) {
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
if (s.charAt(i) > 127) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a 14-bit bitmask score for how well {@code optionId} matches {@code query}.
|
||||
* Returns 0 when there is no match at all.
|
||||
* <br> Read code to know what each bit means lmao
|
||||
*/
|
||||
public static int computeMatchTier(String optionId, String query) {
|
||||
try {
|
||||
if (optionId == null || query == null) return 0;
|
||||
String q = query.toLowerCase(Locale.ROOT).trim();
|
||||
if (q.isEmpty()) return 0;
|
||||
|
||||
String translatedName = getReadableTranslatedName(optionId);
|
||||
String defaultName = getReadableDefaultName(optionId);
|
||||
|
||||
// Single ASCII char: only match when a readable name begins with it
|
||||
if (q.length() == 1 && isOnlyAscii(q)) {
|
||||
return (!translatedName.isEmpty() && translatedName.startsWith(q)) ? 1 : 0;
|
||||
}
|
||||
|
||||
String escaped = Pattern.quote(q);
|
||||
Pattern wholeWord = Pattern.compile(String.format(WHOLE_WORD_REGEX, escaped));
|
||||
Pattern startsWith = Pattern.compile(String.format(STARTS_WITH_REGEX, escaped));
|
||||
|
||||
String rawId = optionId.toLowerCase(Locale.ROOT);
|
||||
String commentText = strippedLanguageString("option." + optionId + ".comment").toLowerCase(Locale.ROOT);
|
||||
|
||||
int score = 0;
|
||||
if (!translatedName.isEmpty() && translatedName.equals(q)) score |= (1 << 13);
|
||||
if (!defaultName.isEmpty() && defaultName.equals(q)) score |= (1 << 12);
|
||||
if (!translatedName.isEmpty() && wholeWord.matcher(translatedName).find()) score |= (1 << 11);
|
||||
if (!defaultName.isEmpty() && wholeWord.matcher(defaultName).find()) score |= (1 << 10);
|
||||
if (!translatedName.isEmpty() && startsWith.matcher(translatedName).find()) score |= (1 << 9);
|
||||
if (!defaultName.isEmpty() && startsWith.matcher(defaultName).find()) score |= (1 << 8);
|
||||
if (wholeWord.matcher(rawId).find()) score |= (1 << 7);
|
||||
if (!commentText.isEmpty() && wholeWord.matcher(commentText).find()) score |= (1 << 6);
|
||||
if (startsWith.matcher(rawId).find()) score |= (1 << 5);
|
||||
if (!commentText.isEmpty() && startsWith.matcher(commentText).find()) score |= (1 << 4);
|
||||
if (!translatedName.isEmpty() && translatedName.contains(q)) score |= (1 << 3);
|
||||
if (!defaultName.isEmpty() && defaultName.contains(q)) score |= (1 << 2);
|
||||
if (rawId.contains(q)) score |= (1 << 1);
|
||||
if (!commentText.isEmpty() && commentText.contains(q)) score |= 1;
|
||||
|
||||
return score;
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** De-duplicates and flattens the pool of registered option elements. */
|
||||
public static List<OptionMenuOptionElement> getAllOptionsFlattened(List<OptionMenuOptionElement> usedOptionElements) {
|
||||
List<OptionMenuOptionElement> flat = new ArrayList<>();
|
||||
List<String> seen = new ArrayList<>();
|
||||
for (OptionMenuOptionElement el : usedOptionElements) {
|
||||
if (el == null) continue;
|
||||
String id = el.optionId != null ? el.optionId : el.toString();
|
||||
if (!seen.contains(id)) {
|
||||
seen.add(id);
|
||||
flat.add(el);
|
||||
}
|
||||
}
|
||||
return flat;
|
||||
}
|
||||
|
||||
/**
|
||||
* A scored search result carrying the original element and all data required for sorting.
|
||||
*/
|
||||
public static final class ScoredOptionElement implements Comparable<ScoredOptionElement> {
|
||||
@Getter
|
||||
private final OptionMenuOptionElement element;
|
||||
public final String readableTranslatedName;
|
||||
public final String readableDefaultName;
|
||||
public final String path;
|
||||
public final int score;
|
||||
public final String query;
|
||||
|
||||
public ScoredOptionElement(OptionMenuOptionElement element,
|
||||
String readableTranslatedName,
|
||||
String readableDefaultName,
|
||||
String path,
|
||||
int score,
|
||||
String query) {
|
||||
this.element = element;
|
||||
this.readableTranslatedName = readableTranslatedName;
|
||||
this.readableDefaultName = readableDefaultName;
|
||||
this.path = path;
|
||||
this.score = score;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull ScoredOptionElement other) {
|
||||
String q = this.query != null ? this.query.toLowerCase(Locale.ROOT).trim() : "";
|
||||
int r;
|
||||
if ((r = compareByReadableNameQuality(this, other)) != 0) return r;
|
||||
if ((r = compareByMatchedWordLength(this, other, q)) != 0) return r;
|
||||
if ((r = compareByWordCount(this, other)) != 0) return r;
|
||||
if ((r = compareByFullScore(this, other)) != 0) return r;
|
||||
if ((r = compareByPrefixBoost(this, other, q)) != 0) return r;
|
||||
if ((r = compareByPathDepth(this, other)) != 0) return r;
|
||||
return compareByAlphabetical(this, other);
|
||||
}
|
||||
|
||||
// 1. How well does the readable name (translated + default) match?
|
||||
private static int compareByReadableNameQuality(ScoredOptionElement a, ScoredOptionElement b) {
|
||||
return Integer.compare(b.score & READABLE_NAME_BITS, a.score & READABLE_NAME_BITS);
|
||||
}
|
||||
|
||||
// 2. Shorter matched word = query covers a higher fraction of that word.
|
||||
private static int compareByMatchedWordLength(ScoredOptionElement a, ScoredOptionElement b, String q) {
|
||||
if (q.isEmpty()) return 0;
|
||||
String aw = findMatchingWord(a.readableTranslatedName, q);
|
||||
if (aw == null) aw = findMatchingWord(a.readableDefaultName, q);
|
||||
String bw = findMatchingWord(b.readableTranslatedName, q);
|
||||
if (bw == null) bw = findMatchingWord(b.readableDefaultName, q);
|
||||
int aLen = aw != null ? aw.length() : Integer.MAX_VALUE;
|
||||
int bLen = bw != null ? bw.length() : Integer.MAX_VALUE;
|
||||
return Integer.compare(aLen, bLen);
|
||||
}
|
||||
|
||||
// 3. Fewer words = more precise ("Bloom" before "Bloom Strength").
|
||||
private static int compareByWordCount(ScoredOptionElement a, ScoredOptionElement b) {
|
||||
String ae = !a.readableTranslatedName.isEmpty() ? a.readableTranslatedName : a.readableDefaultName;
|
||||
String be = !b.readableTranslatedName.isEmpty() ? b.readableTranslatedName : b.readableDefaultName;
|
||||
return Integer.compare(countWords(ae), countWords(be));
|
||||
}
|
||||
|
||||
// 4. Higher raw score wins.
|
||||
private static int compareByFullScore(ScoredOptionElement a, ScoredOptionElement b) {
|
||||
return Integer.compare(b.score, a.score);
|
||||
}
|
||||
|
||||
// 5. Prefix boost: readable name starts with the exact query string.
|
||||
private static int compareByPrefixBoost(ScoredOptionElement a, ScoredOptionElement b, String q) {
|
||||
if (q.isEmpty()) return 0;
|
||||
boolean ap = (a.readableTranslatedName != null && a.readableTranslatedName.startsWith(q))
|
||||
|| (a.readableDefaultName != null && a.readableDefaultName.startsWith(q));
|
||||
boolean bp = (b.readableTranslatedName != null && b.readableTranslatedName.startsWith(q))
|
||||
|| (b.readableDefaultName != null && b.readableDefaultName.startsWith(q));
|
||||
if (ap == bp) return 0;
|
||||
return ap ? -1 : 1;
|
||||
}
|
||||
|
||||
// 6. Shallower option path wins (fewer slashes = closer to root menu).
|
||||
private static int compareByPathDepth(ScoredOptionElement a, ScoredOptionElement b) {
|
||||
return Integer.compare(countSlashes(a.path), countSlashes(b.path));
|
||||
}
|
||||
|
||||
// 7. Alphabetical tie-breaker.
|
||||
private static int compareByAlphabetical(ScoredOptionElement a, ScoredOptionElement b) {
|
||||
String aId = a.element != null ? a.element.optionId : null;
|
||||
String bId = b.element != null ? b.element.optionId : null;
|
||||
if (aId != null && bId != null) return aId.compareTo(bId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static String findMatchingWord(String name, String query) {
|
||||
if (name == null || query.isEmpty()) return null;
|
||||
for (String word : name.split("\\s+")) {
|
||||
if (word.startsWith(query)) return word;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int countWords(String s) {
|
||||
if (s == null || s.trim().isEmpty()) return 0;
|
||||
return s.trim().split("\\s+").length;
|
||||
}
|
||||
|
||||
private static int countSlashes(String path) {
|
||||
if (path == null) return 0;
|
||||
int count = 0;
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
if (path.charAt(i) == '/') count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,3 +145,7 @@ pack.iris.select.title=Auswählen
|
||||
pack.iris.configure.title=Konfigurieren
|
||||
label.iris.true=An
|
||||
label.iris.false=Aus
|
||||
options.iris.search.button=Suchen
|
||||
options.iris.clear.button=Schließen
|
||||
options.iris.search.tooltip=Shader-Einstellungen durchsuchen (Strg + F)
|
||||
options.iris.clear.tooltip=Shader-Suche beenden (Esc)
|
||||
@@ -139,6 +139,10 @@ options.iris.shadowDistance.enabled=Allows you to change the maximum distance fo
|
||||
options.iris.shadowDistance.disabled=Your current shader pack has already set a render distance for shadows; you cannot change it.
|
||||
options.iris.gui.hide=Hide GUI
|
||||
options.iris.gui.show=Show GUI
|
||||
options.iris.search.button=Search
|
||||
options.iris.clear.button=Clear
|
||||
options.iris.search.tooltip=Search Shader Settings (Ctrl + F)
|
||||
options.iris.clear.tooltip=Exit Shader Search (Esc)
|
||||
|
||||
options.dynamic_lights=Dynamic Lights
|
||||
options.dynamic_lights.tooltip=Enables dynamic lights. Modes: OFF - off, FASTEST - 500ms delay, FAST - 250ms delay, FANCY - 50ms delay, REALTIME - no delay.
|
||||
|
||||
@@ -132,3 +132,7 @@ pack.iris.select.title=Выбрать
|
||||
pack.iris.configure.title=Настроить
|
||||
label.iris.true=Вкл
|
||||
label.iris.false=Выкл
|
||||
options.iris.search.button=Поиск
|
||||
options.iris.clear.button=Очистить
|
||||
options.iris.search.tooltip=Поиск настроек шейдеров (Ctrl + F)
|
||||
options.iris.clear.tooltip=Выход из поиска шейдеров (Esc)
|
||||
@@ -138,6 +138,10 @@ options.iris.shadowDistance.enabled=更改渲染阴影的最远距离。超出
|
||||
options.iris.shadowDistance.disabled=当前光影包已重设阴影渲染距离,无法更改
|
||||
options.iris.gui.hide=隐藏 GUI
|
||||
options.iris.gui.show=显示 GUI
|
||||
options.iris.search.button=搜索
|
||||
options.iris.clear.button=清除搜索栏
|
||||
options.iris.search.tooltip=搜索光影包设定 (Ctrl + F)
|
||||
options.iris.clear.tooltip=退出搜索 (Esc)
|
||||
|
||||
options.dynamic_lights=动态光源
|
||||
options.dynamic_lights.tooltip=启用动态光源,模式选择:OFF-关闭,FASTEST-500ms延迟,FAST-250ms延迟,FANCY-50ms延迟,REALTIME-无延迟
|
||||
|
||||
Reference in new issue
Block a user