mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 06:24:50 +01:00
e4f13ad8c8
Somewhat works now, but I think we'll abandon it..
480 lines
10 KiB
Java
480 lines
10 KiB
Java
|
|
import javax.swing.JComponent;
|
|
import java.text.AttributedString;
|
|
import java.text.AttributedCharacterIterator;
|
|
import java.awt.Graphics;
|
|
import java.awt.Font;
|
|
import java.awt.FontMetrics;
|
|
import java.awt.Image;
|
|
import java.awt.geom.Rectangle2D;
|
|
import java.awt.font.TextAttribute;
|
|
import java.awt.event.ComponentListener;
|
|
import java.awt.event.ComponentEvent;
|
|
import java.util.List;
|
|
import java.util.ArrayList;
|
|
import cafe.biskuteri.hinoki.Tree;
|
|
|
|
class
|
|
RichTextPane2 extends JComponent
|
|
implements ComponentListener {
|
|
|
|
private AttributedString
|
|
text;
|
|
|
|
// ---%-@-%---
|
|
|
|
public void
|
|
setText(Tree<String> html, Tree<String> emojiMap)
|
|
{
|
|
Tree<String> commands = turnIntoCommands(html);
|
|
|
|
class AStrSegment {
|
|
String text;
|
|
int offset;
|
|
Object[] values = new Object[Attribute.COUNT];
|
|
/*
|
|
{
|
|
values[3] = (Boolean)true;
|
|
values[4] = (Integer)0;
|
|
values[5] = (Boolean)true;
|
|
values[6] = (Boolean)false;
|
|
}
|
|
*/
|
|
}
|
|
List<AStrSegment> segments = new ArrayList<>();
|
|
|
|
int offset = 0;
|
|
for (Tree<String> command: commands)
|
|
{
|
|
if (command.key.equals("text"))
|
|
{
|
|
StringBuilder b = new StringBuilder();
|
|
Boolean cibl = null;
|
|
Boolean cwhi = null;
|
|
for (char c: command.value.toCharArray())
|
|
{
|
|
Boolean ibl = isBasicLatin(c);
|
|
Boolean whi = Character.isWhitespace(c);
|
|
if (!ibl.equals(cibl) || !whi.equals(cwhi))
|
|
{
|
|
if (b.length() > 0)
|
|
{
|
|
assert cibl != null && cwhi != null;
|
|
AStrSegment s = new AStrSegment();
|
|
s.offset = offset;
|
|
s.text = b.toString();
|
|
s.values[3] = cibl;
|
|
s.values[6] = cwhi;
|
|
segments.add(s);
|
|
offset += s.text.length();
|
|
b.delete(0, b.length());
|
|
}
|
|
cibl = ibl;
|
|
cwhi = whi;
|
|
}
|
|
|
|
b.append(c);
|
|
}
|
|
if (b.length() > 0)
|
|
{
|
|
AStrSegment s = new AStrSegment();
|
|
s.offset = offset;
|
|
s.text = b.toString();
|
|
s.values[3] = cibl;
|
|
s.values[6] = cwhi;
|
|
segments.add(s);
|
|
offset += s.text.length();
|
|
}
|
|
}
|
|
else if (command.key.equals("emoji"))
|
|
{
|
|
AStrSegment s = new AStrSegment();
|
|
s.offset = offset;
|
|
s.values[3] = true;
|
|
s.values[6] = false;
|
|
|
|
String shortcode = command.value;
|
|
String url = null;
|
|
Tree<String> m = emojiMap.get(shortcode);
|
|
if (m != null) url = m.value;
|
|
Image img = ImageApi.remote(url);
|
|
if (img != null)
|
|
{
|
|
s.text = " ";
|
|
s.values[0] = img;
|
|
s.values[1] = shortcode;
|
|
segments.add(s);
|
|
offset += 1;
|
|
}
|
|
else
|
|
{
|
|
s.text = shortcode;
|
|
s.values[0] = null;
|
|
s.values[1] = null;
|
|
segments.add(s);
|
|
offset += shortcode.length();
|
|
}
|
|
}
|
|
else if (command.key.equals("link"))
|
|
{
|
|
AStrSegment s = new AStrSegment();
|
|
s.offset = offset;
|
|
s.text = command.value;
|
|
s.values[2] = command.get("url").value;
|
|
s.values[3] = true;
|
|
s.values[6] = false;
|
|
/*
|
|
* Technically we're supposed to treat
|
|
* the anchor text like a text node.
|
|
* As in, it could be non-Basic-Latin..
|
|
* I'll be Mastodon-specific again, and
|
|
* assume it's a URL or some @ string.
|
|
*/
|
|
}
|
|
}
|
|
|
|
AttributedString astr;
|
|
StringBuilder b = new StringBuilder();
|
|
for (AStrSegment segment: segments)
|
|
{
|
|
b.append(segment.text);
|
|
}
|
|
astr = new AttributedString(b.toString());
|
|
for (AStrSegment segment: segments)
|
|
{
|
|
Object[] v = segment.values;
|
|
astr.addAttribute(
|
|
Attribute.IMAGE, segment.values[0],
|
|
segment.offset,
|
|
segment.offset + segment.text.length()
|
|
);
|
|
astr.addAttribute(
|
|
Attribute.ALT, segment.values[1],
|
|
segment.offset,
|
|
segment.offset + segment.text.length()
|
|
);
|
|
astr.addAttribute(
|
|
Attribute.LINK, segment.values[2],
|
|
segment.offset,
|
|
segment.offset + segment.text.length()
|
|
);
|
|
astr.addAttribute(
|
|
Attribute.BASICLATIN, segment.values[3],
|
|
segment.offset,
|
|
segment.offset + segment.text.length()
|
|
);
|
|
astr.addAttribute(
|
|
Attribute.Y, segment.values[4],
|
|
segment.offset,
|
|
segment.offset + segment.text.length()
|
|
);
|
|
astr.addAttribute(
|
|
Attribute.OFFSCREEN, segment.values[5],
|
|
segment.offset,
|
|
segment.offset + segment.text.length()
|
|
);
|
|
astr.addAttribute(
|
|
Attribute.WHITESPACE, segment.values[6],
|
|
segment.offset,
|
|
segment.offset + segment.text.length()
|
|
);
|
|
}
|
|
|
|
this.text = astr;
|
|
componentResized(null);
|
|
}
|
|
|
|
// - -%- -
|
|
|
|
public void
|
|
componentResized(ComponentEvent eC)
|
|
{
|
|
int w = getWidth(), h = getHeight();
|
|
|
|
// We're going to evaluate the
|
|
// line and off-screen attributes.
|
|
|
|
FontMetrics fm = getFontMetrics(getFont());
|
|
Graphics g = getGraphics();
|
|
int x = 0, y = fm.getAscent();
|
|
|
|
AttributedCharacterIterator it;
|
|
it = text.getIterator();
|
|
|
|
while (it.getIndex() < it.getEndIndex())
|
|
{
|
|
int start = it.getIndex();
|
|
int end = it.getRunLimit();
|
|
|
|
Image img = (Image)
|
|
it.getAttribute(Attribute.IMAGE);
|
|
Boolean ibl = (Boolean)
|
|
it.getAttribute(Attribute.BASICLATIN);
|
|
Boolean whi = (Boolean)
|
|
it.getAttribute(Attribute.WHITESPACE);
|
|
|
|
assert ibl != null;
|
|
assert whi != null;
|
|
|
|
if (img != null)
|
|
{
|
|
int ow = img.getWidth(this);
|
|
int oh = img.getHeight(this);
|
|
int nh = fm.getAscent() + fm.getDescent();
|
|
int nw = ow * nh/oh;
|
|
if (x + nw > w)
|
|
{
|
|
y += fm.getAscent() + fm.getDescent();
|
|
x = nw;
|
|
}
|
|
text.addAttribute(
|
|
Attribute.Y, (Integer)y,
|
|
start, end
|
|
);
|
|
text.addAttribute(
|
|
Attribute.OFFSCREEN, (Boolean)(y > h),
|
|
start, end
|
|
);
|
|
it.setIndex(end);
|
|
}
|
|
else
|
|
{
|
|
int p, xOff = 0;
|
|
for (p = end; p > start; --p)
|
|
{
|
|
Rectangle2D r;
|
|
r = fm.getStringBounds(it, start, p, g);
|
|
xOff = (int)r.getWidth();
|
|
if (x + xOff < w) break;
|
|
}
|
|
if (p == end || whi)
|
|
{
|
|
x += xOff;
|
|
text.addAttribute(
|
|
Attribute.Y, (Integer)y,
|
|
start, end
|
|
);
|
|
text.addAttribute(
|
|
Attribute.OFFSCREEN, (Boolean)(y > h),
|
|
start, end
|
|
);
|
|
it.setIndex(end);
|
|
}
|
|
else if (p <= start)
|
|
{
|
|
y += fm.getAscent() + fm.getDescent();
|
|
x = xOff;
|
|
text.addAttribute(
|
|
Attribute.Y, (Integer)y,
|
|
start, end
|
|
);
|
|
text.addAttribute(
|
|
Attribute.OFFSCREEN, (Boolean)(y > h),
|
|
start, end
|
|
);
|
|
it.setIndex(end);
|
|
}
|
|
else
|
|
{
|
|
text.addAttribute(
|
|
Attribute.Y, (Integer)y,
|
|
start, p
|
|
);
|
|
text.addAttribute(
|
|
Attribute.OFFSCREEN, (Boolean)(y > h),
|
|
start, p
|
|
);
|
|
y += fm.getAscent() + fm.getDescent();
|
|
x = 0;
|
|
it.setIndex(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
text.addAttribute(TextAttribute.FONT, getFont());
|
|
|
|
repaint();
|
|
}
|
|
|
|
protected void
|
|
paintComponent(Graphics g)
|
|
{
|
|
int w = getWidth(), h = getHeight();
|
|
g.clearRect(0, 0, w, h);
|
|
|
|
FontMetrics fm = g.getFontMetrics();
|
|
|
|
AttributedCharacterIterator it;
|
|
it = text.getIterator();
|
|
|
|
((java.awt.Graphics2D)g).setRenderingHint(
|
|
java.awt.RenderingHints.KEY_ANTIALIASING,
|
|
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
|
);
|
|
|
|
int x = 0, y = fm.getAscent();
|
|
while (it.getIndex() < it.getEndIndex())
|
|
{
|
|
int start = it.getIndex();
|
|
int end = it.getRunLimit();
|
|
|
|
Image img = (Image)
|
|
it.getAttribute(Attribute.IMAGE);
|
|
Boolean ibl = (Boolean)
|
|
it.getAttribute(Attribute.BASICLATIN);
|
|
Integer ny = (Integer)
|
|
it.getAttribute(Attribute.Y);
|
|
|
|
if (ny > y)
|
|
{
|
|
y = ny;
|
|
x = 0;
|
|
}
|
|
|
|
if (img != null)
|
|
{
|
|
int ow = img.getWidth(this);
|
|
int oh = img.getHeight(this);
|
|
int nh = fm.getAscent() + fm.getDescent();
|
|
int nw = ow * nh/oh;
|
|
int iy = y + fm.getDescent() - nh;
|
|
g.drawImage(img, x, iy, nw, nh, this);
|
|
x += nw;
|
|
}
|
|
else
|
|
{
|
|
Rectangle2D r;
|
|
r = fm.getStringBounds(it, start, end, g);
|
|
AttributedCharacterIterator sit;
|
|
sit = text.getIterator(null, start, end);
|
|
g.drawString(sit, x, y);
|
|
x += (int)r.getWidth();
|
|
}
|
|
it.setIndex(end);
|
|
}
|
|
}
|
|
|
|
public void
|
|
componentMoved(ComponentEvent eC) { }
|
|
|
|
public void
|
|
componentShown(ComponentEvent eC) { }
|
|
|
|
public void
|
|
componentHidden(ComponentEvent eC) { }
|
|
|
|
// - -%- -
|
|
|
|
private static Boolean
|
|
isBasicLatin(char c)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
private static String
|
|
toText(Tree<String> node)
|
|
{
|
|
Tree<String> children = node.get("children");
|
|
if (children == null)
|
|
{
|
|
boolean text = node.key.equals("text");
|
|
boolean emoji = node.key.equals("emoji");
|
|
assert text || emoji;
|
|
return node.value;
|
|
}
|
|
|
|
StringBuilder b = new StringBuilder();
|
|
for (Tree<String> child: children)
|
|
{
|
|
b.append(toText(child));
|
|
}
|
|
return b.toString();
|
|
}
|
|
|
|
private static Tree<String>
|
|
turnIntoCommands(Tree<String> tag)
|
|
{
|
|
assert tag.key.equals("tag");
|
|
Tree<String> returnee = new Tree<String>();
|
|
|
|
String tagName = tag.get(0).key;
|
|
Tree<String> children = tag.get("children");
|
|
|
|
if (tagName.equals("a"))
|
|
{
|
|
String url = tag.get("href").value;
|
|
Tree<String> addee = new Tree<>();
|
|
addee.key = "link";
|
|
addee.value = toText(tag);
|
|
addee.add(new Tree<>("url", url));
|
|
returnee.add(addee);
|
|
}
|
|
else if (tagName.equals("span"))
|
|
{
|
|
Tree<String> addee = new Tree<>();
|
|
addee.key = "text";
|
|
addee.value = toText(tag);
|
|
returnee.add(addee);
|
|
}
|
|
else if (tagName.equals("br"))
|
|
{
|
|
returnee.add(new Tree<>("text", "\n"));
|
|
}
|
|
else
|
|
{
|
|
for (Tree<String> child: children)
|
|
{
|
|
if (!child.key.equals("tag"))
|
|
{
|
|
returnee.add(child);
|
|
continue;
|
|
}
|
|
child = turnIntoCommands(child);
|
|
for (Tree<String> command: child)
|
|
{
|
|
returnee.add(command);
|
|
}
|
|
}
|
|
if (tagName.equals("p"))
|
|
{
|
|
returnee.add(new Tree<>("text", "\n"));
|
|
returnee.add(new Tree<>("text", "\n"));
|
|
}
|
|
}
|
|
|
|
return returnee;
|
|
}
|
|
|
|
// ---%-@-%---
|
|
|
|
public static class
|
|
Attribute extends AttributedCharacterIterator.Attribute {
|
|
|
|
public static final Attribute
|
|
IMAGE = new Attribute("IMAGE"),
|
|
ALT = new Attribute("ALT"),
|
|
LINK = new Attribute("LINK"),
|
|
BASICLATIN = new Attribute("BASICLATIN"),
|
|
Y = new Attribute("Y"),
|
|
OFFSCREEN = new Attribute("OFFSCREEN"),
|
|
WHITESPACE = new Attribute("WHITESPACE");
|
|
|
|
public static final int
|
|
COUNT = 7;
|
|
|
|
// -=%=-
|
|
|
|
private
|
|
Attribute(String name) { super(name); }
|
|
|
|
}
|
|
|
|
// ---%-@-%---
|
|
|
|
RichTextPane2()
|
|
{
|
|
this.addComponentListener(this);
|
|
text = new AttributedString("");
|
|
}
|
|
|
|
} |