Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -81,6 +82,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 @@ -125,6 +127,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 @@ -499,6 +502,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