biskuteri-cafe-JKomasto2/PostWindow.java

761 lines
20 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.JFrame;
import javax.swing.JPanel;
import javax.swing.JMenuBar;
import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;
import javax.swing.JSeparator;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JScrollBar;
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 javax.swing.MenuSelectionManager;
import javax.swing.MenuElement;
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.awt.MediaTracker;
import java.util.List;
import java.util.ArrayList;
import java.net.URL;
import java.net.MalformedURLException;
import java.io.IOException;
import cafe.biskuteri.hinoki.Tree;
import java.text.BreakIterator;
import java.util.Locale;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.HashMap;
class
PostWindow extends JFrame {
private JKomasto
primaire;
private MastodonApi
api;
private Post
post,
wrapperPost;
// - -%- -
private PostComponent
display;
// - -%- -
private static final DateTimeFormatter
DATE_FORMAT = DateTimeFormatter.ofPattern("d LLLL ''uu"),
TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");
// ---%-@-%---
public synchronized void
use(Post post)
{
assert post != null;
if (post.boostedPost != null)
{
wrapperPost = post;
this.post = post.boostedPost;
post = post.boostedPost;
}
else
{
wrapperPost = null;
this.post = post;
}
display.setAuthorName(post.author.name);
display.setAuthorId(post.author.id);
String oid = api.getAccountDetails().get("id").value;
String aid = post.author.numId;
display.setDeleteEnabled(aid.equals(oid));
post.author.resolveAvatar();
display.setAuthorAvatar(post.author.avatar);
display.setDate(post.date);
display.setTime(post.time);
display.setEmojiUrls(post.emojiUrls);
display.setHtml(post.text);
display.setFavourited(post.favourited);
display.setBoosted(post.boosted);
if (post.attachments.length > 0)
{
post.attachments[0].resolveImage();
display.setMediaPreview(post.attachments[0].image);
}
else display.setMediaPreview(null);
post.resolveApproximateText();
this.setTitle(post.approximateText);
display.resetFocus();
repaint();
}
public void
readEntity(Tree<String> post)
{
use(new Post(post));
}
public synchronized void
openAuthorProfile()
{
ProfileWindow w = new ProfileWindow(primaire);
w.use(post.author);
w.setLocationRelativeTo(this);
w.setVisible(true);
}
public synchronized void
favourite(boolean favourited)
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setFavouriteBoostEnabled(false);
display.paintImmediately(display.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)
{
PostWindow.this.post.favourited = favourited;
}
};
api.setPostFavourited(post.id, favourited, handler);
display.setCursor(null);
display.setFavouriteBoostEnabled(true);
display.repaint();
}
public synchronized void
boost(boolean boosted)
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setFavouriteBoostEnabled(false);
display.paintImmediately(display.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)
{
PostWindow.this.post.boosted = boosted;
}
};
api.setPostBoosted(post.id, boosted, handler);
display.setCursor(null);
display.setFavouriteBoostEnabled(true);
display.repaint();
}
public synchronized void
reply()
{
String ownId = api.getAccountDetails().get("acct").value;
Composition c = Composition.reply(this.post, ownId);
ComposeWindow w = primaire.getComposeWindow();
w.setComposition(c);
if (!w.isVisible())
{
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
}
}
public synchronized void
openMedia()
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
for (Attachment a: post.attachments) a.resolveImage();
ImageWindow w = new ImageWindow();
w.setTitle("Media - " + this.getTitle());
w.setIconImage(this.getIconImage());
w.showAttachments(post.attachments);
w.setLocationRelativeTo(null);
w.setVisible(true);
display.setCursor(null);
}
public synchronized void
deletePost(boolean redraft)
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setDeleteEnabled(false);
display.paintImmediately(display.getBounds());
final String S1 =
"Are you sure you'd like to delete this post?\n";
final String S2 =
"Are you sure you'd like to delete this post?\n"
+ "You are redrafting, so a composition window\n"
+ "should open with its contents filled.";
JOptionPane dialog = new JOptionPane();
dialog.setMessageType(JOptionPane.QUESTION_MESSAGE);
dialog.setMessage(redraft ? S2 : S1);
dialog.setOptions(new String[] { "No", "Yes" });
String title = "Confirm delete";
dialog.createDialog(this, title).setVisible(true);
if (!dialog.getValue().equals("Yes"))
{
display.setCursor(null);
display.setDeleteEnabled(true);
display.paintImmediately(display.getBounds());
return;
}
api.deletePost(post.id, new RequestListener() {
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
PostWindow.this,
"Failed to delete post.."
+ "\n" + eIo.getMessage()
);
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
PostWindow.this,
"Failed to delete post.."
+ "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")"
);
}
public void
requestSucceeded(Tree<String> json)
{
setVisible(false);
if (redraft)
{
Composition c = Composition.recover(json);
ComposeWindow w = new ComposeWindow(primaire);
w.setComposition(c);
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
}
}
});
display.setCursor(null);
display.setDeleteEnabled(true);
display.paintImmediately(display.getBounds());
if (!isVisible()) dispose();
}
public synchronized void
copyPostId()
{
ClipboardApi.serve(post.id);
}
public synchronized void
copyPostLink()
{
ClipboardApi.serve(post.uri);
}
public synchronized void
openReplies()
{
RepliesWindow w = new RepliesWindow(primaire, this);
w.showFor(post.id);
w.setLocation(getX(), getY() + 100);
w.setVisible(true);
}
// ---%-@-%---
PostWindow(JKomasto primaire)
{
this.primaire = primaire;
this.api = primaire.getMastodonApi();
getContentPane().setPreferredSize(new Dimension(360, 260));
pack();
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setLocationByPlatform(true);
display = new PostComponent(this);
setContentPane(display);
setIconImage(primaire.getProgramIcon());
}
}
class
PostComponent extends JPanel
implements ActionListener {
private PostWindow
primaire;
// - -%- -
private List<RichTextPane.Segment>
authorNameOr;
private RichTextPane
authorName;
private RichTextPane3
body;
private JScrollPane
bodyScrollPane;
private JLabel
authorId, time, date;
private String[][]
emojiUrls;
private TwoToggleButton
favouriteBoost,
replyMisc,
nextPrev;
private RoundButton
profile,
media;
private JPopupMenu
miscMenu;
private JMenuItem
openReplies,
copyPostId,
copyPostLink,
deletePost,
redraftPost;
private Image
backgroundImage;
// ---%-@-%---
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
setEmojiUrls(String[][] n)
{
emojiUrls = n;
Map<String, Image> emojis = new HashMap<>();
for (String[] entry: n)
{
emojis.put(entry[0], ImageApi.remote(entry[1]));
}
body.setEmojis(emojis);
}
public void
setHtml(String n)
{
body.setText(BasicHTMLParser.parse(n));
}
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
setDeleteEnabled(boolean a)
{
deletePost.setEnabled(a);
redraftPost.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 (miscMenu.isVisible())
{
Component sel = getSelected(miscMenu);
if (sel == null) return;
assert sel instanceof JMenuItem;
((JMenuItem)sel).doClick();
}
else if (command.startsWith("reply"))
{
primaire.reply();
}
else if (command.startsWith("misc"))
{
int rx = replyMisc.getWidth() / 2;
int ry = replyMisc.getHeight() - miscMenu.getHeight();
miscMenu.show(replyMisc, rx, ry);
}
return;
}
else miscMenu.setVisible(false);
if (src == nextPrev)
{
if (command.startsWith("next"))
{
body.nextPage();
}
else
{
body.previousPage();
}
// First time an interactive element
// doesn't call something in primaire..
return;
}
if (src == media)
{
primaire.openMedia();
return;
}
if (src == openReplies) primaire.openReplies();
if (src == copyPostId) primaire.copyPostId();
if (src == copyPostLink) primaire.copyPostLink();
if (src == deletePost) primaire.deletePost(false);
if (src == redraftPost) primaire.deletePost(true);
}
protected void
paintComponent(Graphics g)
{
g.clearRect(0, 0, getWidth(), getHeight());
int w1 = authorName.getWidth();
FontMetrics fm1 = getFontMetrics(authorName.getFont());
List<RichTextPane.Segment> lay1;
lay1 = RichTextPane.layout(authorNameOr, fm1, w1);
authorName.setText(lay1);
if (backgroundImage != null)
{
int tw = backgroundImage.getWidth(this);
int th = backgroundImage.getHeight(this);
if (tw != -1)
for (int y = 0; y < getHeight(); y += th)
for (int x = 0; x < getWidth(); x += tw)
{
g.drawImage(backgroundImage, x, y, this);
}
}
((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
}
// - -%- -
private static Component
getSelected(JPopupMenu menu)
{
MenuElement[] sel =
MenuSelectionManager.defaultManager()
.getSelectedPath();
/*
* (知) For some reason, the selection model of the
* JPopupMenu doesn't do anything. So we have to
* consult this apparently global menu manager.
*/
for (int o = 0; o < sel.length - 1; ++o)
{
if (sel[o] == menu)
return sel[o + 1].getComponent();
}
return null;
}
// ---%-@-%---
PostComponent(PostWindow primaire)
{
this.primaire = primaire;
emojiUrls = new String[0][];
Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10);
Font f1 = new Font("MotoyaLMaru", Font.PLAIN, 18);
Font f2 = new Font("MotoyaLMaru", Font.PLAIN, 14);
Font f3 = new Font("MotoyaLMaru", Font.PLAIN, 18);
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);
openReplies = new JMenuItem("Browse thread");
copyPostId = new JMenuItem("Copy post ID");
copyPostLink = new JMenuItem("Copy post link");
deletePost = new JMenuItem("Delete post");
redraftPost = new JMenuItem("Delete and redraft post");
openReplies.addActionListener(this);
copyPostId.addActionListener(this);
copyPostLink.addActionListener(this);
deletePost.addActionListener(this);
redraftPost.addActionListener(this);
miscMenu = new JPopupMenu();
miscMenu.add(openReplies);
miscMenu.add(new JSeparator());
miscMenu.add(copyPostId);
miscMenu.add(copyPostLink);
miscMenu.add(new JSeparator());
miscMenu.add(deletePost);
miscMenu.add(new JSeparator());
miscMenu.add(redraftPost);
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.setOpaque(false);
top1.setLayout(new BorderLayout(8, 0));
top1.add(authorId);
top1.add(date, BorderLayout.EAST);
JPanel top2 = new JPanel();
top2.setOpaque(false);
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 RichTextPane3();
body.setFont(f3);
/*
bodyScrollPane = new JScrollPane(
body,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
);
JScrollBar vsb = bodyScrollPane.getVerticalScrollBar();
vsb.setPreferredSize(new Dimension(0, 0));
vsb.setUnitIncrement(16);
bodyScrollPane.setBorder(null);
bodyScrollPane.setFocusable(true);
*/
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);
backgroundImage = ImageApi.local("postWindow");
}
}