Added buggy rich text pane.

Added rendering of emojis.
Added run script.
This commit is contained in:
Snowyfox 2022-04-15 08:54:31 -04:00
parent 511ca1aeef
commit 7ede5e1290
23 changed files with 539 additions and 152 deletions

0
ComposeWindow.java Normal file → Executable file
View File

0
ImageApi.java Normal file → Executable file
View File

6
JKomasto.java Normal file → Executable file
View File

@ -145,7 +145,8 @@ Post {
public String
text,
contentWarning;
contentWarning,
html;
public String
authorId, authorName;
@ -174,6 +175,9 @@ Post {
public Attachment[]
attachments;
public String[][]
emojiUrls;
}

0
LoginWindow.java Normal file → Executable file
View File

236
PostWindow.java Normal file → Executable file
View File

@ -26,6 +26,8 @@ import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.util.List;
import java.util.ArrayList;
import java.net.URL;
import java.net.MalformedURLException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.io.IOException;
@ -96,7 +98,9 @@ implements ActionListener {
postDisplay.setAuthorAvatar(post.authorAvatar);
postDisplay.setDate(DATE_FORMAT.format(post.date));
postDisplay.setTime(TIME_FORMAT.format(post.date));
postDisplay.setEmojiUrls(post.emojiUrls);
postDisplay.setText(post.text);
postDisplay.setHtml(post.html);
postDisplay.setFavourited(post.favourited);
postDisplay.setBoosted(post.boosted);
postDisplay.setMediaPreview(
@ -275,6 +279,7 @@ implements ActionListener {
Post samplePost = new Post();
samplePost.text = "This is a sample post.";
samplePost.html = "";
samplePost.authorId = "snowyfox@biskuteri.cafe";
samplePost.authorName = "snowyfox";
samplePost.date = ZonedDateTime.now();
@ -283,6 +288,7 @@ implements ActionListener {
samplePost.boosted = false;
samplePost.favourited = true;
samplePost.attachments = new Attachment[0];
samplePost.emojiUrls = new String[0][];
showPost(samplePost);
setContentPane(postDisplay);
@ -299,11 +305,20 @@ implements ActionListener {
private PostWindow
primaire;
private String
authorName, authorId, date, time, text;
// - -%- -
private List<RichTextPane.Segment>
authorNameOr, bodyOr;
private RichTextPane
authorName, body;
private JLabel
authorId, time, date;
private String[][]
emojiUrls;
private TwoToggleButton
favouriteBoost,
replyMisc,
@ -316,22 +331,68 @@ implements ActionListener {
// ---%-@-%---
public void
setAuthorName(String n) { authorName = n; }
setAuthorName(String n)
{
authorNameOr = new RichTextPane.Builder().text(n).finish();
}
public void
setAuthorId(String n) { authorId = n; }
setAuthorId(String n) { authorId.setText(n); }
public void
setAuthorAvatar(Image n) { profile.setImage(n); }
public void
setDate(String n) { date = n; }
setDate(String n) { date.setText(n); }
public void
setTime(String n) { time = n; }
setTime(String n) { time.setText(n); }
public void
setText(String n) { text = n; }
setText(String n) { }
public void
setEmojiUrls(String[][] n) { emojiUrls = n; }
public void
setHtml(String n)
{
RichTextPane.Builder b = new RichTextPane.Builder();
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(n);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
String tagName = node.get(0).key;
if (tagName.equals("br"))
b = b.spacer("\n");
if (tagName.equals("/p"))
b = b.spacer("\n").spacer("\n");
if (tagName.equals("a"))
b = b.link(node.get("href").value, null).spacer(" ");
}
if (node.key.equals("text"))
{
for (String word: node.value.split(" "))
b = b.text(word).spacer(" ");
}
if (node.key.equals("emoji"))
{
String shortcode = node.value;
String url = null;
for (String[] entry: emojiUrls)
if (entry[0].equals(shortcode)) url = entry[1];
try {
ImageIcon image = new ImageIcon(new URL(url));
b = b.image(image, node.value);
}
catch (MalformedURLException eMu) {
b = b.text(":×:");
}
}
}
bodyOr = b.finish();
}
public void
setFavourited(boolean a)
@ -424,76 +485,12 @@ implements ActionListener {
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
Font f2 = new Font("IPAGothic", Font.PLAIN, 14);
FontMetrics fm1 = g.getFontMetrics(f1);
FontMetrics fm2 = g.getFontMetrics(f2);
int x1 = 60;
int x4 = getWidth() - 10;
int x2 = x4 - fm2.stringWidth(date);
int x3 = x4 - fm1.stringWidth(time);
int y1 = 10;
int y2 = y1 + fm2.getHeight();
int y3 = y2 + fm1.getHeight();
int y4 = y3 + 8;
Shape defaultClip = g.getClip();
g.setClip(x1, y1, Math.min(x2, x3) - 8 - x1, y4 - y1);
// First time I've used this method..
// Cause, clearRect is not working.
g.setFont(f2);
g.drawString(authorId, x1, y2);
g.setFont(f1);
g.drawString(authorName, x1, y3);
g.setClip(defaultClip);
g.setFont(f2);
g.drawString(date, x2, y2);
g.setFont(f1);
g.drawString(time, x3, y3);
int y = y4;
for (String line: split(text, 40)) {
y += fm1.getHeight();
g.drawString(line, x1, y);
}
}
// - -%- -
private static List<String>
split(String string, int lineLength)
{
List<String> returnee = new ArrayList<>();
StringBuilder line = new StringBuilder();
for (String word: string.split(" "))
{
if (word.length() >= lineLength) {
word = word.substring(0, lineLength - 4) + "...";
}
if (word.matches("\n")) {
returnee.add(empty(line));
continue;
}
if (line.length() + word.length() > lineLength) {
returnee.add(empty(line));
}
line.append(word);
line.append(" ");
}
returnee.add(empty(line));
return returnee;
}
private static String
empty(StringBuilder b)
{
String s = b.toString();
b.delete(0, b.length());
return s;
int w1 = authorName.getWidth();
int w2 = body.getWidth();
FontMetrics fm1 = getFontMetrics(authorName.getFont());
FontMetrics fm2 = getFontMetrics(body.getFont());
authorName.setText(RichTextPane.layout(authorNameOr, fm1, w1));
body.setText(RichTextPane.layout(bodyOr, fm2, w2));
}
// ---%-@-%---
@ -502,48 +499,75 @@ implements ActionListener {
{
this.primaire = primaire;
authorName = authorId = time = text = "";
emojiUrls = new String[0][];
Dimension buttonSize = new Dimension(20, 40);
Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10);
Font f1 = new Font("IPAGothic", Font.PLAIN, 16);
Font f2 = new Font("IPAGothic", Font.PLAIN, 13);
profile = new RoundButton();
favouriteBoost = new TwoToggleButton("favourite", "boost");
replyMisc = new TwoToggleButton("reply", "misc");
nextPrev = new TwoToggleButton("next", "prev");
media = new RoundButton();
profile.addActionListener(this);
favouriteBoost = new TwoToggleButton("favourite", "boost");
favouriteBoost.addActionListener(this);
replyMisc = new TwoToggleButton("reply", "misc");
replyMisc.addActionListener(this);
nextPrev = new TwoToggleButton("next", "prev");
nextPrev.addActionListener(this);
media = new RoundButton();
//media.setPreferredSize(buttonSize);
media.addActionListener(this);
Box ibuttons = Box.createVerticalBox();
ibuttons.setOpaque(false);
ibuttons.add(profile);
ibuttons.add(Box.createVerticalStrut(8));
ibuttons.add(favouriteBoost);
ibuttons.add(Box.createVerticalStrut(8));
ibuttons.add(replyMisc);
ibuttons.add(Box.createVerticalStrut(8));
ibuttons.add(nextPrev);
ibuttons.add(Box.createVerticalStrut(8));
ibuttons.add(media);
ibuttons.setMaximumSize(ibuttons.getPreferredSize());
Box buttons = Box.createVerticalBox();
buttons.setOpaque(false);
buttons.add(ibuttons);
buttons.setBorder(b);
buttons.add(profile);
buttons.add(Box.createVerticalStrut(8));
buttons.add(favouriteBoost);
buttons.add(Box.createVerticalStrut(8));
buttons.add(replyMisc);
buttons.add(Box.createVerticalStrut(8));
buttons.add(nextPrev);
buttons.add(Box.createVerticalStrut(8));
buttons.add(media);
buttons.setMaximumSize(buttons.getPreferredSize());
Box left = Box.createVerticalBox();
left.setOpaque(false);
left.add(buttons);
setLayout(new BorderLayout());
add(buttons, BorderLayout.WEST);
authorId = new JLabel();
authorName = new RichTextPane();
time = new JLabel();
date = new JLabel();
authorId.setFont(f2);
date.setFont(f2);
authorName.setFont(f1);
time.setFont(f1);
setFont(getFont().deriveFont(14f));
JPanel top1 = new JPanel();
top1.setLayout(new BorderLayout(8, 0));
top1.add(authorId);
top1.add(date, BorderLayout.EAST);
JPanel top2 = new JPanel();
top2.setLayout(new BorderLayout(8, 0));
top2.add(authorName);
top2.add(time, BorderLayout.EAST);
Box top = Box.createVerticalBox();
top.add(top1);
top.add(Box.createVerticalStrut(2));
top.add(top2);
body = new RichTextPane();
body.setFont(getFont().deriveFont(14f));
JPanel centre = new JPanel();
centre.setOpaque(false);
centre.setLayout(new BorderLayout(0, 8));
centre.add(top, BorderLayout.NORTH);
centre.add(body);
setLayout(new BorderLayout(8, 0));
add(left, BorderLayout.WEST);
add(centre);
setBorder(b);
}
}

251
RichTextPane.java Normal file
View File

@ -0,0 +1,251 @@
import javax.swing.JComponent;
import javax.swing.ImageIcon;
import java.awt.Graphics;
import java.awt.FontMetrics;
import java.awt.Image;
import java.awt.Color;
import java.util.List;
import java.util.LinkedList;
import java.util.ListIterator;
class
RichTextPane extends JComponent {
private List<Segment>
text;
// ---%-@-%---
public void
setText(List<Segment> text) { this.text = text; }
// - -%- -
protected void
paintComponent(Graphics g)
{
g.setFont(getFont());
FontMetrics fm = g.getFontMetrics(getFont());
g.clearRect(0, 0, getWidth(), getHeight());
for (Segment segment: text)
{
if (segment.image != null) {
int ow = segment.image.getIconWidth();
int oh = segment.image.getIconHeight();
int h = fm.getHeight();
int w = h * ow / oh;
int x = segment.x, y = segment.y;
Image img = segment.image.getImage();
g.drawImage(img, x, y - h, w, h, this);
continue;
}
if (segment.link != null) g.setColor(Color.BLUE);
g.drawString(segment.text, segment.x, segment.y);
g.setColor(getForeground());
}
}
// - -%- -
public static List<Segment>
layout(List<Segment> text, FontMetrics fm, int width)
{
if (width < fm.getMaxAdvance()) return new LinkedList<>();
List<Segment> copy = new LinkedList<>();
for (Segment segment: text) copy.add(segment.clone());
text = copy;
ListIterator<Segment> cursor = text.listIterator();
int dy = fm.getHeight(), x = 0, y = dy;
while (cursor.hasNext())
{
Segment curr = cursor.next();
int dx;
if (curr.image != null) {
int ow = curr.image.getIconWidth();
int oh = curr.image.getIconHeight();
dx = dy * ow / oh;
}
else if (curr.text != null) {
dx = fm.stringWidth(curr.text);
}
else if (curr.link != null) {
curr.text = curr.link;
dx = fm.stringWidth(curr.link);
}
else {
assert false;
dx = 0;
}
// If can readily fit, just do so.
if (x + dx < width || curr.spacer) {
curr.x = x;
curr.y = y;
x += dx;
if (curr.spacer && curr.text.equals("\n")) {
y += dy;
x = 0;
}
continue;
}
// If image, or text that isn't long, just break.
if (curr.image != null || dx < width / 3) {
curr.x = 0;
curr.y = y += dy;
x = dx;
continue;
}
// Greedily split string to fit into line.
int offset = splitForFit(curr.text, fm, width - x);
if (offset == 0) {
cursor.add(curr); cursor.previous();
y += dy;
x = dx;
continue;
}
Segment next = new Segment();
next.text = curr.text.substring(offset);
next.link = curr.link;
cursor.add(next); cursor.previous();
curr.text = curr.text.substring(0, offset);
curr.x = x;
curr.y = y;
y += dy;
x = 0;
continue;
}
return text;
}
// - -%- -
private static int
splitForFit(String s, FontMetrics fm, int width)
{
int max = 0;
for (int o = 1; o < s.length(); max = o++)
if (fm.stringWidth(s.substring(0, o)) > width) break;
return max;
}
// ---%-@-%---
public static class
Segment {
public ImageIcon
image;
public String
link;
public String
text;
public boolean
spacer;
public int
x, y;
// -=%=-
public String
toString()
{
StringBuilder b = new StringBuilder();
b.append(getClass().getName() + "[");
b.append("image=" + image);
b.append(",link=" + link);
b.append(",text=" + text);
b.append(",x=" + x);
b.append(",y=" + y);
b.append("]");
return b.toString();
}
public Segment
clone()
{
Segment segment = new Segment();
segment.image = this.image;
segment.link = this.link;
segment.text = this.text;
segment.spacer = this.spacer;
segment.x = this.x;
segment.y = this.y;
return segment;
}
}
public static class
Builder {
private List<Segment>
returnee;
// -=%=-
public
Builder() { returnee = new LinkedList<>(); }
public Builder
image(ImageIcon image, String text)
{
Segment segment = new Segment();
segment.image = image;
segment.text = text;
returnee.add(segment);
return this;
}
public Builder
link(String link, String text)
{
Segment segment = new Segment();
segment.link = link;
segment.text = text;
returnee.add(segment);
return this;
}
public Builder
text(String text)
{
Segment segment = new Segment();
segment.text = text;
returnee.add(segment);
return this;
}
public Builder
spacer(String text)
{
Segment segment = new Segment();
segment.text = text;
segment.spacer = true;
returnee.add(segment);
return this;
}
public List<Segment>
finish() { return returnee; }
}
// ---%-@-%---
RichTextPane()
{
text = new LinkedList<>();
}
}

View File

@ -1,5 +1,7 @@
import cafe.biskuteri.hinoki.Tree;
import java.util.List;
import java.util.ListIterator;
import java.io.StringReader;
import java.io.Reader;
import java.io.IOException;
@ -9,9 +11,19 @@ RudimentaryHTMLParser {
public static Tree<String>
depthlessRead(String html)
throws IOException
{
return pass2(pass1(html));
try {
return pass3(pass2(pass1(html)));
}
catch (IOException eIo) {
assert false;
/*
* We use only StringReaders, which only throw an
* IOException when they are read after being closed.
* And we don't close them.
*/
return null;
}
}
// - -%- -
@ -20,11 +32,12 @@ RudimentaryHTMLParser {
pass1(String html)
throws IOException
{
Reader r = new StringReader(html);
Reader r = new StringReader(html);
Tree<String> docu = new Tree<String>();
StringBuilder text = new StringBuilder();
StringBuilder emoji = new StringBuilder();
StringBuilder htmlEscape = new StringBuilder();
boolean quoted = false;
boolean quoted = false, inEmoji = false;
int c; while ((c = r.read()) != -1)
{
if (c == '&' || htmlEscape.length() > 0)
@ -77,7 +90,7 @@ RudimentaryHTMLParser {
continue;
}
}
text.append((char)c);
text.append((char)c);
continue;
}
if (text.length() > 0)
@ -96,8 +109,7 @@ RudimentaryHTMLParser {
{
for (Tree<String> node: docu.children)
{
if (node.key.equals("text")) continue;
assert node.key.equals("tag");
if (!node.key.equals("tag")) continue;
Reader r = new StringReader(node.value);
Tree<String> part = new Tree<String>();
@ -149,6 +161,61 @@ RudimentaryHTMLParser {
return docu;
}
private static Tree<String>
pass3(Tree<String> docu)
{
ListIterator<Tree<String>> it = docu.children.listIterator();
while (it.hasNext())
{
Tree<String> node = it.next();
if (!node.key.equals("text")) continue;
it.remove();
StringBuilder t = new StringBuilder();
StringBuilder e = new StringBuilder();
boolean emoji = false;
char pc = ' ';
for (char c: node.value.toCharArray())
{
if (!emoji && c == ':')
{
emoji = true;
if (t.length() > 0) {
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(t);
it.add(text);
}
pc = c;
continue;
}
if (emoji && c == ':')
{
emoji = false;
if (e.length() > 0)
{
Tree<String> shortcode = new Tree<String>();
shortcode.key = "emoji";
shortcode.value = empty(e);
it.add(shortcode);
}
pc = c;
continue;
}
if (emoji) e.append((char)c);
else t.append((char)c);
pc = c;
}
if (t.length() > 0) {
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(t);
it.add(text);
}
}
return docu;
}
private static String
empty(StringBuilder b)
{

28
TestWindow.java Normal file
View File

@ -0,0 +1,28 @@
import javax.swing.*;
import java.awt.*;
import java.util.List;
class
TestWindow extends JFrame {
TestWindow()
{
RichTextPane display = new RichTextPane();
setContentPane(display);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setLocationByPlatform(true);
setSize(320, 240);
setVisible(true);
setVisible(false);
String s = "This is a standard English sentence.";
RichTextPane.Builder b = new RichTextPane.Builder();
for (String word: s.split(" ")) b = b.text(word).spacer(" ");
List<RichTextPane.Segment> text = b.finish();
FontMetrics fm = display.getFontMetrics(display.getFont());
RichTextPane.layout(text, fm, display.getWidth());
display.setText(text);
}
}

76
TimelineWindow.java Normal file → Executable file
View File

@ -339,54 +339,43 @@ implements ActionListener {
addee.date = ZonedDateTime.now();
}
try {
StringBuilder b = new StringBuilder();
Tree<String> nodes =
RudimentaryHTMLParser
.depthlessRead(post.get("content").value);
for (Tree<String> node: nodes.children)
{
if (node.key.equals("tag"))
{
if (node.get(0).key.equals("br")) {
b.append(" \n ");
}
if (node.get(0).key.equals("/p")) {
b.append(" \n \n ");
}
}
if (node.key.equals("text")) {
b.append(node.value);
}
}
addee.text = b.toString();
}
catch (IOException eIo) {
eIo.printStackTrace();
assert false;
}
String s2 = addee.html = post.get("content").value;
StringBuilder b = new StringBuilder();
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(s2);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
String tagName = node.get(0).key;
if (tagName.equals("br")) b.append(" \n ");
if (tagName.equals("/p")) b.append(" \n \n ");
}
if (node.key.equals("text")) b.append(node.value);
if (node.key.equals("emoji")) b.append(node.value);
}
addee.text = b.toString();
String s = post.get("spoiler_text").value;
if (!s.isEmpty()) addee.contentWarning = s;
String s3 = post.get("spoiler_text").value;
if (!s3.isEmpty()) addee.contentWarning = s3;
else addee.contentWarning = null;
Tree<String> account = post.get("account");
addee.authorId = account.get("acct").value;
addee.authorName = account.get("username").value;
addee.authorNumId = account.get("id").value;
String s2 = account.get("display_name").value;
if (!s2.isEmpty()) addee.authorName = s2;
String s3 = account.get("avatar").value;
addee.authorAvatar = ImageApi.remote(s3);
String s4 = account.get("display_name").value;
if (!s4.isEmpty()) addee.authorName = s4;
String s5 = account.get("avatar").value;
addee.authorAvatar = ImageApi.remote(s5);
if (addee.authorAvatar == null) {
s3 = "defaultAvatar";
addee.authorAvatar = ImageApi.local(s3);
s5 = "defaultAvatar";
addee.authorAvatar = ImageApi.local(s5);
}
String f = post.get("favourited").value;
String b = post.get("reblogged").value;
addee.favourited = f.equals("true");
addee.boosted = b.equals("true");
String s6 = post.get("favourited").value;
String s7 = post.get("reblogged").value;
addee.favourited = s6.equals("true");
addee.boosted = s7.equals("true");
Tree<String> as1 = post.get("media_attachments");
Attachment[] as2 = new Attachment[as1.size()];
@ -406,6 +395,17 @@ implements ActionListener {
}
addee.attachments = as2;
Tree<String> es1 = post.get("emojis");
String[][] es2 = new String[es1.size()][];
for (int o = 0; o < es2.length; ++o)
{
Tree<String> e1 = es1.get(o);
String[] e2 = es2[o] = new String[2];
e2[0] = e1.get("shortcode").value;
e2[1] = e1.get("url").value;
}
addee.emojiUrls = es2;
posts.add(addee);
}
return posts;

0
TimelineWindowUpdater.java Normal file → Executable file
View File

0
TwoToggleButton.java Normal file → Executable file
View File

0
graphics/Flags.xcf Normal file → Executable file
View File

0
graphics/Hourglass.xcf Normal file → Executable file
View File

0
graphics/button.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

0
graphics/disabledOverlay.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
graphics/favouriteToggled.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 353 B

After

Width:  |  Height:  |  Size: 353 B

0
graphics/ref1.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

0
graphics/selectedOverlay.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 313 B

0
graphics/test1.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

0
graphics/test2.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

0
graphics/test3.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

0
graphics/test4.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

13
run Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/make -f
CLASSPATH=.:../Hinoki:/usr/share/java/javax.json.jar
c:
javac -cp $(CLASSPATH) *.java
r:
java -cp $(CLASSPATH) -ea JKomasto
cr: c r
.PHONY: c r