Normalised all tabs to 4 spaces, for easier web viewing.

This commit is contained in:
Snowyfox 2022-06-02 14:41:59 -04:00
parent 2719fabef9
commit fa9a5edab0
20 changed files with 3625 additions and 3625 deletions

View File

@ -29,308 +29,308 @@ import cafe.biskuteri.hinoki.Tree;
interface interface
BasicHTMLParser { BasicHTMLParser {
public static Tree<String> public static Tree<String>
parse(String html) parse(String html)
{ {
List<String> segments; List<String> segments;
segments = distinguishTagsFromPcdata(html); segments = distinguishTagsFromPcdata(html);
Tree<String> document; Tree<String> document;
document = toNodes(segments); document = toNodes(segments);
document = splitText(document); document = splitText(document);
document = evaluateHtmlEscapes(document); document = evaluateHtmlEscapes(document);
document = hierarchise(document); document = hierarchise(document);
return document; return document;
} }
// - -%- - // - -%- -
private static List<String> private static List<String>
distinguishTagsFromPcdata(String html) distinguishTagsFromPcdata(String html)
{ {
List<String> returnee = new ArrayList<>(); List<String> returnee = new ArrayList<>();
StringBuilder segment = new StringBuilder(); StringBuilder segment = new StringBuilder();
boolean inTag = false; boolean inTag = false;
for (char c: html.toCharArray()) for (char c: html.toCharArray())
{ {
if (c == '<') if (c == '<')
{ {
String addee = empty(segment); String addee = empty(segment);
if (!addee.isEmpty()) returnee.add(addee); if (!addee.isEmpty()) returnee.add(addee);
inTag = true; inTag = true;
segment.append(c); segment.append(c);
} }
else if (c == '>') else if (c == '>')
{ {
assert inTag; assert inTag;
assert segment.length() > 0; assert segment.length() > 0;
segment.append(c); segment.append(c);
returnee.add(empty(segment)); returnee.add(empty(segment));
inTag = false; inTag = false;
} }
else else
{ {
segment.append(c); segment.append(c);
} }
} }
String addee = empty(segment); String addee = empty(segment);
if (!addee.isEmpty()) returnee.add(addee); if (!addee.isEmpty()) returnee.add(addee);
return returnee; return returnee;
} }
private static Tree<String> private static Tree<String>
toNodes(List<String> segments) toNodes(List<String> segments)
{ {
Tree<String> returnee = new Tree<String>(); Tree<String> returnee = new Tree<String>();
for (String segment: segments) for (String segment: segments)
{ {
boolean isTag = segment.startsWith("<"); boolean isTag = segment.startsWith("<");
Tree<String> node = new Tree<String>(); Tree<String> node = new Tree<String>();
if (!isTag) if (!isTag)
{ {
node.key = "text"; node.key = "text";
node.value = segment; node.value = segment;
returnee.add(node); returnee.add(node);
continue; continue;
} }
node.key = "tag"; node.key = "tag";
String key = null, value = null; String key = null, value = null;
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
boolean inQuotes = false, inValue = false; boolean inQuotes = false, inValue = false;
char[] chars = segment.toCharArray(); char[] chars = segment.toCharArray();
for (int o = 1; o < chars.length - 1; ++o) for (int o = 1; o < chars.length - 1; ++o)
{ {
char c = chars[o]; char c = chars[o];
if (c == '"') if (c == '"')
{ {
inQuotes = !inQuotes; inQuotes = !inQuotes;
} }
else if (inQuotes) else if (inQuotes)
{ {
b.append(c); b.append(c);
} }
else if (c == '=') else if (c == '=')
{ {
assert b.length() > 0; assert b.length() > 0;
key = empty(b); key = empty(b);
inValue = true; inValue = true;
} }
else if (Character.isWhitespace(c)) else if (Character.isWhitespace(c))
{ {
if (b.length() > 0) if (b.length() > 0)
{ {
if (inValue) value = empty(b); if (inValue) value = empty(b);
else key = empty(b); else key = empty(b);
Tree<String> attr = new Tree<String>(); Tree<String> attr = new Tree<String>();
attr.key = key; attr.key = key;
attr.value = value; attr.value = value;
node.add(attr); node.add(attr);
} }
inValue = false; inValue = false;
} }
else else
{ {
b.append(c); b.append(c);
} }
} }
if (b.length() > 0) if (b.length() > 0)
{ {
if (inValue) value = empty(b); if (inValue) value = empty(b);
else key = empty(b); else key = empty(b);
Tree<String> attr = new Tree<String>(); Tree<String> attr = new Tree<String>();
attr.key = key; attr.key = key;
attr.value = value; attr.value = value;
node.add(attr); node.add(attr);
} }
returnee.add(node); returnee.add(node);
} }
return returnee; return returnee;
} }
private static Tree<String> private static Tree<String>
splitText(Tree<String> nodes) splitText(Tree<String> nodes)
{ {
Tree<String> returnee = new Tree<>(); Tree<String> returnee = new Tree<>();
for (Tree<String> node: nodes) for (Tree<String> node: nodes)
{ {
if (node.key.equals("tag")) if (node.key.equals("tag"))
{ {
returnee.add(node); returnee.add(node);
continue; continue;
} }
assert node.key.equals("text"); assert node.key.equals("text");
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
boolean alnum = false, calnum; boolean alnum = false, calnum;
boolean space = false, cspace; boolean space = false, cspace;
boolean emoji = false; boolean emoji = false;
for (char c: node.value.toCharArray()) for (char c: node.value.toCharArray())
{ {
calnum = isMastodonAlnum(c); calnum = isMastodonAlnum(c);
cspace = Character.isWhitespace(c); cspace = Character.isWhitespace(c);
if (c == ':' && !emoji) if (c == ':' && !emoji)
{ {
// See note on #isMastodonAlnum. // See note on #isMastodonAlnum.
if (b.length() > 0)
{
Tree<String> addee = new Tree<>();
addee.key = space ? "space" : "text";
addee.value = empty(b);
returnee.add(addee);
}
emoji = true;
b.append(c);
}
else if (c == ':' && emoji)
{
assert !space;
b.append(c);
Tree<String> addee = new Tree<>();
addee.key = "emoji";
addee.value = empty(b);
returnee.add(addee);
/*
* Technically, addee.value.length()
* could be zero, which probably means
* someone just put two colons in a row,
* maybe for Haskell source code. I'd
* be surprised if Mastodon didn't escape
* it. (If they did, the next step will
* handle them.) Anyways treating it as
* an empty emoji is the correct action.
*/
emoji = false;
calnum = false;
}
else if (cspace != space)
{
if (b.length() > 0) if (b.length() > 0)
{ {
Tree<String> addee = new Tree<>(); Tree<String> addee = new Tree<>();
addee.key = space ? "space" : "text"; addee.key = space ? "space" : "text";
addee.value = empty(b); addee.value = empty(b);
returnee.add(addee); returnee.add(addee);
} }
b.append(c); emoji = true;
} b.append(c);
else }
{ else if (c == ':' && emoji)
b.append(c); {
} assert !space;
/* b.append(c);
* We can specially handle special Tree<String> addee = new Tree<>();
* characters like \n, but I'll opt not to. addee.key = "emoji";
*/ addee.value = empty(b);
returnee.add(addee);
/*
* Technically, addee.value.length()
* could be zero, which probably means
* someone just put two colons in a row,
* maybe for Haskell source code. I'd
* be surprised if Mastodon didn't escape
* it. (If they did, the next step will
* handle them.) Anyways treating it as
* an empty emoji is the correct action.
*/
emoji = false;
calnum = false;
}
else if (cspace != space)
{
if (b.length() > 0)
{
Tree<String> addee = new Tree<>();
addee.key = space ? "space" : "text";
addee.value = empty(b);
returnee.add(addee);
}
b.append(c);
}
else
{
b.append(c);
}
/*
* We can specially handle special
* characters like \n, but I'll opt not to.
*/
alnum = calnum; alnum = calnum;
space = cspace; space = cspace;
} }
if (b.length() > 0) if (b.length() > 0)
{ {
Tree<String> addee = new Tree<>(); Tree<String> addee = new Tree<>();
addee.key = space ? "space" : "text"; addee.key = space ? "space" : "text";
addee.value = empty(b); addee.value = empty(b);
returnee.add(addee); returnee.add(addee);
} }
} }
return returnee; return returnee;
} }
private static Tree<String> private static Tree<String>
evaluateHtmlEscapes(Tree<String> nodes) evaluateHtmlEscapes(Tree<String> nodes)
{ {
for (Tree<String> node: nodes) for (Tree<String> node: nodes)
{ {
node.value = evaluateHtmlEscapes(node.value); node.value = evaluateHtmlEscapes(node.value);
for (Tree<String> attr: node) for (Tree<String> attr: node)
{ {
attr.key = evaluateHtmlEscapes(attr.key); attr.key = evaluateHtmlEscapes(attr.key);
attr.value = evaluateHtmlEscapes(attr.value); attr.value = evaluateHtmlEscapes(attr.value);
} }
} }
return nodes; return nodes;
} }
private static Tree<String> private static Tree<String>
hierarchise(Tree<String> nodes) hierarchise(Tree<String> nodes)
{ {
Tree<String> root = new Tree<String>(); Tree<String> root = new Tree<String>();
root.key = "tag"; root.key = "tag";
root.add(new Tree<>("html", null)); root.add(new Tree<>("html", null));
root.add(new Tree<>("children", null)); root.add(new Tree<>("children", null));
Deque<Tree<String>> parents = new LinkedList<>(); Deque<Tree<String>> parents = new LinkedList<>();
parents.push(root); parents.push(root);
for (Tree<String> node: nodes) for (Tree<String> node: nodes)
{ {
if (node.key.equals("tag")) if (node.key.equals("tag"))
{ {
assert node.size() > 0; assert node.size() > 0;
String tagName = node.get(0).key; String tagName = node.get(0).key;
assert node.get("children") == null; assert node.get("children") == null;
node.add(new Tree<>("children", null)); node.add(new Tree<>("children", null));
boolean isClosing, selfClosing; boolean isClosing, selfClosing;
isClosing = tagName.startsWith("/"); isClosing = tagName.startsWith("/");
selfClosing = node.get("/") != null; selfClosing = node.get("/") != null;
selfClosing |= tagName.equals("br"); selfClosing |= tagName.equals("br");
if (isClosing) if (isClosing)
{ {
assert parents.size() > 1; assert parents.size() > 1;
Tree<String> parent, grandparent; Tree<String> parent, grandparent;
parent = parents.pop(); parent = parents.pop();
grandparent = parents.peek(); grandparent = parents.peek();
String pTagName = parent.get(0).key; String pTagName = parent.get(0).key;
assert tagName.equals("/" + pTagName); assert tagName.equals("/" + pTagName);
grandparent.get("children").add(parent); grandparent.get("children").add(parent);
} }
else if (selfClosing) else if (selfClosing)
{ {
parents.peek().get("children").add(node); parents.peek().get("children").add(node);
} }
else else
{ {
parents.push(node); parents.push(node);
} }
} }
else else
{ {
parents.peek().get("children").add(node); parents.peek().get("children").add(node);
} }
} }
assert parents.size() == 1; assert parents.size() == 1;
return parents.pop(); return parents.pop();
} }
private static String private static String
empty(StringBuilder b) empty(StringBuilder b)
{ {
String s = b.toString(); String s = b.toString();
b.delete(0, b.length()); b.delete(0, b.length());
return s; return s;
} }
private static boolean private static boolean
isMastodonAlnum(char c) isMastodonAlnum(char c)
{ {
return Character.isLetterOrDigit(c); return Character.isLetterOrDigit(c);
/* /*
* Not joking. Mastodon is using the POSIX :alnum: regex * Not joking. Mastodon is using the POSIX :alnum: regex
@ -343,44 +343,44 @@ BasicHTMLParser {
* by text, then try again with the same emoji also * by text, then try again with the same emoji also
* present elsewhere in the post at a valid position.) * present elsewhere in the post at a valid position.)
*/ */
} }
private static String private static String
evaluateHtmlEscapes(String string) evaluateHtmlEscapes(String string)
{ {
if (string == null) return string; if (string == null) return string;
StringBuilder whole = new StringBuilder(); StringBuilder whole = new StringBuilder();
StringBuilder part = new StringBuilder(); StringBuilder part = new StringBuilder();
boolean inEscape = false; boolean inEscape = false;
for (char c: string.toCharArray()) for (char c: string.toCharArray())
{ {
if (inEscape && c == ';') if (inEscape && c == ';')
{ {
part.append(c); part.append(c);
inEscape = false; inEscape = false;
String v = empty(part); String v = empty(part);
if (v.equals("&lt;")) part.append('<'); if (v.equals("&lt;")) part.append('<');
if (v.equals("&gt;")) part.append('>'); if (v.equals("&gt;")) part.append('>');
if (v.equals("&amp;")) part.append('&'); if (v.equals("&amp;")) part.append('&');
if (v.equals("&quot;")) part.append('"'); if (v.equals("&quot;")) part.append('"');
if (v.equals("&apos;")) part.append('\''); if (v.equals("&apos;")) part.append('\'');
if (v.equals("&#39;")) part.append('\''); if (v.equals("&#39;")) part.append('\'');
} }
else if (!inEscape && c == '&') else if (!inEscape && c == '&')
{ {
String v = empty(part); String v = empty(part);
if (!v.isEmpty()) whole.append(v); if (!v.isEmpty()) whole.append(v);
part.append(c); part.append(c);
inEscape = true; inEscape = true;
} }
else else
{ {
part.append(c); part.append(c);
} }
} }
String v = empty(part); String v = empty(part);
if (!v.isEmpty()) whole.append(v); if (!v.isEmpty()) whole.append(v);
return whole.toString(); return whole.toString();
} }
} }

View File

@ -41,41 +41,41 @@ implements Transferable, ClipboardOwner {
{ {
assert string != null; assert string != null;
instance.string = string; instance.string = string;
Toolkit tk = Toolkit.getDefaultToolkit(); Toolkit tk = Toolkit.getDefaultToolkit();
Clipboard cb = tk.getSystemClipboard(); Clipboard cb = tk.getSystemClipboard();
cb.setContents(instance, instance); cb.setContents(instance, instance);
} }
// - -%- - // - -%- -
public String public String
getTransferData(DataFlavor flavour) getTransferData(DataFlavor flavour)
{ {
assert flavour == DataFlavor.stringFlavor; assert flavour == DataFlavor.stringFlavor;
return string; return string;
} }
public DataFlavor[] public DataFlavor[]
getTransferDataFlavors() getTransferDataFlavors()
{ {
return new DataFlavor[] { DataFlavor.stringFlavor }; return new DataFlavor[] { DataFlavor.stringFlavor };
/* /*
* We should probably also support javaJVMLocalObjectMimeType, * We should probably also support javaJVMLocalObjectMimeType,
* so that the compose window can ask for the List<Segment>. * so that the compose window can ask for the List<Segment>.
* Although also like, if we don't store emoji shortcodes in * Although also like, if we don't store emoji shortcodes in
* the image segments, there is no point. Anyways, what is * the image segments, there is no point. Anyways, what is
* important is the string format first, allowing us to * important is the string format first, allowing us to
* copy links or large lengths of text. * copy links or large lengths of text.
*/ */
} }
public boolean public boolean
isDataFlavorSupported(DataFlavor flavour) isDataFlavorSupported(DataFlavor flavour)
{ {
return flavour == DataFlavor.stringFlavor; return flavour == DataFlavor.stringFlavor;
} }
public void public void
lostOwnership(Clipboard clipboard, Transferable contents) { } lostOwnership(Clipboard clipboard, Transferable contents) { }
} }

View File

@ -126,86 +126,86 @@ ComposeWindow extends JFrame {
if (composition.contentWarning != null) if (composition.contentWarning != null)
assert !composition.contentWarning.trim().isEmpty(); assert !composition.contentWarning.trim().isEmpty();
//tabs.setCursor(new Cursor(Cursor.WAIT_CURSOR)); //tabs.setCursor(new Cursor(Cursor.WAIT_CURSOR));
/* /*
* setCursor only works for components that are enabled. * setCursor only works for components that are enabled.
* I don't think there's any technical reason for this, * I don't think there's any technical reason for this,
* but it's what it is.. rely on the enablement visuals * but it's what it is.. rely on the enablement visuals
* or a statusbar to indicate to user. * or a statusbar to indicate to user.
* *
* If we really wanted it, I suspect we could use a glass * If we really wanted it, I suspect we could use a glass
* pane (or a scroll pane with no scrolling) to have an * pane (or a scroll pane with no scrolling) to have an
* enabled pass-through on top. * enabled pass-through on top.
* *
* Technically contentsDisplay and attachmentsDisplay * Technically contentsDisplay and attachmentsDisplay
* themselves aren't disabled. But disabling the tab pane * themselves aren't disabled. But disabling the tab pane
* covers both the tab area and content area. We can't * covers both the tab area and content area. We can't
* just the tab area, except maybe by disabling the * just the tab area, except maybe by disabling the
* individual tabs. * individual tabs.
*/ */
tabs.setEnabled(false); tabs.setEnabled(false);
tabs.setSelectedComponent(contentsDisplay); tabs.setSelectedComponent(contentsDisplay);
contentsDisplay.setSubmitting(true); contentsDisplay.setSubmitting(true);
tabs.paintImmediately(tabs.getBounds()); tabs.paintImmediately(tabs.getBounds());
boolean uploadsOkay = true; boolean uploadsOkay = true;
for (Attachment a: composition.attachments) for (Attachment a: composition.attachments)
{ {
if (a.id != null) continue; if (a.id != null) continue;
// Assume it had already been uploaded. // Assume it had already been uploaded.
api.uploadFile( api.uploadFile(
a.uploadee, a.description, a.uploadee, a.description,
new RequestListener() { new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
ComposeWindow.this, ComposeWindow.this,
"Tried to upload attachment, failed..." "Tried to upload attachment, failed..."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
ComposeWindow.this, ComposeWindow.this,
"Tried to upload attachment, failed..." "Tried to upload attachment, failed..."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")" + "\n(HTTP code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
a.id = json.get("id").value; a.id = json.get("id").value;
} }
}); });
uploadsOkay &= a.id != null; uploadsOkay &= a.id != null;
} }
if (!uploadsOkay) if (!uploadsOkay)
{ {
contentsDisplay.setSubmitting(false); contentsDisplay.setSubmitting(false);
tabs.setEnabled(true); tabs.setEnabled(true);
//tabs.setCursor(null); //tabs.setCursor(null);
return; return;
} }
int amt = composition.attachments.length; int amt = composition.attachments.length;
String[] mediaIDs = new String[amt]; String[] mediaIDs = new String[amt];
for (int o = 0; o < mediaIDs.length; ++o) for (int o = 0; o < mediaIDs.length; ++o)
mediaIDs[o] = composition.attachments[o].id; mediaIDs[o] = composition.attachments[o].id;
api.submit( api.submit(
composition.text, composition.visibility, composition.text, composition.visibility,
composition.replyToPostId, composition.contentWarning, composition.replyToPostId, composition.contentWarning,
mediaIDs, mediaIDs,
new RequestListener() { new RequestListener() {
public void public void
@ -239,8 +239,8 @@ ComposeWindow extends JFrame {
); );
contentsDisplay.setSubmitting(false); contentsDisplay.setSubmitting(false);
tabs.setEnabled(true); tabs.setEnabled(true);
tabs.setCursor(null); tabs.setCursor(null);
} }
// - -%- - // - -%- -
@ -254,14 +254,14 @@ ComposeWindow extends JFrame {
d1.setVisibility(stringFor(composition.visibility)); d1.setVisibility(stringFor(composition.visibility));
d1.setContentWarning(composition.contentWarning); d1.setContentWarning(composition.contentWarning);
AttachmentsComponent d2 = attachmentsDisplay; AttachmentsComponent d2 = attachmentsDisplay;
d2.setAttachments(composition.attachments); d2.setAttachments(composition.attachments);
} }
private synchronized void private synchronized void
syncCompositionToDisplay() syncCompositionToDisplay()
{ {
Composition c = composition; Composition c = composition;
ComposeComponent d1 = contentsDisplay; ComposeComponent d1 = contentsDisplay;
c.text = d1.getText(); c.text = d1.getText();
@ -269,8 +269,8 @@ ComposeWindow extends JFrame {
c.replyToPostId = nonEmpty(d1.getReplyToPostId()); c.replyToPostId = nonEmpty(d1.getReplyToPostId());
c.contentWarning = nonEmpty(d1.getContentWarning()); c.contentWarning = nonEmpty(d1.getContentWarning());
AttachmentsComponent d2 = attachmentsDisplay; AttachmentsComponent d2 = attachmentsDisplay;
c.attachments = d2.getAttachments(); c.attachments = d2.getAttachments();
} }
// - -%- - // - -%- -
@ -509,18 +509,18 @@ implements ActionListener, CaretListener, KeyListener {
Border b3 = BorderFactory.createLineBorder(Color.GRAY); Border b3 = BorderFactory.createLineBorder(Color.GRAY);
Border bc = BorderFactory.createCompoundBorder(b3, b2); Border bc = BorderFactory.createCompoundBorder(b3, b2);
TextActionPopupMenu textActionPopup; TextActionPopupMenu textActionPopup;
textActionPopup = new TextActionPopupMenu(); textActionPopup = new TextActionPopupMenu();
reply = new JTextField(); reply = new JTextField();
JLabel replyLabel = new JLabel("In reply to: "); JLabel replyLabel = new JLabel("In reply to: ");
replyLabel.setLabelFor(reply); replyLabel.setLabelFor(reply);
reply.addMouseListener(textActionPopup); reply.addMouseListener(textActionPopup);
contentWarning = new JTextField(); contentWarning = new JTextField();
JLabel cwLabel = new JLabel("Content warning: "); JLabel cwLabel = new JLabel("Content warning: ");
cwLabel.setLabelFor(contentWarning); cwLabel.setLabelFor(contentWarning);
contentWarning.addMouseListener(textActionPopup); contentWarning.addMouseListener(textActionPopup);
JPanel top = new JPanel(); JPanel top = new JPanel();
top.setOpaque(false); top.setOpaque(false);
@ -560,7 +560,7 @@ implements ActionListener, CaretListener, KeyListener {
text.setBorder(bc); text.setBorder(bc);
text.addCaretListener(this); text.addCaretListener(this);
text.addKeyListener(this); text.addKeyListener(this);
text.addMouseListener(textActionPopup); text.addMouseListener(textActionPopup);
setLayout(new BorderLayout(0, 8)); setLayout(new BorderLayout(0, 8));
add(top, BorderLayout.NORTH); add(top, BorderLayout.NORTH);
@ -592,7 +592,7 @@ implements ComponentListener, ActionListener {
attachment2, attachment2,
attachment3, attachment3,
attachment4, attachment4,
selected; selected;
private JButton private JButton
add; add;
@ -607,83 +607,83 @@ implements ComponentListener, ActionListener {
private JTextArea private JTextArea
description; description;
private JFileChooser private JFileChooser
chooser; chooser;
private UndoManager private UndoManager
descriptionUndos; descriptionUndos;
// ---%-@-%--- // ---%-@-%---
public void public void
setAttachments(Attachment[] n) setAttachments(Attachment[] n)
{ {
working.clear(); working.clear();
if (n != null) for (Attachment attachment: n) if (n != null) for (Attachment attachment: n)
{ {
working.add(attachment); working.add(attachment);
} }
updateButtons(); updateButtons();
} }
public Attachment[] public Attachment[]
getAttachments() getAttachments()
{ {
return working.toArray(new Attachment[0]); return working.toArray(new Attachment[0]);
} }
// - -%- - // - -%- -
private void private void
updateButtons() updateButtons()
{ {
Dimension sz = add.getPreferredSize(); Dimension sz = add.getPreferredSize();
selections.removeAll(); selections.removeAll();
selected = null; selected = null;
if (working.size() > 0) if (working.size() > 0)
{ {
selections.add(attachment1); selections.add(attachment1);
Image i = working.get(0).image; Image i = working.get(0).image;
attachment1.setIcon(new ImageIcon(i)); attachment1.setIcon(new ImageIcon(i));
} }
if (working.size() > 1) if (working.size() > 1)
{ {
selections.add(attachment2); selections.add(attachment2);
Image i = working.get(1).image; Image i = working.get(1).image;
attachment2.setIcon(new ImageIcon(i)); attachment2.setIcon(new ImageIcon(i));
} }
if (working.size() > 2) if (working.size() > 2)
{ {
selections.add(attachment3); selections.add(attachment3);
Image i = working.get(2).image; Image i = working.get(2).image;
attachment3.setIcon(new ImageIcon(i)); attachment3.setIcon(new ImageIcon(i));
} }
if (working.size() > 3) if (working.size() > 3)
{ {
selections.add(attachment4); selections.add(attachment4);
Image i = working.get(3).image; Image i = working.get(3).image;
attachment4.setIcon(new ImageIcon(i)); attachment4.setIcon(new ImageIcon(i));
} }
if (working.size() < 4) selections.add(add); if (working.size() < 4) selections.add(add);
if (working.size() > 3) open(attachment4); if (working.size() > 3) open(attachment4);
else if (working.size() > 2) open(attachment3); else if (working.size() > 2) open(attachment3);
else if (working.size() > 1) open(attachment2); else if (working.size() > 1) open(attachment2);
else if (working.size() > 0) open(attachment1); else if (working.size() > 0) open(attachment1);
int bw = sz.width; int bw = sz.width;
int hgap = 4; int hgap = 4;
int count = selections.getComponents().length; int count = selections.getComponents().length;
int w = count * bw + (count - 1) * hgap; int w = count * bw + (count - 1) * hgap;
int h = bw; int h = bw;
selections.setPreferredSize(new Dimension(w, h)); selections.setPreferredSize(new Dimension(w, h));
selections.setMaximumSize(new Dimension(w, h)); selections.setMaximumSize(new Dimension(w, h));
selections.revalidate(); selections.revalidate();
delete.setEnabled(selected != null); delete.setEnabled(selected != null);
revert.setEnabled(selected != null); revert.setEnabled(selected != null);
description.setEnabled(selected != null); description.setEnabled(selected != null);
} }
public void public void
@ -693,34 +693,34 @@ implements ComponentListener, ActionListener {
if (src == add) if (src == add)
{ {
int r = chooser.showOpenDialog(this); int r = chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return; if (r != JFileChooser.APPROVE_OPTION) return;
File f = chooser.getSelectedFile(); File f = chooser.getSelectedFile();
Attachment a = new Attachment(); Attachment a = new Attachment();
a.uploadee = f; a.uploadee = f;
a.description = ""; a.description = "";
a.type = "unknown"; a.type = "unknown";
String mime = "", primary = ""; String mime = "", primary = "";
try try
{ {
mime = Files.probeContentType(f.toPath()); mime = Files.probeContentType(f.toPath());
primary = mime.split("/")[0]; primary = mime.split("/")[0];
// Too lazy to instantiate a // Too lazy to instantiate a
// javax.activation.MimeType. // javax.activation.MimeType.
} }
catch (IOException eIo) { } catch (IOException eIo) { }
if (primary.equals("image")) if (primary.equals("image"))
{ {
String urlr = f.toURI().toString(); String urlr = f.toURI().toString();
a.image = ImageApi.remote(urlr); a.image = ImageApi.remote(urlr);
a.type = "image"; a.type = "image";
} }
if (selected != null) open(selected); if (selected != null) open(selected);
// Save first before resetting // Save first before resetting
working.add(a); working.add(a);
updateButtons(); updateButtons();
@ -728,87 +728,87 @@ implements ComponentListener, ActionListener {
if (src == delete) if (src == delete)
{ {
assert selected != null; assert selected != null;
working.remove(getAttachmentFor(selected)); working.remove(getAttachmentFor(selected));
updateButtons(); updateButtons();
return; return;
} }
if (src != add && selections.isAncestorOf((Component)src)) if (src != add && selections.isAncestorOf((Component)src))
{ {
assert src instanceof JToggleButton; assert src instanceof JToggleButton;
if (src == selected) preview(getAttachmentFor(src)); if (src == selected) preview(getAttachmentFor(src));
else open((JToggleButton)src); else open((JToggleButton)src);
return; return;
} }
if (src == revert) if (src == revert)
{ {
while (descriptionUndos.canUndo()) while (descriptionUndos.canUndo())
descriptionUndos.undo(); descriptionUndos.undo();
return; return;
} }
} }
private Attachment private Attachment
getAttachmentFor(Object button) getAttachmentFor(Object button)
{ {
if (button == null) return null; if (button == null) return null;
assert button instanceof JToggleButton; assert button instanceof JToggleButton;
assert selections.isAncestorOf((Component)button); assert selections.isAncestorOf((Component)button);
int index = 0; int index = 0;
if (button == attachment4) index = 4; if (button == attachment4) index = 4;
if (button == attachment3) index = 3; if (button == attachment3) index = 3;
if (button == attachment2) index = 2; if (button == attachment2) index = 2;
if (button == attachment1) index = 1; if (button == attachment1) index = 1;
assert index != 0; assert index != 0;
assert index <= working.size(); assert index <= working.size();
return working.get(index - 1); return working.get(index - 1);
} }
private void private void
open(JToggleButton button) open(JToggleButton button)
{ {
assert selections.isAncestorOf(button); assert selections.isAncestorOf(button);
if (selected != null) if (selected != null)
{ {
Attachment a = getAttachmentFor(selected); Attachment a = getAttachmentFor(selected);
a.description = description.getText(); a.description = description.getText();
selected.setSelected(false); selected.setSelected(false);
} }
Attachment a = getAttachmentFor(button); Attachment a = getAttachmentFor(button);
description.setText(a.description); description.setText(a.description);
descriptionUndos.discardAllEdits(); descriptionUndos.discardAllEdits();
(selected = button).setSelected(true); (selected = button).setSelected(true);
} }
public void public void
componentHidden(ComponentEvent eC) componentHidden(ComponentEvent eC)
{ {
if (selected != null) open(selected); if (selected != null) open(selected);
} }
private void private void
preview(Attachment a) preview(Attachment a)
{ {
ImageWindow w = new ImageWindow(); ImageWindow w = new ImageWindow();
w.showAttachments(new Attachment[] { a } ); w.showAttachments(new Attachment[] { a } );
w.setTitle("Attachment preview"); w.setTitle("Attachment preview");
w.setVisible(true); w.setVisible(true);
} }
public void public void
componentShown(ComponentEvent eC) { } componentShown(ComponentEvent eC) { }
public void public void
componentMoved(ComponentEvent eC) { } componentMoved(ComponentEvent eC) { }
public void public void
componentResized(ComponentEvent eC) { } componentResized(ComponentEvent eC) { }
// ---%-@-%--- // ---%-@-%---
@ -824,15 +824,15 @@ implements ComponentListener, ActionListener {
Border bc1 = BorderFactory.createCompoundBorder(b3, b2); Border bc1 = BorderFactory.createCompoundBorder(b3, b2);
Border bc2 = BorderFactory.createCompoundBorder(b4, b2); Border bc2 = BorderFactory.createCompoundBorder(b4, b2);
TextActionPopupMenu textActionPopup; TextActionPopupMenu textActionPopup;
textActionPopup = new TextActionPopupMenu(); textActionPopup = new TextActionPopupMenu();
chooser = new JFileChooser(); chooser = new JFileChooser();
add = new JButton("+"); add = new JButton("+");
add.setPreferredSize(new Dimension(32, 32)); add.setPreferredSize(new Dimension(32, 32));
add.setMargin(new Insets(0, 0, 0, 0)); add.setMargin(new Insets(0, 0, 0, 0));
add.addActionListener(this); add.addActionListener(this);
attachment1 = new JToggleButton("1"); attachment1 = new JToggleButton("1");
attachment2 = new JToggleButton("2"); attachment2 = new JToggleButton("2");
attachment3 = new JToggleButton("3"); attachment3 = new JToggleButton("3");
@ -841,28 +841,28 @@ implements ComponentListener, ActionListener {
attachment2.setMargin(add.getMargin()); attachment2.setMargin(add.getMargin());
attachment3.setMargin(add.getMargin()); attachment3.setMargin(add.getMargin());
attachment4.setMargin(add.getMargin()); attachment4.setMargin(add.getMargin());
attachment1.addActionListener(this); attachment1.addActionListener(this);
attachment2.addActionListener(this); attachment2.addActionListener(this);
attachment3.addActionListener(this); attachment3.addActionListener(this);
attachment4.addActionListener(this); attachment4.addActionListener(this);
selections = new JPanel(); selections = new JPanel();
selections.setOpaque(false); selections.setOpaque(false);
selections.setLayout(new GridLayout(1, 0, 4, 0)); selections.setLayout(new GridLayout(1, 0, 4, 0));
working = new ArrayList<Attachment>(); working = new ArrayList<Attachment>();
Box top = Box.createHorizontalBox(); Box top = Box.createHorizontalBox();
top.add(selections); top.add(selections);
top.add(Box.createGlue()); top.add(Box.createGlue());
delete = new JButton("Delete"); delete = new JButton("Delete");
revert = new JButton("Revert"); revert = new JButton("Revert");
JButton ml = new JButton(""); JButton ml = new JButton("");
JButton mr = new JButton(""); JButton mr = new JButton("");
delete.addActionListener(this); delete.addActionListener(this);
revert.addActionListener(this); revert.addActionListener(this);
Box bottom = Box.createHorizontalBox(); Box bottom = Box.createHorizontalBox();
bottom.add(ml); bottom.add(ml);
bottom.add(mr); bottom.add(mr);
bottom.add(Box.createHorizontalStrut(8)); bottom.add(Box.createHorizontalStrut(8));
@ -876,14 +876,14 @@ implements ComponentListener, ActionListener {
java.awt.Font f = description.getFont(); java.awt.Font f = description.getFont();
description.setFont(f.deriveFont(16f)); description.setFont(f.deriveFont(16f));
description.setBorder(bc1); description.setBorder(bc1);
description.addMouseListener(textActionPopup); description.addMouseListener(textActionPopup);
descriptionLabel = new JLabel("Description"); descriptionLabel = new JLabel("Description");
descriptionLabel.setLabelFor(description); descriptionLabel.setLabelFor(description);
descriptionUndos = new UndoManager(); descriptionUndos = new UndoManager();
description.getDocument(). description.getDocument().
addUndoableEditListener(descriptionUndos); addUndoableEditListener(descriptionUndos);
updateButtons(); updateButtons();
JPanel row1 = new JPanel(); JPanel row1 = new JPanel();
row1.setOpaque(false); row1.setOpaque(false);
@ -891,7 +891,7 @@ implements ComponentListener, ActionListener {
row1.add(descriptionLabel, BorderLayout.NORTH); row1.add(descriptionLabel, BorderLayout.NORTH);
row1.add(description, BorderLayout.CENTER); row1.add(description, BorderLayout.CENTER);
Box centre = Box.createVerticalBox(); Box centre = Box.createVerticalBox();
centre.setBorder(b4); centre.setBorder(b4);
centre.add(row1); centre.add(row1);
@ -901,7 +901,7 @@ implements ComponentListener, ActionListener {
add(bottom, BorderLayout.SOUTH); add(bottom, BorderLayout.SOUTH);
setBorder(b1); setBorder(b1);
this.addComponentListener(this); this.addComponentListener(this);
} }
} }
@ -910,66 +910,66 @@ class
TextActionPopupMenu extends JPopupMenu TextActionPopupMenu extends JPopupMenu
implements MouseListener, ActionListener { implements MouseListener, ActionListener {
private JMenuItem private JMenuItem
copy, copy,
cut, cut,
paste; paste;
private long private long
pressed; pressed;
// ---%-@-%--- // ---%-@-%---
public void public void
mousePressed(MouseEvent eM) mousePressed(MouseEvent eM)
{ {
assert eM.getSource() instanceof JTextComponent; assert eM.getSource() instanceof JTextComponent;
if (!eM.isPopupTrigger()) return; if (!eM.isPopupTrigger()) return;
show((Component)eM.getSource(), eM.getX(), eM.getY()); show((Component)eM.getSource(), eM.getX(), eM.getY());
} }
public void public void
mouseReleased(MouseEvent eM) mouseReleased(MouseEvent eM)
{ {
if (eM.getClickCount() == 0) setVisible(false); if (eM.getClickCount() == 0) setVisible(false);
} }
public void public void
mouseClicked(MouseEvent eM) { } mouseClicked(MouseEvent eM) { }
public void public void
mouseEntered(MouseEvent eM) { } mouseEntered(MouseEvent eM) { }
public void public void
mouseExited(MouseEvent eM) { } mouseExited(MouseEvent eM) { }
public void public void
actionPerformed(ActionEvent eA) actionPerformed(ActionEvent eA)
{ {
assert getInvoker() instanceof JTextComponent; assert getInvoker() instanceof JTextComponent;
JTextComponent inv = (JTextComponent)getInvoker(); JTextComponent inv = (JTextComponent)getInvoker();
if (eA.getSource() == copy) inv.copy(); if (eA.getSource() == copy) inv.copy();
if (eA.getSource() == cut) inv.cut(); if (eA.getSource() == cut) inv.cut();
if (eA.getSource() == paste) inv.paste(); if (eA.getSource() == paste) inv.paste();
} }
// ---%-@-%--- // ---%-@-%---
public public
TextActionPopupMenu() TextActionPopupMenu()
{ {
copy = new JMenuItem("Copy"); copy = new JMenuItem("Copy");
cut = new JMenuItem("Cut"); cut = new JMenuItem("Cut");
paste = new JMenuItem("Paste"); paste = new JMenuItem("Paste");
copy.addActionListener(this); copy.addActionListener(this);
cut.addActionListener(this); cut.addActionListener(this);
paste.addActionListener(this); paste.addActionListener(this);
add(cut); add(cut);
add(copy); add(copy);
add(paste); add(paste);
} }
} }

View File

@ -41,303 +41,303 @@ import java.net.MalformedURLException;
class class
ImageWindow extends JFrame { ImageWindow extends JFrame {
private Attachment[] private Attachment[]
attachments; attachments;
private int private int
offset; offset;
// - -%- - // - -%- -
private ImageComponent private ImageComponent
display; display;
// ---%-@-%--- // ---%-@-%---
public synchronized void public synchronized void
showAttachments(Attachment[] attachments) showAttachments(Attachment[] attachments)
{ {
this.attachments = attachments; this.attachments = attachments;
if (attachments.length == 0) { if (attachments.length == 0) {
display.setImage(null); display.setImage(null);
display.setNext(null); display.setNext(null);
display.setPrev(null); display.setPrev(null);
display.repaint(); display.repaint();
return; return;
} }
toImage(offset = 0); toImage(offset = 0);
} }
public void public void
toNextImage() toNextImage()
{ {
if (attachments.length == 0) return; if (attachments.length == 0) return;
assert offset < attachments.length - 1; assert offset < attachments.length - 1;
toImage(++offset); toImage(++offset);
} }
public void public void
toPrevImage() toPrevImage()
{ {
if (attachments.length == 0) return; if (attachments.length == 0) return;
assert offset > 0; assert offset > 0;
toImage(--offset); toImage(--offset);
} }
// - -%- - // - -%- -
private synchronized void private synchronized void
toImage(int offset) toImage(int offset)
{ {
int last = attachments.length - 1; int last = attachments.length - 1;
assert offset >= 0; assert offset >= 0;
assert offset < attachments.length; assert offset < attachments.length;
Attachment prev, curr, next; Attachment prev, curr, next;
curr = attachments[offset]; curr = attachments[offset];
next = offset < last ? attachments[offset + 1] : null; next = offset < last ? attachments[offset + 1] : null;
prev = offset > 0 ? attachments[offset - 1] : null; prev = offset > 0 ? attachments[offset - 1] : null;
display.setImage(curr.image); display.setImage(curr.image);
display.setNext(next != null ? next.image : null); display.setNext(next != null ? next.image : null);
display.setPrev(prev != null ? prev.image : null); display.setPrev(prev != null ? prev.image : null);
if (!curr.type.equals("image")) if (!curr.type.equals("image"))
display.setToolTipText( display.setToolTipText(
display.getToolTipText() display.getToolTipText()
+ "\n(Media is of type '" + curr.type + "')" + "\n(Media is of type '" + curr.type + "')"
); );
repaint(); repaint();
} }
// ---%-@-%--- // ---%-@-%---
ImageWindow() ImageWindow()
{ {
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(600, 600); setSize(600, 600);
display = new ImageComponent(this); display = new ImageComponent(this);
showAttachments(new Attachment[0]); showAttachments(new Attachment[0]);
setContentPane(display); setContentPane(display);
} }
} }
class class
ImageComponent extends JPanel ImageComponent extends JPanel
implements implements
ActionListener, ActionListener,
MouseListener, MouseMotionListener, MouseWheelListener { MouseListener, MouseMotionListener, MouseWheelListener {
private ImageWindow private ImageWindow
primaire; primaire;
// - -%- - // - -%- -
private Image private Image
image, prevImage, nextImage; image, prevImage, nextImage;
private JPanel private JPanel
buttonArea; buttonArea;
private JButton private JButton
prev, next, toggle; prev, next, toggle;
private boolean private boolean
scaleImage; scaleImage;
private int private int
xOffset, yOffset, zoomLevel; xOffset, yOffset, zoomLevel;
private int private int
dragX, dragY, xPOffset, yPOffset; dragX, dragY, xPOffset, yPOffset;
// ---%-@-%--- // ---%-@-%---
public void public void
setImage(Image image) setImage(Image image)
{ {
this.image = image; this.image = image;
if (image != null) { if (image != null) {
Object p = image.getProperty("comment", this); Object p = image.getProperty("comment", this);
String desc = p instanceof String ? (String)p : null; String desc = p instanceof String ? (String)p : null;
setToolTipText(desc); setToolTipText(desc);
} }
xOffset = yOffset = xPOffset = yPOffset = 0; xOffset = yOffset = xPOffset = yPOffset = 0;
zoomLevel = 100; zoomLevel = 100;
} }
public void public void
setPrev(Image image) setPrev(Image image)
{ {
prev.setEnabled(image != null); prev.setEnabled(image != null);
prev.setText(image == null ? "<" : ""); prev.setText(image == null ? "<" : "");
prev.setIcon(toIcon(image)); prev.setIcon(toIcon(image));
} }
public void public void
setNext(Image image) setNext(Image image)
{ {
next.setEnabled(image != null); next.setEnabled(image != null);
next.setText(image == null ? ">" : ""); next.setText(image == null ? ">" : "");
next.setIcon(toIcon(image)); next.setIcon(toIcon(image));
} }
// - -%- - // - -%- -
public void public void
actionPerformed(ActionEvent eA) actionPerformed(ActionEvent eA)
{ {
if (eA.getSource() == prev) primaire.toPrevImage(); if (eA.getSource() == prev) primaire.toPrevImage();
if (eA.getSource() == next) primaire.toNextImage(); if (eA.getSource() == next) primaire.toNextImage();
if (eA.getSource() == toggle) { if (eA.getSource() == toggle) {
scaleImage = !scaleImage; scaleImage = !scaleImage;
if (scaleImage) toggle.setText("Show unscaled"); if (scaleImage) toggle.setText("Show unscaled");
else toggle.setText("Show scaled to window"); else toggle.setText("Show scaled to window");
setImage(this.image); setImage(this.image);
repaint(); repaint();
} }
} }
public void public void
mousePressed(MouseEvent eM) mousePressed(MouseEvent eM)
{ {
dragX = eM.getX(); dragX = eM.getX();
dragY = eM.getY(); dragY = eM.getY();
} }
public void public void
mouseDragged(MouseEvent eM) mouseDragged(MouseEvent eM)
{ {
int dx = eM.getX() - dragX; int dx = eM.getX() - dragX;
int dy = eM.getY() - dragY; int dy = eM.getY() - dragY;
xPOffset = dx; xPOffset = dx;
yPOffset = dy; yPOffset = dy;
repaint(); repaint();
} }
public void public void
mouseReleased(MouseEvent eM) mouseReleased(MouseEvent eM)
{ {
xOffset += xPOffset; xOffset += xPOffset;
yOffset += yPOffset; yOffset += yPOffset;
xPOffset = yPOffset = 0; xPOffset = yPOffset = 0;
} }
public void public void
mouseWheelMoved(MouseWheelEvent eMw) mouseWheelMoved(MouseWheelEvent eMw)
{ {
zoomLevel += 10 * -eMw.getUnitsToScroll(); zoomLevel += 10 * -eMw.getUnitsToScroll();
if (zoomLevel < 50) zoomLevel = 50; if (zoomLevel < 50) zoomLevel = 50;
if (zoomLevel > 400) zoomLevel = 400; if (zoomLevel > 400) zoomLevel = 400;
repaint(); repaint();
} }
public void public void
mouseEntered(MouseEvent eM) { } mouseEntered(MouseEvent eM) { }
public void public void
mouseExited(MouseEvent eM) { } mouseExited(MouseEvent eM) { }
public void public void
mouseClicked(MouseEvent eM) { } mouseClicked(MouseEvent eM) { }
public void public void
mouseMoved(MouseEvent eM) { } mouseMoved(MouseEvent eM) { }
// - -%- - // - -%- -
private static ImageIcon private static ImageIcon
toIcon(Image image) toIcon(Image image)
{ {
if (image == null) return null; if (image == null) return null;
return new ImageIcon(image); return new ImageIcon(image);
} }
// ---%-@-%--- // ---%-@-%---
private class private class
Painter extends JPanel { Painter extends JPanel {
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
if (image == null) if (image == null)
{ {
String str = String str =
"(There are no images being displayed.)"; "(There are no images being displayed.)";
FontMetrics fm = g.getFontMetrics(); FontMetrics fm = g.getFontMetrics();
int x = (getWidth() - fm.stringWidth(str)) / 2; int x = (getWidth() - fm.stringWidth(str)) / 2;
int y = (getHeight() + fm.getHeight()) / 2; int y = (getHeight() + fm.getHeight()) / 2;
g.drawString(str, x, y); g.drawString(str, x, y);
return; return;
} }
int wo = image.getWidth(this); int wo = image.getWidth(this);
int ho = image.getHeight(this); int ho = image.getHeight(this);
int wn, hn; int wn, hn;
if (wo > ho) { if (wo > ho) {
wn = scaleImage ? getWidth() : wo; wn = scaleImage ? getWidth() : wo;
hn = ho * wn / wo; hn = ho * wn / wo;
} }
else { else {
hn = scaleImage ? getHeight() : ho; hn = scaleImage ? getHeight() : ho;
wn = wo * hn / ho; wn = wo * hn / ho;
} }
wn = wn * zoomLevel / 100; wn = wn * zoomLevel / 100;
hn = hn * zoomLevel / 100; hn = hn * zoomLevel / 100;
int x = (getWidth() - wn) / 2; int x = (getWidth() - wn) / 2;
int y = (getHeight() - hn) / 2; int y = (getHeight() - hn) / 2;
x += xOffset + xPOffset; x += xOffset + xPOffset;
y += yOffset + yPOffset; y += yOffset + yPOffset;
g.drawImage(image, x, y, wn, hn, this); g.drawImage(image, x, y, wn, hn, this);
} }
} }
// ---%-@-%--- // ---%-@-%---
ImageComponent(ImageWindow primaire) ImageComponent(ImageWindow primaire)
{ {
this.primaire = primaire; this.primaire = primaire;
Dimension BUTTON_SIZE = new Dimension(48, 48); Dimension BUTTON_SIZE = new Dimension(48, 48);
setOpaque(false); setOpaque(false);
scaleImage = true; scaleImage = true;
zoomLevel = 100; zoomLevel = 100;
prev = new JButton(); prev = new JButton();
toggle = new JButton("Show unscaled"); toggle = new JButton("Show unscaled");
next = new JButton(); next = new JButton();
prev.setPreferredSize(BUTTON_SIZE); prev.setPreferredSize(BUTTON_SIZE);
next.setPreferredSize(BUTTON_SIZE); next.setPreferredSize(BUTTON_SIZE);
prev.addActionListener(this); prev.addActionListener(this);
toggle.addActionListener(this); toggle.addActionListener(this);
next.addActionListener(this); next.addActionListener(this);
buttonArea = new JPanel(); buttonArea = new JPanel();
buttonArea.setOpaque(false); buttonArea.setOpaque(false);
buttonArea.add(prev); buttonArea.add(prev);
buttonArea.add(toggle); buttonArea.add(toggle);
buttonArea.add(next); buttonArea.add(next);
setPrev(null); setPrev(null);
setNext(null); setNext(null);
setLayout(new BorderLayout()); setLayout(new BorderLayout());
add(buttonArea, BorderLayout.SOUTH); add(buttonArea, BorderLayout.SOUTH);
add(new Painter(), BorderLayout.CENTER); add(new Painter(), BorderLayout.CENTER);
addMouseListener(this); addMouseListener(this);
addMouseMotionListener(this); addMouseMotionListener(this);
addMouseWheelListener(this); addMouseWheelListener(this);
} }
} }

View File

@ -60,20 +60,20 @@ JKomasto {
private LoginWindow private LoginWindow
loginWindow; loginWindow;
private ImageWindow private ImageWindow
mediaWindow; mediaWindow;
private NotificationsWindow private NotificationsWindow
notificationsWindow; notificationsWindow;
private WindowUpdater private WindowUpdater
windowUpdater; windowUpdater;
private MastodonApi private MastodonApi
api; api;
private Image private Image
programIcon; programIcon;
// ---%-@-%--- // ---%-@-%---
@ -85,10 +85,10 @@ JKomasto {
{ {
timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR)); timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
timelineWindow.showLatestPage(); timelineWindow.showLatestPage();
notificationsWindow.showLatestPage(); notificationsWindow.showLatestPage();
timelineWindow.setVisible(true); timelineWindow.setVisible(true);
loginWindow.dispose(); loginWindow.dispose();
timelineWindow.setCursor(null); timelineWindow.setCursor(null);
} }
@ -99,83 +99,83 @@ JKomasto {
public ComposeWindow public ComposeWindow
getComposeWindow() { return composeWindow; } getComposeWindow() { return composeWindow; }
public ImageWindow public ImageWindow
getMediaWindow() { return mediaWindow; } getMediaWindow() { return mediaWindow; }
public NotificationsWindow public NotificationsWindow
getNotificationsWindow() { return notificationsWindow; } getNotificationsWindow() { return notificationsWindow; }
public WindowUpdater public WindowUpdater
getWindowUpdater() { return windowUpdater; } getWindowUpdater() { return windowUpdater; }
public Image public Image
getProgramIcon() { return programIcon; } getProgramIcon() { return programIcon; }
// ---%-@-%--- // ---%-@-%---
private static class private static class
MetalTheme extends OceanTheme { MetalTheme extends OceanTheme {
private ColorUIResource private ColorUIResource
lightPink = new ColorUIResource(246, 240, 240), lightPink = new ColorUIResource(246, 240, 240),
mildPink = new ColorUIResource(238, 233, 233), mildPink = new ColorUIResource(238, 233, 233),
white = new ColorUIResource(250, 250, 250), white = new ColorUIResource(250, 250, 250),
darkPink = new ColorUIResource(242, 230, 230), darkPink = new ColorUIResource(242, 230, 230),
veryDarkPink = new ColorUIResource(164, 160, 160); veryDarkPink = new ColorUIResource(164, 160, 160);
// -=%=- // -=%=-
public ColorUIResource public ColorUIResource
getPrimary2() { return darkPink; } getPrimary2() { return darkPink; }
public ColorUIResource public ColorUIResource
getSecondary2() { return white; } getSecondary2() { return white; }
public ColorUIResource public ColorUIResource
getSecondary3() { return mildPink; } getSecondary3() { return mildPink; }
public ColorUIResource public ColorUIResource
getSecondary1() { return veryDarkPink; } getSecondary1() { return veryDarkPink; }
public ColorUIResource public ColorUIResource
getPrimary1() { return veryDarkPink; } getPrimary1() { return veryDarkPink; }
public void public void
addCustomEntriesToTable(UIDefaults table) addCustomEntriesToTable(UIDefaults table)
{ {
super.addCustomEntriesToTable(table); super.addCustomEntriesToTable(table);
table.put( table.put(
"TabbedPane.tabAreaBackground", "TabbedPane.tabAreaBackground",
getPrimary1() getPrimary1()
); );
table.put( table.put(
"TabbedPane.contentAreaColor", "TabbedPane.contentAreaColor",
getSecondary3() getSecondary3()
); );
table.put( table.put(
"TabbedPane.selected", "TabbedPane.selected",
getSecondary3() getSecondary3()
); );
table.put( table.put(
"MenuBar.gradient", "MenuBar.gradient",
java.util.Arrays.asList(new Object[] { java.util.Arrays.asList(new Object[] {
1f, 0f, 1f, 0f,
getWhite(), getWhite(),
getSecondary3(), getSecondary3(),
getSecondary1() getSecondary1()
}) })
); );
} }
} }
// ---%-@-%--- // ---%-@-%---
public static void public static void
main(String... args) main(String... args)
{ {
//System.setProperty("swing.boldMetal", "false"); //System.setProperty("swing.boldMetal", "false");
MetalLookAndFeel.setCurrentTheme(new MetalTheme()); MetalLookAndFeel.setCurrentTheme(new MetalTheme());
new JKomasto().loginWindow.setVisible(true); new JKomasto().loginWindow.setVisible(true);
} }
@ -187,24 +187,24 @@ JKomasto {
{ {
api = new MastodonApi(); api = new MastodonApi();
windowUpdater = new WindowUpdater(this); windowUpdater = new WindowUpdater(this);
programIcon = ImageApi.local("kettle"); programIcon = ImageApi.local("kettle");
timelineWindow = new TimelineWindow(this); timelineWindow = new TimelineWindow(this);
composeWindow = new ComposeWindow(this); composeWindow = new ComposeWindow(this);
autoViewWindow = new PostWindow(this); autoViewWindow = new PostWindow(this);
loginWindow = new LoginWindow(this); loginWindow = new LoginWindow(this);
mediaWindow = new ImageWindow(); mediaWindow = new ImageWindow();
notificationsWindow = new NotificationsWindow(this); notificationsWindow = new NotificationsWindow(this);
autoViewWindow.setTitle("Auto view - JKomasto"); autoViewWindow.setTitle("Auto view - JKomasto");
composeWindow.dispose(); composeWindow.dispose();
autoViewWindow.dispose(); autoViewWindow.dispose();
timelineWindow.dispose(); timelineWindow.dispose();
mediaWindow.dispose(); mediaWindow.dispose();
notificationsWindow.dispose(); notificationsWindow.dispose();
timelineWindow.setLocationByPlatform(true); timelineWindow.setLocationByPlatform(true);
loginWindow.setLocationByPlatform(true); loginWindow.setLocationByPlatform(true);
} }
@ -236,13 +236,13 @@ TimelineType {
enum enum
NotificationType { NotificationType {
MENTION, MENTION,
BOOST, BOOST,
FAVOURITE, FAVOURITE,
FOLLOW, FOLLOW,
FOLLOWREQ, FOLLOWREQ,
POLL, POLL,
ALERT ALERT
} }
@ -257,8 +257,8 @@ TimelinePage {
public String public String
accountNumId, listId; accountNumId, listId;
public Post[] public Post[]
posts; posts;
// ---%-@-%--- // ---%-@-%---
@ -279,14 +279,14 @@ TimelinePage {
class class
Notification { Notification {
public NotificationType public NotificationType
type; type;
public String public String
id; id;
public String public String
postId, postText, actorNumId, actorName; postId, postText, actorNumId, actorName;
} }
@ -348,35 +348,35 @@ Post {
assert text != null; assert text != null;
if (approximateText != null) return; if (approximateText != null) return;
Tree<String> nodes; Tree<String> nodes;
nodes = RudimentaryHTMLParser.depthlessRead(text); nodes = RudimentaryHTMLParser.depthlessRead(text);
if (nodes.size() == 0) if (nodes.size() == 0)
{ {
approximateText = "-"; approximateText = "-";
return; return;
} }
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
Tree<String> first = nodes.get(0); Tree<String> first = nodes.get(0);
for (Tree<String> node: nodes) for (Tree<String> node: nodes)
{ {
if (node.key.equals("tag")) if (node.key.equals("tag"))
{ {
if (node.get(0).key.equals("br")) if (node.get(0).key.equals("br"))
b.append("; "); b.append("; ");
if (node.get(0).key.equals("p") && node != first) if (node.get(0).key.equals("p") && node != first)
b.append("; "); b.append("; ");
} }
if (node.key.equals("emoji")) if (node.key.equals("emoji"))
{ {
b.append(":" + node.value + ":"); b.append(":" + node.value + ":");
} }
if (node.key.equals("text")) if (node.key.equals("text"))
{ {
b.append(node.value); b.append(node.value);
} }
} }
approximateText = b.toString(); approximateText = b.toString();
} }
public void public void
@ -402,57 +402,57 @@ Post {
public public
Post(Tree<String> entity) Post(Tree<String> entity)
{ {
id = entity.get("id").value; id = entity.get("id").value;
uri = entity.get("url").value; uri = entity.get("url").value;
if (uri == null) uri = entity.get("uri").value; if (uri == null) uri = entity.get("uri").value;
author = new Account(entity.get("account")); author = new Account(entity.get("account"));
String v = entity.get("visibility").value; String v = entity.get("visibility").value;
boolean p = v.equals("public"); boolean p = v.equals("public");
boolean u = v.equals("unlisted"); boolean u = v.equals("unlisted");
boolean f = v.equals("private"); boolean f = v.equals("private");
boolean m = v.equals("direct"); boolean m = v.equals("direct");
if (p) visibility = PostVisibility.PUBLIC; if (p) visibility = PostVisibility.PUBLIC;
if (u) visibility = PostVisibility.UNLISTED; if (u) visibility = PostVisibility.UNLISTED;
if (f) visibility = PostVisibility.FOLLOWERS; if (f) visibility = PostVisibility.FOLLOWERS;
if (m) visibility = PostVisibility.MENTIONED; if (m) visibility = PostVisibility.MENTIONED;
dateTime = dateTime =
ZonedDateTime.parse(entity.get("created_at").value) ZonedDateTime.parse(entity.get("created_at").value)
.withZoneSameInstant(ZoneId.systemDefault()); .withZoneSameInstant(ZoneId.systemDefault());
date = DATE_FORMAT.format(dateTime); date = DATE_FORMAT.format(dateTime);
time = TIME_FORMAT.format(dateTime); time = TIME_FORMAT.format(dateTime);
text = entity.get("content").value; text = entity.get("content").value;
String st = entity.get("spoiler_text").value; String st = entity.get("spoiler_text").value;
contentWarning = st.trim().isEmpty() ? null : st; contentWarning = st.trim().isEmpty() ? null : st;
String favourited = entity.get("favourited").value; String favourited = entity.get("favourited").value;
String boosted = entity.get("reblogged").value; String boosted = entity.get("reblogged").value;
this.favourited = favourited.equals("true"); this.favourited = favourited.equals("true");
this.boosted = boosted.equals("true"); this.boosted = boosted.equals("true");
Tree<String> media = entity.get("media_attachments"); Tree<String> media = entity.get("media_attachments");
attachments = new Attachment[media.size()]; attachments = new Attachment[media.size()];
for (int o = 0; o < attachments.length; ++o) for (int o = 0; o < attachments.length; ++o)
{ {
attachments[o] = new Attachment(media.get(o)); attachments[o] = new Attachment(media.get(o));
} }
Tree<String> emojis = entity.get("emojis"); Tree<String> emojis = entity.get("emojis");
emojiUrls = new String[emojis.size()][]; emojiUrls = new String[emojis.size()][];
for (int o = 0; o < emojiUrls.length; ++o) for (int o = 0; o < emojiUrls.length; ++o)
{ {
Tree<String> emoji = emojis.get(o); Tree<String> emoji = emojis.get(o);
String[] mapping = emojiUrls[o] = new String[2]; String[] mapping = emojiUrls[o] = new String[2];
mapping[0] = ":" + emoji.get("shortcode").value + ":"; mapping[0] = ":" + emoji.get("shortcode").value + ":";
mapping[1] = emoji.get("url").value; mapping[1] = emoji.get("url").value;
} }
Tree<String> boostedPost = entity.get("reblog"); Tree<String> boostedPost = entity.get("reblog");
if (boostedPost.size() > 0) if (boostedPost.size() > 0)
this.boostedPost = new Post(boostedPost); this.boostedPost = new Post(boostedPost);
Tree<String> mentions = entity.get("mentions"); Tree<String> mentions = entity.get("mentions");
@ -508,7 +508,7 @@ Account {
resolveFormattedName() resolveFormattedName()
{ {
assert name != null; assert name != null;
formattedName = formattedName =
new RichTextPane.Builder().text(name).finish(); new RichTextPane.Builder().text(name).finish();
} }
@ -531,27 +531,27 @@ Account {
numId = entity.get("id").value; numId = entity.get("id").value;
id = entity.get("acct").value; id = entity.get("acct").value;
String displayName = entity.get("display_name").value; String displayName = entity.get("display_name").value;
String username = entity.get("username").value; String username = entity.get("username").value;
name = displayName.isEmpty() ? username : displayName; name = displayName.isEmpty() ? username : displayName;
avatarUrl = entity.get("avatar").value; avatarUrl = entity.get("avatar").value;
creationDate = creationDate =
ZonedDateTime.parse(entity.get("created_at").value) ZonedDateTime.parse(entity.get("created_at").value)
.withZoneSameInstant(ZoneId.systemDefault()); .withZoneSameInstant(ZoneId.systemDefault());
String c1 = entity.get("following_count").value; String c1 = entity.get("following_count").value;
String c2 = entity.get("followers_count").value; String c2 = entity.get("followers_count").value;
String c3 = entity.get("statuses_count").value; String c3 = entity.get("statuses_count").value;
try { try {
followedCount = (int)Double.parseDouble(c1); followedCount = (int)Double.parseDouble(c1);
followerCount = (int)Double.parseDouble(c2); followerCount = (int)Double.parseDouble(c2);
postCount = (int)Double.parseDouble(c3); postCount = (int)Double.parseDouble(c3);
} }
catch (NumberFormatException eNf) { catch (NumberFormatException eNf) {
assert false; assert false;
} }
Tree<String> fs = entity.get("fields"); Tree<String> fs = entity.get("fields");
fields = new String[fs.size()][]; fields = new String[fs.size()][];
@ -577,20 +577,20 @@ Attachment {
public String public String
id; id;
public String public String
type; type;
public String public String
url; url;
public String public String
description; description;
public Image public Image
image; image;
public File public File
uploadee; uploadee;
// ---%-@-%--- // ---%-@-%---
@ -639,8 +639,8 @@ Composition {
public Attachment[] public Attachment[]
attachments; attachments;
private File private File
uploadee; uploadee;
// ---%-@-%--- // ---%-@-%---
@ -653,33 +653,33 @@ Composition {
Composition c = new Composition(); Composition c = new Composition();
Tree<String> boosted = entity.get("reblog"); Tree<String> boosted = entity.get("reblog");
if (boosted.size() > 0) entity = boosted; if (boosted.size() > 0) entity = boosted;
String st = entity.get("spoiler_text").value; String st = entity.get("spoiler_text").value;
String ri = entity.get("id").value; String ri = entity.get("id").value;
c.contentWarning = st.trim().isEmpty() ? null : st; c.contentWarning = st.trim().isEmpty() ? null : st;
c.replyToPostId = ri.trim().isEmpty() ? null : ri; c.replyToPostId = ri.trim().isEmpty() ? null : ri;
Tree<String> author = entity.get("account"); Tree<String> author = entity.get("account");
String authorId = author.get("acct").value; String authorId = author.get("acct").value;
String authorNumId = author.get("id").value; String authorNumId = author.get("id").value;
c.text = ""; c.text = "";
if (!authorNumId.equals(ownNumId)) if (!authorNumId.equals(ownNumId))
c.text = "@" + authorId + " "; c.text = "@" + authorId + " ";
String visibility = entity.get("visibility").value; String visibility = entity.get("visibility").value;
boolean p = visibility.equals("public"); boolean p = visibility.equals("public");
boolean u = visibility.equals("unlisted"); boolean u = visibility.equals("unlisted");
boolean f = visibility.equals("private"); boolean f = visibility.equals("private");
boolean m = visibility.equals("direct"); boolean m = visibility.equals("direct");
assert p || u || f || m; assert p || u || f || m;
if (p) c.visibility = PostVisibility.PUBLIC; if (p) c.visibility = PostVisibility.PUBLIC;
if (u) c.visibility = PostVisibility.UNLISTED; if (u) c.visibility = PostVisibility.UNLISTED;
if (f) c.visibility = PostVisibility.FOLLOWERS; if (f) c.visibility = PostVisibility.FOLLOWERS;
if (m) c.visibility = PostVisibility.MENTIONED; if (m) c.visibility = PostVisibility.MENTIONED;
// Less eye strain arranged this way. // Less eye strain arranged this way.
return c; return c;
} }
public static Composition public static Composition
@ -693,17 +693,17 @@ Composition {
c.replyToPostId = entity.get("in_reply_to_id").value; c.replyToPostId = entity.get("in_reply_to_id").value;
String visibility = entity.get("visibility").value; String visibility = entity.get("visibility").value;
boolean p = visibility.equals("public"); boolean p = visibility.equals("public");
boolean u = visibility.equals("unlisted"); boolean u = visibility.equals("unlisted");
boolean f = visibility.equals("private"); boolean f = visibility.equals("private");
boolean m = visibility.equals("direct"); boolean m = visibility.equals("direct");
assert p || u || f || m; assert p || u || f || m;
if (p) c.visibility = PostVisibility.PUBLIC; if (p) c.visibility = PostVisibility.PUBLIC;
if (u) c.visibility = PostVisibility.UNLISTED; if (u) c.visibility = PostVisibility.UNLISTED;
if (f) c.visibility = PostVisibility.FOLLOWERS; if (f) c.visibility = PostVisibility.FOLLOWERS;
if (m) c.visibility = PostVisibility.MENTIONED; if (m) c.visibility = PostVisibility.MENTIONED;
return c; return c;
} }
public static Composition public static Composition
@ -712,9 +712,9 @@ Composition {
if (post.boostedPost != null) post = post.boostedPost; if (post.boostedPost != null) post = post.boostedPost;
Composition c = new Composition(); Composition c = new Composition();
c.replyToPostId = post.id; c.replyToPostId = post.id;
c.visibility = post.visibility; c.visibility = post.visibility;
c.contentWarning = post.contentWarning; c.contentWarning = post.contentWarning;
StringBuilder text = new StringBuilder(); StringBuilder text = new StringBuilder();
for (String id: post.mentions) for (String id: post.mentions)

Binary file not shown.

View File

@ -50,8 +50,8 @@ LoginWindow extends JFrame {
private JKomasto private JKomasto
primaire; primaire;
private MastodonApi private MastodonApi
api; api;
// - -%- - // - -%- -
@ -62,7 +62,7 @@ LoginWindow extends JFrame {
serverContacted = false, serverContacted = false,
haveAppCredentials = false, haveAppCredentials = false,
haveAccessToken = false, haveAccessToken = false,
haveAccountDetails = false; haveAccountDetails = false;
// ---%-@-%--- // ---%-@-%---
@ -81,325 +81,325 @@ LoginWindow extends JFrame {
b.append(prefix4 + " Have account details\n"); b.append(prefix4 + " Have account details\n");
display.setText(b.toString()); display.setText(b.toString());
display.paintImmediately(display.getBounds(null)); display.paintImmediately(display.getBounds(null));
} }
public void public void
useCache() useCache()
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
try try
{ {
api.loadCache(); api.loadCache();
haveAppCredentials = true; haveAppCredentials = true;
haveAccessToken = true; haveAccessToken = true;
display.setInstanceUrl(api.getInstanceUrl()); display.setInstanceUrl(api.getInstanceUrl());
updateStatusDisplay(); updateStatusDisplay();
} }
catch (IOException eIo) catch (IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
this, this,
"We couldn't get login details from the cache.." "We couldn't get login details from the cache.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage() + "\n" + eIo.getClass() + ": " + eIo.getMessage()
); );
display.setAutoLoginToggled(false); display.setAutoLoginToggled(false);
} }
display.setCursor(null); display.setCursor(null);
if (!haveAccessToken) return; if (!haveAccessToken) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAccountDetails(new RequestListener() { api.getAccountDetails(new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get account details, failed.." "Tried to get account details, failed.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get account details, failed.." "Tried to get account details, failed.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")" + "\n(HTTP error code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
api.setAccountDetails(json); api.setAccountDetails(json);
serverContacted = true; serverContacted = true;
haveAccountDetails = true; haveAccountDetails = true;
updateStatusDisplay(); updateStatusDisplay();
} }
}); });
display.setCursor(null); display.setCursor(null);
if (!haveAccountDetails) return; if (!haveAccountDetails) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
try try
{ {
api.saveToCache(); api.saveToCache();
} }
catch (IOException eIo) catch (IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
this, this,
"We couldn't save login details into the cache.." "We couldn't save login details into the cache.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage() + "\n" + eIo.getClass() + ": " + eIo.getMessage()
); );
} }
display.setCursor(null); display.setCursor(null);
primaire.finishedLogin(); primaire.finishedLogin();
} }
public void public void
useInstanceUrl() useInstanceUrl()
{ {
if (display.isAutoLoginToggled()) { useCache(); return; } if (display.isAutoLoginToggled()) { useCache(); return; }
String url = display.getInstanceUrl(); String url = display.getInstanceUrl();
if (url.trim().isEmpty()) { if (url.trim().isEmpty()) {
// Should we show an error dialog..? // Should we show an error dialog..?
display.setInstanceUrl(""); display.setInstanceUrl("");
return; return;
} }
if (!hasProtocol(url)) { if (!hasProtocol(url)) {
url = "https://" + url; url = "https://" + url;
display.setInstanceUrl(url); display.setInstanceUrl(url);
display.paintImmediately(display.getBounds(null)); display.paintImmediately(display.getBounds(null));
} }
serverContacted = false; serverContacted = false;
haveAppCredentials = false; haveAppCredentials = false;
haveAccessToken = false; haveAccessToken = false;
haveAccountDetails = false; haveAccountDetails = false;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.testUrlConnection(url, new RequestListener() { api.testUrlConnection(url, new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to connect to URL, failed.." "Tried to connect to URL, failed.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage() + "\n" + eIo.getClass() + ": " + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> response) requestFailed(int httpCode, Tree<String> response)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to connect to URL, failed.." "Tried to connect to URL, failed.."
+ "\n" + response.get("body").value + "\n" + response.get("body").value
); );
} }
public void public void
requestSucceeded(Tree<String> response) requestSucceeded(Tree<String> response)
{ {
serverContacted = true; serverContacted = true;
updateStatusDisplay(); updateStatusDisplay();
} }
}); });
display.setCursor(null); display.setCursor(null);
if (!serverContacted) return; if (!serverContacted) return;
api.setInstanceUrl(url); api.setInstanceUrl(url);
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAppCredentials(new RequestListener() { api.getAppCredentials(new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get app credentials, failed.." "Tried to get app credentials, failed.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get app credentials, failed.." "Tried to get app credentials, failed.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")" + "\n(HTTP error code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
api.setAppCredentials(json); api.setAppCredentials(json);
haveAppCredentials = true; haveAppCredentials = true;
updateStatusDisplay(); updateStatusDisplay();
} }
}); });
display.setCursor(null); display.setCursor(null);
if (!haveAppCredentials) return; if (!haveAppCredentials) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
URI uri = api.getAuthorisationURL(); URI uri = api.getAuthorisationURL();
final String MESSAGE1 = final String MESSAGE1 =
"We will need you to login through a web browser,\n" "We will need you to login through a web browser,\n"
+ "and they will give you an authorisation code\n" + "and they will give you an authorisation code\n"
+ "that you will paste here. Sorry..!"; + "that you will paste here. Sorry..!";
boolean supported = boolean supported =
Desktop.isDesktopSupported() Desktop.isDesktopSupported()
&& Desktop.getDesktop().isSupported(Desktop.Action.BROWSE); && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE);
if (supported) if (supported)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
MESSAGE1, MESSAGE1,
"Authorisation code renewal", "Authorisation code renewal",
JOptionPane.INFORMATION_MESSAGE JOptionPane.INFORMATION_MESSAGE
); );
try { Desktop.getDesktop().browse(uri); } try { Desktop.getDesktop().browse(uri); }
catch (IOException eIo) { supported = false; } catch (IOException eIo) { supported = false; }
} }
if (!supported) if (!supported)
{ {
final String MESSAGE2 = final String MESSAGE2 =
"\nWe cannot use Desktop.browse(URI) on your\n" "\nWe cannot use Desktop.browse(URI) on your\n"
+ "computer.. You'll have to open your web\n" + "computer.. You'll have to open your web\n"
+ "browser yourself, and copy this URL in."; + "browser yourself, and copy this URL in.";
JTextField field = new JTextField(); JTextField field = new JTextField();
field.setText(uri.toString()); field.setText(uri.toString());
field.setPreferredSize(new Dimension(120, 32)); field.setPreferredSize(new Dimension(120, 32));
field.selectAll(); field.selectAll();
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
new Object[] { MESSAGE1, MESSAGE2, field }, new Object[] { MESSAGE1, MESSAGE2, field },
"Authorisation code renewal", "Authorisation code renewal",
JOptionPane.INFORMATION_MESSAGE JOptionPane.INFORMATION_MESSAGE
); );
} }
display.receiveAuthorisationCode(); display.receiveAuthorisationCode();
display.setCursor(null); display.setCursor(null);
} }
public void public void
useAuthorisationCode() useAuthorisationCode()
{ {
String code = display.getAuthorisationCode(); String code = display.getAuthorisationCode();
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAccessToken(code, new RequestListener() { api.getAccessToken(code, new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get app token, failed.." "Tried to get app token, failed.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get app token, failed.." "Tried to get app token, failed.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")" + "\n(HTTP error code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
api.setAccessToken(json); api.setAccessToken(json);
haveAccessToken = true; haveAccessToken = true;
updateStatusDisplay(); updateStatusDisplay();
} }
}); });
display.setCursor(null); display.setCursor(null);
if (!haveAccessToken) return; if (!haveAccessToken) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getAccountDetails(new RequestListener() { api.getAccountDetails(new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get account details, failed.." "Tried to get account details, failed.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
LoginWindow.this, LoginWindow.this,
"Tried to get account details, failed.." "Tried to get account details, failed.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP error code: " + httpCode + ")" + "\n(HTTP error code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
api.setAccountDetails(json); api.setAccountDetails(json);
haveAccountDetails = true; haveAccountDetails = true;
updateStatusDisplay(); updateStatusDisplay();
} }
}); });
display.setCursor(null); display.setCursor(null);
if (!haveAccountDetails) return; if (!haveAccountDetails) return;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
try try
{ {
api.saveToCache(); api.saveToCache();
} }
catch (IOException eIo) catch (IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
this, this,
"We couldn't save login details into the cache.." "We couldn't save login details into the cache.."
+ "\n" + eIo.getClass() + ": " + eIo.getMessage() + "\n" + eIo.getClass() + ": " + eIo.getMessage()
); );
} }
display.setCursor(null); display.setCursor(null);
primaire.finishedLogin(); primaire.finishedLogin();
} }
// - -%- - // - -%- -
public static boolean public static boolean
hasProtocol(String url) { return url.matches("^.+://.*"); } hasProtocol(String url) { return url.matches("^.+://.*"); }
// ---%-@-%--- // ---%-@-%---
@ -407,7 +407,7 @@ LoginWindow extends JFrame {
{ {
super("JKomasto - Login"); super("JKomasto - Login");
this.primaire = primaire; this.primaire = primaire;
this.api = primaire.getMastodonApi(); this.api = primaire.getMastodonApi();
setLocationByPlatform(true); setLocationByPlatform(true);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
@ -419,7 +419,7 @@ LoginWindow extends JFrame {
setContentPane(display); setContentPane(display);
pack(); pack();
setIconImage(primaire.getProgramIcon()); setIconImage(primaire.getProgramIcon());
} }
} }
@ -451,7 +451,7 @@ implements ActionListener {
private JPanel private JPanel
labelArea, labelArea,
forField, forField,
accountsPanel; accountsPanel;
private JCheckBox private JCheckBox
autoLoginToggle; autoLoginToggle;
@ -482,11 +482,11 @@ implements ActionListener {
statusDisplay.setText(status); statusDisplay.setText(status);
} }
public void public void
setAutoLoginToggled(boolean a) { autoLoginToggle.setSelected(a); } setAutoLoginToggled(boolean a) { autoLoginToggle.setSelected(a); }
public boolean public boolean
isAutoLoginToggled() { return autoLoginToggle.isSelected(); } isAutoLoginToggled() { return autoLoginToggle.isSelected(); }
public void public void
actionPerformed(ActionEvent eA) actionPerformed(ActionEvent eA)
@ -502,11 +502,11 @@ implements ActionListener {
{ {
labelArea.remove(authorisationCodeLabel); labelArea.remove(authorisationCodeLabel);
forField.remove(authorisationCodeButton); forField.remove(authorisationCodeButton);
accountsPanel.remove(authorisationCodeField); accountsPanel.remove(authorisationCodeField);
labelArea.add(instanceUrlLabel, BorderLayout.NORTH); labelArea.add(instanceUrlLabel, BorderLayout.NORTH);
forField.add(instanceUrlButton, BorderLayout.EAST); forField.add(instanceUrlButton, BorderLayout.EAST);
accountsPanel.add(instanceUrlField); accountsPanel.add(instanceUrlField);
revalidate(); revalidate();
} }
public void public void
@ -514,11 +514,11 @@ implements ActionListener {
{ {
labelArea.remove(instanceUrlLabel); labelArea.remove(instanceUrlLabel);
forField.remove(instanceUrlButton); forField.remove(instanceUrlButton);
accountsPanel.remove(instanceUrlField); accountsPanel.remove(instanceUrlField);
labelArea.add(authorisationCodeLabel, BorderLayout.NORTH); labelArea.add(authorisationCodeLabel, BorderLayout.NORTH);
forField.add(authorisationCodeButton, BorderLayout.EAST); forField.add(authorisationCodeButton, BorderLayout.EAST);
accountsPanel.add(authorisationCodeField); accountsPanel.add(authorisationCodeField);
revalidate(); revalidate();
} }
// ---%-@-%--- // ---%-@-%---
@ -595,7 +595,7 @@ implements ActionListener {
statusDisplay.setBorder(bi); statusDisplay.setBorder(bi);
statusDisplay.setFont(f2); statusDisplay.setFont(f2);
receiveInstanceUrl(); receiveInstanceUrl();
setLayout(new BorderLayout(0, 8)); setLayout(new BorderLayout(0, 8));
add(accountsPanel, BorderLayout.NORTH); add(accountsPanel, BorderLayout.NORTH);

View File

@ -295,7 +295,7 @@ MastodonApi {
submit( submit(
String text, PostVisibility visibility, String text, PostVisibility visibility,
String replyTo, String contentWarning, String replyTo, String contentWarning,
String[] mediaIDs, String[] mediaIDs,
RequestListener handler) RequestListener handler)
{ {
String token = accessToken.get("access_token").value; String token = accessToken.get("access_token").value;
@ -320,7 +320,7 @@ MastodonApi {
HttpURLConnection conn = cast(endpoint.openConnection()); HttpURLConnection conn = cast(endpoint.openConnection());
String s1 = "Bearer " + token; String s1 = "Bearer " + token;
conn.setRequestProperty("Authorization", s1); conn.setRequestProperty("Authorization", s1);
String s2 = Integer.toString(handler.hashCode()); String s2 = Integer.toString(handler.hashCode());
conn.setRequestProperty("Idempotency-Key", s2); conn.setRequestProperty("Idempotency-Key", s2);
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
@ -335,9 +335,9 @@ MastodonApi {
if (contentWarning != null) { if (contentWarning != null) {
output.write("&spoiler_text=" + contentWarning); output.write("&spoiler_text=" + contentWarning);
} }
for (String mediaID: mediaIDs) { for (String mediaID: mediaIDs) {
output.write("&media_ids[]=" + mediaID); output.write("&media_ids[]=" + mediaID);
} }
output.close(); output.close();
@ -457,77 +457,77 @@ MastodonApi {
uploadFile(File file, String alt, RequestListener handler) uploadFile(File file, String alt, RequestListener handler)
{ {
assert file != null; assert file != null;
assert alt != null; assert alt != null;
assert file.canRead(); assert file.canRead();
String bct = String bct =
"multipart/form-data; " "multipart/form-data; "
+ "boundary=\"JKomastoFileUpload\""; + "boundary=\"JKomastoFileUpload\"";
String fsb = "--JKomastoFileUpload\r\n"; String fsb = "--JKomastoFileUpload\r\n";
String feb = "\r\n--JKomastoFileUpload--\r\n"; String feb = "\r\n--JKomastoFileUpload--\r\n";
String fcd = String fcd =
"Content-Disposition: form-data; " "Content-Disposition: form-data; "
+ "name=\"file\"; " + "name=\"file\"; "
+ "filename=\"" + file.getName() + "\"\r\n"; + "filename=\"" + file.getName() + "\"\r\n";
String fct = "Content-Type: image/png\r\n\r\n"; String fct = "Content-Type: image/png\r\n\r\n";
int contentLength = 0; int contentLength = 0;
contentLength += fsb.length(); contentLength += fsb.length();
contentLength += feb.length(); contentLength += feb.length();
contentLength += fcd.length(); contentLength += fcd.length();
contentLength += fct.length(); contentLength += fct.length();
contentLength += file.length(); contentLength += file.length();
/* /*
* () This was an absurdity to debug. Contrary to * () This was an absurdity to debug. Contrary to
* cURL, Java sets default values for some headers, * cURL, Java sets default values for some headers,
* some of which are restricted, meaning you can't * some of which are restricted, meaning you can't
* arbitrarily change them. Content-Length is one * arbitrarily change them. Content-Length is one
* of them, set to 2^14-1 bytes. I'm pretty sure * of them, set to 2^14-1 bytes. I'm pretty sure
* the file I was uploading was under this, but * the file I was uploading was under this, but
* anyways one of the two parties was stopping me * anyways one of the two parties was stopping me
* from finishing transferring my form data. * from finishing transferring my form data.
* *
* They didn't mention this in the Javadocs. * They didn't mention this in the Javadocs.
* I noticed HttpURLConnection#setChunkedStreamingMode * I noticed HttpURLConnection#setChunkedStreamingMode
* and #setFixedLengthStreamingMode by accident. * and #setFixedLengthStreamingMode by accident.
* Turns out, the latter is how I do what cURL and * Turns out, the latter is how I do what cURL and
* Firefox are doing - precalculate the exact size * Firefox are doing - precalculate the exact size
* of the body and set the content length to it. * of the body and set the content length to it.
* Unfortunately, this is not flexible, we have to * Unfortunately, this is not flexible, we have to
* be exact. Thankfully, my answers pass.. * be exact. Thankfully, my answers pass..
* *
* On the other side, Mastodon is obtuse as usual. * On the other side, Mastodon is obtuse as usual.
* They had code that basically throws a generic 500 * They had code that basically throws a generic 500
* upon any sort of error from their library[1]. What * upon any sort of error from their library[1]. What
* problem the library had with my requests, I could * problem the library had with my requests, I could
* never know. There is an undocumented requirement * never know. There is an undocumented requirement
* that you must put a filename in the content * that you must put a filename in the content
* disposition. That one I found by guessing. * disposition. That one I found by guessing.
* *
* I solved this with the help of -Djavax.net.debug, * I solved this with the help of -Djavax.net.debug,
* which revealed to me how my headers and body * which revealed to me how my headers and body
* differed from cURL and Firefox. If this issue * differed from cURL and Firefox. If this issue
* happens again, I advise giving up. * happens again, I advise giving up.
* *
* [1] app/controllers/api/v1/media_controller.rb * [1] app/controllers/api/v1/media_controller.rb
* #create. 3 March 2022 * #create. 3 March 2022
*/ */
String token = accessToken.get("access_token").value; String token = accessToken.get("access_token").value;
String url = instanceUrl + "/api/v1/media/"; String url = instanceUrl + "/api/v1/media/";
try try
{ {
String s1 = "?description=" + encode(alt); String s1 = "?description=" + encode(alt);
URL endpoint = new URL(url + s1); URL endpoint = new URL(url + s1);
HttpURLConnection conn = cast(endpoint.openConnection()); HttpURLConnection conn = cast(endpoint.openConnection());
String s2 = "Bearer " + token; String s2 = "Bearer " + token;
conn.setRequestProperty("Authorization", s2); conn.setRequestProperty("Authorization", s2);
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
conn.setFixedLengthStreamingMode(contentLength); conn.setFixedLengthStreamingMode(contentLength);
conn.setRequestProperty("Content-Type", bct); conn.setRequestProperty("Content-Type", bct);
conn.setRequestProperty("Accept", "*/*"); conn.setRequestProperty("Accept", "*/*");
conn.connect(); conn.connect();
OutputStream ostream = conn.getOutputStream(); OutputStream ostream = conn.getOutputStream();
InputStream istream = new FileInputStream(file); InputStream istream = new FileInputStream(file);
@ -536,12 +536,12 @@ MastodonApi {
ostream.write(fcd.getBytes()); ostream.write(fcd.getBytes());
ostream.write(fct.getBytes()); ostream.write(fct.getBytes());
int c; while ((c = istream.read()) != -1) int c; while ((c = istream.read()) != -1)
ostream.write(c); ostream.write(c);
ostream.write(feb.getBytes()); ostream.write(feb.getBytes());
istream.close(); istream.close();
ostream.close(); ostream.close();
doStandardJsonReturn(conn, handler); doStandardJsonReturn(conn, handler);
} }
@ -550,7 +550,7 @@ MastodonApi {
public void public void
monitorTimeline( monitorTimeline(
TimelineType type, ServerSideEventsListener handler) TimelineType type, ServerSideEventsListener handler)
{ {
String token = accessToken.get("access_token").value; String token = accessToken.get("access_token").value;
@ -584,7 +584,7 @@ MastodonApi {
conn.setReadTimeout(500); conn.setReadTimeout(500);
Reader input = ireader(conn.getInputStream()); Reader input = ireader(conn.getInputStream());
BufferedReader br = new BufferedReader(input); BufferedReader br = new BufferedReader(input);
Thread thread = Thread.currentThread(); Thread thread = Thread.currentThread();
while (true) try while (true) try
{ {
String line = br.readLine(); String line = br.readLine();
@ -629,7 +629,7 @@ MastodonApi {
int code = conn.getResponseCode(); int code = conn.getResponseCode();
if (code >= 300) if (code >= 300)
{ {
Reader input = ireader(conn.getErrorStream()); Reader input = ireader(conn.getErrorStream());
Tree<String> response = fromPlain(input); Tree<String> response = fromPlain(input);
input.close(); input.close();
handler.requestFailed(code, response); handler.requestFailed(code, response);
@ -663,13 +663,13 @@ MastodonApi {
// - -%- - // - -%- -
private static String private static String
deescape(String string) deescape(String string)
{ {
if (string == null) return string; if (string == null) return string;
string = string.replaceAll("\n", "\\\\n"); string = string.replaceAll("\n", "\\\\n");
return string; return string;
} }
private static Tree<String> private static Tree<String>
fromPlain(Reader r) fromPlain(Reader r)
@ -699,27 +699,27 @@ MastodonApi {
} }
} }
private static HttpURLConnection private static HttpURLConnection
cast(URLConnection conn) cast(URLConnection conn)
{ {
return (HttpURLConnection)conn; return (HttpURLConnection)conn;
} }
private static InputStreamReader private static InputStreamReader
ireader(InputStream is) ireader(InputStream is)
throws IOException throws IOException
{ {
assert is != null; assert is != null;
return new InputStreamReader(is); return new InputStreamReader(is);
} }
private static OutputStreamWriter private static OutputStreamWriter
owriter(OutputStream os) owriter(OutputStream os)
throws IOException throws IOException
{ {
assert os != null; assert os != null;
return new OutputStreamWriter(os); return new OutputStreamWriter(os);
} }
// ---%-@-%--- // ---%-@-%---

View File

@ -48,80 +48,80 @@ import java.awt.event.ComponentEvent;
class class
NotificationsWindow extends JFrame { NotificationsWindow extends JFrame {
private JKomasto private JKomasto
primaire; primaire;
private List<Notification> private List<Notification>
notifications; notifications;
private MastodonApi private MastodonApi
api; api;
// - -%- - // - -%- -
private NotificationsComponent private NotificationsComponent
display; display;
private boolean private boolean
showingLatest; showingLatest;
// - -%- - // - -%- -
private static int private static int
ROW_COUNT = NotificationsComponent.ROW_COUNT; ROW_COUNT = NotificationsComponent.ROW_COUNT;
// ---%-@-%--- // ---%-@-%---
public synchronized void public synchronized void
readEntity(Tree<String> entity) readEntity(Tree<String> entity)
{ {
notifications = new ArrayList<>(); notifications = new ArrayList<>();
for (Tree<String> t: entity) for (Tree<String> t: entity)
{ {
Notification n = new Notification(); Notification n = new Notification();
n.id = t.get("id").value; n.id = t.get("id").value;
String type = t.get("type").value; String type = t.get("type").value;
if (type.equals("favourite")) if (type.equals("favourite"))
n.type = NotificationType.FAVOURITE; n.type = NotificationType.FAVOURITE;
else if (type.equals("reblog")) else if (type.equals("reblog"))
n.type = NotificationType.BOOST; n.type = NotificationType.BOOST;
else if (type.equals("mention")) else if (type.equals("mention"))
n.type = NotificationType.MENTION; n.type = NotificationType.MENTION;
else if (type.equals("follow")) else if (type.equals("follow"))
n.type = NotificationType.FOLLOW; n.type = NotificationType.FOLLOW;
else if (type.equals("follow_request")) else if (type.equals("follow_request"))
n.type = NotificationType.FOLLOWREQ; n.type = NotificationType.FOLLOWREQ;
else if (type.equals("poll")) else if (type.equals("poll"))
n.type = NotificationType.POLL; n.type = NotificationType.POLL;
else if (type.equals("status")) else if (type.equals("status"))
n.type = NotificationType.ALERT; n.type = NotificationType.ALERT;
Tree<String> actor = t.get("account"); Tree<String> actor = t.get("account");
String aid, aname, adisp; String aid, aname, adisp;
aid = actor.get("id").value; aid = actor.get("id").value;
aname = actor.get("username").value; aname = actor.get("username").value;
adisp = actor.get("display_name").value; adisp = actor.get("display_name").value;
if (!adisp.isEmpty()) n.actorName = adisp; if (!adisp.isEmpty()) n.actorName = adisp;
else n.actorName = aname; else n.actorName = aname;
n.actorNumId = aid; n.actorNumId = aid;
if (n.type != NotificationType.FOLLOW) if (n.type != NotificationType.FOLLOW)
{ {
Post post = new Post(t.get("status")); Post post = new Post(t.get("status"));
post.resolveApproximateText(); post.resolveApproximateText();
n.postId = post.id; n.postId = post.id;
n.postText = post.approximateText; n.postText = post.approximateText;
} }
notifications.add(n); notifications.add(n);
} }
} }
public void public void
refresh() refresh()
{ {
String firstId = null; String firstId = null;
if (!showingLatest) if (!showingLatest)
{ {
@ -134,53 +134,53 @@ NotificationsWindow extends JFrame {
if (notifications.size() < ROW_COUNT) showLatestPage(); if (notifications.size() < ROW_COUNT) showLatestPage();
display.showNotifications(notifications); display.showNotifications(notifications);
display.repaint(); display.repaint();
} }
} }
public void public void
showLatestPage() showLatestPage()
{ {
if (fetchPage(null, null)) if (fetchPage(null, null))
{ {
display.showNotifications(notifications); display.showNotifications(notifications);
showingLatest = true; showingLatest = true;
primaire.getWindowUpdater().add(this); primaire.getWindowUpdater().add(this);
} }
} }
public void public void
showPrevPage() showPrevPage()
{ {
assert !notifications.isEmpty(); assert !notifications.isEmpty();
if (fetchPage(null, notifications.get(0).id)) if (fetchPage(null, notifications.get(0).id))
{ {
if (notifications.size() < ROW_COUNT) showLatestPage(); if (notifications.size() < ROW_COUNT) showLatestPage();
display.showNotifications(notifications); display.showNotifications(notifications);
showingLatest = false; showingLatest = false;
primaire.getWindowUpdater().remove(this); primaire.getWindowUpdater().remove(this);
} }
} }
public void public void
showNextPage() showNextPage()
{ {
assert !notifications.isEmpty(); assert !notifications.isEmpty();
int last = notifications.size() - 1; int last = notifications.size() - 1;
if (fetchPage(notifications.get(last).id, null)) if (fetchPage(notifications.get(last).id, null))
{ {
display.showNotifications(notifications); display.showNotifications(notifications);
showingLatest = false; showingLatest = false;
primaire.getWindowUpdater().remove(this); primaire.getWindowUpdater().remove(this);
} }
} }
// - -%- - // - -%- -
private boolean private boolean
fetchPage(String maxId, String minId) fetchPage(String maxId, String minId)
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
class Handler implements RequestListener { class Handler implements RequestListener {
boolean boolean
succeeded = false; succeeded = false;
@ -207,31 +207,31 @@ NotificationsWindow extends JFrame {
} }
} }
Handler handler = new Handler(); Handler handler = new Handler();
api.getNotifications(ROW_COUNT, maxId, minId, handler); api.getNotifications(ROW_COUNT, maxId, minId, handler);
display.setCursor(null); display.setCursor(null);
repaint(); repaint();
return handler.succeeded; return handler.succeeded;
} }
// ---%-@-%--- // ---%-@-%---
NotificationsWindow(JKomasto primaire) NotificationsWindow(JKomasto primaire)
{ {
super("Notifications"); super("Notifications");
this.primaire = primaire; this.primaire = primaire;
this.api = primaire.getMastodonApi(); this.api = primaire.getMastodonApi();
notifications = new ArrayList<>(); notifications = new ArrayList<>();
display = new NotificationsComponent(this); display = new NotificationsComponent(this);
display.setPreferredSize(new Dimension(256, 260)); display.setPreferredSize(new Dimension(256, 260));
setContentPane(display); setContentPane(display);
pack(); pack();
setIconImage(primaire.getProgramIcon()); setIconImage(primaire.getProgramIcon());
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setVisible(true); setVisible(true);
} }
} }
@ -239,88 +239,88 @@ class
NotificationsComponent extends JPanel NotificationsComponent extends JPanel
implements ActionListener { implements ActionListener {
private NotificationsWindow private NotificationsWindow
primaire; primaire;
private JButton private JButton
prev, next; prev, next;
// - -%- - // - -%- -
private List<NotificationComponent> private List<NotificationComponent>
rows; rows;
// - -%- - // - -%- -
static final int static final int
ROW_COUNT = 10; ROW_COUNT = 10;
// ---%-@-%--- // ---%-@-%---
public void public void
showNotifications(List<Notification> notifications) showNotifications(List<Notification> notifications)
{ {
assert notifications.size() == rows.size(); assert notifications.size() == rows.size();
for (int o = 0; o < rows.size(); ++o) for (int o = 0; o < rows.size(); ++o)
{ {
Notification n = notifications.get(o); Notification n = notifications.get(o);
NotificationComponent c = rows.get(o); NotificationComponent c = rows.get(o);
c.setName(n.actorName); c.setName(n.actorName);
switch (n.type) switch (n.type)
{ {
case MENTION: c.setType("mentioned"); break; case MENTION: c.setType("mentioned"); break;
case BOOST: c.setType("boosted"); break; case BOOST: c.setType("boosted"); break;
case FAVOURITE: c.setType("favourited"); break; case FAVOURITE: c.setType("favourited"); break;
case FOLLOW: c.setType("followed"); break; case FOLLOW: c.setType("followed"); break;
case FOLLOWREQ: c.setType("req. follow"); break; case FOLLOWREQ: c.setType("req. follow"); break;
case POLL: c.setType("poll ended"); break; case POLL: c.setType("poll ended"); break;
case ALERT: c.setType("posted"); break; case ALERT: c.setType("posted"); break;
} }
c.setText(n.postText); c.setText(n.postText);
} }
} }
// - -%- - // - -%- -
public void public void
actionPerformed(ActionEvent eA) actionPerformed(ActionEvent eA)
{ {
if (eA.getSource() == prev) primaire.showPrevPage(); if (eA.getSource() == prev) primaire.showPrevPage();
if (eA.getSource() == next) primaire.showNextPage(); if (eA.getSource() == next) primaire.showNextPage();
} }
// ---%-@-%--- // ---%-@-%---
NotificationsComponent(NotificationsWindow primaire) NotificationsComponent(NotificationsWindow primaire)
{ {
this.primaire = primaire; this.primaire = primaire;
Border b = BorderFactory.createEmptyBorder(8, 8, 8, 8); Border b = BorderFactory.createEmptyBorder(8, 8, 8, 8);
rows = new ArrayList<>(); rows = new ArrayList<>();
for (int n = ROW_COUNT; n > 0; --n) for (int n = ROW_COUNT; n > 0; --n)
rows.add(new NotificationComponent()); rows.add(new NotificationComponent());
prev = new JButton("<"); prev = new JButton("<");
next = new JButton(">"); next = new JButton(">");
prev.addActionListener(this); prev.addActionListener(this);
next.addActionListener(this); next.addActionListener(this);
JPanel centre = new JPanel(); JPanel centre = new JPanel();
centre.setLayout(new GridLayout(ROW_COUNT, 1)); centre.setLayout(new GridLayout(ROW_COUNT, 1));
for (NotificationComponent c: rows) centre.add(c); for (NotificationComponent c: rows) centre.add(c);
Box bottom = Box.createHorizontalBox(); Box bottom = Box.createHorizontalBox();
bottom.add(Box.createGlue()); bottom.add(Box.createGlue());
bottom.add(prev); bottom.add(prev);
bottom.add(Box.createHorizontalStrut(8)); bottom.add(Box.createHorizontalStrut(8));
bottom.add(next); bottom.add(next);
bottom.setBorder(b); bottom.setBorder(b);
setLayout(new BorderLayout()); setLayout(new BorderLayout());
add(centre); add(centre);
add(bottom, BorderLayout.SOUTH); add(bottom, BorderLayout.SOUTH);
} }
} }
@ -328,145 +328,145 @@ class
NotificationComponent extends JComponent NotificationComponent extends JComponent
implements ComponentListener { implements ComponentListener {
private JLabel private JLabel
type; type;
private ImageComponent private ImageComponent
typeImg; typeImg;
private JLabel private JLabel
name, text; name, text;
// ---%-@-%--- // ---%-@-%---
public void public void
setType(String n) setType(String n)
{ {
type.setText(n); type.setText(n);
typeImg.image = ImageApi.local(n + "Notification"); typeImg.image = ImageApi.local(n + "Notification");
} }
public void public void
setName(String n) { name.setText(n); } setName(String n) { name.setText(n); }
public void public void
setText(String n) { text.setText(n); } setText(String n) { text.setText(n); }
// - -%- - // - -%- -
public void public void
doLayout() doLayout()
{ {
int w = getWidth(), h = getHeight(); int w = getWidth(), h = getHeight();
int x0 = w * 0/20; int x0 = w * 0/20;
int x1 = w * 7/20; int x1 = w * 7/20;
int x2 = w * 13/20; int x2 = w * 13/20;
int x3 = w * 15/20; int x3 = w * 15/20;
int x4 = w * 20/20; int x4 = w * 20/20;
name.setLocation(x0 + 4, 0); name.setLocation(x0 + 4, 0);
name.setSize((x1 - 4) - (x0 + 4), h); name.setSize((x1 - 4) - (x0 + 4), h);
type.setLocation(x1 + 4, 0); type.setLocation(x1 + 4, 0);
type.setSize((x2 - 1) - (x1 + 4), h); type.setSize((x2 - 1) - (x1 + 4), h);
typeImg.setLocation((x2 + 1), 2); typeImg.setLocation((x2 + 1), 2);
typeImg.setSize((x3 - 4) - (x2 + 1), (h - 2) - 2); typeImg.setSize((x3 - 4) - (x2 + 1), (h - 2) - 2);
text.setLocation(x3 + 4, 0); text.setLocation(x3 + 4, 0);
text.setSize((x4 - 4) - (x3 + 4), h); text.setSize((x4 - 4) - (x3 + 4), h);
} }
public void public void
componentResized(ComponentEvent eC) { doLayout(); } componentResized(ComponentEvent eC) { doLayout(); }
public void public void
componentShown(ComponentEvent eC) { } componentShown(ComponentEvent eC) { }
public void public void
componentHidden(ComponentEvent eC) { } componentHidden(ComponentEvent eC) { }
public void public void
componentMoved(ComponentEvent eC) { } componentMoved(ComponentEvent eC) { }
// ---%-@-%--- // ---%-@-%---
private static class private static class
ImageComponent extends JComponent { ImageComponent extends JComponent {
private Image private Image
image, image,
scaled; scaled;
private boolean private boolean
snapdown = false; snapdown = false;
// -=%=- // -=%=-
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
if (image == null) return; if (image == null) return;
int w = getWidth(), h = getHeight(); int w = getWidth(), h = getHeight();
int ow = image.getWidth(this); int ow = image.getWidth(this);
int oh = image.getHeight(this); int oh = image.getHeight(this);
int nh = h; int nh = h;
int nw = ow * nh/oh; int nw = ow * nh/oh;
if (snapdown) if (snapdown)
{ {
int sw, sh; int sw, sh;
for (sw = 1; sw < nw; sw *= 2); for (sw = 1; sw < nw; sw *= 2);
for (sh = 1; sh < nh; sh *= 2); for (sh = 1; sh < nh; sh *= 2);
nw = sw / 2; nw = sw / 2;
nh = sh / 2; nh = sh / 2;
} }
if (scaled == null) if (scaled == null)
scaled = image.getScaledInstance( scaled = image.getScaledInstance(
nw, nh, nw, nh,
Image.SCALE_SMOOTH Image.SCALE_SMOOTH
); );
int x = (w - nw) / 2; int x = (w - nw) / 2;
int y = (h - nh) / 2; int y = (h - nh) / 2;
g.drawImage(scaled, x, y, this); g.drawImage(scaled, x, y, this);
} }
} }
// ---%-@-%--- // ---%-@-%---
NotificationComponent() NotificationComponent()
{ {
Font f = new Font("Dialog", Font.PLAIN, 12); Font f = new Font("Dialog", Font.PLAIN, 12);
Font f1 = f.deriveFont(Font.PLAIN, 14); Font f1 = f.deriveFont(Font.PLAIN, 14);
Font f2 = f.deriveFont(Font.PLAIN, 11); Font f2 = f.deriveFont(Font.PLAIN, 11);
Font f3 = f.deriveFont(Font.ITALIC, 14); Font f3 = f.deriveFont(Font.ITALIC, 14);
Color c = new Color(0, 0, 0, 25); Color c = new Color(0, 0, 0, 25);
Border b1 = BorderFactory.createMatteBorder(0, 0, 1, 0, c); Border b1 = BorderFactory.createMatteBorder(0, 0, 1, 0, c);
name = new JLabel(); name = new JLabel();
name.setFont(f1); name.setFont(f1);
type = new JLabel(); type = new JLabel();
type.setFont(f2); type.setFont(f2);
type.setHorizontalAlignment(JLabel.RIGHT); type.setHorizontalAlignment(JLabel.RIGHT);
typeImg = new ImageComponent(); typeImg = new ImageComponent();
text = new JLabel(); text = new JLabel();
text.setFont(f3); text.setFont(f3);
setLayout(null); setLayout(null);
add(name); add(name);
add(type); add(type);
add(typeImg); add(typeImg);
add(text); add(text);
this.addComponentListener(this); this.addComponentListener(this);
setBorder(b1); setBorder(b1);
} }
} }

View File

@ -74,9 +74,9 @@ PostWindow extends JFrame {
private MastodonApi private MastodonApi
api; api;
private Post private Post
post, post,
wrapperPost; wrapperPost;
// - -%- - // - -%- -
@ -109,21 +109,21 @@ PostWindow extends JFrame {
} }
display.setAuthorName(post.author.name); display.setAuthorName(post.author.name);
display.setAuthorId(post.author.id); display.setAuthorId(post.author.id);
String oid = api.getAccountDetails().get("id").value; String oid = api.getAccountDetails().get("id").value;
String aid = post.author.numId; String aid = post.author.numId;
display.setDeleteEnabled(aid.equals(oid)); display.setDeleteEnabled(aid.equals(oid));
post.author.resolveAvatar(); post.author.resolveAvatar();
display.setAuthorAvatar(post.author.avatar); display.setAuthorAvatar(post.author.avatar);
display.setDate(post.date); display.setDate(post.date);
display.setTime(post.time); display.setTime(post.time);
display.setEmojiUrls(post.emojiUrls); display.setEmojiUrls(post.emojiUrls);
display.setHtml(post.text); display.setHtml(post.text);
display.setFavourited(post.favourited); display.setFavourited(post.favourited);
display.setBoosted(post.boosted); display.setBoosted(post.boosted);
@ -137,29 +137,29 @@ PostWindow extends JFrame {
post.resolveApproximateText(); post.resolveApproximateText();
this.setTitle(post.approximateText); this.setTitle(post.approximateText);
display.resetFocus(); display.resetFocus();
repaint(); repaint();
} }
public void public void
readEntity(Tree<String> post) readEntity(Tree<String> post)
{ {
use(new Post(post)); use(new Post(post));
} }
public synchronized void public synchronized void
openAuthorProfile() openAuthorProfile()
{ {
ProfileWindow w = new ProfileWindow(primaire); ProfileWindow w = new ProfileWindow(primaire);
w.use(post.author); w.use(post.author);
w.setLocationRelativeTo(this); w.setLocationRelativeTo(this);
w.setVisible(true); w.setVisible(true);
} }
public synchronized void public synchronized void
favourite(boolean favourited) favourite(boolean favourited)
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setFavouriteBoostEnabled(false); display.setFavouriteBoostEnabled(false);
display.paintImmediately(display.getBounds()); display.paintImmediately(display.getBounds());
RequestListener handler = new RequestListener() { RequestListener handler = new RequestListener() {
@ -188,11 +188,11 @@ PostWindow extends JFrame {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
PostWindow.this.post.favourited = favourited; PostWindow.this.post.favourited = favourited;
} }
}; };
api.setPostFavourited(post.id, favourited, handler); api.setPostFavourited(post.id, favourited, handler);
display.setCursor(null); display.setCursor(null);
display.setFavouriteBoostEnabled(true); display.setFavouriteBoostEnabled(true);
display.repaint(); display.repaint();
@ -234,7 +234,7 @@ PostWindow extends JFrame {
} }
}; };
api.setPostBoosted(post.id, boosted, handler); api.setPostBoosted(post.id, boosted, handler);
display.setCursor(null); display.setCursor(null);
display.setFavouriteBoostEnabled(true); display.setFavouriteBoostEnabled(true);
display.repaint(); display.repaint();
@ -246,9 +246,9 @@ PostWindow extends JFrame {
String ownId = api.getAccountDetails().get("acct").value; String ownId = api.getAccountDetails().get("acct").value;
Composition c = Composition.reply(this.post, ownId); Composition c = Composition.reply(this.post, ownId);
ComposeWindow w = primaire.getComposeWindow(); ComposeWindow w = primaire.getComposeWindow();
w.setComposition(c); w.setComposition(c);
if (!w.isVisible()) if (!w.isVisible())
{ {
w.setLocation(getX(), getY() + 100); w.setLocation(getX(), getY() + 100);
w.setVisible(true); w.setVisible(true);
} }
@ -271,62 +271,62 @@ PostWindow extends JFrame {
display.setCursor(null); display.setCursor(null);
} }
public synchronized void public synchronized void
deletePost(boolean redraft) deletePost(boolean redraft)
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
display.setDeleteEnabled(false); display.setDeleteEnabled(false);
display.paintImmediately(display.getBounds()); display.paintImmediately(display.getBounds());
final String S1 = final String S1 =
"Are you sure you'd like to delete this post?\n"; "Are you sure you'd like to delete this post?\n";
final String S2 = final String S2 =
"Are you sure you'd like to delete this post?\n" "Are you sure you'd like to delete this post?\n"
+ "You are redrafting, so a composition window\n" + "You are redrafting, so a composition window\n"
+ "should open with its contents filled."; + "should open with its contents filled.";
JOptionPane dialog = new JOptionPane(); JOptionPane dialog = new JOptionPane();
dialog.setMessageType(JOptionPane.QUESTION_MESSAGE); dialog.setMessageType(JOptionPane.QUESTION_MESSAGE);
dialog.setMessage(redraft ? S2 : S1); dialog.setMessage(redraft ? S2 : S1);
dialog.setOptions(new String[] { "No", "Yes" }); dialog.setOptions(new String[] { "No", "Yes" });
String title = "Confirm delete"; String title = "Confirm delete";
dialog.createDialog(this, title).setVisible(true); dialog.createDialog(this, title).setVisible(true);
if (!dialog.getValue().equals("Yes")) if (!dialog.getValue().equals("Yes"))
{ {
display.setCursor(null); display.setCursor(null);
display.setDeleteEnabled(true); display.setDeleteEnabled(true);
display.paintImmediately(display.getBounds()); display.paintImmediately(display.getBounds());
return; return;
} }
api.deletePost(post.id, new RequestListener() { api.deletePost(post.id, new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
PostWindow.this, PostWindow.this,
"Failed to delete post.." "Failed to delete post.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
PostWindow.this, PostWindow.this,
"Failed to delete post.." "Failed to delete post.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")" + "\n(HTTP code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
setVisible(false); setVisible(false);
if (redraft) if (redraft)
{ {
Composition c = Composition.recover(json); Composition c = Composition.recover(json);
ComposeWindow w = new ComposeWindow(primaire); ComposeWindow w = new ComposeWindow(primaire);
@ -334,36 +334,36 @@ PostWindow extends JFrame {
w.setLocation(getX(), getY() + 100); w.setLocation(getX(), getY() + 100);
w.setVisible(true); w.setVisible(true);
} }
} }
}); });
display.setCursor(null); display.setCursor(null);
display.setDeleteEnabled(true); display.setDeleteEnabled(true);
display.paintImmediately(display.getBounds()); display.paintImmediately(display.getBounds());
if (!isVisible()) dispose(); if (!isVisible()) dispose();
} }
public synchronized void public synchronized void
copyPostId() copyPostId()
{ {
ClipboardApi.serve(post.id); ClipboardApi.serve(post.id);
} }
public synchronized void public synchronized void
copyPostLink() copyPostLink()
{ {
ClipboardApi.serve(post.uri); ClipboardApi.serve(post.uri);
} }
public synchronized void public synchronized void
openReplies() openReplies()
{ {
RepliesWindow w = new RepliesWindow(primaire, this); RepliesWindow w = new RepliesWindow(primaire, this);
w.showFor(post.id); w.showFor(post.id);
w.setLocation(getX(), getY() + 100); w.setLocation(getX(), getY() + 100);
w.setVisible(true); w.setVisible(true);
} }
// ---%-@-%--- // ---%-@-%---
@ -380,7 +380,7 @@ PostWindow extends JFrame {
display = new PostComponent(this); display = new PostComponent(this);
setContentPane(display); setContentPane(display);
setIconImage(primaire.getProgramIcon()); setIconImage(primaire.getProgramIcon());
} }
} }
@ -396,23 +396,23 @@ implements ActionListener {
// - -%- - // - -%- -
private List<RichTextPane.Segment> private List<RichTextPane.Segment>
authorNameOr; authorNameOr;
private RichTextPane private RichTextPane
authorName; authorName;
private RichTextPane3 private RichTextPane3
body; body;
private JScrollPane private JScrollPane
bodyScrollPane; bodyScrollPane;
private JLabel private JLabel
authorId, time, date; authorId, time, date;
private String[][] private String[][]
emojiUrls; emojiUrls;
private TwoToggleButton private TwoToggleButton
favouriteBoost, favouriteBoost,
@ -423,32 +423,32 @@ implements ActionListener {
profile, profile,
media; media;
private JPopupMenu private JPopupMenu
miscMenu; miscMenu;
private JMenuItem private JMenuItem
openReplies, openReplies,
copyPostId, copyPostId,
copyPostLink, copyPostLink,
deletePost, deletePost,
redraftPost; redraftPost;
private Image private Image
backgroundImage; backgroundImage;
// ---%-@-%--- // ---%-@-%---
public void public void
setAuthorName(String n) setAuthorName(String n)
{ {
authorNameOr = new RichTextPane.Builder().text(n).finish(); authorNameOr = new RichTextPane.Builder().text(n).finish();
} }
public void public void
setAuthorId(String n) { authorId.setText(n); } setAuthorId(String n) { authorId.setText(n); }
public void public void
setAuthorAvatar(Image n) { profile.setImage(n); } setAuthorAvatar(Image n) { profile.setImage(n); }
public void public void
setDate(String n) { date.setText(n); } setDate(String n) { date.setText(n); }
@ -456,24 +456,24 @@ implements ActionListener {
public void public void
setTime(String n) { time.setText(n); } setTime(String n) { time.setText(n); }
public void public void
setEmojiUrls(String[][] n) setEmojiUrls(String[][] n)
{ {
emojiUrls = n; emojiUrls = n;
Map<String, Image> emojis = new HashMap<>(); Map<String, Image> emojis = new HashMap<>();
for (String[] entry: n) for (String[] entry: n)
{ {
emojis.put(entry[0], ImageApi.remote(entry[1])); emojis.put(entry[0], ImageApi.remote(entry[1]));
} }
body.setEmojis(emojis); body.setEmojis(emojis);
} }
public void public void
setHtml(String n) setHtml(String n)
{ {
body.setText(BasicHTMLParser.parse(n)); body.setText(BasicHTMLParser.parse(n));
} }
public void public void
setFavourited(boolean a) setFavourited(boolean a)
@ -497,19 +497,19 @@ implements ActionListener {
favouriteBoost.setEnabled(a); favouriteBoost.setEnabled(a);
} }
public void public void
setDeleteEnabled(boolean a) setDeleteEnabled(boolean a)
{ {
deletePost.setEnabled(a); deletePost.setEnabled(a);
redraftPost.setEnabled(a); redraftPost.setEnabled(a);
} }
public void public void
setMediaPreview(Image n) { media.setImage(n); } setMediaPreview(Image n) { media.setImage(n); }
public void public void
resetFocus() resetFocus()
{ {
media.requestFocusInWindow(); media.requestFocusInWindow();
} }
@ -551,14 +551,14 @@ implements ActionListener {
} }
else if (command.startsWith("reply")) else if (command.startsWith("reply"))
{ {
primaire.reply(); primaire.reply();
}
else if (command.startsWith("misc"))
{
int rx = replyMisc.getWidth() / 2;
int ry = replyMisc.getHeight() - miscMenu.getHeight();
miscMenu.show(replyMisc, rx, ry);
} }
else if (command.startsWith("misc"))
{
int rx = replyMisc.getWidth() / 2;
int ry = replyMisc.getHeight() - miscMenu.getHeight();
miscMenu.show(replyMisc, rx, ry);
}
return; return;
} }
else miscMenu.setVisible(false); else miscMenu.setVisible(false);
@ -567,14 +567,14 @@ implements ActionListener {
{ {
if (command.startsWith("next")) if (command.startsWith("next"))
{ {
body.nextPage(); body.nextPage();
} }
else else
{ {
body.previousPage(); body.previousPage();
} }
// First time an interactive element // First time an interactive element
// doesn't call something in primaire.. // doesn't call something in primaire..
return; return;
} }
@ -582,13 +582,13 @@ implements ActionListener {
{ {
primaire.openMedia(); primaire.openMedia();
return; return;
} }
if (src == openReplies) primaire.openReplies(); if (src == openReplies) primaire.openReplies();
if (src == copyPostId) primaire.copyPostId(); if (src == copyPostId) primaire.copyPostId();
if (src == copyPostLink) primaire.copyPostLink(); if (src == copyPostLink) primaire.copyPostLink();
if (src == deletePost) primaire.deletePost(false); if (src == deletePost) primaire.deletePost(false);
if (src == redraftPost) primaire.deletePost(true); if (src == redraftPost) primaire.deletePost(true);
} }
@ -597,28 +597,28 @@ implements ActionListener {
{ {
g.clearRect(0, 0, getWidth(), getHeight()); g.clearRect(0, 0, getWidth(), getHeight());
int w1 = authorName.getWidth(); int w1 = authorName.getWidth();
FontMetrics fm1 = getFontMetrics(authorName.getFont()); FontMetrics fm1 = getFontMetrics(authorName.getFont());
List<RichTextPane.Segment> lay1; List<RichTextPane.Segment> lay1;
lay1 = RichTextPane.layout(authorNameOr, fm1, w1); lay1 = RichTextPane.layout(authorNameOr, fm1, w1);
authorName.setText(lay1); authorName.setText(lay1);
if (backgroundImage != null) if (backgroundImage != null)
{ {
int tw = backgroundImage.getWidth(this); int tw = backgroundImage.getWidth(this);
int th = backgroundImage.getHeight(this); int th = backgroundImage.getHeight(this);
if (tw != -1) if (tw != -1)
for (int y = 0; y < getHeight(); y += th) for (int y = 0; y < getHeight(); y += th)
for (int x = 0; x < getWidth(); x += tw) for (int x = 0; x < getWidth(); x += tw)
{ {
g.drawImage(backgroundImage, x, y, this); g.drawImage(backgroundImage, x, y, this);
} }
} }
((java.awt.Graphics2D)g).setRenderingHint( ((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON java.awt.RenderingHints.VALUE_ANTIALIAS_ON
); );
} }
// - -%- - // - -%- -
@ -648,18 +648,18 @@ implements ActionListener {
{ {
this.primaire = primaire; this.primaire = primaire;
emojiUrls = new String[0][]; emojiUrls = new String[0][];
Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10); Border b = BorderFactory.createEmptyBorder(10, 10, 10, 10);
Font f1 = new Font("MotoyaLMaru", Font.PLAIN, 18); Font f1 = new Font("MotoyaLMaru", Font.PLAIN, 18);
Font f2 = new Font("MotoyaLMaru", Font.PLAIN, 14); Font f2 = new Font("MotoyaLMaru", Font.PLAIN, 14);
Font f3 = new Font("MotoyaLMaru", Font.PLAIN, 18); Font f3 = new Font("MotoyaLMaru", Font.PLAIN, 18);
profile = new RoundButton(); profile = new RoundButton();
favouriteBoost = new TwoToggleButton("favourite", "boost"); favouriteBoost = new TwoToggleButton("favourite", "boost");
replyMisc = new TwoToggleButton("reply", "misc"); replyMisc = new TwoToggleButton("reply", "misc");
nextPrev = new TwoToggleButton("next", "prev"); nextPrev = new TwoToggleButton("next", "prev");
media = new RoundButton(); media = new RoundButton();
profile.addActionListener(this); profile.addActionListener(this);
favouriteBoost.addActionListener(this); favouriteBoost.addActionListener(this);
replyMisc.addActionListener(this); replyMisc.addActionListener(this);
@ -669,22 +669,22 @@ implements ActionListener {
openReplies = new JMenuItem("Browse thread"); openReplies = new JMenuItem("Browse thread");
copyPostId = new JMenuItem("Copy post ID"); copyPostId = new JMenuItem("Copy post ID");
copyPostLink = new JMenuItem("Copy post link"); 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); openReplies.addActionListener(this);
copyPostId.addActionListener(this); copyPostId.addActionListener(this);
copyPostLink.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(openReplies);
miscMenu.add(new JSeparator()); miscMenu.add(new JSeparator());
miscMenu.add(copyPostId); miscMenu.add(copyPostId);
miscMenu.add(copyPostLink); miscMenu.add(copyPostLink);
miscMenu.add(new JSeparator()); miscMenu.add(new JSeparator());
miscMenu.add(deletePost); miscMenu.add(deletePost);
miscMenu.add(new JSeparator()); miscMenu.add(new JSeparator());
miscMenu.add(redraftPost); miscMenu.add(redraftPost);
Box buttons = Box.createVerticalBox(); Box buttons = Box.createVerticalBox();
buttons.setOpaque(false); buttons.setOpaque(false);
@ -702,59 +702,59 @@ implements ActionListener {
left.setOpaque(false); left.setOpaque(false);
left.add(buttons); left.add(buttons);
authorId = new JLabel(); authorId = new JLabel();
authorName = new RichTextPane(); authorName = new RichTextPane();
time = new JLabel(); time = new JLabel();
date = new JLabel(); date = new JLabel();
authorId.setFont(f2); authorId.setFont(f2);
date.setFont(f2); date.setFont(f2);
authorName.setFont(f1); authorName.setFont(f1);
time.setFont(f1); time.setFont(f1);
JPanel top1 = new JPanel(); JPanel top1 = new JPanel();
top1.setOpaque(false); top1.setOpaque(false);
top1.setLayout(new BorderLayout(8, 0)); top1.setLayout(new BorderLayout(8, 0));
top1.add(authorId); top1.add(authorId);
top1.add(date, BorderLayout.EAST); top1.add(date, BorderLayout.EAST);
JPanel top2 = new JPanel(); JPanel top2 = new JPanel();
top2.setOpaque(false); top2.setOpaque(false);
top2.setLayout(new BorderLayout(8, 0)); top2.setLayout(new BorderLayout(8, 0));
top2.add(authorName); top2.add(authorName);
top2.add(time, BorderLayout.EAST); top2.add(time, BorderLayout.EAST);
Box top = Box.createVerticalBox(); Box top = Box.createVerticalBox();
top.add(top1); top.add(top1);
top.add(Box.createVerticalStrut(2)); top.add(Box.createVerticalStrut(2));
top.add(top2); top.add(top2);
body = new RichTextPane3(); body = new RichTextPane3();
body.setFont(f3); body.setFont(f3);
/* /*
bodyScrollPane = new JScrollPane( bodyScrollPane = new JScrollPane(
body, body,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
); );
JScrollBar vsb = bodyScrollPane.getVerticalScrollBar(); JScrollBar vsb = bodyScrollPane.getVerticalScrollBar();
vsb.setPreferredSize(new Dimension(0, 0)); vsb.setPreferredSize(new Dimension(0, 0));
vsb.setUnitIncrement(16); vsb.setUnitIncrement(16);
bodyScrollPane.setBorder(null); bodyScrollPane.setBorder(null);
bodyScrollPane.setFocusable(true); bodyScrollPane.setFocusable(true);
*/ */
JPanel centre = new JPanel(); JPanel centre = new JPanel();
centre.setOpaque(false); centre.setOpaque(false);
centre.setLayout(new BorderLayout(0, 8)); centre.setLayout(new BorderLayout(0, 8));
centre.add(top, BorderLayout.NORTH); centre.add(top, BorderLayout.NORTH);
centre.add(body); centre.add(body);
setLayout(new BorderLayout(8, 0)); setLayout(new BorderLayout(8, 0));
add(left, BorderLayout.WEST); add(left, BorderLayout.WEST);
add(centre); add(centre);
setBorder(b); setBorder(b);
backgroundImage = ImageApi.local("postWindow"); backgroundImage = ImageApi.local("postWindow");
} }
} }

View File

@ -123,7 +123,7 @@ ProfileWindow extends JFrame {
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
TimelineWindow w = new TimelineWindow(primaire); TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(account.numId); w.showAuthorPosts(account.numId);
w.showLatestPage(); w.showLatestPage();
w.setLocationRelativeTo(this); w.setLocationRelativeTo(this);
@ -269,9 +269,9 @@ implements ActionListener {
int acx = ax + (aw / 2); int acx = ax + (aw / 2);
int acy = ay + (ah / 2); int acy = ay + (ah / 2);
Shape defaultClip = g.getClip(); Shape defaultClip = g.getClip();
g.setClip(new Ellipse2D.Float(ax, ay, aw, ah)); g.setClip(new Ellipse2D.Float(ax, ay, aw, ah));
g.drawImage(avatar, ax, ay, aw, ah, this); g.drawImage(avatar, ax, ay, aw, ah, this);
g.setClip(defaultClip); g.setClip(defaultClip);
g.setColor(new Color(0, 0, 0, 50)); g.setColor(new Color(0, 0, 0, 50));
g.fillRect(0, acy - dy1, acx - dx1, 2); g.fillRect(0, acy - dy1, acx - dx1, 2);

View File

@ -189,7 +189,7 @@ RepliesWindow extends JFrame {
setContentPane(display); setContentPane(display);
setSize(384, 224); setSize(384, 224);
setIconImage(primaire.getProgramIcon()); setIconImage(primaire.getProgramIcon());
} }
} }
@ -242,13 +242,13 @@ implements TreeSelectionListener {
if (p == null) if (p == null)
{ {
assert false; assert false;
/* /*
* Besides descendants possibly not being in order, * Besides descendants possibly not being in order,
* the top of the thread might be deleted and so * the top of the thread might be deleted and so
* thread.top gets set to the given post. Which * thread.top gets set to the given post. Which
* sibling replies aren't replying to, resulting * sibling replies aren't replying to, resulting
* in assertion failure. * in assertion failure.
*/ */
continue; continue;
} }
@ -257,8 +257,8 @@ implements TreeSelectionListener {
} }
tree.setModel(new DefaultTreeModel(root)); tree.setModel(new DefaultTreeModel(root));
for (int o = 0; o < tree.getRowCount(); ++o) for (int o = 0; o < tree.getRowCount(); ++o)
tree.expandRow(o); tree.expandRow(o);
} }
// - -%- - // - -%- -

View File

@ -24,14 +24,14 @@ import java.io.IOException;
interface interface
RequestListener { RequestListener {
void void
connectionFailed(IOException eIo); connectionFailed(IOException eIo);
void void
requestFailed(int httpCode, Tree<String> json); requestFailed(int httpCode, Tree<String> json);
void void
requestSucceeded(Tree<String> json); requestSucceeded(Tree<String> json);
} }

View File

@ -38,396 +38,396 @@ class
RichTextPane extends JComponent RichTextPane extends JComponent
implements MouseListener, MouseMotionListener, KeyListener { implements MouseListener, MouseMotionListener, KeyListener {
private List<Segment> private List<Segment>
text; text;
private int private int
selectionStart, selectionEnd; selectionStart, selectionEnd;
// ---%-@-%--- // ---%-@-%---
public void public void
setText(List<Segment> text) setText(List<Segment> text)
{ {
this.text = text; this.text = text;
selectionStart = selectionEnd = -1; selectionStart = selectionEnd = -1;
} }
public List<Segment> public List<Segment>
getSelection() getSelection()
{ {
List<Segment> returnee = new LinkedList<>(); List<Segment> returnee = new LinkedList<>();
if (selectionEnd == -1) return returnee; if (selectionEnd == -1) return returnee;
if (selectionEnd < selectionStart) { if (selectionEnd < selectionStart) {
int t = selectionEnd; int t = selectionEnd;
selectionEnd = selectionStart; selectionEnd = selectionStart;
selectionStart = t; selectionStart = t;
} }
returnee.addAll(text.subList(selectionStart + 1, selectionEnd)); returnee.addAll(text.subList(selectionStart + 1, selectionEnd));
return returnee; return returnee;
} }
public void public void
copySelection() copySelection()
{ {
assert selectionEnd != -1; assert selectionEnd != -1;
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
for (Segment segment: getSelection()) for (Segment segment: getSelection())
{ {
if (segment.link != null) b.append(segment.link); if (segment.link != null) b.append(segment.link);
else if (segment.text != null) b.append(segment.text); else if (segment.text != null) b.append(segment.text);
} }
ClipboardApi.serve(b.toString()); ClipboardApi.serve(b.toString());
} }
// - -%- - // - -%- -
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
g.setFont(getFont()); g.setFont(getFont());
FontMetrics fm = g.getFontMetrics(getFont()); FontMetrics fm = g.getFontMetrics(getFont());
if (isOpaque()) if (isOpaque())
g.clearRect(0, 0, getWidth(), getHeight()); g.clearRect(0, 0, getWidth(), getHeight());
((java.awt.Graphics2D)g).setRenderingHint( ((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_TEXT_ANTIALIASING, java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
); );
int o = 0; int o = 0;
for (Segment segment: text) for (Segment segment: text)
{ {
if (segment.image != null) { if (segment.image != null) {
int ow = segment.image.getIconWidth(); int ow = segment.image.getIconWidth();
int oh = segment.image.getIconHeight(); int oh = segment.image.getIconHeight();
int h = fm.getAscent() + fm.getDescent(); int h = fm.getAscent() + fm.getDescent();
int w = h * ow / oh; int w = h * ow / oh;
int x = segment.x; int x = segment.x;
int y = segment.y + fm.getDescent(); int y = segment.y + fm.getDescent();
// Interpret segment.y as specifying text baseline // Interpret segment.y as specifying text baseline
Image img = segment.image.getImage(); Image img = segment.image.getImage();
g.drawImage(img, x, y - h, w, h, this); g.drawImage(img, x, y - h, w, h, this);
continue; continue;
} }
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));
g.fillRect(segment.x, segment.y - dy1, dx, dy2); g.fillRect(segment.x, segment.y - dy1, dx, dy2);
g.setColor(getForeground()); g.setColor(getForeground());
} }
if (segment.link != null) g.setColor(Color.BLUE); if (segment.link != null) g.setColor(Color.BLUE);
g.drawString(segment.text, segment.x, segment.y); g.drawString(segment.text, segment.x, segment.y);
g.setColor(getForeground()); g.setColor(getForeground());
++o; ++o;
} }
} }
public void public void
mousePressed(MouseEvent eM) mousePressed(MouseEvent eM)
{ {
requestFocusInWindow(); requestFocusInWindow();
selectionStart = identify(eM.getX(), eM.getY()) - 2; selectionStart = identify(eM.getX(), eM.getY()) - 2;
selectionEnd = -1; selectionEnd = -1;
repaint(); repaint();
} }
public void public void
mouseDragged(MouseEvent eM) mouseDragged(MouseEvent eM)
{ {
selectionEnd = identify(eM.getX(), eM.getY()); selectionEnd = identify(eM.getX(), eM.getY());
repaint(); repaint();
} }
private int private int
identify(int x, int y) identify(int x, int y)
{ {
FontMetrics fm = getFontMetrics(getFont()); FontMetrics fm = getFontMetrics(getFont());
int iy = fm.getAscent(); int iy = fm.getAscent();
int lh = fm.getAscent() + fm.getDescent(); int lh = fm.getAscent() + fm.getDescent();
y -= fm.getDescent(); y -= fm.getDescent();
if (y <= iy) y = iy; if (y <= iy) y = iy;
else y += lh - ((y - iy) % lh); else y += lh - ((y - iy) % lh);
/* /*
* Snaps y to the next baseline. Kind of obtuse, * Snaps y to the next baseline. Kind of obtuse,
* but it wasn't randomly derived, anyways * but it wasn't randomly derived, anyways
* you can test it for 13, 30, 47, etc. * you can test it for 13, 30, 47, etc.
*/ */
int o = 0; int o = 0;
for (Segment segment: text) for (Segment segment: text)
{ {
if (segment.y == y && segment.x > x) break; if (segment.y == y && segment.x > x) break;
if (segment.y > y) break; if (segment.y > y) break;
++o; ++o;
} }
return o; return o;
} }
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;
copySelection(); copySelection();
} }
public void public void
keyReleased(KeyEvent eK) { } keyReleased(KeyEvent eK) { }
public void public void
keyTyped(KeyEvent eK) { } keyTyped(KeyEvent eK) { }
public void public void
mouseClicked(MouseEvent eM) { } mouseClicked(MouseEvent eM) { }
public void public void
mouseReleased(MouseEvent eM) { } mouseReleased(MouseEvent eM) { }
public void public void
mouseEntered(MouseEvent eM) { } mouseEntered(MouseEvent eM) { }
public void public void
mouseExited(MouseEvent eM) { } mouseExited(MouseEvent eM) { }
public void public void
mouseMoved(MouseEvent eM) { } mouseMoved(MouseEvent eM) { }
// - -%- - // - -%- -
public static List<Segment> public static List<Segment>
layout(List<Segment> text, FontMetrics fm, int width) layout(List<Segment> text, FontMetrics fm, int width)
{ {
List<Segment> copy = new LinkedList<>(); List<Segment> copy = new LinkedList<>();
for (Segment segment: text) copy.add(segment.clone()); for (Segment segment: text) copy.add(segment.clone());
text = copy; text = copy;
ListIterator<Segment> cursor = text.listIterator(); ListIterator<Segment> cursor = text.listIterator();
int x = 0, y = fm.getAscent(); int x = 0, y = fm.getAscent();
int dy = fm.getAscent() + fm.getDescent(); int dy = fm.getAscent() + fm.getDescent();
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();
int oh = curr.image.getIconHeight(); int oh = curr.image.getIconHeight();
int nh = fm.getAscent() + fm.getDescent(); int nh = fm.getAscent() + fm.getDescent();
dx = nh * ow / oh; dx = nh * ow / oh;
} }
else if (curr.text != null) { else if (curr.text != null) {
dx = fm.stringWidth(curr.text); dx = fm.stringWidth(curr.text);
} }
else if (curr.link != null) { else if (curr.link != null) {
curr.text = curr.link; curr.text = curr.link;
dx = fm.stringWidth(curr.link); dx = fm.stringWidth(curr.link);
} }
else { else {
assert false; assert false;
dx = 0; dx = 0;
} }
boolean fits = x + dx < width; boolean fits = x + dx < width;
if (fits || curr.spacer) if (fits || curr.spacer)
{ {
curr.x = x; curr.x = x;
curr.y = y; curr.y = y;
x += dx; x += dx;
if (curr.spacer && curr.text.equals("\n")) { if (curr.spacer && curr.text.equals("\n")) {
y += dy; y += dy;
x = 0; x = 0;
} }
continue; continue;
} }
boolean tooLong = dx > width; boolean tooLong = dx > width;
boolean canFitChar = width >= fm.getMaxAdvance(); boolean canFitChar = width >= fm.getMaxAdvance();
boolean splittable = curr.image == null; boolean splittable = curr.image == null;
/* /*
* A bit of redundancy in my conditions, but the point is * A bit of redundancy in my conditions, but the point is
* to exactly express the triggers in my mental model. * to exactly express the triggers in my mental model.
* The conditions should read more like English. * The conditions should read more like English.
*/ */
if (!tooLong || (tooLong && !splittable)) if (!tooLong || (tooLong && !splittable))
{ {
curr.x = 0; curr.x = 0;
curr.y = y += dy; curr.y = y += dy;
x = dx; x = dx;
continue; continue;
} }
assert tooLong && splittable; assert tooLong && splittable;
String s = curr.text; String s = curr.text;
int splitOffset; int splitOffset;
for (splitOffset = 0; splitOffset < s.length(); ++splitOffset) for (splitOffset = 0; splitOffset < s.length(); ++splitOffset)
{ {
String substring = s.substring(0, splitOffset + 1); String substring = s.substring(0, splitOffset + 1);
if (fm.stringWidth(substring) > width) break; if (fm.stringWidth(substring) > width) break;
} }
if (splitOffset == 0) splitOffset = 1; if (splitOffset == 0) splitOffset = 1;
/* /*
* I force a split even if our width supports no characters. * I force a split even if our width supports no characters.
* Because if I don't split, the only alternatives to infinitely * Because if I don't split, the only alternatives to infinitely
* looping downwards is to emplace this segment or ignore it. * looping downwards is to emplace this segment or ignore it.
*/ */
Segment fitted = new Segment(); Segment fitted = new Segment();
fitted.text = s.substring(0, splitOffset); fitted.text = s.substring(0, splitOffset);
fitted.link = curr.link; fitted.link = curr.link;
fitted.x = x; fitted.x = x;
fitted.y = y; fitted.y = y;
cursor.add(fitted); cursor.add(fitted);
curr.text = s.substring(splitOffset); curr.text = s.substring(splitOffset);
y += dy; y += dy;
x = 0; x = 0;
cursor.add(curr); cursor.previous(); cursor.add(curr); cursor.previous();
/* /*
* I had to use a stack and return a new list because, * I had to use a stack and return a new list because,
* splitting can turn a long segment into several spread * splitting can turn a long segment into several spread
* over different lines. Here curr becomes the "after-split" * over different lines. Here curr becomes the "after-split"
* and I push it back to the stack. * and I push it back to the stack.
* *
* If #layout wasn't a separate method, but rather only for * If #layout wasn't a separate method, but rather only for
* graphical painting. Then I don't need a stack nor return * graphical painting. Then I don't need a stack nor return
* a new list. I iterate over the given one, filling the * a new list. I iterate over the given one, filling the
* nodes' geometric information, if a split occurs I save the * nodes' geometric information, if a split occurs I save the
* "after-split" in a variable, which in the next iteration * "after-split" in a variable, which in the next iteration
* I use as curr instead of list.next(). The caller doesn't * I use as curr instead of list.next(). The caller doesn't
* need to know the geometry of these intermediate segments. * need to know the geometry of these intermediate segments.
*/ */
continue; continue;
} }
return text; return text;
} }
// ---%-@-%--- // ---%-@-%---
public static class public static class
Segment { Segment {
public ImageIcon public ImageIcon
image; image;
public String public String
link; link;
public String public String
text; text;
public boolean public boolean
spacer; spacer;
public int public int
x, y; x, y;
// -=%=- // -=%=-
public String public String
toString() toString()
{ {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
b.append(getClass().getName() + "["); b.append(getClass().getName() + "[");
b.append("image=" + image); b.append("image=" + image);
b.append(",link=" + link); b.append(",link=" + link);
b.append(",text=" + text); b.append(",text=" + text);
b.append(",x=" + x); b.append(",x=" + x);
b.append(",y=" + y); b.append(",y=" + y);
b.append("]"); b.append("]");
return b.toString(); return b.toString();
} }
public Segment public Segment
clone() clone()
{ {
Segment segment = new Segment(); Segment segment = new Segment();
segment.image = this.image; segment.image = this.image;
segment.link = this.link; segment.link = this.link;
segment.text = this.text; segment.text = this.text;
segment.spacer = this.spacer; segment.spacer = this.spacer;
segment.x = this.x; segment.x = this.x;
segment.y = this.y; segment.y = this.y;
return segment; return segment;
} }
} }
public static class public static class
Builder { Builder {
private List<Segment> private List<Segment>
returnee; returnee;
// -=%=- // -=%=-
public public
Builder() { returnee = new LinkedList<>(); } Builder() { returnee = new LinkedList<>(); }
public Builder public Builder
image(ImageIcon image, String text) image(ImageIcon image, String text)
{ {
Segment segment = new Segment(); Segment segment = new Segment();
segment.image = image; segment.image = image;
segment.text = text; segment.text = text;
returnee.add(segment); returnee.add(segment);
return this; return this;
} }
public Builder public Builder
link(String link, String text) link(String link, String text)
{ {
Segment segment = new Segment(); Segment segment = new Segment();
segment.link = link; segment.link = link;
segment.text = text; segment.text = text;
returnee.add(segment); returnee.add(segment);
return this; return this;
} }
public Builder public Builder
text(String text) text(String text)
{ {
Segment segment = new Segment(); Segment segment = new Segment();
segment.text = text; segment.text = text;
returnee.add(segment); returnee.add(segment);
return this; return this;
} }
public Builder public Builder
spacer(String text) spacer(String text)
{ {
Segment segment = new Segment(); Segment segment = new Segment();
segment.text = text; segment.text = text;
segment.spacer = true; segment.spacer = true;
returnee.add(segment); returnee.add(segment);
return this; return this;
} }
public List<Segment> public List<Segment>
finish() { return returnee; } finish() { return returnee; }
} }
// ---%-@-%--- // ---%-@-%---
RichTextPane() RichTextPane()
{ {
text = new LinkedList<>(); text = new LinkedList<>();
addMouseListener(this); addMouseListener(this);
addMouseMotionListener(this); addMouseMotionListener(this);
addKeyListener(this); addKeyListener(this);
} }
} }

View File

@ -37,463 +37,463 @@ class
RichTextPane2 extends JComponent RichTextPane2 extends JComponent
implements ComponentListener { implements ComponentListener {
private AttributedString private AttributedString
text; text;
// ---%-@-%--- // ---%-@-%---
public void public void
setText(Tree<String> html, Tree<String> emojiMap) setText(Tree<String> html, Tree<String> emojiMap)
{ {
Tree<String> commands = turnIntoCommands(html); Tree<String> commands = turnIntoCommands(html);
class AStrSegment { class AStrSegment {
String text; String text;
int offset; int offset;
Object[] values = new Object[Attribute.COUNT]; Object[] values = new Object[Attribute.COUNT];
/* /*
{ {
values[3] = (Boolean)true; values[3] = (Boolean)true;
values[4] = (Integer)0; values[4] = (Integer)0;
values[5] = (Boolean)true; values[5] = (Boolean)true;
values[6] = (Boolean)false; values[6] = (Boolean)false;
} }
*/ */
} }
List<AStrSegment> segments = new ArrayList<>(); List<AStrSegment> segments = new ArrayList<>();
int offset = 0; int offset = 0;
for (Tree<String> command: commands) for (Tree<String> command: commands)
{ {
if (command.key.equals("text")) if (command.key.equals("text"))
{ {
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
Boolean cibl = null; Boolean cibl = null;
Boolean cwhi = null; Boolean cwhi = null;
for (char c: command.value.toCharArray()) for (char c: command.value.toCharArray())
{ {
Boolean ibl = isBasicLatin(c); Boolean ibl = isBasicLatin(c);
Boolean whi = Character.isWhitespace(c); Boolean whi = Character.isWhitespace(c);
if (!ibl.equals(cibl) || !whi.equals(cwhi)) if (!ibl.equals(cibl) || !whi.equals(cwhi))
{ {
if (b.length() > 0) if (b.length() > 0)
{ {
assert cibl != null && cwhi != null; assert cibl != null && cwhi != null;
AStrSegment s = new AStrSegment(); AStrSegment s = new AStrSegment();
s.offset = offset; s.offset = offset;
s.text = b.toString(); s.text = b.toString();
s.values[3] = cibl; s.values[3] = cibl;
s.values[6] = cwhi; s.values[6] = cwhi;
segments.add(s); segments.add(s);
offset += s.text.length(); offset += s.text.length();
b.delete(0, b.length()); b.delete(0, b.length());
} }
cibl = ibl; cibl = ibl;
cwhi = whi; cwhi = whi;
} }
b.append(c); b.append(c);
} }
if (b.length() > 0) if (b.length() > 0)
{ {
AStrSegment s = new AStrSegment(); AStrSegment s = new AStrSegment();
s.offset = offset; s.offset = offset;
s.text = b.toString(); s.text = b.toString();
s.values[3] = cibl; s.values[3] = cibl;
s.values[6] = cwhi; s.values[6] = cwhi;
segments.add(s); segments.add(s);
offset += s.text.length(); offset += s.text.length();
} }
} }
else if (command.key.equals("emoji")) else if (command.key.equals("emoji"))
{ {
AStrSegment s = new AStrSegment(); AStrSegment s = new AStrSegment();
s.offset = offset; s.offset = offset;
s.values[3] = true; s.values[3] = true;
s.values[6] = false; s.values[6] = false;
String shortcode = command.value; String shortcode = command.value;
String url = null; String url = null;
Tree<String> m = emojiMap.get(shortcode); Tree<String> m = emojiMap.get(shortcode);
if (m != null) url = m.value; if (m != null) url = m.value;
Image img = ImageApi.remote(url); Image img = ImageApi.remote(url);
if (img != null) if (img != null)
{ {
s.text = " "; s.text = " ";
s.values[0] = img; s.values[0] = img;
s.values[1] = shortcode; s.values[1] = shortcode;
segments.add(s); segments.add(s);
offset += 1; offset += 1;
} }
else else
{ {
s.text = shortcode; s.text = shortcode;
s.values[0] = null; s.values[0] = null;
s.values[1] = null; s.values[1] = null;
segments.add(s); segments.add(s);
offset += shortcode.length(); offset += shortcode.length();
} }
} }
else if (command.key.equals("link")) else if (command.key.equals("link"))
{ {
AStrSegment s = new AStrSegment(); AStrSegment s = new AStrSegment();
s.offset = offset; s.offset = offset;
s.text = command.value; s.text = command.value;
s.values[2] = command.get("url").value; s.values[2] = command.get("url").value;
s.values[3] = true; s.values[3] = true;
s.values[6] = false; s.values[6] = false;
/* /*
* Technically we're supposed to treat * Technically we're supposed to treat
* the anchor text like a text node. * the anchor text like a text node.
* As in, it could be non-Basic-Latin.. * As in, it could be non-Basic-Latin..
* I'll be Mastodon-specific again, and * I'll be Mastodon-specific again, and
* assume it's a URL or some @ string. * assume it's a URL or some @ string.
*/ */
} }
} }
AttributedString astr; AttributedString astr;
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
for (AStrSegment segment: segments) for (AStrSegment segment: segments)
{ {
b.append(segment.text); b.append(segment.text);
} }
astr = new AttributedString(b.toString()); astr = new AttributedString(b.toString());
for (AStrSegment segment: segments) for (AStrSegment segment: segments)
{ {
Object[] v = segment.values; Object[] v = segment.values;
astr.addAttribute( astr.addAttribute(
Attribute.IMAGE, segment.values[0], Attribute.IMAGE, segment.values[0],
segment.offset, segment.offset,
segment.offset + segment.text.length() segment.offset + segment.text.length()
); );
astr.addAttribute( astr.addAttribute(
Attribute.ALT, segment.values[1], Attribute.ALT, segment.values[1],
segment.offset, segment.offset,
segment.offset + segment.text.length() segment.offset + segment.text.length()
); );
astr.addAttribute( astr.addAttribute(
Attribute.LINK, segment.values[2], Attribute.LINK, segment.values[2],
segment.offset, segment.offset,
segment.offset + segment.text.length() segment.offset + segment.text.length()
); );
astr.addAttribute( astr.addAttribute(
Attribute.BASICLATIN, segment.values[3], Attribute.BASICLATIN, segment.values[3],
segment.offset, segment.offset,
segment.offset + segment.text.length() segment.offset + segment.text.length()
); );
astr.addAttribute( astr.addAttribute(
Attribute.Y, segment.values[4], Attribute.Y, segment.values[4],
segment.offset, segment.offset,
segment.offset + segment.text.length() segment.offset + segment.text.length()
); );
astr.addAttribute( astr.addAttribute(
Attribute.OFFSCREEN, segment.values[5], Attribute.OFFSCREEN, segment.values[5],
segment.offset, segment.offset,
segment.offset + segment.text.length() segment.offset + segment.text.length()
); );
astr.addAttribute( astr.addAttribute(
Attribute.WHITESPACE, segment.values[6], Attribute.WHITESPACE, segment.values[6],
segment.offset, segment.offset,
segment.offset + segment.text.length() segment.offset + segment.text.length()
); );
} }
this.text = astr; this.text = astr;
componentResized(null); componentResized(null);
} }
// - -%- - // - -%- -
public void public void
componentResized(ComponentEvent eC) componentResized(ComponentEvent eC)
{ {
int w = getWidth(), h = getHeight(); int w = getWidth(), h = getHeight();
// We're going to evaluate the // We're going to evaluate the
// line and off-screen attributes. // line and off-screen attributes.
FontMetrics fm = getFontMetrics(getFont()); FontMetrics fm = getFontMetrics(getFont());
Graphics g = getGraphics(); Graphics g = getGraphics();
int x = 0, y = fm.getAscent(); int x = 0, y = fm.getAscent();
AttributedCharacterIterator it; AttributedCharacterIterator it;
it = text.getIterator(); it = text.getIterator();
while (it.getIndex() < it.getEndIndex()) while (it.getIndex() < it.getEndIndex())
{ {
int start = it.getIndex(); int start = it.getIndex();
int end = it.getRunLimit(); int end = it.getRunLimit();
Image img = (Image) Image img = (Image)
it.getAttribute(Attribute.IMAGE); it.getAttribute(Attribute.IMAGE);
Boolean ibl = (Boolean) Boolean ibl = (Boolean)
it.getAttribute(Attribute.BASICLATIN); it.getAttribute(Attribute.BASICLATIN);
Boolean whi = (Boolean) Boolean whi = (Boolean)
it.getAttribute(Attribute.WHITESPACE); it.getAttribute(Attribute.WHITESPACE);
assert ibl != null; assert ibl != null;
assert whi != null; assert whi != null;
if (img != null) if (img != null)
{ {
int ow = img.getWidth(this); int ow = img.getWidth(this);
int oh = img.getHeight(this); int oh = img.getHeight(this);
int nh = fm.getAscent() + fm.getDescent(); int nh = fm.getAscent() + fm.getDescent();
int nw = ow * nh/oh; int nw = ow * nh/oh;
if (x + nw > w) if (x + nw > w)
{ {
y += fm.getAscent() + fm.getDescent(); y += fm.getAscent() + fm.getDescent();
x = nw; x = nw;
} }
text.addAttribute( text.addAttribute(
Attribute.Y, (Integer)y, Attribute.Y, (Integer)y,
start, end start, end
); );
text.addAttribute( text.addAttribute(
Attribute.OFFSCREEN, (Boolean)(y > h), Attribute.OFFSCREEN, (Boolean)(y > h),
start, end start, end
); );
it.setIndex(end); it.setIndex(end);
} }
else else
{ {
int p, xOff = 0; int p, xOff = 0;
for (p = end; p > start; --p) for (p = end; p > start; --p)
{ {
Rectangle2D r; Rectangle2D r;
r = fm.getStringBounds(it, start, p, g); r = fm.getStringBounds(it, start, p, g);
xOff = (int)r.getWidth(); xOff = (int)r.getWidth();
if (x + xOff < w) break; if (x + xOff < w) break;
} }
if (p == end || whi) if (p == end || whi)
{ {
x += xOff; x += xOff;
text.addAttribute( text.addAttribute(
Attribute.Y, (Integer)y, Attribute.Y, (Integer)y,
start, end start, end
); );
text.addAttribute( text.addAttribute(
Attribute.OFFSCREEN, (Boolean)(y > h), Attribute.OFFSCREEN, (Boolean)(y > h),
start, end start, end
); );
it.setIndex(end); it.setIndex(end);
} }
else if (p <= start) else if (p <= start)
{ {
y += fm.getAscent() + fm.getDescent(); y += fm.getAscent() + fm.getDescent();
x = xOff; x = xOff;
text.addAttribute( text.addAttribute(
Attribute.Y, (Integer)y, Attribute.Y, (Integer)y,
start, end start, end
); );
text.addAttribute( text.addAttribute(
Attribute.OFFSCREEN, (Boolean)(y > h), Attribute.OFFSCREEN, (Boolean)(y > h),
start, end start, end
); );
it.setIndex(end); it.setIndex(end);
} }
else else
{ {
text.addAttribute( text.addAttribute(
Attribute.Y, (Integer)y, Attribute.Y, (Integer)y,
start, p start, p
); );
text.addAttribute( text.addAttribute(
Attribute.OFFSCREEN, (Boolean)(y > h), Attribute.OFFSCREEN, (Boolean)(y > h),
start, p start, p
); );
y += fm.getAscent() + fm.getDescent(); y += fm.getAscent() + fm.getDescent();
x = 0; x = 0;
it.setIndex(p); it.setIndex(p);
} }
} }
} }
text.addAttribute(TextAttribute.FONT, getFont()); text.addAttribute(TextAttribute.FONT, getFont());
repaint(); repaint();
} }
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
int w = getWidth(), h = getHeight(); int w = getWidth(), h = getHeight();
g.clearRect(0, 0, w, h); g.clearRect(0, 0, w, h);
FontMetrics fm = g.getFontMetrics(); FontMetrics fm = g.getFontMetrics();
AttributedCharacterIterator it; AttributedCharacterIterator it;
it = text.getIterator(); it = text.getIterator();
((java.awt.Graphics2D)g).setRenderingHint( ((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON java.awt.RenderingHints.VALUE_ANTIALIAS_ON
); );
int x = 0, y = fm.getAscent(); int x = 0, y = fm.getAscent();
while (it.getIndex() < it.getEndIndex()) while (it.getIndex() < it.getEndIndex())
{ {
int start = it.getIndex(); int start = it.getIndex();
int end = it.getRunLimit(); int end = it.getRunLimit();
Image img = (Image) Image img = (Image)
it.getAttribute(Attribute.IMAGE); it.getAttribute(Attribute.IMAGE);
Boolean ibl = (Boolean) Boolean ibl = (Boolean)
it.getAttribute(Attribute.BASICLATIN); it.getAttribute(Attribute.BASICLATIN);
Integer ny = (Integer) Integer ny = (Integer)
it.getAttribute(Attribute.Y); it.getAttribute(Attribute.Y);
if (ny > y) if (ny > y)
{ {
y = ny; y = ny;
x = 0; x = 0;
} }
if (img != null) if (img != null)
{ {
int ow = img.getWidth(this); int ow = img.getWidth(this);
int oh = img.getHeight(this); int oh = img.getHeight(this);
int nh = fm.getAscent() + fm.getDescent(); int nh = fm.getAscent() + fm.getDescent();
int nw = ow * nh/oh; int nw = ow * nh/oh;
int iy = y + fm.getDescent() - nh; int iy = y + fm.getDescent() - nh;
g.drawImage(img, x, iy, nw, nh, this); g.drawImage(img, x, iy, nw, nh, this);
x += nw; x += nw;
} }
else else
{ {
Rectangle2D r; Rectangle2D r;
r = fm.getStringBounds(it, start, end, g); r = fm.getStringBounds(it, start, end, g);
AttributedCharacterIterator sit; AttributedCharacterIterator sit;
sit = text.getIterator(null, start, end); sit = text.getIterator(null, start, end);
g.drawString(sit, x, y); g.drawString(sit, x, y);
x += (int)r.getWidth(); x += (int)r.getWidth();
} }
it.setIndex(end); it.setIndex(end);
} }
} }
public void public void
componentMoved(ComponentEvent eC) { } componentMoved(ComponentEvent eC) { }
public void public void
componentShown(ComponentEvent eC) { } componentShown(ComponentEvent eC) { }
public void public void
componentHidden(ComponentEvent eC) { } componentHidden(ComponentEvent eC) { }
// - -%- - // - -%- -
private static Boolean private static Boolean
isBasicLatin(char c) isBasicLatin(char c)
{ {
return true; return true;
} }
private static String private static String
toText(Tree<String> node) toText(Tree<String> node)
{ {
Tree<String> children = node.get("children"); Tree<String> children = node.get("children");
if (children == null) if (children == null)
{ {
boolean text = node.key.equals("text"); boolean text = node.key.equals("text");
boolean emoji = node.key.equals("emoji"); boolean emoji = node.key.equals("emoji");
assert text || emoji; assert text || emoji;
return node.value; return node.value;
} }
StringBuilder b = new StringBuilder(); StringBuilder b = new StringBuilder();
for (Tree<String> child: children) for (Tree<String> child: children)
{ {
b.append(toText(child)); b.append(toText(child));
} }
return b.toString(); return b.toString();
} }
private static Tree<String> private static Tree<String>
turnIntoCommands(Tree<String> tag) turnIntoCommands(Tree<String> tag)
{ {
assert tag.key.equals("tag"); assert tag.key.equals("tag");
Tree<String> returnee = new Tree<String>(); Tree<String> returnee = new Tree<String>();
String tagName = tag.get(0).key; String tagName = tag.get(0).key;
Tree<String> children = tag.get("children"); Tree<String> children = tag.get("children");
if (tagName.equals("a")) if (tagName.equals("a"))
{ {
String url = tag.get("href").value; String url = tag.get("href").value;
Tree<String> addee = new Tree<>(); Tree<String> addee = new Tree<>();
addee.key = "link"; addee.key = "link";
addee.value = toText(tag); addee.value = toText(tag);
addee.add(new Tree<>("url", url)); addee.add(new Tree<>("url", url));
returnee.add(addee); returnee.add(addee);
} }
else if (tagName.equals("span")) else if (tagName.equals("span"))
{ {
Tree<String> addee = new Tree<>(); Tree<String> addee = new Tree<>();
addee.key = "text"; addee.key = "text";
addee.value = toText(tag); addee.value = toText(tag);
returnee.add(addee); returnee.add(addee);
} }
else if (tagName.equals("br")) else if (tagName.equals("br"))
{ {
returnee.add(new Tree<>("text", "\n")); returnee.add(new Tree<>("text", "\n"));
} }
else else
{ {
for (Tree<String> child: children) for (Tree<String> child: children)
{ {
if (!child.key.equals("tag")) if (!child.key.equals("tag"))
{ {
returnee.add(child); returnee.add(child);
continue; continue;
} }
child = turnIntoCommands(child); child = turnIntoCommands(child);
for (Tree<String> command: child) for (Tree<String> command: child)
{ {
returnee.add(command); returnee.add(command);
} }
} }
if (tagName.equals("p")) if (tagName.equals("p"))
{ {
returnee.add(new Tree<>("text", "\n")); returnee.add(new Tree<>("text", "\n"));
returnee.add(new Tree<>("text", "\n")); returnee.add(new Tree<>("text", "\n"));
} }
} }
return returnee; return returnee;
} }
// ---%-@-%--- // ---%-@-%---
public static class public static class
Attribute extends AttributedCharacterIterator.Attribute { Attribute extends AttributedCharacterIterator.Attribute {
public static final Attribute public static final Attribute
IMAGE = new Attribute("IMAGE"), IMAGE = new Attribute("IMAGE"),
ALT = new Attribute("ALT"), ALT = new Attribute("ALT"),
LINK = new Attribute("LINK"), LINK = new Attribute("LINK"),
BASICLATIN = new Attribute("BASICLATIN"), BASICLATIN = new Attribute("BASICLATIN"),
Y = new Attribute("Y"), Y = new Attribute("Y"),
OFFSCREEN = new Attribute("OFFSCREEN"), OFFSCREEN = new Attribute("OFFSCREEN"),
WHITESPACE = new Attribute("WHITESPACE"); WHITESPACE = new Attribute("WHITESPACE");
public static final int public static final int
COUNT = 7; COUNT = 7;
// -=%=- // -=%=-
private private
Attribute(String name) { super(name); } Attribute(String name) { super(name); }
} }
// ---%-@-%--- // ---%-@-%---
RichTextPane2() RichTextPane2()
{ {
this.addComponentListener(this); this.addComponentListener(this);
text = new AttributedString(""); text = new AttributedString("");
} }
} }

View File

@ -45,232 +45,232 @@ implements
ComponentListener, ComponentListener,
MouseListener, MouseMotionListener, KeyListener { MouseListener, MouseMotionListener, KeyListener {
private Tree<String> private Tree<String>
html; html;
private Map<String, Image> private Map<String, Image>
emojis; emojis;
private Map<Tree<String>, Position> private Map<Tree<String>, Position>
layout; layout;
private Tree<String> private Tree<String>
layoutEnd, selStart, selEnd; layoutEnd, selStart, selEnd;
private int private int
startingLine, lastLine; startingLine, lastLine;
// ---%-@-%--- // ---%-@-%---
public void public void
setText(Tree<String> html) setText(Tree<String> html)
{ {
assert html != null; assert html != null;
this.html = html; this.html = html;
if (!isValid()) return; if (!isValid()) return;
assert html.key != null; assert html.key != null;
assert html.key.equals("tag"); assert html.key.equals("tag");
assert html.get("children") != null; assert html.get("children") != null;
FontMetrics fm = getFontMetrics(getFont()); FontMetrics fm = getFontMetrics(getFont());
Position cursor = new Position(0, 1); Position cursor = new Position(0, 1);
// Manually negate if first element is a break. // Manually negate if first element is a break.
Tree<String> children = html.get("children"); Tree<String> children = html.get("children");
if (children.size() > 0) if (children.size() > 0)
{ {
Tree<String> first = children.get(0); Tree<String> first = children.get(0);
if (first.key.equals("tag")) if (first.key.equals("tag"))
{ {
String tagName = first.get(0).key; String tagName = first.get(0).key;
if (tagName.equals("br")) cursor.line -= 1; if (tagName.equals("br")) cursor.line -= 1;
if (tagName.equals("p")) cursor.line -= 2; if (tagName.equals("p")) cursor.line -= 2;
} }
} }
selStart = selEnd = null; selStart = selEnd = null;
layout.clear(); layout.clear();
startingLine = 1; startingLine = 1;
layout(html, fm, cursor); layout(html, fm, cursor);
layout.put(layoutEnd, cursor.clone()); layout.put(layoutEnd, cursor.clone());
lastLine = cursor.line; lastLine = cursor.line;
repaint(); repaint();
int iy = fm.getAscent(); int iy = fm.getAscent();
int lh = fm.getAscent() + fm.getDescent(); int lh = fm.getAscent() + fm.getDescent();
int h = snap2(cursor.line, iy, lh); int h = snap2(cursor.line, iy, lh);
h += fm.getDescent(); h += fm.getDescent();
setPreferredSize(new Dimension(1, h)); setPreferredSize(new Dimension(1, h));
} }
public void public void
setEmojis(Map<String, Image> emojis) setEmojis(Map<String, Image> emojis)
{ {
assert emojis != null; assert emojis != null;
this.emojis = emojis; this.emojis = emojis;
setText(html); setText(html);
} }
public void public void
previousPage() previousPage()
{ {
int advance = getHeightInLines(); int advance = getHeightInLines();
if (startingLine < advance) startingLine = 1; if (startingLine < advance) startingLine = 1;
else startingLine -= advance; else startingLine -= advance;
repaint(); repaint();
} }
public void public void
nextPage() nextPage()
{ {
int advance = getHeightInLines(); int advance = getHeightInLines();
if (lastLine - startingLine < advance) return; if (lastLine - startingLine < advance) return;
else startingLine += advance; else startingLine += advance;
repaint(); repaint();
} }
// - -%- - // - -%- -
private void private void
layout(Tree<String> node, FontMetrics fm, Position cursor) layout(Tree<String> node, FontMetrics fm, Position cursor)
{ {
assert cursor != null; assert cursor != null;
if (node.key.equals("space")) if (node.key.equals("space"))
{ {
int w = fm.stringWidth(node.value); int w = fm.stringWidth(node.value);
if (cursor.x + w < getWidth()) if (cursor.x + w < getWidth())
{ {
layout.put(node, cursor.clone()); layout.put(node, cursor.clone());
cursor.x += w; cursor.x += w;
} }
else else
{ {
layout.put(node, cursor.clone()); layout.put(node, cursor.clone());
++cursor.line; ++cursor.line;
cursor.x = 0; cursor.x = 0;
} }
} }
else if (node.key.equals("text")) else if (node.key.equals("text"))
{ {
int w = fm.stringWidth(node.value); int w = fm.stringWidth(node.value);
if (cursor.x + w < getWidth()) if (cursor.x + w < getWidth())
{ {
layout.put(node, cursor.clone()); layout.put(node, cursor.clone());
cursor.x += w; cursor.x += w;
} }
else if (w < getWidth()) else if (w < getWidth())
{ {
++cursor.line; ++cursor.line;
cursor.x = 0; cursor.x = 0;
layout.put(node, cursor.clone()); layout.put(node, cursor.clone());
cursor.x += w; cursor.x += w;
} }
else else
{ {
StringBuilder rem = new StringBuilder(); StringBuilder rem = new StringBuilder();
rem.append(node.value); rem.append(node.value);
int mw = getWidth(); int mw = getWidth();
int aw = mw - cursor.x; int aw = mw - cursor.x;
w = fm.charWidth(node.value.charAt(0)); w = fm.charWidth(node.value.charAt(0));
if (w >= aw) if (w >= aw)
{ {
++cursor.line; ++cursor.line;
cursor.x = 0; cursor.x = 0;
} }
while (rem.length() > 0) while (rem.length() > 0)
{ {
int l = 2; int l = 2;
for (; l <= rem.length(); ++l) for (; l <= rem.length(); ++l)
{ {
String substr = rem.substring(0, l); String substr = rem.substring(0, l);
w = fm.stringWidth(substr); w = fm.stringWidth(substr);
if (w >= aw) break; if (w >= aw) break;
} }
String substr = rem.substring(0, --l); String substr = rem.substring(0, --l);
w = fm.stringWidth(substr); w = fm.stringWidth(substr);
Tree<String> temp = new Tree<>(); Tree<String> temp = new Tree<>();
temp.key = node.key; temp.key = node.key;
temp.value = substr; temp.value = substr;
layout.put(temp, cursor.clone()); layout.put(temp, cursor.clone());
rem.delete(0, l); rem.delete(0, l);
boolean more = rem.length() != 0; boolean more = rem.length() != 0;
if (more) ++cursor.line; if (more) ++cursor.line;
cursor.x = more ? 0 : w; cursor.x = more ? 0 : w;
aw = mw; aw = mw;
} }
} }
} }
else if (node.key.equals("emoji")) else if (node.key.equals("emoji"))
{ {
Image image = emojis.get(node.value); Image image = emojis.get(node.value);
int w; if (image != null) int w; if (image != null)
{ {
int ow = image.getWidth(this); int ow = image.getWidth(this);
int oh = image.getHeight(this); int oh = image.getHeight(this);
int h = fm.getAscent() + fm.getDescent(); int h = fm.getAscent() + fm.getDescent();
w = ow * h/oh; w = ow * h/oh;
} }
else else
{ {
w = fm.stringWidth(node.value); w = fm.stringWidth(node.value);
} }
if (cursor.x + w < getWidth()) if (cursor.x + w < getWidth())
{ {
layout.put(node, cursor.clone()); layout.put(node, cursor.clone());
cursor.x += w; cursor.x += w;
} }
else else
{ {
++cursor.line; ++cursor.line;
cursor.x = 0; cursor.x = 0;
layout.put(node, cursor.clone()); layout.put(node, cursor.clone());
cursor.x += w; cursor.x += w;
} }
} }
else if (node.key.equals("tag")) else if (node.key.equals("tag"))
{ {
String tagName = node.get(0).key; String tagName = node.get(0).key;
Tree<String> children = node.get("children"); Tree<String> children = node.get("children");
// We won't place tag nodes on the layout. // We won't place tag nodes on the layout.
if (tagName.equals("br")) if (tagName.equals("br"))
{ {
++cursor.line; ++cursor.line;
cursor.x = 0; cursor.x = 0;
} }
else if (tagName.equals("p")) else if (tagName.equals("p"))
{ {
//cursor.line += 3/2; //cursor.line += 3/2;
cursor.line += 2; cursor.line += 2;
// We don't have vertical cursor movement // We don't have vertical cursor movement
// other than the line. Maybe fix in the // other than the line. Maybe fix in the
// future..? // future..?
cursor.x = 0; cursor.x = 0;
} }
for (Tree<String> child: children) for (Tree<String> child: children)
{ {
// Shallow copy this child node, // Shallow copy this child node,
Tree<String> aug = new Tree<>(); Tree<String> aug = new Tree<>();
aug.key = child.key; aug.key = child.key;
aug.value = child.value; aug.value = child.value;
for (Tree<String> gc: child) aug.add(gc); for (Tree<String> gc: child) aug.add(gc);
// Append all of our attributes. We'd like // Append all of our attributes. We'd like
// those like href to end up at the text // those like href to end up at the text
// nodes. This might collide with our // nodes. This might collide with our
// child node's attributes, for now I'll // child node's attributes, for now I'll
// assume that's not an issue. // assume that's not an issue.
for (int o = 1; o < node.size(); ++o) for (int o = 1; o < node.size(); ++o)
{ {
Tree<String> attr = node.get(o); Tree<String> attr = node.get(o);
@ -279,29 +279,29 @@ implements
} }
layout(aug, fm, cursor); layout(aug, fm, cursor);
} }
} }
else assert false; else assert false;
} }
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
final Color LINK_COLOUR = Color.BLUE; final Color LINK_COLOUR = Color.BLUE;
final Color PLAIN_COLOUR = getForeground(); final Color PLAIN_COLOUR = getForeground();
final Color SEL_COLOUR = new Color(0, 0, 0, 25); final Color SEL_COLOUR = new Color(0, 0, 0, 25);
g.setFont(getFont()); g.setFont(getFont());
FontMetrics fm = g.getFontMetrics(); FontMetrics fm = g.getFontMetrics();
int iy = fm.getAscent(); int iy = fm.getAscent();
int lh = fm.getAscent() + fm.getDescent(); int lh = fm.getAscent() + fm.getDescent();
int asc = fm.getAscent(); int asc = fm.getAscent();
int w = getWidth(), h = getHeight(); int w = getWidth(), h = getHeight();
if (isOpaque()) g.clearRect(0, 0, w, h); if (isOpaque()) g.clearRect(0, 0, w, h);
if (selEnd != null) if (selEnd != null)
{ {
Position ssp = layout.get(selStart); Position ssp = layout.get(selStart);
assert ssp != null; assert ssp != null;
Position sep = layout.get(selEnd); Position sep = layout.get(selEnd);
@ -318,81 +318,81 @@ implements
sep = ssp; sep = ssp;
} }
int ls = 1 + ssp.line - startingLine; int ls = 1 + ssp.line - startingLine;
int le = 1 + sep.line - startingLine; int le = 1 + sep.line - startingLine;
int ys = snap2(ls, iy, lh) - asc; int ys = snap2(ls, iy, lh) - asc;
int ye = snap2(le, iy, lh) - asc; int ye = snap2(le, iy, lh) - asc;
g.setColor(SEL_COLOUR); g.setColor(SEL_COLOUR);
if (ssp.line == sep.line) if (ssp.line == sep.line)
{ {
g.fillRect(ssp.x, ys, sep.x - ssp.x, lh); g.fillRect(ssp.x, ys, sep.x - ssp.x, lh);
} }
else else
{ {
g.fillRect(ssp.x, ys, w - ssp.x, lh); g.fillRect(ssp.x, ys, w - ssp.x, lh);
for (int l = ls + 1; l < le; ++l) for (int l = ls + 1; l < le; ++l)
{ {
int y = snap2(l, iy, lh) - asc; int y = snap2(l, iy, lh) - asc;
g.fillRect(0, y, w, lh); g.fillRect(0, y, w, lh);
} }
g.fillRect(0, ye, sep.x, lh); g.fillRect(0, ye, sep.x, lh);
} }
} }
((java.awt.Graphics2D)g).setRenderingHint( ((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON java.awt.RenderingHints.VALUE_ANTIALIAS_ON
); );
g.setColor(getForeground()); g.setColor(getForeground());
for (Tree<String> node: layout.keySet()) for (Tree<String> node: layout.keySet())
{ {
Position position = layout.get(node); Position position = layout.get(node);
int x = position.x; int x = position.x;
int line = 1 + position.line - startingLine; int line = 1 + position.line - startingLine;
int y = snap2(line, iy, lh); int y = snap2(line, iy, lh);
if (y > h) continue; if (y > h) continue;
if (node.key.equals("text")) if (node.key.equals("text"))
{ {
boolean isLink = node.get("href") != null; boolean isLink = node.get("href") != null;
if (isLink) g.setColor(LINK_COLOUR); if (isLink) g.setColor(LINK_COLOUR);
g.drawString(node.value, x, y); g.drawString(node.value, x, y);
if (isLink) g.setColor(PLAIN_COLOUR); if (isLink) g.setColor(PLAIN_COLOUR);
} }
else if (node.key.equals("emoji")) else if (node.key.equals("emoji"))
{ {
Image image = emojis.get(node.value); Image image = emojis.get(node.value);
Image scaled = emojis.get(node.value + "_scaled"); Image scaled = emojis.get(node.value + "_scaled");
if (scaled != null) if (scaled != null)
{ {
y -= asc; y -= asc;
g.drawImage(scaled, x, y, this); g.drawImage(scaled, x, y, this);
} }
else if (image != null) else if (image != null)
{ {
scaled = image.getScaledInstance( scaled = image.getScaledInstance(
-1, fm.getAscent() + fm.getDescent(), -1, fm.getAscent() + fm.getDescent(),
Image.SCALE_SMOOTH Image.SCALE_SMOOTH
); );
// I hope #getScaledInstance knows how to // I hope #getScaledInstance knows how to
// wait if the image is yet to be loaded. // wait if the image is yet to be loaded.
emojis.put(node.value + "_scaled", scaled); emojis.put(node.value + "_scaled", scaled);
} }
else else
{ {
g.drawString(node.value, x, y); g.drawString(node.value, x, y);
} }
} }
else continue; else continue;
} }
} }
public void public void
mousePressed(MouseEvent eM) mousePressed(MouseEvent eM)
{ {
if (eM.getButton() != MouseEvent.BUTTON1) return; if (eM.getButton() != MouseEvent.BUTTON1) return;
selStart = identifyNodeAt(eM.getX(), eM.getY()); selStart = identifyNodeAt(eM.getX(), eM.getY());
selEnd = null; selEnd = null;
repaint(); repaint();
@ -408,71 +408,71 @@ implements
repaint(); repaint();
} }
public void public void
mouseMoved(MouseEvent eM) mouseMoved(MouseEvent eM)
{ {
Tree<String> h = identifyNodeAt(eM.getX(), eM.getY()); Tree<String> h = identifyNodeAt(eM.getX(), eM.getY());
if (h == null || h.get("href") == null) if (h == null || h.get("href") == null)
{ {
setToolTipText(""); setToolTipText("");
} }
else else
{ {
setToolTipText(h.get("href").value); setToolTipText(h.get("href").value);
} }
} }
private Tree<String> private Tree<String>
identifyNodeAt(int x, int y) identifyNodeAt(int x, int y)
{ {
FontMetrics fm = getFontMetrics(getFont()); FontMetrics fm = getFontMetrics(getFont());
int initial = fm.getAscent(); int initial = fm.getAscent();
int advance = fm.getAscent() + fm.getDescent(); int advance = fm.getAscent() + fm.getDescent();
int line = isnap2(y, initial, advance); int line = isnap2(y, initial, advance);
Tree<String> returnee = null; Tree<String> returnee = null;
Position closest = new Position(0, 0); Position closest = new Position(0, 0);
for (Tree<String> node: layout.keySet()) for (Tree<String> node: layout.keySet())
{ {
Position position = layout.get(node); Position position = layout.get(node);
assert position != null; assert position != null;
if (position.line != line) continue; if (position.line != line) continue;
if (position.x > x) continue; if (position.x > x) continue;
if (position.x >= closest.x) if (position.x >= closest.x)
{ {
returnee = node; returnee = node;
closest = position; closest = position;
} }
} }
return returnee; return returnee;
} }
private Tree<String> private Tree<String>
identifyNodeAfter(int x, int y) identifyNodeAfter(int x, int y)
{ {
FontMetrics fm = getFontMetrics(getFont()); FontMetrics fm = getFontMetrics(getFont());
int initial = fm.getAscent(); int initial = fm.getAscent();
int advance = fm.getAscent() + fm.getDescent(); int advance = fm.getAscent() + fm.getDescent();
int line = isnap2(y, initial, advance); int line = isnap2(y, initial, advance);
Tree<String> returnee = null; Tree<String> returnee = null;
Position closest = new Position(Integer.MAX_VALUE, 0); Position closest = new Position(Integer.MAX_VALUE, 0);
for (Tree<String> node: layout.keySet()) for (Tree<String> node: layout.keySet())
{ {
Position position = layout.get(node); Position position = layout.get(node);
assert position != null; assert position != null;
if (position.line != line) continue; if (position.line != line) continue;
if (position.x < x) continue; if (position.x < x) continue;
if (position.x < closest.x) if (position.x < closest.x)
{ {
returnee = node; returnee = node;
closest = position; closest = position;
} }
} }
return returnee; return returnee;
} }
public void public void
keyPressed(KeyEvent eK) keyPressed(KeyEvent eK)
@ -494,14 +494,14 @@ implements
} }
private String private String
getSelectedText() getSelectedText()
{ {
assert selStart != null && selEnd != null; assert selStart != null && selEnd != null;
Position ssp = layout.get(selStart); Position ssp = layout.get(selStart);
Position sep = layout.get(selEnd); Position sep = layout.get(selEnd);
assert ssp != null && sep != null; assert ssp != null && sep != null;
if (ssp.compareTo(sep) > 0) if (ssp.compareTo(sep) > 0)
{ {
Position temp = ssp; Position temp = ssp;
ssp = sep; ssp = sep;
@ -515,8 +515,8 @@ implements
Position position = layout.get(node); Position position = layout.get(node);
assert position != null; assert position != null;
boolean after = position.compareTo(ssp) >= 0; boolean after = position.compareTo(ssp) >= 0;
boolean before = position.compareTo(sep) < 0; boolean before = position.compareTo(sep) < 0;
if (!(after && before)) continue; if (!(after && before)) continue;
// Just throw them in a pile for now.. // Just throw them in a pile for now..
@ -549,35 +549,35 @@ implements
boolean s = node.key.equals("space"); boolean s = node.key.equals("space");
assert t || e || s; assert t || e || s;
b.append(node.value); b.append(node.value);
/* /*
* I actually want to copy the link if the node is * I actually want to copy the link if the node is
* associated with one. However, a link has * associated with one. However, a link has
* multiple text nodes, so I'd end up copying * multiple text nodes, so I'd end up copying
* multiple times. The correct action is to * multiple times. The correct action is to
* associate the nodes with the same link object, * associate the nodes with the same link object,
* then mark that as copied. Or, associate the * then mark that as copied. Or, associate the
* nodes with their superiors in the HTML, then * nodes with their superiors in the HTML, then
* walk up until we find an anchor with a href. * walk up until we find an anchor with a href.
* Then again, have to mark that as copied too. * Then again, have to mark that as copied too.
* *
* I can also walk the HTML and copy any that are * I can also walk the HTML and copy any that are
* in the selected region, careful to copy an * in the selected region, careful to copy an
* anchor's href in stead of the anchor contents. * anchor's href in stead of the anchor contents.
* I'd need a guarantee that my walking order is * I'd need a guarantee that my walking order is
* the same as how they were rendered on the screen. * the same as how they were rendered on the screen.
*/ */
} }
return b.toString(); return b.toString();
} }
private int private int
getHeightInLines() getHeightInLines()
{ {
FontMetrics fm = getFontMetrics(getFont()); FontMetrics fm = getFontMetrics(getFont());
int initial = fm.getAscent(); int initial = fm.getAscent();
int advance = fm.getAscent() + fm.getDescent(); int advance = fm.getAscent() + fm.getDescent();
return isnap2(getHeight(), initial, advance) - 1; return isnap2(getHeight(), initial, advance) - 1;
} }
public void public void
keyReleased(KeyEvent eK) { } keyReleased(KeyEvent eK) { }
@ -597,107 +597,107 @@ implements
public void public void
mouseExited(MouseEvent eM) { } mouseExited(MouseEvent eM) { }
public void public void
componentResized(ComponentEvent eC) { setText(html); } componentResized(ComponentEvent eC) { setText(html); }
public void public void
componentMoved(ComponentEvent eC) { } componentMoved(ComponentEvent eC) { }
public void public void
componentShown(ComponentEvent eC) { } componentShown(ComponentEvent eC) { }
public void public void
componentHidden(ComponentEvent eC) { } componentHidden(ComponentEvent eC) { }
// - -%- - // - -%- -
private static int private static int
snap2(int blocks, int initial, int advance) snap2(int blocks, int initial, int advance)
{ {
return initial + (blocks - 1) * advance; return initial + (blocks - 1) * advance;
// If you'd like to go behind the first line 1, // If you'd like to go behind the first line 1,
// note that the first negative line is 0. // note that the first negative line is 0.
} }
private static int private static int
isnap2(int units, int initial, int advance) isnap2(int units, int initial, int advance)
{ {
int offset = units - initial; int offset = units - initial;
return 2 + bfloor(offset - 1, advance); return 2 + bfloor(offset - 1, advance);
// Not yet sure how this behaves for negative numbers. // Not yet sure how this behaves for negative numbers.
} }
private static int private static int
bfloor(int units, int block) bfloor(int units, int block)
{ {
if (units < 0) return (units / block) - 1; if (units < 0) return (units / block) - 1;
else return units / block; else return units / block;
} }
// ---%-@-%--- // ---%-@-%---
private static class private static class
Position { Position {
int int
x, line; x, line;
// -=%=- // -=%=-
public int public int
compareTo(Position other) compareTo(Position other)
{ {
if (line < other.line) return -1; if (line < other.line) return -1;
if (line > other.line) return 1; if (line > other.line) return 1;
if (x < other.x) return -1; if (x < other.x) return -1;
if (x > other.x) return 1; if (x > other.x) return 1;
return 0; return 0;
} }
public String public String
toString() toString()
{ {
return "(" + x + "," + line + ")"; return "(" + x + "," + line + ")";
} }
// -=%=- // -=%=-
public public
Position(int x, int line) Position(int x, int line)
{ {
this.x = x; this.x = x;
this.line = line; this.line = line;
} }
public Position public Position
clone() clone()
{ {
return new Position(x, line); return new Position(x, line);
} }
} }
// ---%-@-%--- // ---%-@-%---
RichTextPane3() RichTextPane3()
{ {
layout = new HashMap<>(); layout = new HashMap<>();
layoutEnd = new Tree<>("text", ""); layoutEnd = new Tree<>("text", "");
emojis = new HashMap<>(); emojis = new HashMap<>();
Tree<String> blank = new Tree<>(); Tree<String> blank = new Tree<>();
blank.key = "tag"; blank.key = "tag";
blank.add(new Tree<>("html", null)); blank.add(new Tree<>("html", null));
blank.add(new Tree<>("children", null)); blank.add(new Tree<>("children", null));
setText(blank); setText(blank);
this.addComponentListener(this); this.addComponentListener(this);
this.addMouseListener(this); this.addMouseListener(this);
this.addMouseMotionListener(this); this.addMouseMotionListener(this);
this.addKeyListener(this); this.addKeyListener(this);
setFocusable(true); setFocusable(true);
// A keyboard user can still copy by tabbing in // A keyboard user can still copy by tabbing in
// and selecting all. // and selecting all.
} }
} }

View File

@ -31,18 +31,18 @@ RudimentaryHTMLParser {
public static Tree<String> public static Tree<String>
depthlessRead(String html) depthlessRead(String html)
{ {
try { try {
return pass3(pass2(pass1(html))); return pass3(pass2(pass1(html)));
} }
catch (IOException eIo) { catch (IOException eIo) {
assert false; assert false;
/* /*
* We use only StringReaders, which only throw an * We use only StringReaders, which only throw an
* IOException when they are read after being closed. * IOException when they are read after being closed.
* And we don't close them. * And we don't close them.
*/ */
return null; return null;
} }
} }
// - -%- - // - -%- -
@ -51,10 +51,10 @@ RudimentaryHTMLParser {
pass1(String html) pass1(String html)
throws IOException throws IOException
{ {
Reader r = new StringReader(html); Reader r = new StringReader(html);
Tree<String> docu = new Tree<String>(); Tree<String> docu = new Tree<String>();
StringBuilder text = new StringBuilder(); StringBuilder text = new StringBuilder();
StringBuilder emoji = new StringBuilder(); StringBuilder emoji = new StringBuilder();
StringBuilder htmlEscape = new StringBuilder(); StringBuilder htmlEscape = new StringBuilder();
boolean quoted = false, inEmoji = false; boolean quoted = false, inEmoji = false;
int c; while ((c = r.read()) != -1) int c; while ((c = r.read()) != -1)
@ -110,7 +110,7 @@ RudimentaryHTMLParser {
continue; continue;
} }
} }
text.append((char)c); text.append((char)c);
continue; continue;
} }
if (text.length() > 0) if (text.length() > 0)
@ -181,9 +181,9 @@ RudimentaryHTMLParser {
return docu; return docu;
} }
private static Tree<String> private static Tree<String>
pass3(Tree<String> docu) pass3(Tree<String> docu)
{ {
Tree<String> returnee = new Tree<String>(); Tree<String> returnee = new Tree<String>();
for (Tree<String> node: docu) for (Tree<String> node: docu)
@ -194,39 +194,39 @@ RudimentaryHTMLParser {
continue; continue;
} }
StringBuilder value = new StringBuilder(); StringBuilder value = new StringBuilder();
for (String segment: whitespaceSplit(node.value)) for (String segment: whitespaceSplit(node.value))
{ {
boolean st = segment.startsWith(":"); boolean st = segment.startsWith(":");
boolean ed = segment.endsWith(":"); boolean ed = segment.endsWith(":");
if (st && ed) if (st && ed)
{ {
Tree<String> text = new Tree<String>(); Tree<String> text = new Tree<String>();
text.key = "text"; text.key = "text";
text.value = empty(value); text.value = empty(value);
returnee.add(text); returnee.add(text);
Tree<String> emoji = new Tree<String>(); Tree<String> emoji = new Tree<String>();
emoji.key = "emoji"; emoji.key = "emoji";
emoji.value = segment; emoji.value = segment;
returnee.add(emoji); returnee.add(emoji);
} }
else else
{ {
value.append(segment); value.append(segment);
} }
} }
if (value.length() > 0) if (value.length() > 0)
{ {
Tree<String> text = new Tree<String>(); Tree<String> text = new Tree<String>();
text.key = "text"; text.key = "text";
text.value = empty(value); text.value = empty(value);
returnee.add(text); returnee.add(text);
} }
} }
return returnee; return returnee;
} }
private static String private static String
empty(StringBuilder b) empty(StringBuilder b)
@ -236,25 +236,25 @@ RudimentaryHTMLParser {
return s; return s;
} }
private static List<String> private static List<String>
whitespaceSplit(String text) whitespaceSplit(String text)
{ {
List<String> returnee = new ArrayList<>(); List<String> returnee = new ArrayList<>();
StringBuilder segment = new StringBuilder(); StringBuilder segment = new StringBuilder();
boolean isWhitespace = false; boolean isWhitespace = false;
for (char c: text.toCharArray()) for (char c: text.toCharArray())
{ {
boolean diff = isWhitespace ^ Character.isWhitespace(c); boolean diff = isWhitespace ^ Character.isWhitespace(c);
if (diff) { if (diff) {
returnee.add(empty(segment)); returnee.add(empty(segment));
isWhitespace = !isWhitespace; isWhitespace = !isWhitespace;
} }
segment.append(c); segment.append(c);
} }
returnee.add(empty(segment)); returnee.add(empty(segment));
return returnee; return returnee;
} }
// ---%-@-%--- // ---%-@-%---

View File

@ -90,9 +90,9 @@ implements ActionListener {
openMessages, openMessages,
openLocal, openLocal,
openFederated, openFederated,
openNotifications, openNotifications,
openOwnProfile, openOwnProfile,
openProfile, openProfile,
createPost, createPost,
openAutoPostView, openAutoPostView,
quit; quit;
@ -117,101 +117,101 @@ implements ActionListener {
this.page = page; this.page = page;
List<PostPreviewComponent> previews; List<PostPreviewComponent> previews;
previews = display.getPostPreviews(); previews = display.getPostPreviews();
int available = page.posts.length; int available = page.posts.length;
int max = previews.size(); int max = previews.size();
assert available <= max; assert available <= max;
for (int o = 0; o < available; ++o) for (int o = 0; o < available; ++o)
{ {
PostPreviewComponent preview = previews.get(o); PostPreviewComponent preview = previews.get(o);
Post post = page.posts[o]; Post post = page.posts[o];
preview.setTopLeft(post.author.name); preview.setTopLeft(post.author.name);
if (post.boostedPost != null) if (post.boostedPost != null)
{ {
String s = "boosted by " + post.author.name; String s = "boosted by " + post.author.name;
preview.setTopLeft(s); preview.setTopLeft(s);
post = post.boostedPost; post = post.boostedPost;
} }
String flags = ""; String flags = "";
if (post.attachments.length > 0) flags += "a"; if (post.attachments.length > 0) flags += "a";
post.resolveRelativeTime(); post.resolveRelativeTime();
preview.setTopRight(flags + " " + post.relativeTime); preview.setTopRight(flags + " " + post.relativeTime);
post.resolveApproximateText(); post.resolveApproximateText();
if (post.contentWarning != null) if (post.contentWarning != null)
preview.setBottom("(" + post.contentWarning + ")"); preview.setBottom("(" + post.contentWarning + ")");
else else
preview.setBottom(post.approximateText); preview.setBottom(post.approximateText);
} }
for (int o = available; o < max; ++o) for (int o = available; o < max; ++o)
{ {
previews.get(o).reset(); previews.get(o).reset();
} }
boolean full = !(available < PREVIEW_COUNT); boolean full = !(available < PREVIEW_COUNT);
display.setNextPageAvailable(full); display.setNextPageAvailable(full);
display.setPreviousPageAvailable(true); display.setPreviousPageAvailable(true);
display.resetFocus(); display.resetFocus();
} }
public void public void
readEntity(Tree<String> postEntityArray) readEntity(Tree<String> postEntityArray)
{ {
TimelinePage page = new TimelinePage(postEntityArray); TimelinePage page = new TimelinePage(postEntityArray);
page.type = this.page.type; page.type = this.page.type;
page.accountNumId = this.page.accountNumId; page.accountNumId = this.page.accountNumId;
page.listId = this.page.listId; page.listId = this.page.listId;
use(page); use(page);
} }
public void public void
refresh() refresh()
{ {
String firstId = null; String firstId = null;
if (!showingLatest) if (!showingLatest)
{ {
assert page.posts != null; assert page.posts != null;
assert page.posts.length != 0; assert page.posts.length != 0;
firstId = page.posts[0].id; firstId = page.posts[0].id;
} }
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
page.type, page.type,
PREVIEW_COUNT, firstId, null, PREVIEW_COUNT, firstId, null,
page.accountNumId, page.listId, page.accountNumId, page.listId,
new RequestListener() { new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")" + "\n(HTTP code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
if (json.size() < PREVIEW_COUNT) if (json.size() < PREVIEW_COUNT)
{ {
showLatestPage(); showLatestPage();
return; return;
} }
@ -221,37 +221,37 @@ implements ActionListener {
} }
); );
display.setCursor(null); display.setCursor(null);
} }
public void public void
showLatestPage() showLatestPage()
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
page.type, page.type,
PREVIEW_COUNT, null, null, PREVIEW_COUNT, null, null,
page.accountNumId, page.listId, page.accountNumId, page.listId,
new RequestListener() { new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")" + "\n(HTTP code: " + httpCode + ")"
); );
} }
public void public void
@ -274,32 +274,32 @@ implements ActionListener {
assert page.posts.length != 0; assert page.posts.length != 0;
String lastId = page.posts[page.posts.length - 1].id; String lastId = page.posts[page.posts.length - 1].id;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
page.type, page.type,
PREVIEW_COUNT, lastId, null, PREVIEW_COUNT, lastId, null,
page.accountNumId, page.listId, page.accountNumId, page.listId,
new RequestListener() { new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")" + "\n(HTTP code: " + httpCode + ")"
); );
} }
public void public void
@ -312,7 +312,7 @@ implements ActionListener {
// quietly cancel. // quietly cancel.
return; return;
} }
readEntity(json); readEntity(json);
showingLatest = false; showingLatest = false;
windowUpdater.remove(TimelineWindow.this); windowUpdater.remove(TimelineWindow.this);
} }
@ -332,36 +332,36 @@ implements ActionListener {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
page.type, page.type,
PREVIEW_COUNT, null, firstId, PREVIEW_COUNT, null, firstId,
page.accountNumId, page.listId, page.accountNumId, page.listId,
new RequestListener() { new RequestListener() {
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Failed to fetch page.." "Failed to fetch page.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")" + "\n(HTTP code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
if (json.size() < PREVIEW_COUNT) if (json.size() < PREVIEW_COUNT)
{ {
showLatestPage(); showLatestPage();
return; return;
} }
@ -386,12 +386,12 @@ implements ActionListener {
setTitle(toString(type) + " timeline - JKomasto"); setTitle(toString(type) + " timeline - JKomasto");
String f = type.toString().toLowerCase(); String f = type.toString().toLowerCase();
display.setBackgroundImage(ImageApi.local(f)); display.setBackgroundImage(ImageApi.local(f));
/* /*
* () Java's image renderer draws images with transparency * () Java's image renderer draws images with transparency
* darker than GIMP does. Overcompensate in lightening. * darker than GIMP does. Overcompensate in lightening.
*/ */
display.repaint(); display.repaint();
} }
public synchronized TimelineType public synchronized TimelineType
@ -410,9 +410,9 @@ implements ActionListener {
display.repaint(); display.repaint();
} }
public synchronized void public synchronized void
openOwnProfile() openOwnProfile()
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
Tree<String> accountDetails = api.getAccountDetails(); Tree<String> accountDetails = api.getAccountDetails();
@ -422,118 +422,118 @@ implements ActionListener {
w.setVisible(true); w.setVisible(true);
display.setCursor(null); display.setCursor(null);
} }
public void public void
openProfile() openProfile()
{ {
String query = JOptionPane.showInputDialog( String query = JOptionPane.showInputDialog(
this, this,
"Whose account do you want to see?\n" "Whose account do you want to see?\n"
+ "Type an account name with the instance,\n" + "Type an account name with the instance,\n"
+ "or a display name if you can't remember.\n", + "or a display name if you can't remember.\n",
"Profile search", "Profile search",
JOptionPane.PLAIN_MESSAGE JOptionPane.PLAIN_MESSAGE
); );
if (query == null) return; if (query == null) return;
class Handler implements RequestListener { class Handler implements RequestListener {
public Tree<String> public Tree<String>
json; json;
// -=%=- // -=%=-
public void public void
connectionFailed(IOException eIo) connectionFailed(IOException eIo)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Tried to fetch accounts, but it failed.." "Tried to fetch accounts, but it failed.."
+ "\n" + eIo.getMessage() + "\n" + eIo.getMessage()
); );
} }
public void public void
requestFailed(int httpCode, Tree<String> json) requestFailed(int httpCode, Tree<String> json)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
TimelineWindow.this, TimelineWindow.this,
"Tried to fetch accounts, but it failed.." "Tried to fetch accounts, but it failed.."
+ "\n" + json.get("error").value + "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")" + "\n(HTTP code: " + httpCode + ")"
); );
} }
public void public void
requestSucceeded(Tree<String> json) { this.json = json; } requestSucceeded(Tree<String> json) { this.json = json; }
} }
// () Have to create a named class because // () Have to create a named class because
// it has to hold the variable. // it has to hold the variable.
Handler handler = new Handler(); Handler handler = new Handler();
api.getAccounts(query, handler); api.getAccounts(query, handler);
if (handler.json == null) return; if (handler.json == null) return;
if (handler.json.size() == 0) if (handler.json.size() == 0)
{ {
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
this, this,
"There were no results from the query.. ☹️" "There were no results from the query.. ☹️"
); );
return; return;
} }
Tree<String> openee = null; Tree<String> openee = null;
if (query.startsWith("@")) query = query.substring(1); if (query.startsWith("@")) query = query.substring(1);
List<Object> message = new ArrayList<>(); List<Object> message = new ArrayList<>();
message.add("Maybe one of these?"); message.add("Maybe one of these?");
ButtonGroup selGroup = new ButtonGroup(); ButtonGroup selGroup = new ButtonGroup();
for (Tree<String> account: handler.json) for (Tree<String> account: handler.json)
{ {
String dname = account.get("display_name").value; String dname = account.get("display_name").value;
String acct = account.get("acct").value; String acct = account.get("acct").value;
if (query.equals(acct)) { if (query.equals(acct)) {
openee = account; openee = account;
break; break;
} }
JRadioButton b = new JRadioButton(); JRadioButton b = new JRadioButton();
b.setText(dname + " (" + acct + ")"); b.setText(dname + " (" + acct + ")");
selGroup.add(b); selGroup.add(b);
message.add(b); message.add(b);
} }
if (openee == null) if (openee == null)
{ {
int response = JOptionPane.showConfirmDialog( int response = JOptionPane.showConfirmDialog(
this, this,
message.toArray(), message.toArray(),
"Search results", "Search results",
JOptionPane.OK_CANCEL_OPTION JOptionPane.OK_CANCEL_OPTION
); );
if (response == JOptionPane.CANCEL_OPTION) return; if (response == JOptionPane.CANCEL_OPTION) return;
for (int o = 1; o < message.size(); ++o) for (int o = 1; o < message.size(); ++o)
{ {
JRadioButton b = (JRadioButton)message.get(o); JRadioButton b = (JRadioButton)message.get(o);
if (selGroup.isSelected(b.getModel())) if (selGroup.isSelected(b.getModel()))
{ {
openee = handler.json.get(o - 1); openee = handler.json.get(o - 1);
break; break;
} }
} }
if (openee == null) return; if (openee == null) return;
/* /*
* It seems like this can happen if someone * It seems like this can happen if someone
* presses escape out of the confirm dialog. * presses escape out of the confirm dialog.
* I don't know why that doesn't map to cancel. * I don't know why that doesn't map to cancel.
*/ */
} }
ProfileWindow w = new ProfileWindow(primaire); ProfileWindow w = new ProfileWindow(primaire);
w.use(new Account(openee)); w.use(new Account(openee));
w.setLocationByPlatform(true); w.setLocationByPlatform(true);
w.setVisible(true); w.setVisible(true);
} }
// - -%- - // - -%- -
@ -577,13 +577,13 @@ implements ActionListener {
setTimelineType(TimelineType.LOCAL); setTimelineType(TimelineType.LOCAL);
showLatestPage(); showLatestPage();
} }
if (src == openOwnProfile) if (src == openOwnProfile)
{ {
openOwnProfile(); openOwnProfile();
} }
if (src == openProfile) if (src == openProfile)
{ {
openProfile(); openProfile();
} }
if (src == createPost) if (src == createPost)
{ {
@ -595,15 +595,15 @@ implements ActionListener {
w.setLocation(getX() + 10 + getWidth(), getY()); w.setLocation(getX() + 10 + getWidth(), getY());
w.setVisible(true); w.setVisible(true);
} }
if (src == openNotifications) if (src == openNotifications)
{ {
NotificationsWindow w = NotificationsWindow w =
primaire.getNotificationsWindow(); primaire.getNotificationsWindow();
if (!w.isVisible()) if (!w.isVisible())
{ {
w.setLocationByPlatform(true); w.setLocationByPlatform(true);
w.setVisible(true); w.setVisible(true);
} }
} }
if (src == flipToNewestPost) if (src == flipToNewestPost)
{ {
@ -650,17 +650,17 @@ implements ActionListener {
openHome = new JMenuItem("Open home timeline"); openHome = new JMenuItem("Open home timeline");
openFederated = new JMenuItem("Open federated timeline"); openFederated = new JMenuItem("Open federated timeline");
openNotifications = new JMenuItem("Open notifications"); openNotifications = new JMenuItem("Open notifications");
openOwnProfile = new JMenuItem("Open own profile"); openOwnProfile = new JMenuItem("Open own profile");
openProfile = new JMenuItem("Open profile.."); openProfile = new JMenuItem("Open profile..");
createPost = new JMenuItem("Create a post"); createPost = new JMenuItem("Create a post");
openAutoPostView = new JMenuItem("Open auto post view"); openAutoPostView = new JMenuItem("Open auto post view");
quit = new JMenuItem("Quit"); quit = new JMenuItem("Quit");
openHome.addActionListener(this); openHome.addActionListener(this);
openFederated.addActionListener(this); openFederated.addActionListener(this);
openNotifications.addActionListener(this); openNotifications.addActionListener(this);
openOwnProfile.addActionListener(this); openOwnProfile.addActionListener(this);
openProfile.addActionListener(this); openProfile.addActionListener(this);
createPost.addActionListener(this); createPost.addActionListener(this);
openAutoPostView.addActionListener(this); openAutoPostView.addActionListener(this);
quit.addActionListener(this); quit.addActionListener(this);
@ -672,9 +672,9 @@ implements ActionListener {
programMenu.setMnemonic(KeyEvent.VK_P); programMenu.setMnemonic(KeyEvent.VK_P);
programMenu.add(openHome); programMenu.add(openHome);
programMenu.add(openFederated); programMenu.add(openFederated);
programMenu.add(openNotifications); programMenu.add(openNotifications);
programMenu.add(openOwnProfile); programMenu.add(openOwnProfile);
programMenu.add(openProfile); programMenu.add(openProfile);
programMenu.add(new JSeparator()); programMenu.add(new JSeparator());
programMenu.add(createPost); programMenu.add(createPost);
programMenu.add(openAutoPostView); programMenu.add(openAutoPostView);
@ -696,8 +696,8 @@ implements ActionListener {
display.setPreviousPageAvailable(false); display.setPreviousPageAvailable(false);
setContentPane(display); setContentPane(display);
setTimelineType(TimelineType.HOME); setTimelineType(TimelineType.HOME);
setIconImage(primaire.getProgramIcon()); setIconImage(primaire.getProgramIcon());
} }
} }
@ -727,8 +727,8 @@ implements
private boolean private boolean
hoverSelect; hoverSelect;
private Image private Image
backgroundImage; backgroundImage;
// - -%- - // - -%- -
@ -759,46 +759,46 @@ implements
public void public void
setHoverSelect(boolean n) { hoverSelect = n; } setHoverSelect(boolean n) { hoverSelect = n; }
public void public void
setBackgroundImage(Image n) { backgroundImage = n; } setBackgroundImage(Image n) { backgroundImage = n; }
public void public void
resetFocus() { postPreviews.get(0).requestFocusInWindow(); } resetFocus() { postPreviews.get(0).requestFocusInWindow(); }
// - -%- - // - -%- -
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
int w = getWidth(), h = getHeight(); int w = getWidth(), h = getHeight();
g.clearRect(0, 0, w, h); g.clearRect(0, 0, w, h);
if (backgroundImage != null) if (backgroundImage != null)
{ {
int b = h * 5 / 10; int b = h * 5 / 10;
int iw = backgroundImage.getWidth(this); int iw = backgroundImage.getWidth(this);
int ih = backgroundImage.getHeight(this); int ih = backgroundImage.getHeight(this);
if (ih > iw) { if (ih > iw) {
ih = ih * b / iw; ih = ih * b / iw;
iw = b; iw = b;
} }
else { else {
iw = iw * b / ih; iw = iw * b / ih;
ih = b; ih = b;
} }
int x = w - iw, y = h - ih; int x = w - iw, y = h - ih;
g.drawImage(backgroundImage, x, y, iw, ih, this); g.drawImage(backgroundImage, x, y, iw, ih, this);
} }
((java.awt.Graphics2D)g).setRenderingHint( ((java.awt.Graphics2D)g).setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUE_ANTIALIAS_ON java.awt.RenderingHints.VALUE_ANTIALIAS_ON
); );
} }
private void private void
select(Object c) select(Object c)
{ {
assert c instanceof PostPreviewComponent; assert c instanceof PostPreviewComponent;
for (PostPreviewComponent p: postPreviews) for (PostPreviewComponent p: postPreviews)
@ -814,25 +814,25 @@ implements
p.repaint(); p.repaint();
} }
} }
} }
private void private void
deselect(Object c) deselect(Object c)
{ {
assert c instanceof PostPreviewComponent; assert c instanceof PostPreviewComponent;
PostPreviewComponent p = (PostPreviewComponent)c; PostPreviewComponent p = (PostPreviewComponent)c;
p.setSelected(false); p.setSelected(false);
p.repaint(); p.repaint();
} }
private void private void
open(Object c) open(Object c)
{ {
int o = postPreviews.indexOf(c); int o = postPreviews.indexOf(c);
assert o != -1; assert o != -1;
primaire.previewOpened(1 + o); primaire.previewOpened(1 + o);
} }
public void public void
focusGained(FocusEvent eF) { select(eF.getSource()); } focusGained(FocusEvent eF) { select(eF.getSource()); }
@ -840,14 +840,14 @@ implements
public void public void
focusLost(FocusEvent eF) { deselect(eF.getSource()); } focusLost(FocusEvent eF) { deselect(eF.getSource()); }
public void public void
mouseClicked(MouseEvent eM) mouseClicked(MouseEvent eM)
{ {
if (eM.getClickCount() == 2) open(eM.getSource()); if (eM.getClickCount() == 2) open(eM.getSource());
else select(eM.getSource()); else select(eM.getSource());
} }
public void public void
mouseEntered(MouseEvent eM) mouseEntered(MouseEvent eM)
{ {
if (!hoverSelect) return; if (!hoverSelect) return;
@ -968,8 +968,8 @@ PostPreviewComponent extends JComponent {
private JLabel private JLabel
topLeft, topRight, bottom; topLeft, topRight, bottom;
private boolean private boolean
selected; selected;
// ---%-@-%--- // ---%-@-%---
@ -982,7 +982,7 @@ PostPreviewComponent extends JComponent {
public void public void
setBottom(String text) { bottom.setText(text); } setBottom(String text) { bottom.setText(text); }
public void public void
reset() reset()
{ {
setTopLeft(" "); setTopLeft(" ");
@ -1001,11 +1001,11 @@ PostPreviewComponent extends JComponent {
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
if (selected) if (selected)
{ {
g.setColor(new Color(0, 0, 0, 25)); g.setColor(new Color(0, 0, 0, 25));
g.fillRect(0, 0, getWidth(), getHeight()); g.fillRect(0, 0, getWidth(), getHeight());
} }
} }
// ---%-@-%--- // ---%-@-%---
@ -1032,20 +1032,20 @@ PostPreviewComponent extends JComponent {
top.add(Box.createGlue()); top.add(Box.createGlue());
top.add(topRight); top.add(topRight);
bottom = new JLabel(); bottom = new JLabel();
bottom.setFont(f3); bottom.setFont(f3);
bottom.setOpaque(false); bottom.setOpaque(false);
JPanel left = new JPanel(); JPanel left = new JPanel();
left.setOpaque(false); left.setOpaque(false);
left.setLayout(new BorderLayout()); left.setLayout(new BorderLayout());
left.add(top, BorderLayout.NORTH); left.add(top, BorderLayout.NORTH);
left.add(bottom); left.add(bottom);
setFocusable(true); setFocusable(true);
setOpaque(false); setOpaque(false);
setLayout(new BorderLayout()); setLayout(new BorderLayout());
add(left); add(left);
} }
} }

View File

@ -56,10 +56,10 @@ implements KeyListener, MouseListener, FocusListener {
primaryToggled = false, primaryToggled = false,
secondaryToggled = false; secondaryToggled = false;
private Image private Image
primaryToggledIcon, primaryToggledIcon,
secondaryToggledIcon, secondaryToggledIcon,
primaryUntoggledIcon, primaryUntoggledIcon,
secondaryUntoggledIcon; secondaryUntoggledIcon;
private int private int
@ -116,7 +116,7 @@ implements KeyListener, MouseListener, FocusListener {
// - -%- - // - -%- -
private void private void
announce(String name, boolean toggled) announce(String name, boolean toggled)
{ {
ActionEvent eA = new ActionEvent( ActionEvent eA = new ActionEvent(
@ -134,17 +134,17 @@ implements KeyListener, MouseListener, FocusListener {
g.drawImage(button, 0, 0, this); g.drawImage(button, 0, 0, this);
if (!isEnabled()) if (!isEnabled())
g.drawImage(disabledOverlay, 0, 0, this); g.drawImage(disabledOverlay, 0, 0, this);
if (isFocusOwner()) if (isFocusOwner())
g.drawImage(selectedOverlay, 0, 0, this); g.drawImage(selectedOverlay, 0, 0, this);
if (secondaryToggled) if (secondaryToggled)
g.drawImage(secondaryToggledIcon, 0, 0, this); g.drawImage(secondaryToggledIcon, 0, 0, this);
else else
g.drawImage(secondaryUntoggledIcon, 0, 0, this); g.drawImage(secondaryUntoggledIcon, 0, 0, this);
if (primaryToggled) if (primaryToggled)
g.drawImage(primaryToggledIcon, 0, 0, this); g.drawImage(primaryToggledIcon, 0, 0, this);
else else
g.drawImage(primaryUntoggledIcon, 0, 0, this); g.drawImage(primaryUntoggledIcon, 0, 0, this);
} }
@ -176,11 +176,11 @@ implements KeyListener, MouseListener, FocusListener {
requestFocusInWindow(); requestFocusInWindow();
} }
public void public void
focusGained(FocusEvent eF) { repaint(); } focusGained(FocusEvent eF) { repaint(); }
public void public void
focusLost(FocusEvent eF) { repaint(); } focusLost(FocusEvent eF) { repaint(); }
public void public void
@ -214,8 +214,8 @@ implements KeyListener, MouseListener, FocusListener {
this.secondaryName = secondaryName; this.secondaryName = secondaryName;
setModel(new DefaultButtonModel()); setModel(new DefaultButtonModel());
setFocusable(true); setFocusable(true);
setOpaque(false); setOpaque(false);
int w = button.getWidth(null); int w = button.getWidth(null);
int h = button.getHeight(null); int h = button.getHeight(null);
@ -224,7 +224,7 @@ implements KeyListener, MouseListener, FocusListener {
this.addKeyListener(this); this.addKeyListener(this);
this.addMouseListener(this); this.addMouseListener(this);
this.addFocusListener(this); this.addFocusListener(this);
} }
private void private void
@ -232,17 +232,17 @@ implements KeyListener, MouseListener, FocusListener {
{ {
String p1 = "graphics/" + primaryName + "Toggled.png"; String p1 = "graphics/" + primaryName + "Toggled.png";
String p2 = "graphics/" + secondaryName + "Toggled.png"; String p2 = "graphics/" + secondaryName + "Toggled.png";
String p3 = "graphics/" + primaryName + "Untoggled.png"; String p3 = "graphics/" + primaryName + "Untoggled.png";
String p4 = "graphics/" + secondaryName + "Untoggled.png"; String p4 = "graphics/" + secondaryName + "Untoggled.png";
URL u1 = getClass().getResource(p1); URL u1 = getClass().getResource(p1);
URL u2 = getClass().getResource(p2); URL u2 = getClass().getResource(p2);
URL u3 = getClass().getResource(p3); URL u3 = getClass().getResource(p3);
URL u4 = getClass().getResource(p4); URL u4 = getClass().getResource(p4);
if (u1 == null) primaryToggledIcon = null; if (u1 == null) primaryToggledIcon = null;
else primaryToggledIcon = new ImageIcon(u1).getImage(); else primaryToggledIcon = new ImageIcon(u1).getImage();
if (u2 == null) secondaryToggledIcon = null; if (u2 == null) secondaryToggledIcon = null;
else secondaryToggledIcon = new ImageIcon(u2).getImage(); else secondaryToggledIcon = new ImageIcon(u2).getImage();
if (u3 == null) primaryUntoggledIcon = null; if (u3 == null) primaryUntoggledIcon = null;
else primaryUntoggledIcon = new ImageIcon(u3).getImage(); else primaryUntoggledIcon = new ImageIcon(u3).getImage();
if (u4 == null) secondaryUntoggledIcon = null; if (u4 == null) secondaryUntoggledIcon = null;
else secondaryUntoggledIcon = new ImageIcon(u4).getImage(); else secondaryUntoggledIcon = new ImageIcon(u4).getImage();
@ -270,16 +270,16 @@ class
RoundButton extends AbstractButton RoundButton extends AbstractButton
implements KeyListener, MouseListener, FocusListener { implements KeyListener, MouseListener, FocusListener {
private Image private Image
image; image;
// - -%- - // - -%- -
private Image private Image
copy, copy,
scaled; scaled;
private int private int
nextEventID = ActionEvent.ACTION_FIRST; nextEventID = ActionEvent.ACTION_FIRST;
// - -%- - // - -%- -
@ -289,99 +289,99 @@ implements KeyListener, MouseListener, FocusListener {
selectedOverlay, selectedOverlay,
disabledOverlay; disabledOverlay;
private static Shape private static Shape
roundClip; roundClip;
// ---%-@-%--- // ---%-@-%---
public void public void
setImage(Image n) setImage(Image n)
{ {
image = n; image = n;
copy = null; copy = null;
scaled = null; scaled = null;
if (image != null) if (image != null)
{ {
image.flush(); image.flush();
prepareImage(image, this); prepareImage(image, this);
} }
} }
// - -%- - // - -%- -
public boolean public boolean
imageUpdate(Image img, int f, int x, int y, int w, int h) imageUpdate(Image img, int f, int x, int y, int w, int h)
{ {
// AbstractButton overrode this to refuse updates for // AbstractButton overrode this to refuse updates for
// images that aren't the button's icon. We don't use // images that aren't the button's icon. We don't use
// the icon, so we're overriding it back. Also, we have // the icon, so we're overriding it back. Also, we have
// some async work to do regarding the images. // some async work to do regarding the images.
if ((f & (ABORT|ERROR)) != 0) return false; if ((f & (ABORT|ERROR)) != 0) return false;
boolean all = (f & ALLBITS) != 0; boolean all = (f & ALLBITS) != 0;
boolean frame = (f & FRAMEBITS) != 0; boolean frame = (f & FRAMEBITS) != 0;
boolean some = (f & SOMEBITS) != 0; boolean some = (f & SOMEBITS) != 0;
if (frame && img != this.image) return false; if (frame && img != this.image) return false;
if (img == this.image && (all || frame)) if (img == this.image && (all || frame))
{ {
int ow = img.getWidth(null); int ow = img.getWidth(null);
int oh = img.getHeight(null); int oh = img.getHeight(null);
if (copy == null) if (copy == null)
{ {
int type = BufferedImage.TYPE_INT_ARGB; int type = BufferedImage.TYPE_INT_ARGB;
copy = new BufferedImage(ow, oh, type); copy = new BufferedImage(ow, oh, type);
} }
Graphics g = copy.getGraphics(); Graphics g = copy.getGraphics();
g.drawImage(img, 0, 0, null); g.drawImage(img, 0, 0, null);
g.dispose(); g.dispose();
int algo = Image.SCALE_SMOOTH; int algo = Image.SCALE_SMOOTH;
Rectangle b = roundClip.getBounds(); Rectangle b = roundClip.getBounds();
int sw = ow > oh ? -1 : b.width; int sw = ow > oh ? -1 : b.width;
int sh = oh > ow ? -1 : b.height; int sh = oh > ow ? -1 : b.height;
scaled = copy.getScaledInstance(sw, sh, algo); scaled = copy.getScaledInstance(sw, sh, algo);
/* /*
* We create a scaled instance from a BufferedImage copy * We create a scaled instance from a BufferedImage copy
* rather than this.image directly, to avoid a ClassCast * rather than this.image directly, to avoid a ClassCast
* Exception bug in the JDK, where ColorModel was * Exception bug in the JDK, where ColorModel was
* incorrectly casting an int array of input data into * incorrectly casting an int array of input data into
* a byte array. I'm not sure why that bug exists nor * a byte array. I'm not sure why that bug exists nor
* why they haven't noticed it, but. * why they haven't noticed it, but.
*/ */
repaint(); repaint();
} }
if (img == scaled && (some || all)) if (img == scaled && (some || all))
{ {
repaint(); repaint();
} }
return all ? false : true; return all ? false : true;
} }
protected void protected void
paintComponent(Graphics g) paintComponent(Graphics g)
{ {
g.drawImage(button, 0, 0, this); g.drawImage(button, 0, 0, this);
if (!isEnabled()) if (!isEnabled())
g.drawImage(disabledOverlay, 0, 0, this); g.drawImage(disabledOverlay, 0, 0, this);
if (isFocusOwner()) if (isFocusOwner())
g.drawImage(selectedOverlay, 0, 0, this); g.drawImage(selectedOverlay, 0, 0, this);
if (scaled == null) return; if (scaled == null) return;
Rectangle b = roundClip.getBounds(); Rectangle b = roundClip.getBounds();
Shape defaultClip = g.getClip(); Shape defaultClip = g.getClip();
g.setClip(roundClip); g.setClip(roundClip);
g.drawImage(scaled, b.x, b.y, this); g.drawImage(scaled, b.x, b.y, this);
getParent().repaint(); getParent().repaint();
// I don't know why, but when we repaint ourselves, our // I don't know why, but when we repaint ourselves, our
// parent doesn't repaint, so nothing seems to happen. // parent doesn't repaint, so nothing seems to happen.
g.setClip(defaultClip); g.setClip(defaultClip);
} }
private void private void
@ -396,31 +396,31 @@ implements KeyListener, MouseListener, FocusListener {
public void public void
keyPressed(KeyEvent eK) keyPressed(KeyEvent eK)
{ {
if (eK.getKeyCode() != KeyEvent.VK_SPACE) return; if (eK.getKeyCode() != KeyEvent.VK_SPACE) return;
doClick(); doClick();
} }
public void public void
mouseClicked(MouseEvent eM) { announce(); } mouseClicked(MouseEvent eM) { announce(); }
public void public void
focusGained(FocusEvent eF) { repaint(); } focusGained(FocusEvent eF) { repaint(); }
public void public void
focusLost(FocusEvent eF) { repaint(); } focusLost(FocusEvent eF) { repaint(); }
public void public void
mousePressed(MouseEvent eM) { } mousePressed(MouseEvent eM) { }
public void public void
mouseReleased(MouseEvent eM) { } mouseReleased(MouseEvent eM) { }
public void public void
mouseEntered(MouseEvent eM) { } mouseEntered(MouseEvent eM) { }
public void public void
mouseExited(MouseEvent eM) { } mouseExited(MouseEvent eM) { }
public void public void
keyReleased(KeyEvent eK) { } keyReleased(KeyEvent eK) { }
@ -437,16 +437,16 @@ implements KeyListener, MouseListener, FocusListener {
if (button == null) loadCommonImages(); if (button == null) loadCommonImages();
setModel(new DefaultButtonModel()); setModel(new DefaultButtonModel());
setFocusable(true); setFocusable(true);
setOpaque(false); setOpaque(false);
int w = button.getWidth(null); int w = button.getWidth(null);
int h = button.getHeight(null); int h = button.getHeight(null);
setPreferredSize(new Dimension(w, h)); setPreferredSize(new Dimension(w, h));
this.addKeyListener(this); this.addKeyListener(this);
this.addMouseListener(this); this.addMouseListener(this);
this.addFocusListener(this); this.addFocusListener(this);
} }
// - -%- - // - -%- -
@ -463,13 +463,13 @@ implements KeyListener, MouseListener, FocusListener {
disabledOverlay = new ImageIcon(u2).getImage(); disabledOverlay = new ImageIcon(u2).getImage();
selectedOverlay = new ImageIcon(u3).getImage(); selectedOverlay = new ImageIcon(u3).getImage();
int radius = 6; int radius = 6;
roundClip = new Ellipse2D.Float( roundClip = new Ellipse2D.Float(
radius, radius,
radius, radius,
button.getWidth(null) - (2 * radius), button.getWidth(null) - (2 * radius),
button.getHeight(null) - (2 * radius) button.getHeight(null) - (2 * radius)
); );
} }
} }

View File

@ -43,11 +43,11 @@ WindowUpdater {
private List<TimelineWindow> private List<TimelineWindow>
timelineWindows; timelineWindows;
private List<NotificationsWindow> private List<NotificationsWindow>
notificationWindows; notificationWindows;
private Clip private Clip
notificationSound; notificationSound;
private Connection private Connection
publicConn, publicConn,
@ -65,14 +65,14 @@ WindowUpdater {
userConn.reevaluate(); userConn.reevaluate();
} }
public synchronized void public synchronized void
add(NotificationsWindow updatee) add(NotificationsWindow updatee)
{ {
if (!notificationWindows.contains(updatee)) if (!notificationWindows.contains(updatee))
notificationWindows.add(updatee); notificationWindows.add(updatee);
userConn.reevaluate(); userConn.reevaluate();
} }
public synchronized void public synchronized void
remove(TimelineWindow updatee) remove(TimelineWindow updatee)
@ -82,7 +82,7 @@ WindowUpdater {
userConn.reevaluate(); userConn.reevaluate();
} }
public synchronized void public synchronized void
remove(NotificationsWindow updatee) remove(NotificationsWindow updatee)
{ {
notificationWindows.remove(updatee); notificationWindows.remove(updatee);
@ -144,21 +144,21 @@ WindowUpdater {
stop() stop()
{ {
stopping = true; stopping = true;
thread.interrupt(); thread.interrupt();
try try
{ {
thread.join(3000); thread.join(3000);
/* /*
* That thread should notice it is * That thread should notice it is
* interrupted ppromptly, and close. * interrupted ppromptly, and close.
*/ */
if (thread.isAlive()) printStackTrace(thread); if (thread.isAlive()) printStackTrace(thread);
} }
catch (InterruptedException eIt) catch (InterruptedException eIt)
{ {
assert false; assert false;
} }
thread = null; thread = null;
} }
public void public void
@ -186,21 +186,21 @@ WindowUpdater {
thread.notifyAll(); thread.notifyAll();
} }
event = new StringBuilder(); event = new StringBuilder();
data = new StringBuilder(); data = new StringBuilder();
api.monitorTimeline(type, this); api.monitorTimeline(type, this);
// monitorTimeline should not return until // monitorTimeline should not return until
// the connection is closed, or this thread // the connection is closed, or this thread
// is interrupted. // is interrupted.
System.err.println( System.err.println(
"Stopped monitoring." "Stopped monitoring."
+ thread + " " + Thread.currentThread() + thread + " " + Thread.currentThread()
); );
if (thread == Thread.currentThread()) thread = null; if (thread == Thread.currentThread()) thread = null;
/* /*
* This isn't thread safe. But I'd like the * This isn't thread safe. But I'd like the
* restart after sleep mode, so. * restart after sleep mode, so.
*/ */
} }
public void public void
@ -209,7 +209,7 @@ WindowUpdater {
if (line.startsWith(":")) return; if (line.startsWith(":")) return;
if (line.isEmpty()) if (line.isEmpty())
{ {
handle(event.toString(), data.toString()); handle(event.toString(), data.toString());
event.delete(0, event.length()); event.delete(0, event.length());
data.delete(0, event.length()); data.delete(0, event.length());
@ -299,38 +299,38 @@ WindowUpdater {
this.api = primaire.getMastodonApi(); this.api = primaire.getMastodonApi();
this.timelineWindows = new ArrayList<>(); this.timelineWindows = new ArrayList<>();
this.notificationWindows = new ArrayList<>(); this.notificationWindows = new ArrayList<>();
publicConn = new Connection(); publicConn = new Connection();
publicConn.type = TimelineType.FEDERATED; publicConn.type = TimelineType.FEDERATED;
userConn = new Connection(); userConn = new Connection();
userConn.type = TimelineType.HOME; userConn.type = TimelineType.HOME;
loadNotificationSound(); loadNotificationSound();
} }
void void
loadNotificationSound() loadNotificationSound()
{ {
URL url = getClass().getResource("KDE_Dialog_Appear.wav"); URL url = getClass().getResource("KDE_Dialog_Appear.wav");
try { try {
Clip clip = AudioSystem.getClip(); Clip clip = AudioSystem.getClip();
clip.open(AudioSystem.getAudioInputStream(url)); clip.open(AudioSystem.getAudioInputStream(url));
notificationSound = clip; notificationSound = clip;
} }
catch (LineUnavailableException eLu) { catch (LineUnavailableException eLu) {
assert false; assert false;
} }
catch (UnsupportedAudioFileException eUa) { catch (UnsupportedAudioFileException eUa) {
assert false; assert false;
} }
catch (IOException eIo) { catch (IOException eIo) {
assert false; assert false;
} }
catch (IllegalArgumentException eIa) { catch (IllegalArgumentException eIa) {
assert false; assert false;
} }
} }
} }