From 9c75464b18c69ff3258471a2c500ea8967d9a675 Mon Sep 17 00:00:00 2001 From: Snowyfox Date: Fri, 29 Apr 2022 13:44:38 -0400 Subject: [PATCH] Added replies window, based on JTree --- ClipboardApi.java | 62 +++++++++ MastodonApi.java | 163 +++++++++++++++------- PostWindow.java | 339 ++++++++++++--------------------------------- RepliesWindow.java | 300 +++++++++++++++++++++++++++++++++++++++ RichTextPane.java | 70 +++------- graphics/test1.png | Bin 0 -> 2170 bytes graphics/test2.png | Bin 0 -> 2257 bytes graphics/test3.png | Bin 0 -> 2220 bytes graphics/test4.png | Bin 0 -> 2374 bytes notifOptions.txt | 10 ++ notifOptions.txt~ | 9 ++ run | 5 +- 12 files changed, 605 insertions(+), 353 deletions(-) create mode 100755 ClipboardApi.java create mode 100755 RepliesWindow.java create mode 100755 graphics/test1.png create mode 100755 graphics/test2.png create mode 100755 graphics/test3.png create mode 100755 graphics/test4.png create mode 100644 notifOptions.txt create mode 100644 notifOptions.txt~ diff --git a/ClipboardApi.java b/ClipboardApi.java new file mode 100755 index 0000000..2f4e907 --- /dev/null +++ b/ClipboardApi.java @@ -0,0 +1,62 @@ + +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.DataFlavor; +import java.awt.Toolkit; + +class +ClipboardApi +implements Transferable, ClipboardOwner { + + private static final ClipboardApi + instance = new ClipboardApi(); + + private static String + string; + +// ---%-@-%--- + + public static void + serve(String string) + { + assert string != null; + instance.string = string; + Toolkit tk = Toolkit.getDefaultToolkit(); + Clipboard cb = tk.getSystemClipboard(); + cb.setContents(instance, instance); + } + +// - -%- - + + public String + getTransferData(DataFlavor flavour) + { + assert flavour == DataFlavor.stringFlavor; + return string; + } + + public DataFlavor[] + getTransferDataFlavors() + { + return new DataFlavor[] { DataFlavor.stringFlavor }; + /* + * We should probably also support javaJVMLocalObjectMimeType, + * so that the compose window can ask for the List. + * Although also like, if we don't store emoji shortcodes in + * the image segments, there is no point. Anyways, what is + * important is the string format first, allowing us to + * copy links or large lengths of text. + */ + } + + public boolean + isDataFlavorSupported(DataFlavor flavour) + { + return flavour == DataFlavor.stringFlavor; + } + + public void + lostOwnership(Clipboard clipboard, Transferable contents) { } + +} diff --git a/MastodonApi.java b/MastodonApi.java index d05b122..9bd8032 100644 --- a/MastodonApi.java +++ b/MastodonApi.java @@ -180,22 +180,22 @@ MastodonApi { getTimelinePage( TimelineType type, int count, String maxId, String minId, - String accountId, String listId, + String accountId, String listId, RequestListener handler) { String token = accessToken.get("access_token").value; - assert !(accountId != null && listId != null); + assert !(accountId != null && listId != null); String url = instanceUrl + "/api/v1"; if (accountId != null) { url += "/accounts/" + accountId + "/statuses"; } - else if (listId != null) - { - url += "/lists/" + listId; - } + else if (listId != null) + { + url += "/lists/" + listId; + } else switch (type) { case FEDERATED: @@ -325,11 +325,11 @@ MastodonApi { catch (IOException eIo) { handler.connectionFailed(eIo); } } - public void - getNotifications( - int count, String maxId, String minId, - RequestListener handler) - { + public void + getNotifications( + int count, String maxId, String minId, + RequestListener handler) + { String token = accessToken.get("access_token").value; String url = instanceUrl + "/api/v1/notifications"; @@ -349,51 +349,93 @@ MastodonApi { doStandardJsonReturn(conn, handler); } catch (IOException eIo) { handler.connectionFailed(eIo); } - } + } - public void - deletePost(String postId, RequestListener handler) - { - String token = accessToken.get("access_token").value; + public void + deletePost(String postId, RequestListener handler) + { + String token = accessToken.get("access_token").value; - String url = instanceUrl + "/api/v1/statuses/" + postId; - try - { - URL endpoint = new URL(url); - HttpURLConnection conn; - conn = (HttpURLConnection)endpoint.openConnection(); - String s1 = "Bearer " + token; - conn.setRequestProperty("Authorization", s1); - conn.setRequestMethod("DELETE"); - conn.connect(); + String url = instanceUrl + "/api/v1/statuses/" + postId; + try + { + URL endpoint = new URL(url); + HttpURLConnection conn; + conn = (HttpURLConnection)endpoint.openConnection(); + String s1 = "Bearer " + token; + conn.setRequestProperty("Authorization", s1); + conn.setRequestMethod("DELETE"); + conn.connect(); - doStandardJsonReturn(conn, handler); - } - catch (IOException eIo) { handler.connectionFailed(eIo); } - } + doStandardJsonReturn(conn, handler); + } + catch (IOException eIo) { handler.connectionFailed(eIo); } + } - public void - getAccounts(String query, RequestListener handler) - { - assert query != null; - String token = accessToken.get("access_token").value; + public void + getSpecificPost(String postId, RequestListener handler) + { + String token = accessToken.get("access_token").value; - String url = instanceUrl + "/api/v1/accounts/search"; - url += "?q=" + encode(query); + String url = instanceUrl + "/api/v1/statuses/" + postId; + try + { + URL endpoint = new URL(url); + HttpURLConnection conn; + conn = (HttpURLConnection)endpoint.openConnection(); + String s1 = "Bearer " + token; + conn.setRequestProperty("Authorization", s1); + conn.connect(); - try - { - URL endpoint = new URL(url); - HttpURLConnection conn; - conn = (HttpURLConnection)endpoint.openConnection(); - String s1 = "Bearer " + token; - conn.setRequestProperty("Authorization", s1); - conn.connect(); + doStandardJsonReturn(conn, handler); + } + catch (IOException eIo) { handler.connectionFailed(eIo); } + } - doStandardJsonReturn(conn, handler); - } - catch (IOException eIo) { handler.connectionFailed(eIo); } - } + public void + getPostContext(String postId, RequestListener handler) + { + String token = accessToken.get("access_token").value; + + String s1 = instanceUrl + "/api/v1/statuses/"; + String s2 = postId + "/context"; + String url = s1 + s2; + try + { + URL endpoint = new URL(url); + HttpURLConnection conn; + conn = (HttpURLConnection)endpoint.openConnection(); + String s3 = "Bearer " + token; + conn.setRequestProperty("Authorization", s3); + conn.connect(); + + doStandardJsonReturn(conn, handler); + } + catch (IOException eIo) { handler.connectionFailed(eIo); } + } + + public void + getAccounts(String query, RequestListener handler) + { + assert query != null; + String token = accessToken.get("access_token").value; + + String url = instanceUrl + "/api/v1/accounts/search"; + url += "?q=" + encode(query); + + try + { + URL endpoint = new URL(url); + HttpURLConnection conn; + conn = (HttpURLConnection)endpoint.openConnection(); + String s1 = "Bearer " + token; + conn.setRequestProperty("Authorization", s1); + conn.connect(); + + doStandardJsonReturn(conn, handler); + } + catch (IOException eIo) { handler.connectionFailed(eIo); } + } public void monitorTimeline( @@ -486,6 +528,25 @@ MastodonApi { handler.requestSucceeded(response); } +// - -%- - + + public static void + debugPrint(Tree tree) + { + debugPrint(tree, ""); + } + + public static void + debugPrint(Tree tree, String prefix) + { + System.err.print(prefix); + System.err.print(tree.key); + System.err.print(": "); + System.err.println(tree.value); + for (Tree child: tree) + debugPrint(child, prefix + " "); + } + // - -%- - private static Tree @@ -516,7 +577,7 @@ MastodonApi { } } -// ---%-@-%--- +// ---%-@-%--- public void loadCache() @@ -580,7 +641,7 @@ MastodonApi { w.close(); } -// - -%- - +// - -%- - private static String getCachePath() diff --git a/PostWindow.java b/PostWindow.java index ebcea31..af19740 100755 --- a/PostWindow.java +++ b/PostWindow.java @@ -41,8 +41,7 @@ import java.time.format.DateTimeFormatter; class -PostWindow extends JFrame -implements ActionListener { +PostWindow extends JFrame { private JKomasto primaire; @@ -56,10 +55,7 @@ implements ActionListener { // - -%- - private PostComponent - postDisplay; - - private RepliesComponent - repliesDisplay; + display; // - -%- - @@ -83,21 +79,21 @@ implements ActionListener { String an = author.get("display_name").value; if (an.isEmpty()) an = author.get("username").value; - postDisplay.setAuthorName(an); - postDisplay.setAuthorId(author.get("acct").value); + display.setAuthorName(an); + display.setAuthorId(author.get("acct").value); String aid = author.get("id").value; String oid = api.getAccountDetails().get("id").value; - postDisplay.setDeleteEnabled(aid.equals(oid)); + display.setDeleteEnabled(aid.equals(oid)); String avurl = author.get("avatar").value; - postDisplay.setAuthorAvatar(ImageApi.remote(avurl)); + display.setAuthorAvatar(ImageApi.remote(avurl)); String sdate = post.get("created_at").value; ZonedDateTime date = ZonedDateTime.parse(sdate); date = date.withZoneSameInstant(ZoneId.systemDefault()); - postDisplay.setDate(DATE_FORMAT.format(date)); - postDisplay.setTime(TIME_FORMAT.format(date)); + display.setDate(DATE_FORMAT.format(date)); + display.setTime(TIME_FORMAT.format(date)); String[][] emojiUrls = new String[emojis.size()][]; for (int o = 0; o < emojiUrls.length; ++o) { @@ -106,13 +102,13 @@ implements ActionListener { emojiUrls[o][0] = emoji.get("shortcode").value; emojiUrls[o][1] = emoji.get("url").value; } - postDisplay.setEmojiUrls(emojiUrls); + display.setEmojiUrls(emojiUrls); - postDisplay.setHtml(post.get("content").value); + display.setHtml(post.get("content").value); boolean f = post.get("favourited").value.equals("true"); boolean b = post.get("reblogged").value.equals("true"); - postDisplay.setFavourited(f); - postDisplay.setBoosted(b); + display.setFavourited(f); + display.setBoosted(b); if (media.size() > 0) { @@ -121,14 +117,14 @@ implements ActionListener { String u2 = first.get("text_url").value; String u3 = first.get("url").value; String purl = u1 != null ? u1 : u2 != null ? u2 : u3; - postDisplay.setMediaPreview(ImageApi.remote(purl)); + display.setMediaPreview(ImageApi.remote(purl)); } - else postDisplay.setMediaPreview(null); + else display.setMediaPreview(null); String html = post.get("content").value; setTitle(TimelineComponent.textApproximation(html)); - postDisplay.resetFocus(); + display.resetFocus(); repaint(); } @@ -153,9 +149,9 @@ implements ActionListener { Tree boosted = post.get("reblog"); if (boosted.size() > 0) post = boosted; - postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); - postDisplay.setFavouriteBoostEnabled(false); - postDisplay.paintImmediately(postDisplay.getBounds()); + display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + display.setFavouriteBoostEnabled(false); + display.paintImmediately(display.getBounds()); RequestListener handler = new RequestListener() { public void @@ -189,9 +185,9 @@ implements ActionListener { }; String postId = post.get("id").value; api.setPostFavourited(postId, favourited, handler); - postDisplay.setCursor(null); - postDisplay.setFavouriteBoostEnabled(true); - postDisplay.repaint(); + display.setCursor(null); + display.setFavouriteBoostEnabled(true); + display.repaint(); } public void @@ -201,9 +197,9 @@ implements ActionListener { Tree boosted2 = post.get("reblog"); if (boosted2.size() > 0) post = boosted2; - postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); - postDisplay.setFavouriteBoostEnabled(false); - postDisplay.paintImmediately(postDisplay.getBounds()); + display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + display.setFavouriteBoostEnabled(false); + display.paintImmediately(display.getBounds()); RequestListener handler = new RequestListener() { public void @@ -237,9 +233,9 @@ implements ActionListener { }; String postId = post.get("id").value; api.setPostBoosted(postId, boosted, handler); - postDisplay.setCursor(null); - postDisplay.setFavouriteBoostEnabled(true); - postDisplay.repaint(); + display.setCursor(null); + display.setFavouriteBoostEnabled(true); + display.repaint(); } public void @@ -262,7 +258,7 @@ implements ActionListener { if (vs.equals("unlisted")) v = PostVisibility.UNLISTED; if (vs.equals("private")) v = PostVisibility.FOLLOWERS; if (vs.equals("direct")) v = PostVisibility.MENTIONED; - + Composition c = new Composition(); c.contentWarning = cw; c.text = id1.equals(id2) ? "" : "@" + authorId + " "; @@ -310,9 +306,9 @@ implements ActionListener { public void deletePost(boolean redraft) { - postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); - postDisplay.setDeleteEnabled(false); - postDisplay.paintImmediately(postDisplay.getBounds()); + display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + display.setDeleteEnabled(false); + display.paintImmediately(display.getBounds()); if (redraft) { @@ -354,7 +350,7 @@ implements ActionListener { } String cw = post.get("spoiler_text").value; - + String vs = post.get("visibility").value; PostVisibility v = null; if (vs.equals("public")) v = PostVisibility.PUBLIC; @@ -407,38 +403,47 @@ implements ActionListener { }); - postDisplay.setCursor(null); - postDisplay.setDeleteEnabled(true); - postDisplay.paintImmediately(postDisplay.getBounds()); + display.setCursor(null); + display.setDeleteEnabled(true); + display.paintImmediately(display.getBounds()); if (!isVisible()) dispose(); } -// - -%- - + public void + copyPostId() + { + Tree post = this.post; + Tree reblogged = post.get("reblog"); + if (reblogged.size() > 0) post = reblogged; - public void - actionPerformed(ActionEvent eA) - { - Component src = (Component)eA.getSource(); - if (!(src instanceof JMenuItem)) return; - String text = ((JMenuItem)src).getText(); + ClipboardApi.serve(post.get("id").value); + } - 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(); - } - } + public void + copyPostLink() + { + Tree post = this.post; + Tree reblogged = post.get("reblog"); + if (reblogged.size() > 0) post = reblogged; + + String url = post.get("url").value; + if (url == null) url = post.get("uri").value; + + ClipboardApi.serve(url); + } + + public void + openReplies() + { + Tree post = this.post; + Tree boosted = post.get("reblog"); + if (boosted.size() > 0) post = boosted; + + RepliesWindow w = new RepliesWindow(primaire, this); + w.showFor(post.get("id").value); + w.setLocation(getX(), getY() + 100); + w.setVisible(true); + } // ---%-@-%--- @@ -452,10 +457,9 @@ implements ActionListener { setDefaultCloseOperation(DISPOSE_ON_CLOSE); setLocationByPlatform(true); - postDisplay = new PostComponent(this); - repliesDisplay = new RepliesComponent(); + display = new PostComponent(this); - setContentPane(postDisplay); + setContentPane(display); } } @@ -496,6 +500,9 @@ implements ActionListener { miscMenu; private JMenuItem + openReplies, + copyPostId, + copyPostLink, deletePost, redraftPost; @@ -667,17 +674,11 @@ implements ActionListener { return; } - if (src == deletePost) - { - primaire.deletePost(false); - return; - } - - if (src == redraftPost) - { - primaire.deletePost(true); - 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); } @@ -731,12 +732,24 @@ implements ActionListener { 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(); @@ -805,175 +818,3 @@ implements ActionListener { } } - - - -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); - } - -} diff --git a/RepliesWindow.java b/RepliesWindow.java new file mode 100755 index 0000000..6ac0714 --- /dev/null +++ b/RepliesWindow.java @@ -0,0 +1,300 @@ + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTree; +import javax.swing.JOptionPane; +import javax.swing.BorderFactory; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeSelectionModel; +import javax.swing.event.TreeSelectionListener; +import javax.swing.event.TreeSelectionEvent; +import java.awt.Dimension; +import java.awt.Cursor; +import java.awt.BorderLayout; +import java.util.Enumeration; +import cafe.biskuteri.hinoki.Tree; +import java.io.IOException; + +class +RepliesWindow extends JFrame { + + private JKomasto + primaire; + + private MastodonApi + api; + + private PostWindow + postWindow; + +// - -%- - + + private RepliesComponent + display; + +// ---%-@-%--- + + public void + showFor(String postId) + { + display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + Tree thread = getThread(postId); + if (thread != null) display.showThread(thread); + display.setCursor(null); + if (thread == null) dispose(); + } + +// - -%- - + + public void + postSelected(Tree post) + { + postWindow.displayEntity(post); + } + + private Tree + getThread(String postId) + { + abstract class Handler implements RequestListener { + + boolean + failed = false; + +// -=%=- + + public void + connectionFailed(IOException eIo) + { + JOptionPane.showMessageDialog( + RepliesWindow.this, + "Failed to fetch post context...." + + "\n" + eIo.getMessage() + ); + failed = true; + } + + public void + requestFailed(int httpCode, Tree json) + { + JOptionPane.showMessageDialog( + RepliesWindow.this, + "Failed to fetch post context...." + + "\n" + json.get("error").value + + "\n(HTTP code: " + httpCode + ")" + ); + failed = true; + } + + } + + class TopPostIdGetter extends Handler { + + String + topPostId; + +// -=%=- + + public void + requestSucceeded(Tree json) + { + Tree ancestors = json.get("ancestors"); + if (ancestors.size() == 0) topPostId = postId; + else topPostId = ancestors.get(0).get("id").value; + } + + }; + + class DescendantsGetter extends Handler { + + Tree + descendants; + +// -=%=- + + public void + requestSucceeded(Tree json) + { + descendants = json.get("descendants"); + } + + }; + + class PostGetter extends Handler { + + Tree + post; + +// -=%=- + + public void + requestSucceeded(Tree json) + { + post = json; + } + + } + + TopPostIdGetter phase1 = new TopPostIdGetter(); + api.getPostContext(postId, phase1); + if (phase1.failed) return null; + DescendantsGetter phase2 = new DescendantsGetter(); + api.getPostContext(phase1.topPostId, phase2); + if (phase2.failed) return null; + PostGetter phase3 = new PostGetter(); + api.getSpecificPost(phase1.topPostId, phase3); + if (phase3.failed) return null; + + Tree thread = new Tree(); + phase3.post.key = "top"; + thread.add(phase3.post); + thread.add(phase2.descendants); + return thread; + } + +// ---%-@-%--- + + RepliesWindow(JKomasto primaire, PostWindow postWindow) + { + super("Thread"); + + this.primaire = primaire; + this.api = primaire.getMastodonApi(); + this.postWindow = postWindow; + + display = new RepliesComponent(this); + setContentPane(display); + setSize(384, 224); + } + +} + +class +RepliesComponent extends JPanel +implements TreeSelectionListener { + + private RepliesWindow + primaire; + + private Tree + thread; + +// - -%- - + + private JTree + tree; + +// ---%-@-%--- + + public void + showThread(Tree thread) + { + Enumeration e; + DefaultMutableTreeNode root; + TreeItem item; + item = new TreeItem(thread.get("top")); + root = new DefaultMutableTreeNode(item); + for (Tree desc: thread.get("descendants")) + { + String target = desc.get("in_reply_to_id").value; + assert target != null; + + DefaultMutableTreeNode p = null; + e = root.breadthFirstEnumeration(); + while (e.hasMoreElements()) + { + DefaultMutableTreeNode node; + node = (DefaultMutableTreeNode)e.nextElement(); + item = (TreeItem)node.getUserObject(); + + String postId = item.post.get("id").value; + if (postId.equals(target)) + { + p = node; + break; + } + } + if (p == null) + { + assert false; + continue; + } + + item = new TreeItem(desc); + p.add(new DefaultMutableTreeNode(item)); + } + + tree.setModel(new DefaultTreeModel(root)); + } + +// - -%- - + + public void + valueChanged(TreeSelectionEvent eT) + { + Object selected = eT.getPath().getLastPathComponent(); + assert selected instanceof DefaultMutableTreeNode; + + TreeItem item = (TreeItem) + ((DefaultMutableTreeNode)selected) + .getUserObject(); + + primaire.postSelected(item.post); + } + +// ---%-@-%--- + + private static class + TreeItem { + + public Tree + post; + +// -=%=- + + public String + toString() + { + String html = post.get("content").value; + return TimelineComponent.textApproximation(html); + } + +// -=%=- + + TreeItem(Tree post) + { + this.post = post; + } + + } + +// ---%-@-%--- + + RepliesComponent(RepliesWindow primaire) + { + this.primaire = primaire; + + tree = new JTree(); + tree.setBackground(null); + DefaultTreeCellRenderer renderer; + renderer = new DefaultTreeCellRenderer(); + renderer.setBackgroundNonSelectionColor(null); + renderer.setOpenIcon(null); + renderer.setClosedIcon(null); + renderer.setLeafIcon(null); + tree.setCellRenderer(renderer); + int mode = TreeSelectionModel.SINGLE_TREE_SELECTION; + tree.getSelectionModel().setSelectionMode(mode); + tree.addTreeSelectionListener(this); + tree.setFont(tree.getFont().deriveFont(16f)); + + setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + + setLayout(new BorderLayout()); + add(tree); + } + +} diff --git a/RichTextPane.java b/RichTextPane.java index 42124e1..310c8b6 100644 --- a/RichTextPane.java +++ b/RichTextPane.java @@ -6,7 +6,6 @@ import java.awt.FontMetrics; import java.awt.Image; import java.awt.Color; import java.awt.Dimension; -import java.awt.Toolkit; import java.util.List; import java.util.LinkedList; import java.util.ListIterator; @@ -15,16 +14,10 @@ import java.awt.event.MouseMotionListener; import java.awt.event.MouseEvent; import java.awt.event.KeyListener; import java.awt.event.KeyEvent; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.ClipboardOwner; -import java.awt.datatransfer.Transferable; -import java.awt.datatransfer.DataFlavor; class RichTextPane extends JComponent -implements - MouseListener, MouseMotionListener, KeyListener, - Transferable, ClipboardOwner { +implements MouseListener, MouseMotionListener, KeyListener { private List text; @@ -56,6 +49,19 @@ implements return returnee; } + public void + copySelection() + { + assert selectionEnd != -1; + StringBuilder b = new StringBuilder(); + for (Segment segment: getSelection()) + { + if (segment.link != null) b.append(segment.link); + else if (segment.text != null) b.append(segment.text); + } + ClipboardApi.serve(b.toString()); + } + // - -%- - protected void @@ -83,7 +89,7 @@ implements if (o > selectionStart && o < selectionEnd) { - int dx = fm.stringWidth(segment.text); + int dx = fm.stringWidth(segment.text); int dy1 = fm.getAscent(); int dy2 = dy1 + fm.getDescent(); g.setColor(new Color(0, 0, 0, 15)); @@ -139,54 +145,16 @@ implements return o; } - public String - getTransferData(DataFlavor flavour) - { - assert flavour == DataFlavor.stringFlavor; - StringBuilder b = new StringBuilder(); - for (Segment segment: getSelection()) - { - if (segment.link != null) b.append(segment.link); - else if (segment.text != null) b.append(segment.text); - } - return b.toString(); - } - - public DataFlavor[] - getTransferDataFlavors() - { - return new DataFlavor[] { DataFlavor.stringFlavor }; - /* - * We should probably also support javaJVMLocalObjectMimeType, - * so that the compose window can ask for the List. - * Although also like, if we don't store emoji shortcodes in - * the image segments, there is no point. Anyways, what is - * important is the string format first, allowing us to - * copy links or large lengths of text. - */ - } - - public boolean - isDataFlavorSupported(DataFlavor flavour) - { - return flavour == DataFlavor.stringFlavor; - } - public void keyPressed(KeyEvent eK) { if (selectionEnd == -1) return; if (eK.getKeyCode() != KeyEvent.VK_C) return; if (!eK.isControlDown()) return; - Toolkit tk = Toolkit.getDefaultToolkit(); - Clipboard cb = tk.getSystemClipboard(); - cb.setContents(this, this); + copySelection(); } - public void - lostOwnership(Clipboard clipboard, Transferable contents) { } - public void keyReleased(KeyEvent eK) { } @@ -222,7 +190,7 @@ implements while (cursor.hasNext()) { Segment curr = cursor.next(); - + int dx; if (curr.image != null) { int ow = curr.image.getIconWidth(); @@ -243,7 +211,7 @@ implements } boolean fits = x + dx < width; - + if (fits || curr.spacer) { curr.x = x; @@ -434,4 +402,4 @@ implements addKeyListener(this); } -} \ No newline at end of file +} diff --git a/graphics/test1.png b/graphics/test1.png new file mode 100755 index 0000000000000000000000000000000000000000..17d196ec6eca31691a6002a22d281f3585c07242 GIT binary patch literal 2170 zcmV-=2!;2FP)WFU8GbZ8()Nlj2>E@cM*00-VlL_t(o!>yS8Q&VRa z$3M?=@6DURgb>6LMJdWUwcA-;U0=pgTeVWlsz0sFNPp!Y6K8a0$C0YDjEXz$6f3)~ z9m{qV6fGhk1ri`Pxp}$wKKnyLNaQUb=ci2OdHLS+obNf`b3|%pMzIc4N?~|BASFVG zW!K=kSD zN3f;_scC42k>9`4)=E5>OQ-RNLI@$S(`j<46dz-{-%mP~!nbMDs%uvTSo)>eM1qYp z>GdKtZT(NId%XxDNYBimTzBO!Ej;^b>q^bdWKdF~`Fv{!I1kQ>#a6sT^LP+amH||* zi<8Y(MmE>QPNmQTfyx0^3eG4TIx#W9&rhEsgy7Ey4-$<=adWvmi!1bY)->@1gNwsp zJ>Ybo58dx4X_{Q?>_pdf&YnHX(PPKBdGlu8vtIAA9@l+7be|8+<5>VAC5a{Olphje zS#UxiGz}vVz*UM^EJkl{FIU>zi9{l3y3XA@cj)No;96&A(MzTKjX(gY>kJGGFg-oJ zXpP_J<9UDo(g1VmG@4f++1O9YpQ z71K1Si9`UX3I=fsiMcdJJsuBN+S^e|Et^ZFl$gn6c^@d-m*EHZE^mzs}{hHk49KO-*BE(xm*rc zDYkCiifh}8a;K(gjEs&h02ES^OQq2LeuNO1rb*YWTU@w!u_AD*nQ5Bby>q9ShN_kb zkq@nT_wIZQ&gUxK?+2y$M|U^gl2}=*&9igfD?JY$AcWv@TiZf%cAR`AS;Bks77m5z zfBF>N@Oa23lSobD_U+sJ^}>ZRlC0HJb8~Y9OGLowGiUNjD3d9wo=VE0uItRs%^{I8 z&t9cC^~D#w92_LRN(EWYg25pF?eE7Zq??a5+i{RW2yk6&%c82PifA-SEFP!x>eY1} zdj9-*vL(#ydGLUTJw51NZ?S^tUN5@WTUJRb5(~Lhif}kgbtFPlQxo5Hb#eH}5$fvd zDh8meKnKPKC$&KsR%Sf^ki&w10b~=r1StzCWrnMDa*SXf&NzydeMVk4izueZw z-!5M!QeC|^&A41IVOthb2qKXPSK8a@?d>HNi{TU)xJcU-{#Q!nSzP$PyStm|>1iCt zA!S)qMigAjuM zJb6Mi8bv8ZOG^v3ZPV4&#f=*`7S7IOD$U2YH8ez^s){{LO-sL?ZRc}w zHknlE+1WJ*{)b}X=+UD{DY^gP0UaG3DAy%znpl>_&%gY_>4G$yocNvE+9-)cf=zz^ zvd=s8D6d zVpHs{8p_W8@u#1tsjfyBLgd@Z3Y(loY)_{_2*TkoD23y? z{OidR_I>sluIrM^<=D1!XHo5Js;Z*Cp#h~75x<`wzyF@5#>Pc*3*e6j4&)QH5eTdc zU@4JG)$#s)`*{2A9doA1;bX_j8!f|N%hs&`-0kY(^vRQpjvF2sK^EEp31H35ZEQm> zC5;UY?5wZn*AIFOfbr2${`c$|XHK0Wo6VMq64!MZ8y{y!efDVxo> zj)R-YY>ZU0ZC(uzv!}V4$%zScDX9*Hn2g2Hy_Bgc=g z&=*U&Apmt<$1n_D4Ggd~8YLQuplKR)(I~o7cnpIVgM-EOkNWx$LX?49ved0K$XSd1 z{njl~i3EQr^cDcWK7Wo7f^aCr%;Y5he)6Q4^8ikM`DNwfx2$Ovss1shlq$0H)}1?C zYHclJZ13a82q8Fi`t;hwf+rZnTB7q`Pqmr1+M1f;Z_BcH)YpfUa#?T{4KSbg_tVta zxFOz5Sr$I8mluPB#NzQqgjn|oc>L{K%w&>=?c0mffwh|BI1G=BFg8Ap5Q65WCboU@ zNwEd}D1h_!W^j;^(NUUr?wo%` w*||CFRW8sqjnL-J96WKNOx`=TT~Yo2KVZY?hru)C8UO$Q07*qoM6N<$f>5(CBme*a literal 0 HcmV?d00001 diff --git a/graphics/test2.png b/graphics/test2.png new file mode 100755 index 0000000000000000000000000000000000000000..bc41328ba67c7504a5b7aac8865aecb885423962 GIT binary patch literal 2257 zcmV;?2rl=DP)WFU8GbZ8()Nlj2>E@cM*00=cnL_t(o!>yR>Z&T+T z$3N%h*p7o^aw#D=Bu#M98p^2FmNm4{W~ftvY3fBxtp5S(yY6)-Rg=~?Yqfu%tDsdQ zO={7ox~gj#9hW2^TIT{G6bdnRLL3suv5${^F3(=XImF~*C*dm{S+<|c_x!Hk=aKYO zO0W(HA<#S?kYyxET5$}HgIOrx@%xb#g@uu}ZDZzgW%p6nLdq%>@CE`%vJ5~WlfgDk z5M|y`k|@|Vn%_?@ohDFMhpZ?lnl?XwwW*a@&@>Eu!61?(VHpOduCo!SJ|BjzrrfC2;Wn8uxPCk#2WfZS>?F1Ld z<))`s-NYLVB6~b2>ikl+W#JSG7_+mLC7a`5={l<4zx<1@mz*Ta)HF4r`TbnFe3>JM z4)MDqM;I6!MDzPyi>_qthyO2RGL^+(CFH6b8bFpwBoh4kx##%utFMSeBE;iyIyyQK zLg3go*~!TzbE|?uJVk4nx?W=GoMBM2dGpE^loe$~a=_k{IPiZf@ql zOE0l|_wHp4(cad^_}JLeB=H4<*rr*Mt+J-!3x$vsh1<7p^Fmh_7kYaMhr>%2j>Tf^ ze(EUzTDNbe3I1Y0Y6U!EOS(eFUvk!jIQz?<2oD`;E zh;$<1KKX1`ETHMJ$Hd7KC&X%aV#f|4gbmBUiN(Zlf4|UEDWL**0|6|5_57!^33e=CEv=uC6W$y1t}#DvClZ9;Yo5nU6t9mN9jm>c+;BSn1lg zuOf2g5R1jw8Hw5 zH#RoL&0Dt!Z`JaK~I;o)_; zvZbYkzkm24fnvGYC@kAXl_Uwrve1)BT3cJmX0r?p4RNr$dtLGO)z@DW{qaZl`+|!< z`SjBzE_N)dY|oY#H|@;5d+dxvIC}IbiHQkb=<1@kw|7khy$>mq&w6_J>-+D!Nz&8P z!{sYi=)H8QY|mcO>V)HSd{`1{;`2Bvu&CT4sdv{$)0}u{}85ter>c|M4 zot;OStL~!Pgb=8z>b}g8!-p9d7;xu3 z`Q(#4`|Pvqedd`}vv^g43&J+-J^Tunh@z$|p4-Q2|eDm$Mymjmt zk|goxci-igd-k}6ddWOMpPsHrGfJjmh;pLzVPP7J#dv=2UjBFfJgu#*?wDPjoeT~Q zxu0|6!oyF93 ze4!ADi3x^>hskEM^!N7zaP#U_3|+_P^}6y*RuuBN9MQf$Hr3QHmri5bHma)9+R{?8 z{>bPkb)nFFp;k1_EuL$^Lbgk?%+5#zgg^kdZror~C`2F_#57H|Zr|=I?CP2t>KYml zLeSLG!nw0&X>V&QS=YAh`IvtC>3PAb`Tgr6_;CGPK*;N5?CxFCnGDZ8|9q(|(=-~3 zar@<|Q*`g&zhu2Tu^4jk_Cf~8rPGi0KrYL)wY0FkxtZaI&KLmW@iho=wm}#2azki>bw{CGa9!HWSJd0adG0uHHAA#bv$C>ZH zCpk4mrdR<+Mn{n(iMB|jv``xxyl+_`I5{;%C={BPT(V3{u}REQ{YBJjm@kcgSS3fM6#|c@m8k3seSJuhR6=gqUcA~x&RX!#FTOyZo8yV%*#cnr+BGCeqBa;L zl}vK!@@2QB0qlSI<;t($3YmKlO&!p+hzPM_xYhYy!z?D^haBuU~xclX-Vj3*Ev zS3HVt1dB^ZL$SpHCl(_yK8_?wMB3Zg`uO9BVq4n~!3F$0I!Y`aC(_YD zZ7_(cs%1+}YIfGmR#}z_H#f6$&mM#jnECvpCAb)aSFSLdPGgnXKvfiin>O*Q7hf#7 fn6fRaqW=GXR#js)1Nr$}00000NkvXXu0mjfoIqDg literal 0 HcmV?d00001 diff --git a/graphics/test3.png b/graphics/test3.png new file mode 100755 index 0000000000000000000000000000000000000000..86e400e14e6b3bf302979bf912b1e2fe357b8da0 GIT binary patch literal 2220 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 s#{m{e!sB-H`$rzhYE0>xmKQ