// -*- mode:java; encoding:utf-8 -*-
// vim:set fileencoding=utf-8:
// @homepage@
package example;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.Area;
import java.awt.geom.PathIterator;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.plaf.TextUI;
import javax.swing.text.*;
import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;
import javax.swing.text.Highlighter.HighlightPainter;
public final class MainPanel extends JPanel {
private static final String TEXT = String.join("\n",
"Trail: Creating a GUI with JFC/Swing",
"Lesson: Learning Swing by Example",
"This lesson explains the concepts you need to",
" use Swing components in building a user interface.",
" First we examine the simplest Swing application you can write.",
" Then we present several progressively complicated examples of creating",
" user interfaces using components in the javax.swing package.",
" We cover several Swing components, such as buttons, labels, and text areas.",
" The handling of events is also discussed,",
" as are layout management and accessibility.",
" This lesson ends with a set of questions and exercises",
" so you can test yourself on what you've learned.",
"https://docs.oracle.com/javase/tutorial/uiswing/learn/index.html");
private MainPanel() {
super(new BorderLayout(5, 5));
JTextArea textArea = new JTextArea(TEXT) {
@Override public void updateUI() {
super.updateUI();
Caret caret = new RoundedSelectionCaret();
caret.setBlinkRate(UIManager.getInt("TextArea.caretBlinkRate"));
setCaret(caret);
((DefaultHighlighter) getHighlighter()).setDrawsLayeredHighlights(false);
}
};
JCheckBox check = new JCheckBox("setLineWrap / setWrapStyleWord:");
check.addActionListener(e -> {
boolean b = ((JCheckBox) e.getSource()).isSelected();
textArea.setLineWrap(b);
textArea.setWrapStyleWord(b);
});
add(new JScrollPane(textArea));
add(check, BorderLayout.SOUTH);
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
setPreferredSize(new Dimension(320, 240));
}
public static void main(String[] args) {
EventQueue.invokeLater(MainPanel::createAndShowGui);
}
private static void createAndShowGui() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (UnsupportedLookAndFeelException ignored) {
Toolkit.getDefaultToolkit().beep();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
Logger.getGlobal().severe(ex::getMessage);
return;
}
JFrame frame = new JFrame("@title@");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.getContentPane().add(new MainPanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
class RoundedSelectionCaret extends DefaultCaret {
@Override protected HighlightPainter getSelectionPainter() {
return new RoundedSelectionHighlightPainter();
}
@Override protected synchronized void damage(Rectangle r) {
JTextComponent c = getComponent();
int startOffset = c.getSelectionStart();
int endOffset = c.getSelectionEnd();
if (startOffset == endOffset) {
super.damage(r);
} else {
TextUI mapper = c.getUI();
try {
Rectangle p0 = mapper.modelToView(c, startOffset);
Rectangle p1 = mapper.modelToView(c, endOffset);
int h = (int) (p1.getMaxY() - p0.getMinY());
Rectangle rr = new Rectangle(0, p0.y, c.getWidth(), h);
c.repaint(rr);
} catch (BadLocationException ex) {
UIManager.getLookAndFeel().provideErrorFeedback(c);
}
}
}
}
class RoundedSelectionHighlightPainter extends DefaultHighlightPainter {
protected RoundedSelectionHighlightPainter() {
super(null);
}
@Override public void paint(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color color = c.getSelectionColor();
g2.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 200));
try {
Area area = getLinesArea(c, offs0, offs1);
for (Area a : GeomUtils.singularization(area)) {
List<Point2D> lst = GeomUtils.convertAreaToPoint2DList(a);
GeomUtils.flatteningStepsOnRightSide(lst, 3d * 2d);
g2.fill(GeomUtils.convertRoundedPath(lst, 3d));
}
} catch (BadLocationException ex) {
// can't render
Logger.getGlobal().severe(ex::getMessage);
}
g2.dispose();
}
private static Area getLinesArea(JTextComponent c, int offs0, int offs1)
throws BadLocationException {
TextUI mapper = c.getUI();
Area area = new Area();
int cur = offs0;
do {
int startOffset = Utilities.getRowStart(c, cur);
int endOffset = Utilities.getRowEnd(c, cur);
Rectangle p0 = mapper.modelToView(c, Math.max(startOffset, offs0));
Rectangle p1 = mapper.modelToView(c, Math.min(endOffset, offs1));
if (p0.x != p1.x) {
area.add(new Area(p0.union(p1)));
} else {
p0.width += 6;
area.add(new Area(p0));
}
cur = endOffset + 1;
} while (cur < offs1);
return area;
}
}
final class GeomUtils {
private GeomUtils() {
/* Singleton */
}
public static List<Point2D> convertAreaToPoint2DList(Area area) {
List<Point2D> list = new ArrayList<>();
PathIterator pi = area.getPathIterator(null);
double[] cd = new double[6];
while (!pi.isDone()) {
switch (pi.currentSegment(cd)) {
case PathIterator.SEG_MOVETO:
case PathIterator.SEG_LINETO:
list.add(new Point2D.Double(cd[0], cd[1]));
break;
default:
break;
}
pi.next();
}
return list;
}
public static void flatteningStepsOnRightSide(List<Point2D> list, double arc) {
int sz = list.size();
for (int i = 0; i < sz; i++) {
int i1 = (i + 1) % sz;
int i2 = (i + 2) % sz;
int i3 = (i + 3) % sz;
Point2D pt0 = list.get(i);
Point2D pt1 = list.get(i1);
Point2D pt2 = list.get(i2);
Point2D pt3 = list.get(i3);
double dx1 = pt2.getX() - pt1.getX();
if (Math.abs(dx1) > 1.0e-1 && Math.abs(dx1) < arc) {
double max = Math.max(pt0.getX(), pt2.getX());
replace(list, i, max, pt0.getY());
replace(list, i1, max, pt1.getY());
replace(list, i2, max, pt2.getY());
replace(list, i3, max, pt3.getY());
}
}
}
private static void replace(List<Point2D> list, int i, double x, double y) {
list.remove(i);
list.add(i, new Point2D.Double(x, y));
}
/**
* Rounding the corners of a Rectilinear Polygon.
*/
public static Path2D convertRoundedPath(List<Point2D> list, double arc) {
double kappa = 4d * (Math.sqrt(2d) - 1d) / 3d; // = 0.55228...;
double akv = arc - arc * kappa;
int sz = list.size();
Point2D pt0 = list.get(0);
Path2D path = new Path2D.Double();
path.moveTo(pt0.getX() + arc, pt0.getY());
for (int i = 0; i < sz; i++) {
Point2D prv = list.get((i - 1 + sz) % sz);
Point2D cur = list.get(i);
Point2D nxt = list.get((i + 1) % sz);
double dx0 = signum(cur.getX() - prv.getX(), arc);
double dy0 = signum(cur.getY() - prv.getY(), arc);
double dx1 = signum(nxt.getX() - cur.getX(), arc);
double dy1 = signum(nxt.getY() - cur.getY(), arc);
path.curveTo(
cur.getX() - dx0 * akv, cur.getY() - dy0 * akv,
cur.getX() + dx1 * akv, cur.getY() + dy1 * akv,
cur.getX() + dx1 * arc, cur.getY() + dy1 * arc);
path.lineTo(nxt.getX() - dx1 * arc, nxt.getY() - dy1 * arc);
}
path.closePath();
return path;
}
private static double signum(double v, double arc) {
return Math.abs(v) < arc ? 0d : Math.signum(v);
}
public static List<Area> singularization(Area rect) {
List<Area> list = new ArrayList<>();
Path2D path = new Path2D.Double();
PathIterator pi = rect.getPathIterator(null);
double[] cd = new double[6];
while (!pi.isDone()) {
switch (pi.currentSegment(cd)) {
case PathIterator.SEG_MOVETO:
path.moveTo(cd[0], cd[1]);
break;
case PathIterator.SEG_LINETO:
path.lineTo(cd[0], cd[1]);
break;
case PathIterator.SEG_QUADTO:
path.quadTo(cd[0], cd[1], cd[2], cd[3]);
break;
case PathIterator.SEG_CUBICTO:
path.curveTo(cd[0], cd[1], cd[2], cd[3], cd[4], cd[5]);
break;
case PathIterator.SEG_CLOSE:
path.closePath();
list.add(new Area(path));
path.reset();
break;
default:
break;
}
pi.next();
}
return list;
}
}