ChatboxTextInput: support line wrapping
This commit is contained in:
@@ -24,7 +24,10 @@
|
|||||||
*/
|
*/
|
||||||
package net.runelite.client.game.chatbox;
|
package net.runelite.client.game.chatbox;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import java.awt.Point;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.datatransfer.DataFlavor;
|
import java.awt.datatransfer.DataFlavor;
|
||||||
@@ -33,11 +36,15 @@ import java.awt.datatransfer.UnsupportedFlavorException;
|
|||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.IntPredicate;
|
import java.util.function.IntPredicate;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.ToIntFunction;
|
import java.util.function.ToIntFunction;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.runelite.api.FontID;
|
import net.runelite.api.FontID;
|
||||||
@@ -57,6 +64,7 @@ import net.runelite.client.util.Text;
|
|||||||
public class ChatboxTextInput extends ChatboxInput implements KeyListener, MouseListener
|
public class ChatboxTextInput extends ChatboxInput implements KeyListener, MouseListener
|
||||||
{
|
{
|
||||||
private static final int CURSOR_FLASH_RATE_MILLIS = 1000;
|
private static final int CURSOR_FLASH_RATE_MILLIS = 1000;
|
||||||
|
private static final Pattern BREAK_MATCHER = Pattern.compile("[^a-zA-Z0-9']");
|
||||||
|
|
||||||
private final ChatboxPanelManager chatboxPanelManager;
|
private final ChatboxPanelManager chatboxPanelManager;
|
||||||
private final ClientThread clientThread;
|
private final ClientThread clientThread;
|
||||||
@@ -66,9 +74,20 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse
|
|||||||
return i -> i >= 32 && i < 127;
|
return i -> i >= 32 && i < 127;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class Line
|
||||||
|
{
|
||||||
|
private final int start;
|
||||||
|
private final int end;
|
||||||
|
private final String text;
|
||||||
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private String prompt;
|
private String prompt;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private int lines;
|
||||||
|
|
||||||
private StringBuffer value = new StringBuffer();
|
private StringBuffer value = new StringBuffer();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -98,9 +117,9 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse
|
|||||||
@Getter
|
@Getter
|
||||||
private boolean built = false;
|
private boolean built = false;
|
||||||
|
|
||||||
// This is a lambda so I can have atomic updates for it's captures
|
// These are lambdas for atomic updates
|
||||||
private ToIntFunction<MouseEvent> getCharOffset = null;
|
|
||||||
private Predicate<MouseEvent> isInBounds = null;
|
private Predicate<MouseEvent> isInBounds = null;
|
||||||
|
private ToIntFunction<MouseEvent> getCharOffset = null;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected ChatboxTextInput(ChatboxPanelManager chatboxPanelManager, ClientThread clientThread)
|
protected ChatboxTextInput(ChatboxPanelManager chatboxPanelManager, ClientThread clientThread)
|
||||||
@@ -109,6 +128,16 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse
|
|||||||
this.clientThread = clientThread;
|
this.clientThread = clientThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChatboxTextInput lines(int lines)
|
||||||
|
{
|
||||||
|
this.lines = lines;
|
||||||
|
if (built)
|
||||||
|
{
|
||||||
|
clientThread.invoke(this::update);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ChatboxTextInput prompt(String prompt)
|
public ChatboxTextInput prompt(String prompt)
|
||||||
{
|
{
|
||||||
this.prompt = prompt;
|
this.prompt = prompt;
|
||||||
@@ -232,103 +261,209 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse
|
|||||||
|
|
||||||
protected void buildEdit(int x, int y, int w, int h)
|
protected void buildEdit(int x, int y, int w, int h)
|
||||||
{
|
{
|
||||||
|
final List<Line> editLines = new ArrayList<>();
|
||||||
|
|
||||||
Widget container = chatboxPanelManager.getContainerWidget();
|
Widget container = chatboxPanelManager.getContainerWidget();
|
||||||
|
|
||||||
String lt = Text.escapeJagex(value.substring(0, this.cursorStart));
|
final Widget cursor = container.createChild(-1, WidgetType.RECTANGLE);
|
||||||
String mt = Text.escapeJagex(value.substring(this.cursorStart, this.cursorEnd));
|
long start = System.currentTimeMillis();
|
||||||
String rt = Text.escapeJagex(value.substring(this.cursorEnd));
|
cursor.setOnTimerListener((JavaScriptCallback) ev ->
|
||||||
|
{
|
||||||
Widget leftText = container.createChild(-1, WidgetType.TEXT);
|
boolean on = (System.currentTimeMillis() - start) % CURSOR_FLASH_RATE_MILLIS > (CURSOR_FLASH_RATE_MILLIS / 2);
|
||||||
Widget cursor = container.createChild(-1, WidgetType.RECTANGLE);
|
cursor.setOpacity(on ? 255 : 0);
|
||||||
Widget middleText = container.createChild(-1, WidgetType.TEXT);
|
});
|
||||||
Widget rightText = container.createChild(-1, WidgetType.TEXT);
|
cursor.setTextColor(0xFFFFFF);
|
||||||
|
cursor.setHasListener(true);
|
||||||
leftText.setFontId(fontID);
|
cursor.setFilled(true);
|
||||||
FontTypeFace font = leftText.getFont();
|
cursor.setFontId(fontID);
|
||||||
|
|
||||||
|
FontTypeFace font = cursor.getFont();
|
||||||
if (h <= 0)
|
if (h <= 0)
|
||||||
{
|
{
|
||||||
h = font.getBaseline();
|
h = font.getBaseline();
|
||||||
}
|
}
|
||||||
|
|
||||||
int ltw = font.getTextWidth(lt);
|
final int oy = y;
|
||||||
int mtw = font.getTextWidth(mt);
|
final int ox = x;
|
||||||
int rtw = font.getTextWidth(rt);
|
final int oh = h;
|
||||||
|
|
||||||
int fullWidth = ltw + mtw + rtw;
|
int breakIndex = -1;
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
int ox = x;
|
for (int i = 0; i < value.length(); i++)
|
||||||
if (w > 0)
|
|
||||||
{
|
{
|
||||||
x += (w - fullWidth) / 2;
|
int count = i - sb.length();
|
||||||
}
|
final String c = value.charAt(i) + "";
|
||||||
|
sb.append(c);
|
||||||
int ltx = x;
|
if (BREAK_MATCHER.matcher(c).matches())
|
||||||
int mtx = ltx + ltw;
|
|
||||||
int rtx = mtx + mtw;
|
|
||||||
|
|
||||||
leftText.setText(lt);
|
|
||||||
leftText.setOriginalX(ltx);
|
|
||||||
leftText.setOriginalY(y);
|
|
||||||
leftText.setOriginalWidth(ltw);
|
|
||||||
leftText.setOriginalHeight(h);
|
|
||||||
leftText.revalidate();
|
|
||||||
|
|
||||||
if (!mt.isEmpty())
|
|
||||||
{
|
|
||||||
cursor.setTextColor(0x113399);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cursor.setTextColor(0xFFFFFF);
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
cursor.setOnTimerListener((JavaScriptCallback) ev ->
|
|
||||||
{
|
{
|
||||||
boolean on = (System.currentTimeMillis() - start) % CURSOR_FLASH_RATE_MILLIS > (CURSOR_FLASH_RATE_MILLIS / 2);
|
breakIndex = sb.length();
|
||||||
cursor.setOpacity(on ? 255 : 0);
|
}
|
||||||
});
|
|
||||||
cursor.setHasListener(true);
|
if (i == value.length() - 1)
|
||||||
|
{
|
||||||
|
Line line = new Line(count, count + sb.length() - 1, sb.toString());
|
||||||
|
editLines.add(line);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (font.getTextWidth(sb.toString() + value.charAt(i + 1)) < w)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editLines.size() < this.lines - 1 || this.lines == 0)
|
||||||
|
{
|
||||||
|
if (breakIndex > 1)
|
||||||
|
{
|
||||||
|
String str = sb.substring(0, breakIndex);
|
||||||
|
Line line = new Line(count, count + str.length() - 1, str);
|
||||||
|
editLines.add(line);
|
||||||
|
|
||||||
|
sb.replace(0, breakIndex, "");
|
||||||
|
breakIndex = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Line line = new Line(count, count + sb.length() - 1, sb.toString());
|
||||||
|
editLines.add(line);
|
||||||
|
sb.replace(0, sb.length(), "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cursor.setFilled(true);
|
|
||||||
cursor.setOriginalX(mtx - 1);
|
|
||||||
cursor.setOriginalY(y);
|
|
||||||
cursor.setOriginalWidth(2 + mtw);
|
|
||||||
cursor.setOriginalHeight(h);
|
|
||||||
cursor.revalidate();
|
|
||||||
|
|
||||||
middleText.setText(mt);
|
Rectangle bounds = new Rectangle(container.getCanvasLocation().getX() + container.getWidth(), y, 0, editLines.size() * oh);
|
||||||
middleText.setFontId(fontID);
|
for (int i = 0; i < editLines.size() || i == 0; i++)
|
||||||
middleText.setOriginalX(mtx);
|
{
|
||||||
middleText.setOriginalY(y);
|
final Line line = editLines.size() > 0 ? editLines.get(i) : new Line(0, 0, "");
|
||||||
middleText.setOriginalWidth(mtw);
|
final String text = line.text;
|
||||||
middleText.setOriginalHeight(h);
|
final int len = text.length();
|
||||||
middleText.setTextColor(0xFFFFFF);
|
|
||||||
middleText.revalidate();
|
|
||||||
|
|
||||||
rightText.setText(rt);
|
String lt = Text.escapeJagex(text);
|
||||||
rightText.setFontId(fontID);
|
String mt = "";
|
||||||
rightText.setOriginalX(rtx);
|
String rt = "";
|
||||||
rightText.setOriginalY(y);
|
|
||||||
rightText.setOriginalWidth(rtw);
|
final boolean isStartLine = cursorOnLine(cursorStart, line.start, line.end)
|
||||||
rightText.setOriginalHeight(h);
|
|| (cursorOnLine(cursorStart, line.start, line.end + 1) && i == editLines.size() - 1);
|
||||||
rightText.revalidate();
|
|
||||||
|
final boolean isEndLine = cursorOnLine(cursorEnd, line.start, line.end);
|
||||||
|
|
||||||
|
if (isStartLine || isEndLine || (cursorEnd > line.end && cursorStart < line.start))
|
||||||
|
{
|
||||||
|
final int cIdx = Ints.constrainToRange(cursorStart - line.start, 0, len);
|
||||||
|
final int ceIdx = Ints.constrainToRange(cursorEnd - line.start, 0, len);
|
||||||
|
|
||||||
|
lt = Text.escapeJagex(text.substring(0, cIdx));
|
||||||
|
mt = Text.escapeJagex(text.substring(cIdx, ceIdx));
|
||||||
|
rt = Text.escapeJagex(text.substring(ceIdx));
|
||||||
|
}
|
||||||
|
|
||||||
|
final int ltw = font.getTextWidth(lt);
|
||||||
|
final int mtw = font.getTextWidth(mt);
|
||||||
|
final int rtw = font.getTextWidth(rt);
|
||||||
|
final int fullWidth = ltw + mtw + rtw;
|
||||||
|
|
||||||
|
int ltx = ox;
|
||||||
|
if (w > 0)
|
||||||
|
{
|
||||||
|
ltx += (w - fullWidth) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int mtx = ltx + ltw;
|
||||||
|
final int rtx = mtx + mtw;
|
||||||
|
|
||||||
|
if (ltx < bounds.x)
|
||||||
|
{
|
||||||
|
bounds.setLocation(ltx, bounds.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullWidth > bounds.width)
|
||||||
|
{
|
||||||
|
bounds.setSize(fullWidth, bounds.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editLines.size() == 0 || isStartLine)
|
||||||
|
{
|
||||||
|
cursor.setOriginalX(mtx - 1);
|
||||||
|
cursor.setOriginalY(y);
|
||||||
|
cursor.setOriginalWidth(2);
|
||||||
|
cursor.setOriginalHeight(h);
|
||||||
|
cursor.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Strings.isNullOrEmpty(lt))
|
||||||
|
{
|
||||||
|
final Widget leftText = container.createChild(-1, WidgetType.TEXT);
|
||||||
|
leftText.setFontId(fontID);
|
||||||
|
leftText.setText(lt);
|
||||||
|
leftText.setOriginalX(ltx);
|
||||||
|
leftText.setOriginalY(y);
|
||||||
|
leftText.setOriginalWidth(ltw);
|
||||||
|
leftText.setOriginalHeight(h);
|
||||||
|
leftText.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Strings.isNullOrEmpty(mt))
|
||||||
|
{
|
||||||
|
final Widget background = container.createChild(-1, WidgetType.RECTANGLE);
|
||||||
|
background.setTextColor(0x113399);
|
||||||
|
background.setFilled(true);
|
||||||
|
background.setOriginalX(mtx - 1);
|
||||||
|
background.setOriginalY(y);
|
||||||
|
background.setOriginalWidth(2 + mtw);
|
||||||
|
background.setOriginalHeight(h);
|
||||||
|
background.revalidate();
|
||||||
|
|
||||||
|
final Widget middleText = container.createChild(-1, WidgetType.TEXT);
|
||||||
|
middleText.setText(mt);
|
||||||
|
middleText.setFontId(fontID);
|
||||||
|
middleText.setOriginalX(mtx);
|
||||||
|
middleText.setOriginalY(y);
|
||||||
|
middleText.setOriginalWidth(mtw);
|
||||||
|
middleText.setOriginalHeight(h);
|
||||||
|
middleText.setTextColor(0xFFFFFF);
|
||||||
|
middleText.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Strings.isNullOrEmpty(rt))
|
||||||
|
{
|
||||||
|
final Widget rightText = container.createChild(-1, WidgetType.TEXT);
|
||||||
|
rightText.setText(rt);
|
||||||
|
rightText.setFontId(fontID);
|
||||||
|
rightText.setOriginalX(rtx);
|
||||||
|
rightText.setOriginalY(y);
|
||||||
|
rightText.setOriginalWidth(rtw);
|
||||||
|
rightText.setOriginalHeight(h);
|
||||||
|
rightText.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
y += h;
|
||||||
|
}
|
||||||
|
|
||||||
net.runelite.api.Point ccl = container.getCanvasLocation();
|
net.runelite.api.Point ccl = container.getCanvasLocation();
|
||||||
int canvasX = ltx + ccl.getX();
|
|
||||||
Rectangle bounds = new Rectangle(ccl.getX() + ox, ccl.getY() + y, w > 0 ? w : fullWidth, h);
|
|
||||||
|
|
||||||
String tsValue = value.toString();
|
isInBounds = ev -> bounds.contains(new Point(ev.getX() - ccl.getX(), ev.getY() - ccl.getY()));
|
||||||
isInBounds = ev -> bounds.contains(ev.getPoint());
|
|
||||||
getCharOffset = ev ->
|
getCharOffset = ev ->
|
||||||
{
|
{
|
||||||
if (fullWidth <= 0)
|
if (bounds.width <= 0)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int cx = ev.getX() - canvasX;
|
int cx = ev.getX() - ccl.getX() - ox;
|
||||||
|
int cy = ev.getY() - ccl.getY() - oy;
|
||||||
|
|
||||||
int charIndex = (tsValue.length() * cx) / fullWidth;
|
int currentLine = Ints.constrainToRange(cy / oh, 0, editLines.size() - 1);
|
||||||
|
|
||||||
|
final Line line = editLines.get(currentLine);
|
||||||
|
final String tsValue = line.text;
|
||||||
|
int charIndex = tsValue.length();
|
||||||
|
int fullWidth = font.getTextWidth(tsValue);
|
||||||
|
|
||||||
|
int tx = ox;
|
||||||
|
if (w > 0)
|
||||||
|
{
|
||||||
|
tx += (w - fullWidth) / 2;
|
||||||
|
}
|
||||||
|
cx -= tx;
|
||||||
|
|
||||||
// `i` is used to track max execution time incase there is a font with ligature width data that causes this to fail
|
// `i` is used to track max execution time incase there is a font with ligature width data that causes this to fail
|
||||||
for (int i = tsValue.length(); i >= 0 && charIndex >= 0 && charIndex <= tsValue.length(); i--)
|
for (int i = tsValue.length(); i >= 0 && charIndex >= 0 && charIndex <= tsValue.length(); i--)
|
||||||
@@ -353,19 +488,16 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charIndex < 0)
|
charIndex = Ints.constrainToRange(charIndex, 0, tsValue.length());
|
||||||
{
|
return line.start + charIndex;
|
||||||
charIndex = 0;
|
|
||||||
}
|
|
||||||
if (charIndex > tsValue.length())
|
|
||||||
{
|
|
||||||
charIndex = tsValue.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
return charIndex;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean cursorOnLine(final int cursor, final int start, final int end)
|
||||||
|
{
|
||||||
|
return (cursor >= start) && (cursor <= end);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void open()
|
protected void open()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user