biskuteri-cafe-JKomasto2/RichTextPane3.java
2022-05-17 03:14:16 -04:00

526 lines
12 KiB
Java

import javax.swing.JComponent;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.FontMetrics;
import java.awt.Font;
import java.awt.Image;
import java.awt.Color;
import java.awt.event.ComponentListener;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseEvent;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.ArrayList;
import cafe.biskuteri.hinoki.Tree;
class
RichTextPane3 extends JComponent
implements
ComponentListener,
MouseListener, MouseMotionListener, KeyListener {
private Tree<String>
html;
private Map<String, Image>
emojis;
private Map<Tree<String>, Point>
layout;
private Tree<String>
layoutEnd, selStart, selEnd;
// ---%-@-%---
public void
setText(Tree<String> html)
{
assert html != null;
this.html = html;
if (!isValid()) return;
assert html.key.equals("tag");
assert html.get("children") != null;
FontMetrics fm = getFontMetrics(getFont());
int iy = fm.getAscent();
Point cursor = new Point(0, iy);
Tree<String> nodes = html.get("children");
if (nodes.size() > 0)
{
Tree<String> first = nodes.get(0);
if (first.key.equals("tag"))
{
String tagName = first.get(0).key;
if (tagName.equals("p"))
{
int lh = fm.getAscent() + fm.getDescent();
cursor.y -= lh * 2;
}
}
}
selStart = selEnd = null;
layout.clear();
layout(html, fm, cursor);
layout.put(layoutEnd, new Point(cursor));
repaint();
}
public void
setEmojis(Map<String, Image> emojis)
{
assert emojis != null;
this.emojis = emojis;
setText(html);
}
// - -%- -
private void
layout(Tree<String> node, FontMetrics fm, Point cursor)
{
assert cursor != null;
int lh = fm.getAscent() + fm.getDescent();
if (node.key.equals("space"))
{
int w = fm.stringWidth(node.value);
if (cursor.x + w < getWidth())
{
layout.put(node, new Point(cursor));
cursor.x += w;
}
else
{
layout.put(node, new Point(cursor));
cursor.y += lh;
cursor.x = 0;
}
}
else if (node.key.equals("text"))
{
int w = fm.stringWidth(node.value);
if (cursor.x + w < getWidth())
{
layout.put(node, new Point(cursor));
cursor.x += w;
}
else if (w < getWidth())
{
cursor.y += lh;
cursor.x = 0;
layout.put(node, new Point(cursor));
cursor.x += w;
}
else
{
StringBuilder rem = new StringBuilder();
rem.append(node.value);
int mw = getWidth();
int aw = mw - cursor.x;
w = fm.charWidth(node.value.charAt(0));
if (w >= aw)
{
cursor.y += lh;
cursor.x = 0;
}
while (rem.length() > 0)
{
int l = 2;
for (; l <= rem.length(); ++l)
{
String substr = rem.substring(0, l);
w = fm.stringWidth(substr);
if (w >= aw) break;
}
String substr = rem.substring(0, --l);
w = fm.stringWidth(substr);
Tree<String> temp = new Tree<>();
temp.key = node.key;
temp.value = substr;
layout.put(temp, new Point(cursor));
rem.delete(0, l);
if (rem.length() == 0)
{
cursor.x = w;
}
else
{
cursor.y += lh;
cursor.x = 0;
}
aw = mw;
}
}
}
else if (node.key.equals("emoji"))
{
Image image = emojis.get(node.value);
int w;
if (image != null)
{
int ow = image.getWidth(this);
int oh = image.getHeight(this);
int h = lh;
w = ow * h/oh;
}
else
{
w = fm.stringWidth(node.value);
}
if (cursor.x + w < getWidth())
{
layout.put(node, new Point(cursor));
cursor.x += w;
}
else
{
cursor.y += lh;
cursor.x = 0;
layout.put(node, new Point(cursor));
cursor.x += w;
}
}
else if (node.key.equals("tag"))
{
String tagName = node.get(0).key;
Tree<String> children = node.get("children");
// We won't place tag nodes on the layout.
if (tagName.equals("br"))
{
cursor.y += lh;
cursor.x = 0;
}
else if (tagName.equals("p"))
{
//cursor.y += lh * 3/2;
cursor.y += lh * 2;
// Our selection algorithm assumes equidistant
// lines. Laziest fix is collect and sort line
// Ys from height.
cursor.x = 0;
}
for (Tree<String> child: children)
{
// Shallow copy this child node.
Tree<String> aug = new Tree<>();
aug.key = child.key;
aug.value = child.value;
for (Tree<String> gc: child) aug.add(gc);
// Append all of our attributes. We'd like those
// like href to end up at the text nodes. This
// might collide with our child node's attributes,
// for now I'll assume that's not an issue.
for (int o = 1; o < node.size(); ++o)
{
Tree<String> attr = node.get(o);
if (attr == children) continue;
aug.add(attr);
}
layout(aug, fm, cursor);
}
}
else assert false;
}
protected void
paintComponent(Graphics g)
{
final Color LINK_COLOUR = Color.BLUE;
final Color PLAIN_COLOUR = getForeground();
final Color SEL_COLOUR = new Color(0, 0, 0, 25);
g.setFont(getFont());
FontMetrics fm = g.getFontMetrics();
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
if (selEnd != null)
{
Point ssp = layout.get(selStart);
assert ssp != null;
Point sep = layout.get(selEnd);
assert sep != null;
/*
* (知) One way these can go null is if we clear
* the layout but don't clear the selection.
*/
boolean flip = ssp.y > sep.y;
flip |= sep.y == ssp.y && sep.x < ssp.x;
if (flip)
{
Point temp = ssp;
ssp = sep;
sep = ssp;
}
int w = getWidth();
int asc = fm.getAscent();
int lh = fm.getAscent() + fm.getDescent();
g.setColor(SEL_COLOUR);
if (ssp.y == sep.y)
{
g.fillRect(ssp.x, ssp.y - asc, sep.x - ssp.x, lh);
}
else
{
g.fillRect(ssp.x, ssp.y - asc, w - ssp.x, lh);
for (int y = ssp.y + lh; y < sep.y; y += lh)
{
g.fillRect(0, y - asc, w, lh);
}
g.fillRect(0, sep.y - asc, sep.x, lh);
}
}
g.setColor(getForeground());
for (Tree<String> node: layout.keySet())
{
if (node.key.equals("text"))
{
boolean isLink = node.get("href") != null;
if (isLink) g.setColor(LINK_COLOUR);
Point point = layout.get(node);
g.drawString(node.value, point.x, point.y);
if (isLink) g.setColor(PLAIN_COLOUR);
}
else if (node.key.equals("emoji"))
{
Point point = layout.get(node);
Image image = emojis.get(node.value);
if (image != null)
{
int ow = image.getWidth(this);
int oh = image.getHeight(this);
int nh = fm.getAscent() + fm.getDescent();
int nw = ow * nh/oh;
int x = point.x;
int y = point.y - fm.getAscent();
g.drawImage(image, x, y, nw, nh, this);
}
else
{
int x = point.x;
int y = point.y;
g.drawString(node.value, x, y);
}
}
else continue;
}
}
public void
mousePressed(MouseEvent eM)
{
selStart = identifyNodeAt(eM.getX(), eM.getY());
selEnd = null;
repaint();
}
public void
mouseDragged(MouseEvent eM)
{
if (selStart == null) return;
selEnd = identifyNodeAt(eM.getX(), eM.getY());
if (selEnd == null) selEnd = layoutEnd;
repaint();
}
private Tree<String>
identifyNodeAt(int x, int y)
{
FontMetrics fm = getFontMetrics(getFont());
int initial = fm.getAscent();
int advance = fm.getAscent() + fm.getDescent();
y = snap(y, initial, advance);
Tree<String> returnee = null;
int maxX = 0;
for (Tree<String> node: layout.keySet())
{
Point point = layout.get(node);
assert point != null;
if (point.y != y) continue;
if (point.x > x) continue;
if (point.x >= maxX)
{
maxX = point.x;
returnee = node;
}
}
return returnee;
}
public void
keyPressed(KeyEvent eK)
{
if (!eK.isControlDown()) return;
switch (eK.getKeyCode())
{
case KeyEvent.VK_C:
ClipboardApi.serve(getSelectedText());
break;
case KeyEvent.VK_A:
selStart = identifyNodeAt(0, 0);
selEnd = layoutEnd;
repaint();
break;
}
}
private String
getSelectedText()
{
assert selStart != null && selEnd != null;
Point ssp = layout.get(selStart);
Point sep = layout.get(selEnd);
assert ssp != null && sep != null;
boolean flip = ssp.y > sep.y;
flip |= sep.y == ssp.y && sep.x < ssp.x;
if (flip)
{
Point temp = ssp;
ssp = sep;
sep = ssp;
}
List<Tree<String>> selected = new ArrayList<>();
List<Point> points = new ArrayList<>();
for (Tree<String> node: layout.keySet())
{
Point point = layout.get(node);
assert point != null;
boolean c1 = point.y == ssp.y && point.x >= ssp.x;
boolean c2 = point.y == sep.y && point.x < sep.x;
boolean c3 = point.y > ssp.y && point.y < sep.y;
if (!(c1 || c2 || c3)) continue;
// Just throw them in a pile for now..
selected.add(node);
points.add(point);
}
// Now sort them into reading order.
Tree<String> n1, n2;
Point p1, p2;
for (int eo = 1; eo < points.size(); ++eo)
for (int o = points.size() - 1; o >= eo; --o)
{
n1 = selected.get(o - 1); n2 = selected.get(o);
p1 = points.get(o - 1); p2 = points.get(o);
boolean c1 = p2.y < p1.y;
boolean c2 = p2.y == p1.y && p2.x < p1.x;
if (!(c1 || c2)) continue;
selected.set(o - 1, n2);
selected.set(o, n1);
points.set(o - 1, p2);
points.set(o, p1);
}
StringBuilder b = new StringBuilder();
for (Tree<String> node: selected)
{
boolean t = node.key.equals("text");
boolean e = node.key.equals("emoji");
boolean s = node.key.equals("space");
assert t || e || s;
b.append(node.value); // Same behaviour for all.
}
return b.toString();
}
public void
keyReleased(KeyEvent eK) { }
public void
keyTyped(KeyEvent eK) { }
public void
mouseReleased(MouseEvent eM) { }
public void
mouseClicked(MouseEvent eM) { }
public void
mouseMoved(MouseEvent eM) { }
public void
mouseEntered(MouseEvent eM) { }
public void
mouseExited(MouseEvent eM) { }
public void
componentResized(ComponentEvent eC) { setText(html); }
public void
componentMoved(ComponentEvent eC) { }
public void
componentShown(ComponentEvent eC) { }
public void
componentHidden(ComponentEvent eC) { }
// - -%- -
private static int
snap(int value, int initial, int advance)
{
int offset = value - initial;
if (offset <= 0) offset = 0;
else {
int lines = 1 + ((offset - 1) / advance);
offset = advance * lines;
}
return initial + offset;
}
// ---%-@-%---
RichTextPane3()
{
layout = new HashMap<>();
layoutEnd = new Tree<>("text", "");
emojis = new HashMap<>();
setText(new Tree<String>());
this.addComponentListener(this);
this.addMouseListener(this);
this.addMouseMotionListener(this);
this.addKeyListener(this);
}
}