Added replies window, based on JTree

This commit is contained in:
Snowyfox 2022-04-29 13:44:38 -04:00
parent 39526a145f
commit 9c75464b18
12 changed files with 605 additions and 353 deletions

62
ClipboardApi.java Executable file
View File

@ -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<Segment>.
* 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) { }
}

View File

@ -180,22 +180,22 @@ MastodonApi {
getTimelinePage( getTimelinePage(
TimelineType type, TimelineType type,
int count, String maxId, String minId, int count, String maxId, String minId,
String accountId, String listId, String accountId, String listId,
RequestListener handler) RequestListener handler)
{ {
String token = accessToken.get("access_token").value; String token = accessToken.get("access_token").value;
assert !(accountId != null && listId != null); assert !(accountId != null && listId != null);
String url = instanceUrl + "/api/v1"; String url = instanceUrl + "/api/v1";
if (accountId != null) if (accountId != null)
{ {
url += "/accounts/" + accountId + "/statuses"; url += "/accounts/" + accountId + "/statuses";
} }
else if (listId != null) else if (listId != null)
{ {
url += "/lists/" + listId; url += "/lists/" + listId;
} }
else switch (type) else switch (type)
{ {
case FEDERATED: case FEDERATED:
@ -325,11 +325,11 @@ MastodonApi {
catch (IOException eIo) { handler.connectionFailed(eIo); } catch (IOException eIo) { handler.connectionFailed(eIo); }
} }
public void public void
getNotifications( getNotifications(
int count, String maxId, String minId, int count, String maxId, String minId,
RequestListener handler) RequestListener handler)
{ {
String token = accessToken.get("access_token").value; String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/notifications"; String url = instanceUrl + "/api/v1/notifications";
@ -349,51 +349,93 @@ MastodonApi {
doStandardJsonReturn(conn, handler); doStandardJsonReturn(conn, handler);
} }
catch (IOException eIo) { handler.connectionFailed(eIo); } catch (IOException eIo) { handler.connectionFailed(eIo); }
} }
public void public void
deletePost(String postId, RequestListener handler) deletePost(String postId, RequestListener handler)
{ {
String token = accessToken.get("access_token").value; String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/statuses/" + postId; String url = instanceUrl + "/api/v1/statuses/" + postId;
try try
{ {
URL endpoint = new URL(url); URL endpoint = new URL(url);
HttpURLConnection conn; HttpURLConnection conn;
conn = (HttpURLConnection)endpoint.openConnection(); conn = (HttpURLConnection)endpoint.openConnection();
String s1 = "Bearer " + token; String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1); conn.setRequestProperty("Authorization", s1);
conn.setRequestMethod("DELETE"); conn.setRequestMethod("DELETE");
conn.connect(); conn.connect();
doStandardJsonReturn(conn, handler); doStandardJsonReturn(conn, handler);
} }
catch (IOException eIo) { handler.connectionFailed(eIo); } catch (IOException eIo) { handler.connectionFailed(eIo); }
} }
public void public void
getAccounts(String query, RequestListener handler) getSpecificPost(String postId, RequestListener handler)
{ {
assert query != null; String token = accessToken.get("access_token").value;
String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/accounts/search"; String url = instanceUrl + "/api/v1/statuses/" + postId;
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();
try doStandardJsonReturn(conn, handler);
{ }
URL endpoint = new URL(url); catch (IOException eIo) { handler.connectionFailed(eIo); }
HttpURLConnection conn; }
conn = (HttpURLConnection)endpoint.openConnection();
String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1);
conn.connect();
doStandardJsonReturn(conn, handler); public void
} getPostContext(String postId, RequestListener handler)
catch (IOException eIo) { handler.connectionFailed(eIo); } {
} 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 public void
monitorTimeline( monitorTimeline(
@ -486,6 +528,25 @@ MastodonApi {
handler.requestSucceeded(response); handler.requestSucceeded(response);
} }
// - -%- -
public static void
debugPrint(Tree<String> tree)
{
debugPrint(tree, "");
}
public static void
debugPrint(Tree<String> tree, String prefix)
{
System.err.print(prefix);
System.err.print(tree.key);
System.err.print(": ");
System.err.println(tree.value);
for (Tree<String> child: tree)
debugPrint(child, prefix + " ");
}
// - -%- - // - -%- -
private static Tree<String> private static Tree<String>
@ -516,7 +577,7 @@ MastodonApi {
} }
} }
// ---%-@-%--- // ---%-@-%---
public void public void
loadCache() loadCache()
@ -580,7 +641,7 @@ MastodonApi {
w.close(); w.close();
} }
// - -%- - // - -%- -
private static String private static String
getCachePath() getCachePath()

View File

@ -41,8 +41,7 @@ import java.time.format.DateTimeFormatter;
class class
PostWindow extends JFrame PostWindow extends JFrame {
implements ActionListener {
private JKomasto private JKomasto
primaire; primaire;
@ -56,10 +55,7 @@ implements ActionListener {
// - -%- - // - -%- -
private PostComponent private PostComponent
postDisplay; display;
private RepliesComponent
repliesDisplay;
// - -%- - // - -%- -
@ -83,21 +79,21 @@ implements ActionListener {
String an = author.get("display_name").value; String an = author.get("display_name").value;
if (an.isEmpty()) an = author.get("username").value; if (an.isEmpty()) an = author.get("username").value;
postDisplay.setAuthorName(an); display.setAuthorName(an);
postDisplay.setAuthorId(author.get("acct").value); display.setAuthorId(author.get("acct").value);
String aid = author.get("id").value; String aid = author.get("id").value;
String oid = api.getAccountDetails().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; String avurl = author.get("avatar").value;
postDisplay.setAuthorAvatar(ImageApi.remote(avurl)); display.setAuthorAvatar(ImageApi.remote(avurl));
String sdate = post.get("created_at").value; String sdate = post.get("created_at").value;
ZonedDateTime date = ZonedDateTime.parse(sdate); ZonedDateTime date = ZonedDateTime.parse(sdate);
date = date.withZoneSameInstant(ZoneId.systemDefault()); date = date.withZoneSameInstant(ZoneId.systemDefault());
postDisplay.setDate(DATE_FORMAT.format(date)); display.setDate(DATE_FORMAT.format(date));
postDisplay.setTime(TIME_FORMAT.format(date)); display.setTime(TIME_FORMAT.format(date));
String[][] emojiUrls = new String[emojis.size()][]; String[][] emojiUrls = new String[emojis.size()][];
for (int o = 0; o < emojiUrls.length; ++o) { for (int o = 0; o < emojiUrls.length; ++o) {
@ -106,13 +102,13 @@ implements ActionListener {
emojiUrls[o][0] = emoji.get("shortcode").value; emojiUrls[o][0] = emoji.get("shortcode").value;
emojiUrls[o][1] = emoji.get("url").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 f = post.get("favourited").value.equals("true");
boolean b = post.get("reblogged").value.equals("true"); boolean b = post.get("reblogged").value.equals("true");
postDisplay.setFavourited(f); display.setFavourited(f);
postDisplay.setBoosted(b); display.setBoosted(b);
if (media.size() > 0) if (media.size() > 0)
{ {
@ -121,14 +117,14 @@ implements ActionListener {
String u2 = first.get("text_url").value; String u2 = first.get("text_url").value;
String u3 = first.get("url").value; String u3 = first.get("url").value;
String purl = u1 != null ? u1 : u2 != null ? u2 : u3; 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; String html = post.get("content").value;
setTitle(TimelineComponent.textApproximation(html)); setTitle(TimelineComponent.textApproximation(html));
postDisplay.resetFocus(); display.resetFocus();
repaint(); repaint();
} }
@ -153,9 +149,9 @@ implements ActionListener {
Tree<String> boosted = post.get("reblog"); Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted; if (boosted.size() > 0) post = boosted;
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
postDisplay.setFavouriteBoostEnabled(false); display.setFavouriteBoostEnabled(false);
postDisplay.paintImmediately(postDisplay.getBounds()); display.paintImmediately(display.getBounds());
RequestListener handler = new RequestListener() { RequestListener handler = new RequestListener() {
public void public void
@ -189,9 +185,9 @@ implements ActionListener {
}; };
String postId = post.get("id").value; String postId = post.get("id").value;
api.setPostFavourited(postId, favourited, handler); api.setPostFavourited(postId, favourited, handler);
postDisplay.setCursor(null); display.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true); display.setFavouriteBoostEnabled(true);
postDisplay.repaint(); display.repaint();
} }
public void public void
@ -201,9 +197,9 @@ implements ActionListener {
Tree<String> boosted2 = post.get("reblog"); Tree<String> boosted2 = post.get("reblog");
if (boosted2.size() > 0) post = boosted2; if (boosted2.size() > 0) post = boosted2;
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
postDisplay.setFavouriteBoostEnabled(false); display.setFavouriteBoostEnabled(false);
postDisplay.paintImmediately(postDisplay.getBounds()); display.paintImmediately(display.getBounds());
RequestListener handler = new RequestListener() { RequestListener handler = new RequestListener() {
public void public void
@ -237,9 +233,9 @@ implements ActionListener {
}; };
String postId = post.get("id").value; String postId = post.get("id").value;
api.setPostBoosted(postId, boosted, handler); api.setPostBoosted(postId, boosted, handler);
postDisplay.setCursor(null); display.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true); display.setFavouriteBoostEnabled(true);
postDisplay.repaint(); display.repaint();
} }
public void public void
@ -262,7 +258,7 @@ implements ActionListener {
if (vs.equals("unlisted")) v = PostVisibility.UNLISTED; if (vs.equals("unlisted")) v = PostVisibility.UNLISTED;
if (vs.equals("private")) v = PostVisibility.FOLLOWERS; if (vs.equals("private")) v = PostVisibility.FOLLOWERS;
if (vs.equals("direct")) v = PostVisibility.MENTIONED; if (vs.equals("direct")) v = PostVisibility.MENTIONED;
Composition c = new Composition(); Composition c = new Composition();
c.contentWarning = cw; c.contentWarning = cw;
c.text = id1.equals(id2) ? "" : "@" + authorId + " "; c.text = id1.equals(id2) ? "" : "@" + authorId + " ";
@ -310,9 +306,9 @@ implements ActionListener {
public void public void
deletePost(boolean redraft) deletePost(boolean redraft)
{ {
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
postDisplay.setDeleteEnabled(false); display.setDeleteEnabled(false);
postDisplay.paintImmediately(postDisplay.getBounds()); display.paintImmediately(display.getBounds());
if (redraft) if (redraft)
{ {
@ -354,7 +350,7 @@ implements ActionListener {
} }
String cw = post.get("spoiler_text").value; String cw = post.get("spoiler_text").value;
String vs = post.get("visibility").value; String vs = post.get("visibility").value;
PostVisibility v = null; PostVisibility v = null;
if (vs.equals("public")) v = PostVisibility.PUBLIC; if (vs.equals("public")) v = PostVisibility.PUBLIC;
@ -407,38 +403,47 @@ implements ActionListener {
}); });
postDisplay.setCursor(null); display.setCursor(null);
postDisplay.setDeleteEnabled(true); display.setDeleteEnabled(true);
postDisplay.paintImmediately(postDisplay.getBounds()); display.paintImmediately(display.getBounds());
if (!isVisible()) dispose(); if (!isVisible()) dispose();
} }
// - -%- - public void
copyPostId()
{
Tree<String> post = this.post;
Tree<String> reblogged = post.get("reblog");
if (reblogged.size() > 0) post = reblogged;
public void ClipboardApi.serve(post.get("id").value);
actionPerformed(ActionEvent eA) }
{
Component src = (Component)eA.getSource();
if (!(src instanceof JMenuItem)) return;
String text = ((JMenuItem)src).getText();
if (text.equals("Post")) public void
{ copyPostLink()
setContentPane(postDisplay); {
revalidate(); Tree<String> post = this.post;
/* Tree<String> reblogged = post.get("reblog");
* () Setting a content pane in itself doesn't if (reblogged.size() > 0) post = reblogged;
* do anything to the content pane. Validation
* of the validate root does. Which happens on String url = post.get("url").value;
* window realisation, or by manual call. if (url == null) url = post.get("uri").value;
*/
} ClipboardApi.serve(url);
else if (text.equals("Replies")) }
{
setContentPane(repliesDisplay); public void
revalidate(); openReplies()
} {
} Tree<String> post = this.post;
Tree<String> 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); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setLocationByPlatform(true); setLocationByPlatform(true);
postDisplay = new PostComponent(this); display = new PostComponent(this);
repliesDisplay = new RepliesComponent();
setContentPane(postDisplay); setContentPane(display);
} }
} }
@ -496,6 +500,9 @@ implements ActionListener {
miscMenu; miscMenu;
private JMenuItem private JMenuItem
openReplies,
copyPostId,
copyPostLink,
deletePost, deletePost,
redraftPost; redraftPost;
@ -667,17 +674,11 @@ implements ActionListener {
return; return;
} }
if (src == deletePost) if (src == openReplies) primaire.openReplies();
{ if (src == copyPostId) primaire.copyPostId();
primaire.deletePost(false); if (src == copyPostLink) primaire.copyPostLink();
return; if (src == deletePost) primaire.deletePost(false);
} if (src == redraftPost) primaire.deletePost(true);
if (src == redraftPost)
{
primaire.deletePost(true);
return;
}
} }
@ -731,12 +732,24 @@ implements ActionListener {
nextPrev.addActionListener(this); nextPrev.addActionListener(this);
media.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"); deletePost = new JMenuItem("Delete post");
redraftPost = new JMenuItem("Delete and redraft post"); redraftPost = new JMenuItem("Delete and redraft post");
openReplies.addActionListener(this);
copyPostId.addActionListener(this);
copyPostLink.addActionListener(this);
deletePost.addActionListener(this); deletePost.addActionListener(this);
redraftPost.addActionListener(this); redraftPost.addActionListener(this);
miscMenu = new JPopupMenu(); 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(deletePost);
miscMenu.add(new JSeparator());
miscMenu.add(redraftPost); miscMenu.add(redraftPost);
Box buttons = Box.createVerticalBox(); Box buttons = Box.createVerticalBox();
@ -805,175 +818,3 @@ implements ActionListener {
} }
} }
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);
}
}

300
RepliesWindow.java Executable file
View File

@ -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<String> thread = getThread(postId);
if (thread != null) display.showThread(thread);
display.setCursor(null);
if (thread == null) dispose();
}
// - -%- -
public void
postSelected(Tree<String> post)
{
postWindow.displayEntity(post);
}
private Tree<String>
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<String> 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<String> json)
{
Tree<String> ancestors = json.get("ancestors");
if (ancestors.size() == 0) topPostId = postId;
else topPostId = ancestors.get(0).get("id").value;
}
};
class DescendantsGetter extends Handler {
Tree<String>
descendants;
// -=%=-
public void
requestSucceeded(Tree<String> json)
{
descendants = json.get("descendants");
}
};
class PostGetter extends Handler {
Tree<String>
post;
// -=%=-
public void
requestSucceeded(Tree<String> 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<String> thread = new Tree<String>();
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<String>
thread;
// - -%- -
private JTree
tree;
// ---%-@-%---
public void
showThread(Tree<String> thread)
{
Enumeration<TreeNode> e;
DefaultMutableTreeNode root;
TreeItem item;
item = new TreeItem(thread.get("top"));
root = new DefaultMutableTreeNode(item);
for (Tree<String> 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<String>
post;
// -=%=-
public String
toString()
{
String html = post.get("content").value;
return TimelineComponent.textApproximation(html);
}
// -=%=-
TreeItem(Tree<String> 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);
}
}

View File

@ -6,7 +6,6 @@ import java.awt.FontMetrics;
import java.awt.Image; import java.awt.Image;
import java.awt.Color; import java.awt.Color;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.Toolkit;
import java.util.List; import java.util.List;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.ListIterator; import java.util.ListIterator;
@ -15,16 +14,10 @@ import java.awt.event.MouseMotionListener;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
import java.awt.event.KeyEvent; 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 class
RichTextPane extends JComponent RichTextPane extends JComponent
implements implements MouseListener, MouseMotionListener, KeyListener {
MouseListener, MouseMotionListener, KeyListener,
Transferable, ClipboardOwner {
private List<Segment> private List<Segment>
text; text;
@ -56,6 +49,19 @@ implements
return returnee; 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 protected void
@ -83,7 +89,7 @@ implements
if (o > selectionStart && o < selectionEnd) if (o > selectionStart && o < selectionEnd)
{ {
int dx = fm.stringWidth(segment.text); int dx = fm.stringWidth(segment.text);
int dy1 = fm.getAscent(); int dy1 = fm.getAscent();
int dy2 = dy1 + fm.getDescent(); int dy2 = dy1 + fm.getDescent();
g.setColor(new Color(0, 0, 0, 15)); g.setColor(new Color(0, 0, 0, 15));
@ -139,54 +145,16 @@ implements
return o; 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<Segment>.
* 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 public void
keyPressed(KeyEvent eK) keyPressed(KeyEvent eK)
{ {
if (selectionEnd == -1) return; if (selectionEnd == -1) return;
if (eK.getKeyCode() != KeyEvent.VK_C) return; if (eK.getKeyCode() != KeyEvent.VK_C) return;
if (!eK.isControlDown()) return; if (!eK.isControlDown()) return;
Toolkit tk = Toolkit.getDefaultToolkit(); copySelection();
Clipboard cb = tk.getSystemClipboard();
cb.setContents(this, this);
} }
public void
lostOwnership(Clipboard clipboard, Transferable contents) { }
public void public void
keyReleased(KeyEvent eK) { } keyReleased(KeyEvent eK) { }
@ -222,7 +190,7 @@ implements
while (cursor.hasNext()) while (cursor.hasNext())
{ {
Segment curr = cursor.next(); Segment curr = cursor.next();
int dx; int dx;
if (curr.image != null) { if (curr.image != null) {
int ow = curr.image.getIconWidth(); int ow = curr.image.getIconWidth();
@ -243,7 +211,7 @@ implements
} }
boolean fits = x + dx < width; boolean fits = x + dx < width;
if (fits || curr.spacer) if (fits || curr.spacer)
{ {
curr.x = x; curr.x = x;
@ -434,4 +402,4 @@ implements
addKeyListener(this); addKeyListener(this);
} }
} }

BIN
graphics/test1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
graphics/test2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
graphics/test3.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
graphics/test4.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

10
notifOptions.txt Normal file
View File

@ -0,0 +1,10 @@
KDE_Chimes_1.ogg
KDE_Dialog_Appear.wav
KDE_Event_1.ogg
KDE_Event_2.ogg
KDE_Logout_3.ogg
KDE_TypeWriter_Bell.ogg
KDE_Window_DeIconify.ogg
KDE_Window_Iconify.ogg
KDE_Window_UnMaximize.wav
pop.wav

9
notifOptions.txt~ Normal file
View File

@ -0,0 +1,9 @@
KDE_Chimes_1.ogg
KDE_Dialog_Appear.wav
KDE_Event_1.ogg
KDE_Event_2.ogg
KDE_Logout_3.ogg
KDE_TypeWriter_Bell.ogg
KDE_Window_DeIconify.ogg
KDE_Window_Iconify.ogg
KDE_Window_UnMaximize.wav

5
run
View File

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