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(
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<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>
@ -516,7 +577,7 @@ MastodonApi {
}
}
// ---%-@-%---
// ---%-@-%---
public void
loadCache()
@ -580,7 +641,7 @@ MastodonApi {
w.close();
}
// - -%- -
// - -%- -
private static String
getCachePath()

View File

@ -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<String> 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<String> 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<String> post = this.post;
Tree<String> 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<String> post = this.post;
Tree<String> 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<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);
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<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.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<Segment>
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<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
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);
}
}
}

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
CLASSPATH=.:../Hinoki:/usr/share/java/javax.json.jar
OPTIONS=
c:
javac -cp $(CLASSPATH) *.java
javac -cp $(CLASSPATH) $(OPTIONS) *.java
r:
java -cp $(CLASSPATH) -ea JKomasto
java -cp $(CLASSPATH) $(OPTIONS) -ea JKomasto
cr: c r