mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 05:14:50 +01:00
e6fea4c061
(Before this, JKomasto and sometimes the Mastodon web client would get '411 Record Not Found' when submitting the same text after deleting and redrafting. Presumably the Mastodon server caches both whether an idempotency key was fulfilled and which post it leads to, and for some reason it looks up the second and fails.)
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("");
|
|
}
|
|
|
|
} |