biskuteri-cafe-JKomasto2/PostWindow.java
Snowyfox 695d1057a2 Fixed RichTextPane somewhat.
Switched to BreakIterator.
2022-04-15 13:07:22 -04:00

759 lines
18 KiB
Java
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.BorderFactory;
import javax.swing.border.Border;
import javax.swing.JOptionPane;
import javax.swing.ImageIcon;
import java.awt.Graphics;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Shape;
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.Cursor;
import java.awt.Image;
import java.awt.Component;
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;
import cafe.biskuteri.hinoki.Tree;
import java.text.BreakIterator;
import java.util.Locale;
class
PostWindow extends JFrame
implements ActionListener {
private JKomasto
primaire;
private MastodonApi
api;
private Post
post;
// - -%- -
private PostComponent
postDisplay;
private RepliesComponent
repliesDisplay;
// - -%- -
private static final DateTimeFormatter
DATE_FORMAT = DateTimeFormatter.ofPattern("d LLLL ''uu"),
TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
// ---%-@-%---
public void
showPost(Post post)
{
assert post != null;
this.post = post;
List<RepliesComponent.Reply> replies = null;
{
List<Post> posts = null;
// We should make a request to JKomasto here.
}
if (replies == null)
{
RepliesComponent.Reply reply1, reply2, reply3;
reply1 = new RepliesComponent.Reply();
reply1.author = "Black tea";
reply1.text = "Rich..";
reply2 = new RepliesComponent.Reply();
reply2.author = "Green tea";
reply2.text = "Clean!";
reply3 = new RepliesComponent.Reply();
reply3.author = "Coffee";
reply3.text = "sleepy..";
replies = new ArrayList<>();
replies.add(reply1);
replies.add(reply2);
replies.add(reply3);
}
postDisplay.setAuthorName(post.authorName);
postDisplay.setAuthorId(post.authorId);
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(
post.attachments.length == 0
? null
: post.attachments[0].image
);
repliesDisplay.setReplies(replies);
postDisplay.resetFocus();
repaint();
}
public void
openAuthorProfile()
{
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(post.authorNumId);
w.showLatestPage();
w.setLocationRelativeTo(this);
w.setVisible(true);
}
public void
favourite(boolean favourited)
{
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR));
postDisplay.setFavouriteBoostEnabled(false);
postDisplay.paintImmediately(postDisplay.getBounds());
RequestListener handler = new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
PostWindow.this,
"Tried to favourite post, failed.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
PostWindow.this,
"Tried to favourite post, failed.."
+ "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
post.favourited = favourited;
}
};
api.setPostFavourited(post.postId, favourited, handler);
postDisplay.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint();
}
public void
boost(boolean boosted)
{
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR));
postDisplay.setFavouriteBoostEnabled(false);
postDisplay.paintImmediately(postDisplay.getBounds());
RequestListener handler = new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
PostWindow.this,
"Tried to boost post, failed.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
PostWindow.this,
"Tried to boost post, failed.."
+ "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
post.boosted = boosted;
}
};
api.setPostBoosted(post.postId, boosted, handler);
postDisplay.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint();
}
public void
reply()
{
ComposeWindow w = primaire.getComposeWindow();
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
Composition c = new Composition();
c.text = "@" + post.authorId + " ";
c.visibility = PostVisibility.PUBLIC;
c.replyToPostId = post.postId;
w.setComposition(c);
}
public void
openMedia()
{
ImageWindow w = primaire.getMediaWindow();
w.showAttachments(post.attachments);
int l = Math.min(40, post.text.length());
w.setTitle(post.text.substring(0, l));
if (!w.isVisible()) {
w.setLocationRelativeTo(null);
w.setVisible(true);
}
}
// - -%- -
public void
actionPerformed(ActionEvent eA)
{
Component src = (Component)eA.getSource();
if (!(src instanceof JMenuItem)) return;
String text = ((JMenuItem)src).getText();
if (text.equals("Post"))
{
setContentPane(postDisplay);
revalidate();
/*
* (知) Setting a content pane in itself doesn't
* do anything to the content pane. Validation
* of the validate root does. Which happens on
* window realisation, or by manual call.
*/
}
else if (text.equals("Replies"))
{
setContentPane(repliesDisplay);
revalidate();
}
}
// ---%-@-%---
PostWindow(JKomasto primaire)
{
this.primaire = primaire;
this.api = primaire.getMastodonApi();
getContentPane().setPreferredSize(new Dimension(360, 270));
pack();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setLocationByPlatform(true);
postDisplay = new PostComponent(this);
repliesDisplay = new RepliesComponent();
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();
samplePost.visibility = PostVisibility.MENTIONED;
samplePost.postId = "000000000";
samplePost.boosted = false;
samplePost.favourited = true;
samplePost.attachments = new Attachment[0];
samplePost.emojiUrls = new String[0][];
showPost(samplePost);
setContentPane(postDisplay);
}
}
class
PostComponent extends JPanel
implements ActionListener {
private PostWindow
primaire;
// - -%- -
private List<RichTextPane.Segment>
authorNameOr, bodyOr;
private RichTextPane
authorName, body;
private JLabel
authorId, time, date;
private String[][]
emojiUrls;
private TwoToggleButton
favouriteBoost,
replyMisc,
nextPrev;
private RoundButton
profile,
media;
// ---%-@-%---
public void
setAuthorName(String n)
{
authorNameOr = new RichTextPane.Builder().text(n).finish();
}
public void
setAuthorId(String n) { authorId.setText(n); }
public void
setAuthorAvatar(Image n) { profile.setImage(n); }
public void
setDate(String n) { date.setText(n); }
public void
setTime(String n) { time.setText(n); }
public void
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"))
{
BreakIterator it = BreakIterator.getWordInstance(Locale.ROOT);
String text = node.value;
it.setText(text);
int start = it.first(), end = it.next();
while (end != BreakIterator.DONE)
{
String word = text.substring(start, end);
char c = word.isEmpty() ? ' ' : word.charAt(0);
boolean w = Character.isWhitespace(c);
b = w ? b.spacer(word) : b.text(word);
start = end;
end = it.next();
}
}
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)
{
favouriteBoost.removeActionListener(this);
favouriteBoost.setPrimaryToggled(a);
favouriteBoost.addActionListener(this);
}
public void
setBoosted(boolean a)
{
favouriteBoost.removeActionListener(this);
favouriteBoost.setSecondaryToggled(a);
favouriteBoost.addActionListener(this);
}
public void
setFavouriteBoostEnabled(boolean a)
{
favouriteBoost.setEnabled(a);
}
public void
setMediaPreview(Image n) { media.setImage(n); }
public void
resetFocus() { media.requestFocusInWindow(); }
// - -%- -
public void
actionPerformed(ActionEvent eA)
{
Component src = (Component)eA.getSource();
String command = eA.getActionCommand();
if (src == profile)
{
primaire.openAuthorProfile();
return;
}
if (src == favouriteBoost)
{
if (command.equals("favouriteOn"))
primaire.favourite(true);
if (command.equals("favouriteOff"))
primaire.favourite(false);
if (command.equals("boostOn"))
primaire.boost(true);
if (command.equals("boostOff"))
primaire.boost(false);
return;
}
if (src == replyMisc)
{
if (command.startsWith("reply")) primaire.reply();
return;
}
if (src == nextPrev)
{
if (command.equals("next"))
{
}
else
{
}
return;
}
if (src == media)
{
primaire.openMedia();
return;
}
}
protected void
paintComponent(Graphics g)
{
g.clearRect(0, 0, getWidth(), getHeight());
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
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));
}
// ---%-@-%---
PostComponent(PostWindow primaire)
{
this.primaire = primaire;
emojiUrls = new String[0][];
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.addActionListener(this);
replyMisc.addActionListener(this);
nextPrev.addActionListener(this);
media.addActionListener(this);
Box buttons = Box.createVerticalBox();
buttons.setOpaque(false);
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);
authorId = new JLabel();
authorName = new RichTextPane();
time = new JLabel();
date = new JLabel();
authorId.setFont(f2);
date.setFont(f2);
authorName.setFont(f1);
time.setFont(f1);
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);
}
}
class
RepliesComponent extends JPanel {
private List<RepliesComponent.Reply>
replies;
// - -%- -
private JButton
prevPage, nextPage;
private JLabel
pageLabel;
private ReplyPreviewComponent[]
previews;
// ---%-@-%---
public void
setReplies(List<RepliesComponent.Reply> replies)
{
assert replies != null;
this.replies = replies;
displayPage(1);
}
// - -%- -
private void
displayPage(int pageNumber)
{
assert pageNumber > 0;
assert this.replies != null;
List<RepliesComponent.Reply> page;
{
int oS = (pageNumber - 1) * 8;
int oE = Math.min(oS + 8, replies.size());
if (oS > oE) page = new ArrayList<>();
else page = this.replies.subList(oS, oE);
}
for (int o = 0; o < page.size(); ++o)
{
assert o < previews.length;
ReplyPreviewComponent preview = previews[o];
Reply reply = replies.get(o);
preview.setAuthorName(reply.author);
preview.setText(reply.text);
preview.setVisible(true);
}
for (int o = page.size(); o < previews.length; ++o)
{
ReplyPreviewComponent preview = previews[o];
preview.setVisible(false);
}
int pages = 1 + ((replies.size() - 1) / 8);
pageLabel.setText(pageNumber + "/" + pages);
prevPage.setEnabled(pageNumber > 1);
nextPage.setEnabled(pageNumber < pages);
}
// ---%-@-%---
public static class
Reply {
public String
author;
public String
text;
}
// ---%-@-%---
RepliesComponent()
{
prevPage = new JButton("<");
nextPage = new JButton(">");
prevPage.setEnabled(false);
nextPage.setEnabled(false);
pageLabel = new JLabel();
Box bottom = Box.createHorizontalBox();
bottom.add(Box.createGlue());
bottom.add(prevPage);
bottom.add(Box.createHorizontalStrut(8));
bottom.add(pageLabel);
bottom.add(Box.createHorizontalStrut(8));
bottom.add(nextPage);
JPanel centre = new JPanel();
centre.setOpaque(false);
centre.setLayout(new GridLayout(0, 1, 0, 2));
previews = new ReplyPreviewComponent[8];
for (int o = 0; o < previews.length; ++o)
{
previews[o] = new ReplyPreviewComponent();
previews[o].setVisible(false);
centre.add(previews[o]);
}
setLayout(new BorderLayout(0, 8));
add(centre, BorderLayout.CENTER);
add(bottom, BorderLayout.SOUTH);
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
setReplies(new ArrayList<>());
}
}
class
ReplyPreviewComponent extends JButton {
private String
author;
private String
text;
// ---%-@-%---
@Override
public void
setText(String text)
{
assert text != null;
this.text = text;
setText();
}
public void
setAuthorName(String author)
{
assert author != null;
this.author = author;
setText();
}
// - -%- -
private void
setText()
{
StringBuilder text = new StringBuilder();
text.append(this.author);
text.append(" @ ");
text.append(this.text);
super.setText(text.toString());
}
protected void
paintComponent(Graphics g)
{
g.drawString(getText(), 8, 2 * getHeight() / 3);
}
}