MultilineTextField.java

net.minecraft.client.gui.components.MultilineTextField

信息

  • 全限定名:net.minecraft.client.gui.components.MultilineTextField
  • 类型:public class
  • 包:net.minecraft.client.gui.components
  • 源码路径:src/main/java/net/minecraft/client/gui/components/MultilineTextField.java
  • 起始行号:L19
  • 职责:

    TODO

字段/常量

  • LOGGER

    • 类型: Logger
    • 修饰符: private static final
    • 源码定位: L20
    • 说明:

      TODO

  • NO_LIMIT

    • 类型: int
    • 修饰符: public static final
    • 源码定位: L21
    • 说明:

      TODO

  • LINE_SEEK_PIXEL_BIAS

    • 类型: int
    • 修饰符: private static final
    • 源码定位: L22
    • 说明:

      TODO

  • font

    • 类型: Font
    • 修饰符: private final
    • 源码定位: L23
    • 说明:

      TODO

  • displayLines

    • 类型: List<MultilineTextField.StringView>
    • 修饰符: private final
    • 源码定位: L24
    • 说明:

      TODO

  • value

    • 类型: String
    • 修饰符: private
    • 源码定位: L25
    • 说明:

      TODO

  • cursor

    • 类型: int
    • 修饰符: private
    • 源码定位: L26
    • 说明:

      TODO

  • selectCursor

    • 类型: int
    • 修饰符: private
    • 源码定位: L27
    • 说明:

      TODO

  • selecting

    • 类型: boolean
    • 修饰符: private
    • 源码定位: L28
    • 说明:

      TODO

  • characterLimit

    • 类型: int
    • 修饰符: private
    • 源码定位: L29
    • 说明:

      TODO

  • lineLimit

    • 类型: int
    • 修饰符: private
    • 源码定位: L30
    • 说明:

      TODO

  • width

    • 类型: int
    • 修饰符: private final
    • 源码定位: L31
    • 说明:

      TODO

  • valueListener

    • 类型: Consumer<String>
    • 修饰符: private
    • 源码定位: L32
    • 说明:

      TODO

  • cursorListener

    • 类型: Runnable
    • 修饰符: private
    • 源码定位: L33
    • 说明:

      TODO

内部类/嵌套类型

  • net.minecraft.client.gui.components.MultilineTextField.StringView
    • 类型: record
    • 修饰符: protected
    • 源码定位: L407
    • 说明:

      TODO

构造器

public MultilineTextField(Font font, int width) @ L35

  • 构造器名:MultilineTextField
  • 源码定位:L35
  • 修饰符:public

参数:

  • font: Font
  • width: int

说明:

TODO

方法

下面的方法块按源码顺序生成。

public int characterLimit() @ L41

  • 方法名:characterLimit
  • 源码定位:L41
  • 返回类型:int
  • 修饰符:public

参数:

说明:

TODO

public void setCharacterLimit(int characterLimit) @ L45

  • 方法名:setCharacterLimit
  • 源码定位:L45
  • 返回类型:void
  • 修饰符:public

参数:

  • characterLimit: int

说明:

TODO

public void setLineLimit(int lineLimit) @ L53

  • 方法名:setLineLimit
  • 源码定位:L53
  • 返回类型:void
  • 修饰符:public

参数:

  • lineLimit: int

说明:

TODO

public boolean hasCharacterLimit() @ L61

  • 方法名:hasCharacterLimit
  • 源码定位:L61
  • 返回类型:boolean
  • 修饰符:public

参数:

说明:

TODO

public boolean hasLineLimit() @ L65

  • 方法名:hasLineLimit
  • 源码定位:L65
  • 返回类型:boolean
  • 修饰符:public

参数:

说明:

TODO

public void setValueListener(Consumer<String> valueListener) @ L69

  • 方法名:setValueListener
  • 源码定位:L69
  • 返回类型:void
  • 修饰符:public

参数:

  • valueListener: Consumer

说明:

TODO

public void setCursorListener(Runnable cursorListener) @ L73

  • 方法名:setCursorListener
  • 源码定位:L73
  • 返回类型:void
  • 修饰符:public

参数:

  • cursorListener: Runnable

说明:

TODO

public void setValue(String value) @ L77

  • 方法名:setValue
  • 源码定位:L77
  • 返回类型:void
  • 修饰符:public

参数:

  • value: String

说明:

TODO

public void setValue(String value, boolean allowOverflowLineLimit) @ L81

  • 方法名:setValue
  • 源码定位:L81
  • 返回类型:void
  • 修饰符:public

参数:

  • value: String
  • allowOverflowLineLimit: boolean

说明:

TODO

public String value() @ L91

  • 方法名:value
  • 源码定位:L91
  • 返回类型:String
  • 修饰符:public

参数:

说明:

TODO

public void insertText(String input) @ L95

  • 方法名:insertText
  • 源码定位:L95
  • 返回类型:void
  • 修饰符:public

参数:

  • input: String

说明:

TODO

public void deleteText(int dir) @ L109

  • 方法名:deleteText
  • 源码定位:L109
  • 返回类型:void
  • 修饰符:public

参数:

  • dir: int

说明:

TODO

public int cursor() @ L117

  • 方法名:cursor
  • 源码定位:L117
  • 返回类型:int
  • 修饰符:public

参数:

说明:

TODO

public void setSelecting(boolean selecting) @ L121

  • 方法名:setSelecting
  • 源码定位:L121
  • 返回类型:void
  • 修饰符:public

参数:

  • selecting: boolean

说明:

TODO

public MultilineTextField.StringView getSelected() @ L125

  • 方法名:getSelected
  • 源码定位:L125
  • 返回类型:MultilineTextField.StringView
  • 修饰符:public

参数:

说明:

TODO

public int getLineCount() @ L129

  • 方法名:getLineCount
  • 源码定位:L129
  • 返回类型:int
  • 修饰符:public

参数:

说明:

TODO

public int getLineAtCursor() @ L133

  • 方法名:getLineAtCursor
  • 源码定位:L133
  • 返回类型:int
  • 修饰符:public

参数:

说明:

TODO

public MultilineTextField.StringView getLineView(int lineIndex) @ L144

  • 方法名:getLineView
  • 源码定位:L144
  • 返回类型:MultilineTextField.StringView
  • 修饰符:public

参数:

  • lineIndex: int

说明:

TODO

public void seekCursor(Whence whence, int cursor) @ L148

  • 方法名:seekCursor
  • 源码定位:L148
  • 返回类型:void
  • 修饰符:public

参数:

  • whence: Whence
  • cursor: int

说明:

TODO

public void seekCursorLine(int lineOffset) @ L167

  • 方法名:seekCursorLine
  • 源码定位:L167
  • 返回类型:void
  • 修饰符:public

参数:

  • lineOffset: int

说明:

TODO

public void seekCursorToPoint(double x, double y) @ L176

  • 方法名:seekCursorToPoint
  • 源码定位:L176
  • 返回类型:void
  • 修饰符:public

参数:

  • x: double
  • y: double

说明:

TODO

public void selectWordAtCursor() @ L184

  • 方法名:selectWordAtCursor
  • 源码定位:L184
  • 返回类型:void
  • 修饰符:public

参数:

说明:

TODO

public boolean keyPressed(KeyEvent event) @ L191

  • 方法名:keyPressed
  • 源码定位:L191
  • 返回类型:boolean
  • 修饰符:public

参数:

  • event: KeyEvent

说明:

TODO

public Iterable<MultilineTextField.StringView> iterateLines() @ L289

  • 方法名:iterateLines
  • 源码定位:L289
  • 返回类型:Iterable<MultilineTextField.StringView>
  • 修饰符:public

参数:

说明:

TODO

public boolean hasSelection() @ L293

  • 方法名:hasSelection
  • 源码定位:L293
  • 返回类型:boolean
  • 修饰符:public

参数:

说明:

TODO

public String getSelectedText() @ L297

  • 方法名:getSelectedText
  • 源码定位:L297
  • 返回类型:String
  • 修饰符:public

参数:

说明:

TODO

private MultilineTextField.StringView getCursorLineView() @ L303

  • 方法名:getCursorLineView
  • 源码定位:L303
  • 返回类型:MultilineTextField.StringView
  • 修饰符:private

参数:

说明:

TODO

private MultilineTextField.StringView getCursorLineView(int lineOffset) @ L307

  • 方法名:getCursorLineView
  • 源码定位:L307
  • 返回类型:MultilineTextField.StringView
  • 修饰符:private

参数:

  • lineOffset: int

说明:

TODO

public MultilineTextField.StringView getPreviousWord() @ L317

  • 方法名:getPreviousWord
  • 源码定位:L317
  • 返回类型:MultilineTextField.StringView
  • 修饰符:public

参数:

说明:

TODO

public MultilineTextField.StringView getNextWord() @ L336

  • 方法名:getNextWord
  • 源码定位:L336
  • 返回类型:MultilineTextField.StringView
  • 修饰符:public

参数:

说明:

TODO

private int getWordEndPosition(int from) @ L355

  • 方法名:getWordEndPosition
  • 源码定位:L355
  • 返回类型:int
  • 修饰符:private

参数:

  • from: int

说明:

TODO

private void onValueChange() @ L365

  • 方法名:onValueChange
  • 源码定位:L365
  • 返回类型:void
  • 修饰符:private

参数:

说明:

TODO

private void reflowDisplayLines() @ L371

  • 方法名:reflowDisplayLines
  • 源码定位:L371
  • 返回类型:void
  • 修饰符:private

参数:

说明:

TODO

private String truncateFullText(String input) @ L387

  • 方法名:truncateFullText
  • 源码定位:L387
  • 返回类型:String
  • 修饰符:private

参数:

  • input: String

说明:

TODO

private String truncateInsertionText(String input) @ L391

  • 方法名:truncateInsertionText
  • 源码定位:L391
  • 返回类型:String
  • 修饰符:private

参数:

  • input: String

说明:

TODO

private boolean overflowsLineLimit(String newValue) @ L401

  • 方法名:overflowsLineLimit
  • 源码定位:L401
  • 返回类型:boolean
  • 修饰符:private

参数:

  • newValue: String

说明:

TODO

代码

@OnlyIn(Dist.CLIENT)
public class MultilineTextField {
    private static final Logger LOGGER = LogUtils.getLogger();
    public static final int NO_LIMIT = Integer.MAX_VALUE;
    private static final int LINE_SEEK_PIXEL_BIAS = 2;
    private final Font font;
    private final List<MultilineTextField.StringView> displayLines = Lists.newArrayList();
    private String value;
    private int cursor;
    private int selectCursor;
    private boolean selecting;
    private int characterLimit = Integer.MAX_VALUE;
    private int lineLimit = Integer.MAX_VALUE;
    private final int width;
    private Consumer<String> valueListener = s -> {};
    private Runnable cursorListener = () -> {};
 
    public MultilineTextField(Font font, int width) {
        this.font = font;
        this.width = width;
        this.setValue("");
    }
 
    public int characterLimit() {
        return this.characterLimit;
    }
 
    public void setCharacterLimit(int characterLimit) {
        if (characterLimit < 0) {
            throw new IllegalArgumentException("Character limit cannot be negative");
        } else {
            this.characterLimit = characterLimit;
        }
    }
 
    public void setLineLimit(int lineLimit) {
        if (lineLimit < 0) {
            throw new IllegalArgumentException("Character limit cannot be negative");
        } else {
            this.lineLimit = lineLimit;
        }
    }
 
    public boolean hasCharacterLimit() {
        return this.characterLimit != Integer.MAX_VALUE;
    }
 
    public boolean hasLineLimit() {
        return this.lineLimit != Integer.MAX_VALUE;
    }
 
    public void setValueListener(Consumer<String> valueListener) {
        this.valueListener = valueListener;
    }
 
    public void setCursorListener(Runnable cursorListener) {
        this.cursorListener = cursorListener;
    }
 
    public void setValue(String value) {
        this.setValue(value, false);
    }
 
    public void setValue(String value, boolean allowOverflowLineLimit) {
        String newValue = this.truncateFullText(value);
        if (allowOverflowLineLimit || !this.overflowsLineLimit(newValue)) {
            this.value = newValue;
            this.cursor = this.value.length();
            this.selectCursor = this.cursor;
            this.onValueChange();
        }
    }
 
    public String value() {
        return this.value;
    }
 
    public void insertText(String input) {
        if (!input.isEmpty() || this.hasSelection()) {
            String text = this.truncateInsertionText(StringUtil.filterText(input, true));
            MultilineTextField.StringView selected = this.getSelected();
            String newValue = new StringBuilder(this.value).replace(selected.beginIndex, selected.endIndex, text).toString();
            if (!this.overflowsLineLimit(newValue)) {
                this.value = newValue;
                this.cursor = selected.beginIndex + text.length();
                this.selectCursor = this.cursor;
                this.onValueChange();
            }
        }
    }
 
    public void deleteText(int dir) {
        if (!this.hasSelection()) {
            this.selectCursor = Mth.clamp(this.cursor + dir, 0, this.value.length());
        }
 
        this.insertText("");
    }
 
    public int cursor() {
        return this.cursor;
    }
 
    public void setSelecting(boolean selecting) {
        this.selecting = selecting;
    }
 
    public MultilineTextField.StringView getSelected() {
        return new MultilineTextField.StringView(Math.min(this.selectCursor, this.cursor), Math.max(this.selectCursor, this.cursor));
    }
 
    public int getLineCount() {
        return this.displayLines.size();
    }
 
    public int getLineAtCursor() {
        for (int i = 0; i < this.displayLines.size(); i++) {
            MultilineTextField.StringView view = this.displayLines.get(i);
            if (this.cursor >= view.beginIndex && this.cursor <= view.endIndex) {
                return i;
            }
        }
 
        return -1;
    }
 
    public MultilineTextField.StringView getLineView(int lineIndex) {
        return this.displayLines.get(Mth.clamp(lineIndex, 0, this.displayLines.size() - 1));
    }
 
    public void seekCursor(Whence whence, int cursor) {
        switch (whence) {
            case ABSOLUTE:
                this.cursor = cursor;
                break;
            case RELATIVE:
                this.cursor += cursor;
                break;
            case END:
                this.cursor = this.value.length() + cursor;
        }
 
        this.cursor = Mth.clamp(this.cursor, 0, this.value.length());
        this.cursorListener.run();
        if (!this.selecting) {
            this.selectCursor = this.cursor;
        }
    }
 
    public void seekCursorLine(int lineOffset) {
        if (lineOffset != 0) {
            int oldCursorLeft = this.font.width(this.value.substring(this.getCursorLineView().beginIndex, this.cursor)) + 2;
            MultilineTextField.StringView lineView = this.getCursorLineView(lineOffset);
            int newCursor = this.font.plainSubstrByWidth(this.value.substring(lineView.beginIndex, lineView.endIndex), oldCursorLeft).length();
            this.seekCursor(Whence.ABSOLUTE, lineView.beginIndex + newCursor);
        }
    }
 
    public void seekCursorToPoint(double x, double y) {
        int left = Mth.floor(x);
        int top = Mth.floor(y / 9.0);
        MultilineTextField.StringView lineView = this.displayLines.get(Mth.clamp(top, 0, this.displayLines.size() - 1));
        int clickedColumn = this.font.plainSubstrByWidth(this.value.substring(lineView.beginIndex, lineView.endIndex), left).length();
        this.seekCursor(Whence.ABSOLUTE, lineView.beginIndex + clickedColumn);
    }
 
    public void selectWordAtCursor() {
        MultilineTextField.StringView wordView = this.getPreviousWord();
        this.seekCursor(Whence.ABSOLUTE, wordView.beginIndex);
        this.setSelecting(true);
        this.seekCursor(Whence.ABSOLUTE, wordView.endIndex);
    }
 
    public boolean keyPressed(KeyEvent event) {
        this.selecting = event.hasShiftDown();
        if (event.isSelectAll()) {
            this.cursor = this.value.length();
            this.selectCursor = 0;
            return true;
        } else if (event.isCopy()) {
            Minecraft.getInstance().keyboardHandler.setClipboard(this.getSelectedText());
            return true;
        } else if (event.isPaste()) {
            this.insertText(Minecraft.getInstance().keyboardHandler.getClipboard());
            return true;
        } else if (event.isCut()) {
            Minecraft.getInstance().keyboardHandler.setClipboard(this.getSelectedText());
            this.insertText("");
            return true;
        } else {
            switch (event.key()) {
                case 257:
                case 335:
                    this.insertText("\n");
                    return true;
                case 259:
                    if (event.hasControlDownWithQuirk()) {
                        MultilineTextField.StringView wordView = this.getPreviousWord();
                        this.deleteText(wordView.beginIndex - this.cursor);
                    } else {
                        this.deleteText(-1);
                    }
 
                    return true;
                case 261:
                    if (event.hasControlDownWithQuirk()) {
                        MultilineTextField.StringView wordView = this.getNextWord();
                        this.deleteText(wordView.beginIndex - this.cursor);
                    } else {
                        this.deleteText(1);
                    }
 
                    return true;
                case 262:
                    if (event.hasControlDownWithQuirk()) {
                        MultilineTextField.StringView wordView = this.getNextWord();
                        this.seekCursor(Whence.ABSOLUTE, wordView.beginIndex);
                    } else {
                        this.seekCursor(Whence.RELATIVE, 1);
                    }
 
                    return true;
                case 263:
                    if (event.hasControlDownWithQuirk()) {
                        MultilineTextField.StringView wordView = this.getPreviousWord();
                        this.seekCursor(Whence.ABSOLUTE, wordView.beginIndex);
                    } else {
                        this.seekCursor(Whence.RELATIVE, -1);
                    }
 
                    return true;
                case 264:
                    if (!event.hasControlDownWithQuirk()) {
                        this.seekCursorLine(1);
                    }
 
                    return true;
                case 265:
                    if (!event.hasControlDownWithQuirk()) {
                        this.seekCursorLine(-1);
                    }
 
                    return true;
                case 266:
                    this.seekCursor(Whence.ABSOLUTE, 0);
                    return true;
                case 267:
                    this.seekCursor(Whence.END, 0);
                    return true;
                case 268:
                    if (event.hasControlDownWithQuirk()) {
                        this.seekCursor(Whence.ABSOLUTE, 0);
                    } else {
                        this.seekCursor(Whence.ABSOLUTE, this.getCursorLineView().beginIndex);
                    }
 
                    return true;
                case 269:
                    if (event.hasControlDownWithQuirk()) {
                        this.seekCursor(Whence.END, 0);
                    } else {
                        this.seekCursor(Whence.ABSOLUTE, this.getCursorLineView().endIndex);
                    }
 
                    return true;
                default:
                    return false;
            }
        }
    }
 
    public Iterable<MultilineTextField.StringView> iterateLines() {
        return this.displayLines;
    }
 
    public boolean hasSelection() {
        return this.selectCursor != this.cursor;
    }
 
    @VisibleForTesting
    public String getSelectedText() {
        MultilineTextField.StringView selected = this.getSelected();
        return this.value.substring(selected.beginIndex, selected.endIndex);
    }
 
    private MultilineTextField.StringView getCursorLineView() {
        return this.getCursorLineView(0);
    }
 
    private MultilineTextField.StringView getCursorLineView(int lineOffset) {
        int lineIndex = this.getLineAtCursor();
        if (lineIndex < 0) {
            LOGGER.error("Cursor is not within text (cursor = {}, length = {})", this.cursor, this.value.length());
            return this.displayLines.getLast();
        } else {
            return this.displayLines.get(Mth.clamp(lineIndex + lineOffset, 0, this.displayLines.size() - 1));
        }
    }
 
    @VisibleForTesting
    public MultilineTextField.StringView getPreviousWord() {
        if (this.value.isEmpty()) {
            return MultilineTextField.StringView.EMPTY;
        } else {
            int startPosition = Mth.clamp(this.cursor, 0, this.value.length() - 1);
 
            while (startPosition > 0 && Character.isWhitespace(this.value.charAt(startPosition - 1))) {
                startPosition--;
            }
 
            while (startPosition > 0 && !Character.isWhitespace(this.value.charAt(startPosition - 1))) {
                startPosition--;
            }
 
            return new MultilineTextField.StringView(startPosition, this.getWordEndPosition(startPosition));
        }
    }
 
    @VisibleForTesting
    public MultilineTextField.StringView getNextWord() {
        if (this.value.isEmpty()) {
            return MultilineTextField.StringView.EMPTY;
        } else {
            int startPosition = Mth.clamp(this.cursor, 0, this.value.length() - 1);
 
            while (startPosition < this.value.length() && !Character.isWhitespace(this.value.charAt(startPosition))) {
                startPosition++;
            }
 
            while (startPosition < this.value.length() && Character.isWhitespace(this.value.charAt(startPosition))) {
                startPosition++;
            }
 
            return new MultilineTextField.StringView(startPosition, this.getWordEndPosition(startPosition));
        }
    }
 
    private int getWordEndPosition(int from) {
        int end = from;
 
        while (end < this.value.length() && !Character.isWhitespace(this.value.charAt(end))) {
            end++;
        }
 
        return end;
    }
 
    private void onValueChange() {
        this.reflowDisplayLines();
        this.valueListener.accept(this.value);
        this.cursorListener.run();
    }
 
    private void reflowDisplayLines() {
        this.displayLines.clear();
        if (this.value.isEmpty()) {
            this.displayLines.add(MultilineTextField.StringView.EMPTY);
        } else {
            this.font
                .getSplitter()
                .splitLines(
                    this.value, this.width, Style.EMPTY, false, (style, start, end) -> this.displayLines.add(new MultilineTextField.StringView(start, end))
                );
            if (this.value.charAt(this.value.length() - 1) == '\n') {
                this.displayLines.add(new MultilineTextField.StringView(this.value.length(), this.value.length()));
            }
        }
    }
 
    private String truncateFullText(String input) {
        return this.hasCharacterLimit() ? StringUtil.truncateStringIfNecessary(input, this.characterLimit, false) : input;
    }
 
    private String truncateInsertionText(String input) {
        String truncatedInput = input;
        if (this.hasCharacterLimit()) {
            int remainingCharacters = this.characterLimit - this.value.length();
            truncatedInput = StringUtil.truncateStringIfNecessary(input, remainingCharacters, false);
        }
 
        return truncatedInput;
    }
 
    private boolean overflowsLineLimit(String newValue) {
        return this.hasLineLimit()
            && this.font.getSplitter().splitLines(newValue, this.width, Style.EMPTY).size() + (StringUtil.endsWithNewLine(newValue) ? 1 : 0) > this.lineLimit;
    }
 
    @OnlyIn(Dist.CLIENT)
    protected record StringView(int beginIndex, int endIndex) {
        private static final MultilineTextField.StringView EMPTY = new MultilineTextField.StringView(0, 0);
    }
}

引用的其他类

  • Minecraft

    • 引用位置: 方法调用
    • 关联成员: Minecraft.getInstance()
  • Font

    • 引用位置: 参数/字段
  • Whence

    • 引用位置: 参数
  • KeyEvent

    • 引用位置: 参数
  • Mth

    • 引用位置: 方法调用
    • 关联成员: Mth.clamp(), Mth.floor()
  • StringUtil

    • 引用位置: 方法调用
    • 关联成员: StringUtil.endsWithNewLine(), StringUtil.filterText(), StringUtil.truncateStringIfNecessary()