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.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.io.IOException; import cafe.biskuteri.hinoki.Tree; 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 replies = null; { List 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.setText(post.text); 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 json) { JOptionPane.showMessageDialog( PostWindow.this, "Tried to favourite post, failed.." + "\n" + json.get("error").value + "\n(HTTP error code: " + httpCode + ")" ); } public void requestSucceeded(Tree 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 json) { JOptionPane.showMessageDialog( PostWindow.this, "Tried to boost post, failed.." + "\n" + json.get("error").value + "\n(HTTP error code: " + httpCode + ")" ); } public void requestSucceeded(Tree 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.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]; showPost(samplePost); setContentPane(postDisplay); } } class PostComponent extends JPanel implements ActionListener { private PostWindow primaire; private String authorName, authorId, date, time, text; // - -%- - private TwoToggleButton favouriteBoost, replyMisc, nextPrev; private RoundButton profile, media; // ---%-@-%--- public void setAuthorName(String n) { authorName = n; } public void setAuthorId(String n) { authorId = n; } public void setAuthorAvatar(Image n) { profile.setImage(n); } public void setDate(String n) { date = n; } public void setTime(String n) { time = n; } public void setText(String n) { text = 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 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 ); 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 split(String string, int lineLength) { List 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; } // ---%-@-%--- PostComponent(PostWindow primaire) { this.primaire = primaire; authorName = authorId = time = text = ""; Dimension buttonSize = new Dimension(20, 40); Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10); profile = 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); setLayout(new BorderLayout()); add(buttons, BorderLayout.WEST); setFont(getFont().deriveFont(14f)); } } class RepliesComponent extends JPanel { private List replies; // - -%- - private JButton prevPage, nextPage; private JLabel pageLabel; private ReplyPreviewComponent[] previews; // ---%-@-%--- public void setReplies(List replies) { assert replies != null; this.replies = replies; displayPage(1); } // - -%- - private void displayPage(int pageNumber) { assert pageNumber > 0; assert this.replies != null; List 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); } }