diff --git a/patches/minecraft/net/minecraft/client/gui/GuiChat.java.patch b/patches/minecraft/net/minecraft/client/gui/GuiChat.java.patch new file mode 100644 index 000000000..948ae126a --- /dev/null +++ b/patches/minecraft/net/minecraft/client/gui/GuiChat.java.patch @@ -0,0 +1,175 @@ +--- before/net/minecraft/client/gui/GuiChat.java ++++ after/net/minecraft/client/gui/GuiChat.java +@@ -2,6 +2,9 @@ + + import java.io.IOException; + import javax.annotation.Nullable; ++ ++import com.cleanroommc.client.chat.suggestion.SuggestionList; ++import com.cleanroommc.client.chat.suggestion.SuggestionUpdater; + import net.minecraft.client.Minecraft; + import net.minecraft.util.ITabCompleter; + import net.minecraft.util.TabCompleter; +@@ -26,6 +29,7 @@ + private TabCompleter tabCompleter; + protected GuiTextField inputField; + private String defaultInputFieldText = ""; ++ private SuggestionList suggestionList; + + public GuiChat() + { +@@ -48,6 +52,13 @@ + this.inputField.setText(this.defaultInputFieldText); + this.inputField.setCanLoseFocus(false); + this.tabCompleter = new GuiChat.ChatTabCompleter(this.inputField); ++ this.suggestionList = new SuggestionList(this.inputField); ++ SuggestionUpdater updater = new SuggestionUpdater(this.suggestionList, this.tabCompleter); ++ this.inputField.setGuiResponder(updater); ++ if (this.defaultInputFieldText != null) ++ { ++ updater.setEntryValue(0, this.defaultInputFieldText); ++ } + } + + @Override +@@ -70,6 +81,15 @@ + + if (keyCode == 15) + { ++ if (this.suggestionList.isVisible()) ++ { ++ String selected = this.suggestionList.getSelected(); ++ if (selected == null) { ++ selected = this.suggestionList.getFirst(); ++ } ++ this.suggestionList.applySuggestion(this.inputField, selected); ++ return; ++ } + this.tabCompleter.complete(); + } + else +@@ -79,10 +99,20 @@ + + if (keyCode == 1) + { ++ if (this.suggestionList.isVisible()) ++ { ++ this.suggestionList.hide(); ++ } + this.mc.displayGuiScreen(null); + } + else if (keyCode == 28 || keyCode == 156) + { ++ String selected = this.suggestionList.getSelected(); ++ if (selected != null) ++ { ++ this.suggestionList.applySuggestion(this.inputField, selected); ++ return; ++ } + String s = this.inputField.getText().trim(); + + if (!s.isEmpty()) +@@ -94,10 +124,20 @@ + } + else if (keyCode == 200) + { ++ if (this.suggestionList.isVisible()) ++ { ++ this.suggestionList.selectPrev(); ++ return; ++ } + this.getSentHistory(-1); + } + else if (keyCode == 208) + { ++ if (this.suggestionList.isVisible()) ++ { ++ this.suggestionList.selectNext(); ++ return; ++ } + this.getSentHistory(1); + } + else if (keyCode == 201) +@@ -132,6 +172,13 @@ + i = -1; + } + ++ int mouseX = Mouse.getEventX() * this.width / this.mc.displayWidth; ++ int mouseY = this.height - Mouse.getEventY() * this.height / this.mc.displayHeight - 1; ++ if (this.suggestionList.isMouseOver(this.inputField.x, this.inputField.y, this.inputField.width, mouseX, mouseY)) ++ { ++ this.suggestionList.scroll(i); ++ return; ++ } + if (!isShiftKeyDown()) + { + i *= 7; +@@ -146,6 +193,12 @@ + { + if (mouseButton == 0) + { ++ String clicked = this.suggestionList.mouseClicked(this.inputField.x, this.inputField.y, this.inputField.width, mouseX, mouseY); ++ if (clicked != null) ++ { ++ this.suggestionList.applySuggestion(this.inputField, clicked); ++ return; ++ } + ITextComponent itextcomponent = this.mc.ingameGUI.getChatGUI().getChatComponent(Mouse.getX(), Mouse.getY()); + + if (itextcomponent != null && this.handleComponentClick(itextcomponent)) +@@ -201,7 +254,9 @@ + public void drawScreen(int mouseX, int mouseY, float partialTicks) + { + drawRect(2, this.height - 14, this.width - 2, this.height - 2, Integer.MIN_VALUE); ++ this.suggestionList.drawGhostText(this.inputField, this.fontRenderer); + this.inputField.drawTextBox(); ++ this.suggestionList.drawCommandColor(this.inputField, this.fontRenderer); + ITextComponent itextcomponent = this.mc.ingameGUI.getChatGUI().getChatComponent(Mouse.getX(), Mouse.getY()); + + if (itextcomponent != null && itextcomponent.getStyle().getHoverEvent() != null) +@@ -209,6 +264,7 @@ + this.handleComponentHover(itextcomponent, mouseX, mouseY); + } + ++ this.suggestionList.render(this.inputField.x, this.inputField.y, this.inputField.width, mouseX, mouseY); + super.drawScreen(mouseX, mouseY, partialTicks); + } + +@@ -222,6 +278,7 @@ + public void setCompletions(String... newCompletions) + { + this.tabCompleter.setCompletions(newCompletions); ++ this.suggestionList.updateSuggestions(newCompletions); + } + + @SideOnly(Side.CLIENT) +@@ -232,29 +289,6 @@ + public ChatTabCompleter(GuiTextField p_i46749_1_) + { + super(p_i46749_1_, false); +- } +- +- @Override +- public void complete() +- { +- super.complete(); +- +- if (this.completions.size() > 1) +- { +- StringBuilder stringbuilder = new StringBuilder(); +- +- for (String s : this.completions) +- { +- if (stringbuilder.length() > 0) +- { +- stringbuilder.append(", "); +- } +- +- stringbuilder.append(s); +- } +- +- this.client.ingameGUI.getChatGUI().printChatMessageWithOptionalDeletion(new TextComponentString(stringbuilder.toString()), 1); +- } + } + + @Nullable diff --git a/patches/minecraft/net/minecraft/client/gui/GuiTextField.java.patch b/patches/minecraft/net/minecraft/client/gui/GuiTextField.java.patch index bdb0e09bd..284178533 100644 --- a/patches/minecraft/net/minecraft/client/gui/GuiTextField.java.patch +++ b/patches/minecraft/net/minecraft/client/gui/GuiTextField.java.patch @@ -7,7 +7,19 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import net.minecraft.client.Minecraft; -@@ -631,6 +632,7 @@ +@@ -603,6 +604,11 @@ + return this.cursorPosition; + } + ++ public int getLineScrollOffset() ++ { ++ return this.lineScrollOffset; ++ } ++ + public boolean getEnableBackgroundDrawing() + { + return this.enableBackgroundDrawing; +@@ -631,6 +637,7 @@ } this.isFocused = isFocusedIn; diff --git a/src/main/java/com/cleanroommc/client/chat/suggestion/SuggestionList.java b/src/main/java/com/cleanroommc/client/chat/suggestion/SuggestionList.java new file mode 100644 index 000000000..45d632588 --- /dev/null +++ b/src/main/java/com/cleanroommc/client/chat/suggestion/SuggestionList.java @@ -0,0 +1,253 @@ +package com.cleanroommc.client.chat.suggestion; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiTextField; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.client.ClientCommandHandler; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +@SideOnly(Side.CLIENT) +public class SuggestionList { + + private static final int MAX_VISIBLE = 10; + private static final int ENTRY_HEIGHT = 12; + private static final int PADDING_X = 3; + private static final Set knownCommands = new HashSet<>(); + + private final GuiTextField field; + private List suggestions = Collections.emptyList(); + private int selectedIndex = -1; + private int scrollOffset = 0; + private int cachedWidth = 0; + public SuggestionList(GuiTextField field) { + this.field = field; + knownCommands.addAll(ClientCommandHandler.instance.getCommands().keySet()); + } + + public void setSuggestions(List newSuggestions) { + this.suggestions = newSuggestions; + this.selectedIndex = -1; + this.scrollOffset = 0; + this.cachedWidth = newSuggestions.stream() + .mapToInt(Minecraft.getMinecraft().fontRenderer::getStringWidth) + .max() + .orElse(0) + PADDING_X * 2; + } + + public boolean isInvisible() { + return this.suggestions.isEmpty(); + } + + public boolean isVisible() { + return !this.suggestions.isEmpty(); + } + + public void hide() { + this.suggestions = Collections.emptyList(); + this.selectedIndex = -1; + this.scrollOffset = 0; + this.cachedWidth = 0; + } + + public void selectNext() { + if (this.isInvisible()) { + return; + } + this.selectedIndex = this.selectedIndex < this.suggestions.size() - 1 ? this.selectedIndex + 1 : -1; + this.clampScroll(); + } + + public void selectPrev() { + if (this.isInvisible()) { + return; + } + this.selectedIndex = this.selectedIndex == -1 ? this.suggestions.size() - 1 : Math.max(-1, this.selectedIndex - 1); + this.clampScroll(); + } + + private void clampScroll() { + if (this.selectedIndex == -1) { + this.scrollOffset = 0; + return; + } + if (this.selectedIndex < this.scrollOffset) { + this.scrollOffset = this.selectedIndex; + } else if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) { + this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1; + } + } + + public String getSelected() { + if (this.isVisible() && this.selectedIndex >= 0 && this.selectedIndex < this.suggestions.size()) { + return this.suggestions.get(this.selectedIndex); + } + return null; + } + + public String getFirst() { + return this.suggestions.isEmpty() ? null : this.suggestions.getFirst(); + } + + public void render(int inputX, int inputY, int inputWidth, int mouseX, int mouseY) { + if (this.isInvisible()) { + return; + } + Minecraft mc = Minecraft.getMinecraft(); + int visibleCount = Math.min(MAX_VISIBLE, this.suggestions.size()); + int listHeight = visibleCount * ENTRY_HEIGHT; + int listY = inputY - listHeight - 2; // bottom flush with the top of the input background rect + int listWidth = Math.min(cachedWidth, inputWidth); + Gui.drawRect(inputX, listY, inputX + listWidth, listY + listHeight, 0xC0000000); + for (int i = 0; i < visibleCount; i++) { + int idx = i + this.scrollOffset; + String text = this.suggestions.get(idx); + int entryY = listY + i * ENTRY_HEIGHT; + boolean selected = idx == selectedIndex; + boolean hovered = mouseX >= inputX && mouseX < inputX + listWidth && mouseY >= entryY && mouseY < entryY + ENTRY_HEIGHT; + mc.fontRenderer.drawStringWithShadow(text, inputX + PADDING_X, entryY + 2, (selected || hovered) ? 0xFFFF55 : 0xFFFFFF); + } + if (this.suggestions.size() > MAX_VISIBLE) { + int barX = inputX + listWidth; + int thumbHeight = Math.max(ENTRY_HEIGHT, listHeight * MAX_VISIBLE / this.suggestions.size()); + int maxScroll = this.suggestions.size() - MAX_VISIBLE; + int thumbY = listY + this.scrollOffset * (listHeight - thumbHeight) / maxScroll; + Gui.drawRect(barX, listY, barX + 2, listY + listHeight, 0xFF333333); + Gui.drawRect(barX, thumbY, barX + 2, thumbY + thumbHeight, 0xFFAAAAAA); + } + } + + public void updateSuggestions(String... serverCompletions) { + List list = new ArrayList<>(); + String[] clientComplete = ClientCommandHandler.instance.latestAutoComplete; + if (clientComplete != null) { + for (String s : clientComplete) { + if (!s.isEmpty()) { + list.add(s); + } + } + } + for (String s : serverCompletions) { + if (!s.isEmpty() && !list.contains(s)) { + list.add(s); + } + } + String currentText = this.field.getText(); + if (currentText.isEmpty() || (currentText.startsWith("/") && !currentText.contains(" "))) { + for (String s : list) { + String name = s.startsWith("/") ? s.substring(1) : s; + if (!name.isEmpty()) { + knownCommands.add(name); + } + } + } + if (currentText.isEmpty()) { + return; + } + this.setSuggestions(list); + } + + public void applySuggestion(GuiTextField inputField, String suggestion) { + int wordStart = inputField.getNthWordFromPosWS(-1, inputField.getCursorPosition(), false); + inputField.deleteFromCursor(wordStart - inputField.getCursorPosition()); + inputField.writeText(TextFormatting.getTextWithoutFormattingCodes(suggestion)); + this.hide(); + } + + public void drawGhostText(GuiTextField inputField, FontRenderer fontRenderer) { + if (this.isInvisible()) { + return; + } + String suggestion = this.getSelected(); + if (suggestion == null) { + suggestion = this.getFirst(); + } + int cursorPos = inputField.getCursorPosition(); + int wordStart = inputField.getNthWordFromPosWS(-1, cursorPos, false); + String typedWord = inputField.getText().substring(wordStart, cursorPos); + if (!suggestion.toLowerCase(Locale.ROOT).startsWith(typedWord.toLowerCase(Locale.ROOT))) { + return; + } + String suffix = suggestion.substring(typedWord.length()); + if (suffix.isEmpty()) { + return; + } + int ghostX = inputField.x + fontRenderer.getStringWidth(inputField.getText().substring(0, cursorPos)); + int fieldRight = inputField.x + inputField.width; + if (ghostX >= fieldRight) { + return; + } + while (suffix.length() > 1 && ghostX + fontRenderer.getStringWidth(suffix) > fieldRight) { + suffix = suffix.substring(0, suffix.length() - 1); + } + fontRenderer.drawString(suffix, ghostX, inputField.y, 0xFF808080); + } + + public void drawCommandColor(GuiTextField inputField, FontRenderer fontRenderer) { + String text = inputField.getText(); + if (!text.startsWith("/")) { + return; + } + int firstSpaceIdx = text.indexOf(' '); + String firstWord = firstSpaceIdx == -1 ? text : text.substring(0, firstSpaceIdx); + String cmdName = firstWord.substring(1); + if (cmdName.isEmpty()) { + return; + } + int scrollOffset = inputField.getLineScrollOffset(); + if (scrollOffset >= firstWord.length()) { + return; + } + int color = knownCommands.contains(cmdName) ? 0x55FF55 : 0xFF5555; + fontRenderer.drawStringWithShadow(firstWord.substring(scrollOffset), inputField.x, inputField.y, color); + } + + public boolean isMouseOver(int inputX, int inputY, int inputWidth, int mouseX, int mouseY) { + if (!this.isVisible()) { + return false; + } + int visibleCount = Math.min(MAX_VISIBLE, this.suggestions.size()); + int listHeight = visibleCount * ENTRY_HEIGHT; + int listY = inputY - listHeight - 2; + int listWidth = Math.min(this.cachedWidth, inputWidth); + return mouseX >= inputX && mouseX < inputX + listWidth && mouseY >= listY && mouseY < listY + listHeight; + } + + public void scroll(int wheelDelta) { + if (!this.isVisible() || this.suggestions.size() <= MAX_VISIBLE) { + return; + } + int maxOffset = this.suggestions.size() - MAX_VISIBLE; + this.scrollOffset = Math.clamp(this.scrollOffset + (wheelDelta > 0 ? -1 : 1), 0, maxOffset); + } + + public String mouseClicked(int inputX, int inputY, int inputWidth, int mouseX, int mouseY) { + if (!this.isVisible()) { + return null; + } + int visibleCount = Math.min(MAX_VISIBLE, this.suggestions.size()); + int listHeight = visibleCount * ENTRY_HEIGHT; + int listY = inputY - listHeight - 2; + int listWidth = Math.min(this.cachedWidth, inputWidth); + if (mouseX < inputX || mouseX >= inputX + listWidth || mouseY < listY || mouseY >= listY + listHeight) { + return null; + } + for (int i = 0; i < visibleCount; i++) { + int entryY = listY + i * ENTRY_HEIGHT; + if (mouseY >= entryY && mouseY < entryY + ENTRY_HEIGHT) { + return this.suggestions.get(i + scrollOffset); + } + } + return null; + } + +} diff --git a/src/main/java/com/cleanroommc/client/chat/suggestion/SuggestionUpdater.java b/src/main/java/com/cleanroommc/client/chat/suggestion/SuggestionUpdater.java new file mode 100644 index 000000000..7013d6132 --- /dev/null +++ b/src/main/java/com/cleanroommc/client/chat/suggestion/SuggestionUpdater.java @@ -0,0 +1,55 @@ +package com.cleanroommc.client.chat.suggestion; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiPageButtonList; +import net.minecraft.network.play.client.CPacketTabComplete; +import net.minecraft.util.TabCompleter; +import net.minecraftforge.client.ClientCommandHandler; + +/** + * GuiResponder for the {@link net.minecraft.client.gui.GuiTextField} in {@link net.minecraft.client.gui.GuiChat} + * so that text changes fires a server tab-complete request, populating the {@link SuggestionList} + */ +public class SuggestionUpdater implements GuiPageButtonList.GuiResponder { + + private final SuggestionList suggestionList; + private final TabCompleter tabCompleter; + + private String lastText = ""; + + public SuggestionUpdater(SuggestionList suggestionList, TabCompleter tabCompleter) { + this.suggestionList = suggestionList; + this.tabCompleter = tabCompleter; + Minecraft mc = Minecraft.getMinecraft(); + if (mc.player != null) { + mc.player.connection.sendPacket(new CPacketTabComplete("/", null, false)); + } + } + + @Override + public void setEntryValue(int id, boolean value) { } + + @Override + public void setEntryValue(int id, float value) { } + + @Override + public void setEntryValue(int id, String value) { + if (value.equals(this.lastText)) { + return; + } + this.lastText = value; + if (value.isEmpty()) { + this.suggestionList.hide(); + return; + } + Minecraft mc = Minecraft.getMinecraft(); + if (mc.player == null) { + return; + } + // Client-side commands + ClientCommandHandler.instance.autoComplete(value); + // Server-side completions + mc.player.connection.sendPacket(new CPacketTabComplete(value, this.tabCompleter.getTargetBlockPos(), false)); + } + +}