Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ public TextFieldHelperExtended(Supplier<String> getMessageFn, Consumer<String> s

public String getSelected() {
String value = this.getMessageFn.get();
int i = Math.min(this.getCursorPos(), this.getSelectionPos());
int j = Math.max(this.getCursorPos(), this.getSelectionPos());
// Positions may still refer to a previous, longer value
int i = Math.min(Math.min(this.getCursorPos(), this.getSelectionPos()), value.length());
int j = Math.min(Math.max(this.getCursorPos(), this.getSelectionPos()), value.length());
return value.substring(i, j);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.cyclops.integratedscripting.client.gui.component.input;

import javax.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.Deque;

/**
* Undo and redo history of a text area.
* @author rubensworks
*/
public class TextHistory {

public static final int MAX_SIZE = 500;
public static final long TYPING_GROUP_MILLIS = 1000;

private final Deque<State> undoStack = new ArrayDeque<>();
private final Deque<State> redoStack = new ArrayDeque<>();
private boolean lastWasTyping;
private long lastRecordTime;
private int lastTypingEnd = -1;

/**
* Record the state before an edit.
* Consecutive typing at the same position within a short time is grouped into a single undo step.
* @param before The state before the edit.
* @param typing If the edit was caused by typing a single character.
* @param time The current time in milliseconds.
*/
public void record(State before, boolean typing, long time) {
boolean group = typing && this.lastWasTyping && !this.undoStack.isEmpty()
&& time - this.lastRecordTime < TYPING_GROUP_MILLIS
&& Math.min(before.cursorPos(), before.selectionPos()) == this.lastTypingEnd;
if (!group) {
this.undoStack.push(before);
if (this.undoStack.size() > MAX_SIZE) {
this.undoStack.removeLast();
}
}
this.redoStack.clear();
this.lastWasTyping = typing;
this.lastRecordTime = time;
this.lastTypingEnd = typing ? Math.min(before.cursorPos(), before.selectionPos()) + 1 : -1;
}

/**
* @param current The current state, which will become redoable.
* @return The state to restore, or null if there is nothing to undo.
*/
@Nullable
public State undo(State current) {
if (this.undoStack.isEmpty()) {
return null;
}
this.redoStack.push(current);
this.lastWasTyping = false;
return this.undoStack.pop();
}

/**
* @param current The current state, which will become undoable.
* @return The state to restore, or null if there is nothing to redo.
*/
@Nullable
public State redo(State current) {
if (this.redoStack.isEmpty()) {
return null;
}
this.undoStack.push(current);
this.lastWasTyping = false;
return this.redoStack.pop();
}

public static record State(String value, int cursorPos, int selectionPos) {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public class WidgetTextArea extends EditBox implements GuiEventListener {
@Nullable
private WidgetScrollBar scrollBar;
private int firstRow;
private TextHistory history = new TextHistory();

public WidgetTextArea(Font font, int x, int y, int width, int height, Component narrationMessage, boolean scrollBar, boolean showLineNumbers) {
super(font, x, y, width, height, narrationMessage);
Expand Down Expand Up @@ -152,6 +153,10 @@ public void setListenerCursor(@Nullable IInputListener listenerCursor) {
this.listenerCursor = listenerCursor;
}

public void setHistory(TextHistory history) {
this.history = history;
}

public void setMarkupProvider(@Nullable IMarkupProvider markupProvider) {
this.markupProvider = markupProvider;
}
Expand Down Expand Up @@ -236,9 +241,8 @@ public void tick() {
public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) {
if (mouseButton == 1 && mouseX >= this.getX() && mouseX < this.getX() + this.width
&& mouseY >= this.getY() && mouseY < this.getY() + this.height) {
// Select everything
// Don't select everything on right-click, as typing would then replace the whole script
this.setFocused(true);
textFieldHelper.selectAll();
return true;
} else {
if (mouseButton == 0 && mouseX >= this.getX() && mouseX < this.getX() + this.width
Expand Down Expand Up @@ -312,8 +316,13 @@ public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, doubl

@Override
public boolean keyPressed(int typedChar, int keyCode, int modifiers) {
boolean flag = this.textFieldKeyPressed(typedChar, keyCode, modifiers);
TextHistory.State before = this.getState();
boolean history = this.historyKeyPressed(typedChar, modifiers);
boolean flag = history || this.textFieldKeyPressed(typedChar, keyCode, modifiers);
if (flag) {
if (!history) {
this.recordHistory(before, false);
}
this.clearDisplayCache();
this.setFocused(true);
this.ensureCursorVisible();
Expand All @@ -331,7 +340,9 @@ public boolean keyPressed(int typedChar, int keyCode, int modifiers) {
public boolean charTyped(char typedChar, int keyCode) {
// Don't delegate to EditBox, as that would consume the character into its own (unused) value
if (StringUtil.isAllowedChatCharacter(typedChar)) {
TextHistory.State before = this.getState();
this.textFieldHelper.insertText(Character.toString(typedChar));
this.recordHistory(before, true);
this.clearDisplayCache();
this.setFocused(true);
this.ensureCursorVisible();
Expand All @@ -341,6 +352,56 @@ public boolean charTyped(char typedChar, int keyCode) {
}
}

private TextHistory.State getState() {
return new TextHistory.State(this.getValue(), this.textFieldHelper.getCursorPos(), this.textFieldHelper.getSelectionPos());
}

private void setState(TextHistory.State state) {
this.setValuePassive(state.value());
this.textFieldHelper.setSelectionRange(state.cursorPos(), state.selectionPos());
}

private void recordHistory(TextHistory.State before, boolean typing) {
if (!before.value().equals(this.getValue())) {
this.history.record(before, typing, Util.getMillis());
}
}

/**
* Replace the value as an undoable edit, keeping the cursor position where possible.
* @param value The new value.
*/
public void replaceValue(String value) {
TextHistory.State before = this.getState();
this.setState(new TextHistory.State(value, before.cursorPos(), before.selectionPos()));
this.recordHistory(before, false);
this.clearDisplayCache();
}

// Modifiers are read from the key event instead of the global keyboard state
private static boolean hasControlDown(int modifiers) {
return (modifiers & (Minecraft.ON_OSX ? GLFW.GLFW_MOD_SUPER : GLFW.GLFW_MOD_CONTROL)) != 0;
}

private boolean historyKeyPressed(int key, int modifiers) {
if (!hasControlDown(modifiers)) {
return false;
}
boolean shift = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0;
TextHistory.State state;
if (key == GLFW.GLFW_KEY_Z && !shift) {
state = this.history.undo(this.getState());
} else if (key == GLFW.GLFW_KEY_Y || (key == GLFW.GLFW_KEY_Z && shift)) {
state = this.history.redo(this.getState());
} else {
return false;
}
if (state != null) {
this.setState(state);
}
return true;
}

private boolean textFieldKeyPressed(int p_98153_, int p_98154_, int p_98155_) {
if (Screen.isSelectAll(p_98153_)) {
this.textFieldHelper.selectAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.cyclops.integratedscripting.IntegratedScripting;
import org.cyclops.integratedscripting.Reference;
import org.cyclops.integratedscripting.api.language.ILanguageHandler;
import org.cyclops.integratedscripting.client.gui.component.input.TextHistory;
import org.cyclops.integratedscripting.client.gui.component.input.WidgetDialog;
import org.cyclops.integratedscripting.client.gui.component.input.WidgetTextArea;
import org.cyclops.integratedscripting.client.gui.image.ScriptImages;
Expand Down Expand Up @@ -80,6 +81,7 @@ public class ContainerScreenTerminalScripting extends ContainerScreenExtended<Co
private final Map<Path, Integer> lastScriptCursorPos = Maps.newHashMap();
private final Map<Path, Integer> lastScriptSelectionPos = Maps.newHashMap();
private final Int2ObjectMap<Path> lastDiskScript = new Int2ObjectAVLTreeMap<>();
private final Map<Pair<Integer, Path>, TextHistory> scriptHistories = Maps.newHashMap();

public ContainerScreenTerminalScripting(ContainerTerminalScripting container, Inventory inventory, Component title) {
super(container, inventory, title);
Expand Down Expand Up @@ -126,6 +128,7 @@ public int getTotalRows() {

textArea = new WidgetTextArea(Minecraft.getInstance().font, this.leftPos + SCRIPT_X + 1, this.topPos + SCRIPT_Y + 1, SCRIPT_WIDTH, SCRIPT_HEIGHT, Component.translatable("gui.integratedscripting.script"), true, true);
textArea.setListener(this::onActiveScriptModified);
getMenu().setRemoteScriptChangeListener(this::onRemoteScriptChanged);
textArea.setListenerSelection(this::onSelectionModified);
textArea.setListenerCursor(this::onSelectionModified);
textArea.setMarkupProvider((style, line) -> {
Expand Down Expand Up @@ -463,6 +466,18 @@ public void setFirstRow(int firstRow) {
private void onActiveScriptSelected() {
String script = getMenu().getActiveScript();
this.textArea.setValue(script == null ? "" : script);
Path path = getMenu().getActiveScriptPath();
this.textArea.setHistory(path == null ? new TextHistory() : scriptHistories.computeIfAbsent(Pair.of(getMenu().getActiveDisk(), path), k -> new TextHistory()));
}

private void onRemoteScriptChanged(int disk, Path path) {
// Show changes from outside this editor, such as external file edits, instead of overwriting them on the next edit
if (disk == getMenu().getActiveDisk() && path.equals(getMenu().getActiveScriptPath())) {
String script = getMenu().getActiveScript();
if (script != null && !script.equals(this.textArea.getValue())) {
this.textArea.replaceValue(script);
}
}
}

private void onActiveScriptModified() {
Expand Down
Loading
Loading