biskuteri-cafe-JKomasto2/RichTextPane2.java

500 lines
15 KiB
Java

/* copyright
This file is part of JKomasto2.
Written in 2022 by Usawashi <usawashi16@yahoo.co.jp>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
copyright */
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("");
}
}