diff --git a/BasicHTMLParser.java b/BasicHTMLParser.java
index 3d70e0e..d62b5f6 100644
--- a/BasicHTMLParser.java
+++ b/BasicHTMLParser.java
@@ -15,7 +15,7 @@ BasicHTMLParser {
{
List segments;
segments = distinguishTagsFromPcdata(html);
-
+
Tree document;
document = toNodes(segments);
document = splitText(document);
@@ -151,16 +151,18 @@ BasicHTMLParser {
assert node.key.equals("text");
StringBuilder b = new StringBuilder();
- boolean letter = false, cletter;
+ boolean alnum = false, calnum;
boolean space = false, cspace;
boolean emoji = false;
for (char c: node.value.toCharArray())
{
- cletter = isLetter(c);
+ calnum = isMastodonAlnum(c);
cspace = Character.isWhitespace(c);
- if (c == ':' && !emoji && !letter)
+ if (c == ':' && !emoji)
{
+ // See note on #isMastodonAlnum.
+
if (b.length() > 0)
{
Tree addee = new Tree<>();
@@ -173,7 +175,7 @@ BasicHTMLParser {
}
else if (c == ':' && emoji)
{
- assert letter;
+ assert !space;
b.append(c);
Tree addee = new Tree<>();
addee.key = "emoji";
@@ -190,24 +192,17 @@ BasicHTMLParser {
* an empty emoji is the correct action.
*/
emoji = false;
- cletter = false;
+ calnum = false;
}
- else if (cspace && letter)
+ else if (cspace != space)
{
- assert b.length() > 0;
- Tree addee = new Tree<>();
- addee.key = "text";
- addee.value = empty(b);
- returnee.add(addee);
- b.append(c);
- }
- else if (cletter && space)
- {
- assert b.length() > 0;
- Tree addee = new Tree<>();
- addee.key = "space";
- addee.value = empty(b);
- returnee.add(addee);
+ if (b.length() > 0)
+ {
+ Tree addee = new Tree<>();
+ addee.key = space ? "space" : "text";
+ addee.value = empty(b);
+ returnee.add(addee);
+ }
b.append(c);
}
else
@@ -219,8 +214,8 @@ BasicHTMLParser {
* characters like \n, but I'll opt not to.
*/
- letter = cletter;
- space = cspace;
+ alnum = calnum;
+ space = cspace;
}
if (b.length() > 0)
{
@@ -257,7 +252,7 @@ BasicHTMLParser {
root.key = "tag";
root.add(new Tree<>("html", null));
root.add(new Tree<>("children", null));
-
+
Deque> parents = new LinkedList<>();
parents.push(root);
for (Tree node: nodes)
@@ -315,25 +310,20 @@ BasicHTMLParser {
}
private static boolean
- isPunctuation(char c)
+ isMastodonAlnum(char c)
{
- switch (Character.getType(c))
- {
- case Character.START_PUNCTUATION:
- case Character.END_PUNCTUATION:
- case Character.DASH_PUNCTUATION:
- case Character.CONNECTOR_PUNCTUATION:
- case Character.INITIAL_QUOTE_PUNCTUATION:
- case Character.FINAL_QUOTE_PUNCTUATION:
- case Character.OTHER_PUNCTUATION: return true;
- default: return false;
- }
- }
-
- private static boolean
- isLetter(char c)
- {
- return Character.isLetter(c) || isPunctuation(c);
+ return Character.isLetterOrDigit(c);
+ /*
+ * Not joking. Mastodon is using the POSIX :alnum: regex
+ * character class here (/app/lib/emoji_formatter.rb;
+ * ruby-doc§Regexp). It prevents emojis preceeded by
+ * Japanese like さ too, but not punctuation like tildes
+ * or full stops. This is server-enforced, the web client
+ * does string substitution and supports anything.
+ * (To see this, make a post with an emoji preceeded
+ * by text, then try again with the same emoji also
+ * present elsewhere in the post at a valid position.)
+ */
}
private static String
@@ -374,4 +364,4 @@ BasicHTMLParser {
if (!v.isEmpty()) whole.append(v);
return whole.toString();
}
-}
\ No newline at end of file
+}
diff --git a/PostWindow.java b/PostWindow.java
index 0166e2d..bc750db 100644
--- a/PostWindow.java
+++ b/PostWindow.java
@@ -140,6 +140,7 @@ PostWindow extends JFrame {
emojis.put(entry[0], ImageApi.remote(entry[1]));
}
test2.setEmojis(emojis);
+ test2.requestFocusInWindow();
}
public void
diff --git a/RichTextPane3.java b/RichTextPane3.java
index 9cda6f5..fc5fd7f 100644
--- a/RichTextPane3.java
+++ b/RichTextPane3.java
@@ -5,17 +5,26 @@ import java.awt.Point;
import java.awt.FontMetrics;
import java.awt.Font;
import java.awt.Image;
+import java.awt.Color;
import java.awt.event.ComponentListener;
import java.awt.event.ComponentEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.KeyEvent;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
+import java.util.ArrayList;
import cafe.biskuteri.hinoki.Tree;
class
RichTextPane3 extends JComponent
-implements ComponentListener {
+implements
+ ComponentListener,
+ MouseListener, MouseMotionListener, KeyListener {
private Tree
html;
@@ -26,23 +35,46 @@ implements ComponentListener {
private Map, Point>
layout;
+ private Tree
+ layoutEnd, selStart, selEnd;
+
// ---%-@-%---
public void
setText(Tree html)
{
- assert html != null;
+ assert html != null;
this.html = html;
if (!isValid()) return;
+ assert html.key.equals("tag");
+ assert html.get("children") != null;
+
FontMetrics fm = getFontMetrics(getFont());
int iy = fm.getAscent();
- int fph = (fm.getAscent() + fm.getDescent()) * 3/2;
- Point cursor = new Point(0, iy - fph);
+ Point cursor = new Point(0, iy);
+
+ Tree nodes = html.get("children");
+ if (nodes.size() > 0)
+ {
+ Tree first = nodes.get(0);
+ if (first.key.equals("tag"))
+ {
+ String tagName = first.get(0).key;
+ if (tagName.equals("p"))
+ {
+ int lh = fm.getAscent() + fm.getDescent();
+ cursor.y -= lh * 2;
+ }
+ }
+ }
+
+ selStart = selEnd = null;
layout.clear();
layout(html, fm, cursor);
+ layout.put(layoutEnd, new Point(cursor));
repaint();
}
@@ -59,6 +91,7 @@ implements ComponentListener {
private void
layout(Tree node, FontMetrics fm, Point cursor)
{
+ assert cursor != null;
int lh = fm.getAscent() + fm.getDescent();
if (node.key.equals("space"))
@@ -104,7 +137,7 @@ implements ComponentListener {
cursor.y += lh;
cursor.x = 0;
}
-
+
while (rem.length() > 0)
{
int l = 2;
@@ -116,7 +149,7 @@ implements ComponentListener {
}
String substr = rem.substring(0, --l);
w = fm.stringWidth(substr);
-
+
Tree temp = new Tree<>();
temp.key = node.key;
temp.value = substr;
@@ -169,31 +202,43 @@ implements ComponentListener {
String tagName = node.get(0).key;
Tree children = node.get("children");
+ // We won't place tag nodes on the layout.
+
if (tagName.equals("br"))
{
- layout.put(node, new Point(cursor));
cursor.y += lh;
cursor.x = 0;
}
else if (tagName.equals("p"))
{
- layout.put(node, new Point(cursor));
- cursor.y += lh * 3/2;
+ //cursor.y += lh * 3/2;
+ cursor.y += lh * 2;
+ // Our selection algorithm assumes equidistant
+ // lines. Laziest fix is collect and sort line
+ // Ys from height.
cursor.x = 0;
}
- else if (tagName.equals("a"))
- {
- layout.put(node, new Point(cursor));
- // For now we ignore the link.
- }
- else if (tagName.equals("span"))
- {
- layout.put(node, new Point(cursor));
- }
for (Tree child: children)
{
- layout(child, fm, cursor);
+ // Shallow copy this child node.
+ Tree aug = new Tree<>();
+ aug.key = child.key;
+ aug.value = child.value;
+ for (Tree gc: child) aug.add(gc);
+
+ // Append all of our attributes. We'd like those
+ // like href to end up at the text nodes. This
+ // might collide with our child node's attributes,
+ // for now I'll assume that's not an issue.
+ for (int o = 1; o < node.size(); ++o)
+ {
+ Tree attr = node.get(o);
+ if (attr == children) continue;
+ aug.add(attr);
+ }
+
+ layout(aug, fm, cursor);
}
}
else assert false;
@@ -202,6 +247,10 @@ implements ComponentListener {
protected void
paintComponent(Graphics g)
{
+ final Color LINK_COLOUR = Color.BLUE;
+ final Color PLAIN_COLOUR = getForeground();
+ final Color SEL_COLOUR = new Color(0, 0, 0, 25);
+
g.setFont(getFont());
FontMetrics fm = g.getFontMetrics();
@@ -210,16 +259,59 @@ implements ComponentListener {
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
);
+ if (selEnd != null)
+ {
+ Point ssp = layout.get(selStart);
+ assert ssp != null;
+ Point sep = layout.get(selEnd);
+ assert sep != null;
+ /*
+ * (知) One way these can go null is if we clear
+ * the layout but don't clear the selection.
+ */
+
+ boolean flip = ssp.y > sep.y;
+ flip |= sep.y == ssp.y && sep.x < ssp.x;
+ if (flip)
+ {
+ Point temp = ssp;
+ ssp = sep;
+ sep = ssp;
+ }
+
+ int w = getWidth();
+ int asc = fm.getAscent();
+ int lh = fm.getAscent() + fm.getDescent();
+ g.setColor(SEL_COLOUR);
+ if (ssp.y == sep.y)
+ {
+ g.fillRect(ssp.x, ssp.y - asc, sep.x - ssp.x, lh);
+ }
+ else
+ {
+ g.fillRect(ssp.x, ssp.y - asc, w - ssp.x, lh);
+ for (int y = ssp.y + lh; y < sep.y; y += lh)
+ {
+ g.fillRect(0, y - asc, w, lh);
+ }
+ g.fillRect(0, sep.y - asc, sep.x, lh);
+ }
+ }
+
+ g.setColor(getForeground());
for (Tree node: layout.keySet())
{
- if (node.key.equals("text"))
+ if (node.key.equals("text"))
{
- Point point = layout.get(node);
+ boolean isLink = node.get("href") != null;
+ if (isLink) g.setColor(LINK_COLOUR);
+ Point point = layout.get(node);
g.drawString(node.value, point.x, point.y);
+ if (isLink) g.setColor(PLAIN_COLOUR);
}
else if (node.key.equals("emoji"))
{
- Point point = layout.get(node);
+ Point point = layout.get(node);
Image image = emojis.get(node.value);
if (image != null)
{
@@ -242,26 +334,192 @@ implements ComponentListener {
}
}
+ public void
+ mousePressed(MouseEvent eM)
+ {
+ selStart = identifyNodeAt(eM.getX(), eM.getY());
+ selEnd = null;
+ repaint();
+ }
+
+ public void
+ mouseDragged(MouseEvent eM)
+ {
+ if (selStart == null) return;
+ selEnd = identifyNodeAt(eM.getX(), eM.getY());
+ if (selEnd == null) selEnd = layoutEnd;
+ repaint();
+ }
+
+ private Tree
+ identifyNodeAt(int x, int y)
+ {
+ FontMetrics fm = getFontMetrics(getFont());
+ int initial = fm.getAscent();
+ int advance = fm.getAscent() + fm.getDescent();
+ y = snap(y, initial, advance);
+
+ Tree returnee = null;
+
+ int maxX = 0;
+ for (Tree node: layout.keySet())
+ {
+ Point point = layout.get(node);
+ assert point != null;
+
+ if (point.y != y) continue;
+ if (point.x > x) continue;
+ if (point.x >= maxX)
+ {
+ maxX = point.x;
+ returnee = node;
+ }
+ }
+
+ return returnee;
+ }
+
+ public void
+ keyPressed(KeyEvent eK)
+ {
+ if (!eK.isControlDown()) return;
+ switch (eK.getKeyCode())
+ {
+ case KeyEvent.VK_C:
+ ClipboardApi.serve(getSelectedText());
+ break;
+ case KeyEvent.VK_A:
+ selStart = identifyNodeAt(0, 0);
+ selEnd = layoutEnd;
+ repaint();
+ break;
+ }
+ }
+
+ private String
+ getSelectedText()
+ {
+ assert selStart != null && selEnd != null;
+
+ Point ssp = layout.get(selStart);
+ Point sep = layout.get(selEnd);
+ assert ssp != null && sep != null;
+ boolean flip = ssp.y > sep.y;
+ flip |= sep.y == ssp.y && sep.x < ssp.x;
+ if (flip)
+ {
+ Point temp = ssp;
+ ssp = sep;
+ sep = ssp;
+ }
+
+ List> selected = new ArrayList<>();
+ List points = new ArrayList<>();
+ for (Tree node: layout.keySet())
+ {
+ Point point = layout.get(node);
+ assert point != null;
+
+ boolean c1 = point.y == ssp.y && point.x >= ssp.x;
+ boolean c2 = point.y == sep.y && point.x < sep.x;
+ boolean c3 = point.y > ssp.y && point.y < sep.y;
+ if (!(c1 || c2 || c3)) continue;
+
+ // Just throw them in a pile for now..
+ selected.add(node);
+ points.add(point);
+ }
+
+ // Now sort them into reading order.
+ Tree n1, n2;
+ Point p1, p2;
+ for (int eo = 1; eo < points.size(); ++eo)
+ for (int o = points.size() - 1; o >= eo; --o)
+ {
+ n1 = selected.get(o - 1); n2 = selected.get(o);
+ p1 = points.get(o - 1); p2 = points.get(o);
+
+ boolean c1 = p2.y < p1.y;
+ boolean c2 = p2.y == p1.y && p2.x < p1.x;
+ if (!(c1 || c2)) continue;
+
+ selected.set(o - 1, n2);
+ selected.set(o, n1);
+ points.set(o - 1, p2);
+ points.set(o, p1);
+ }
+
+ StringBuilder b = new StringBuilder();
+ for (Tree node: selected)
+ {
+ boolean t = node.key.equals("text");
+ boolean e = node.key.equals("emoji");
+ boolean s = node.key.equals("space");
+ assert t || e || s;
+ b.append(node.value); // Same behaviour for all.
+ }
+ return b.toString();
+ }
+
+ public void
+ keyReleased(KeyEvent eK) { }
+
+ public void
+ keyTyped(KeyEvent eK) { }
+
+ public void
+ mouseReleased(MouseEvent eM) { }
+
+ public void
+ mouseClicked(MouseEvent eM) { }
+
+ public void
+ mouseMoved(MouseEvent eM) { }
+
+ public void
+ mouseEntered(MouseEvent eM) { }
+
+ public void
+ mouseExited(MouseEvent eM) { }
+
public void
componentResized(ComponentEvent eC) { setText(html); }
public void
componentMoved(ComponentEvent eC) { }
-
+
public void
componentShown(ComponentEvent eC) { }
-
+
public void
componentHidden(ComponentEvent eC) { }
+// - -%- -
+
+ private static int
+ snap(int value, int initial, int advance)
+ {
+ int offset = value - initial;
+ if (offset <= 0) offset = 0;
+ else {
+ int lines = 1 + ((offset - 1) / advance);
+ offset = advance * lines;
+ }
+ return initial + offset;
+ }
+
// ---%-@-%---
RichTextPane3()
{
layout = new HashMap<>();
+ layoutEnd = new Tree<>("text", "");
emojis = new HashMap<>();
setText(new Tree());
this.addComponentListener(this);
+ this.addMouseListener(this);
+ this.addMouseMotionListener(this);
+ this.addKeyListener(this);
}
}
diff --git a/WindowUpdater.java b/WindowUpdater.java
index 3fbb4f4..3125e1a 100644
--- a/WindowUpdater.java
+++ b/WindowUpdater.java
@@ -279,7 +279,8 @@ WindowUpdater {
void
loadNotificationSound()
{
- URL url = getClass().getResource("KDE_Dialog_Appear.wav");
+ //URL url = getClass().getResource("KDE_Dialog_Appear.wav");
+ URL url = getClass().getResource("LinkinPark.wav");
try {
Clip clip = AudioSystem.getClip();
clip.open(AudioSystem.getAudioInputStream(url));
diff --git a/graphics/Federated.xcf b/graphics/Federated.xcf
old mode 100755
new mode 100644
diff --git a/graphics/Flags.xcf b/graphics/Flags.xcf
old mode 100755
new mode 100644
diff --git a/graphics/Hourglass.xcf b/graphics/Hourglass.xcf
old mode 100755
new mode 100644
diff --git a/graphics/boostToggled.png b/graphics/boostToggled.png
old mode 100755
new mode 100644
diff --git a/graphics/boostUntoggled.png b/graphics/boostUntoggled.png
old mode 100755
new mode 100644
diff --git a/graphics/button.png b/graphics/button.png
old mode 100755
new mode 100644
diff --git a/graphics/disabledOverlay.png b/graphics/disabledOverlay.png
old mode 100755
new mode 100644
diff --git a/graphics/favouriteToggled.png b/graphics/favouriteToggled.png
old mode 100755
new mode 100644
diff --git a/graphics/favouriteUntoggled.png b/graphics/favouriteUntoggled.png
old mode 100755
new mode 100644
diff --git a/graphics/federated.png b/graphics/federated.png
old mode 100755
new mode 100644
diff --git a/graphics/miscToggled.png b/graphics/miscToggled.png
old mode 100755
new mode 100644
diff --git a/graphics/miscUntoggled.png b/graphics/miscUntoggled.png
old mode 100755
new mode 100644
diff --git a/graphics/ref1.png b/graphics/ref1.png
old mode 100755
new mode 100644
diff --git a/graphics/replyToggled.png b/graphics/replyToggled.png
old mode 100755
new mode 100644
diff --git a/graphics/replyUntoggled.png b/graphics/replyUntoggled.png
old mode 100755
new mode 100644
diff --git a/graphics/selectedOverlay.png b/graphics/selectedOverlay.png
old mode 100755
new mode 100644
diff --git a/graphics/test1.png b/graphics/test1.png
old mode 100755
new mode 100644
diff --git a/graphics/test2.png b/graphics/test2.png
old mode 100755
new mode 100644
diff --git a/graphics/test3.png b/graphics/test3.png
old mode 100755
new mode 100644
diff --git a/graphics/test4.png b/graphics/test4.png
old mode 100755
new mode 100644