More or less finished HTML text pane.
@ -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<String> addee = new Tree<>();
|
||||
@ -173,7 +175,7 @@ BasicHTMLParser {
|
||||
}
|
||||
else if (c == ':' && emoji)
|
||||
{
|
||||
assert letter;
|
||||
assert !space;
|
||||
b.append(c);
|
||||
Tree<String> 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<String> 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<String> addee = new Tree<>();
|
||||
addee.key = "space";
|
||||
addee.value = empty(b);
|
||||
returnee.add(addee);
|
||||
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
|
||||
@ -219,7 +214,7 @@ BasicHTMLParser {
|
||||
* characters like \n, but I'll opt not to.
|
||||
*/
|
||||
|
||||
letter = cletter;
|
||||
alnum = calnum;
|
||||
space = cspace;
|
||||
}
|
||||
if (b.length() > 0)
|
||||
@ -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
|
||||
|
@ -140,6 +140,7 @@ PostWindow extends JFrame {
|
||||
emojis.put(entry[0], ImageApi.remote(entry[1]));
|
||||
}
|
||||
test2.setEmojis(emojis);
|
||||
test2.requestFocusInWindow();
|
||||
}
|
||||
|
||||
public void
|
||||
|
@ -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<String>
|
||||
html;
|
||||
@ -26,23 +35,46 @@ implements ComponentListener {
|
||||
private Map<Tree<String>, Point>
|
||||
layout;
|
||||
|
||||
private Tree<String>
|
||||
layoutEnd, selStart, selEnd;
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public void
|
||||
setText(Tree<String> 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<String> nodes = html.get("children");
|
||||
if (nodes.size() > 0)
|
||||
{
|
||||
Tree<String> 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<String> node, FontMetrics fm, Point cursor)
|
||||
{
|
||||
assert cursor != null;
|
||||
int lh = fm.getAscent() + fm.getDescent();
|
||||
|
||||
if (node.key.equals("space"))
|
||||
@ -169,31 +202,43 @@ implements ComponentListener {
|
||||
String tagName = node.get(0).key;
|
||||
Tree<String> 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<String> child: children)
|
||||
{
|
||||
layout(child, fm, cursor);
|
||||
// Shallow copy this child node.
|
||||
Tree<String> aug = new Tree<>();
|
||||
aug.key = child.key;
|
||||
aug.value = child.value;
|
||||
for (Tree<String> 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<String> 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<String> 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,6 +334,154 @@ 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<String>
|
||||
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<String> returnee = null;
|
||||
|
||||
int maxX = 0;
|
||||
for (Tree<String> 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<Tree<String>> selected = new ArrayList<>();
|
||||
List<Point> points = new ArrayList<>();
|
||||
for (Tree<String> 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<String> 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<String> 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); }
|
||||
|
||||
@ -254,14 +494,32 @@ implements ComponentListener {
|
||||
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<String>());
|
||||
this.addComponentListener(this);
|
||||
this.addMouseListener(this);
|
||||
this.addMouseMotionListener(this);
|
||||
this.addKeyListener(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
|
0
graphics/Federated.xcf
Executable file → Normal file
0
graphics/Flags.xcf
Executable file → Normal file
0
graphics/Hourglass.xcf
Executable file → Normal file
0
graphics/boostToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
0
graphics/boostUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
0
graphics/button.png
Executable file → Normal file
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
0
graphics/disabledOverlay.png
Executable file → Normal file
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
0
graphics/favouriteToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 353 B |
0
graphics/favouriteUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
0
graphics/federated.png
Executable file → Normal file
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
0
graphics/miscToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
0
graphics/miscUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
0
graphics/ref1.png
Executable file → Normal file
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
0
graphics/replyToggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
0
graphics/replyUntoggled.png
Executable file → Normal file
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
0
graphics/selectedOverlay.png
Executable file → Normal file
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 313 B |
0
graphics/test1.png
Executable file → Normal file
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
0
graphics/test2.png
Executable file → Normal file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
0
graphics/test3.png
Executable file → Normal file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
0
graphics/test4.png
Executable file → Normal file
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |