changeset 1965:7d091712bd82 default tip

fork DefaultHighlighter
author Franklin Schmidt <fschmidt@gmail.com>
date Sat, 07 Jun 2025 23:53:55 -0600
parents 593ebfad2ef4
children
files src/luan/modules/swing/DefaultHighlighter.java src/luan/modules/swing/TextAreaLuan.java
diffstat 2 files changed, 680 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/swing/DefaultHighlighter.java	Sat Jun 07 23:53:55 2025 -0600
@@ -0,0 +1,674 @@
+/*
+ * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
+ * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+package luan.modules.swing;
+
+import java.util.Vector;
+import java.awt.*;
+import javax.swing.plaf.*;
+import javax.swing.*;
+//import javax.swing.text.*;
+import javax.swing.text.LayeredHighlighter;
+import javax.swing.text.JTextComponent;
+import javax.swing.text.Highlighter;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.View;
+import javax.swing.text.Position;
+import javax.swing.text.Document;
+import goodjava.logging.Logger;
+import goodjava.logging.LoggerFactory;
+
+/**
+ * Implements the Highlighter interfaces.  Implements a simple highlight
+ * painter that renders in a solid color.
+ *
+ * @author  Timothy Prinzing - with hack fix by fschmidt
+ * @see     Highlighter
+ */
+public class DefaultHighlighter extends LayeredHighlighter {
+	private static final Logger logger = LoggerFactory.getLogger(DefaultHighlighter.class);
+
+	/**
+	 * Creates a new DefaultHighlighther object.
+	 */
+	public DefaultHighlighter() {
+		drawsLayeredHighlights = true;
+	}
+
+	// ---- Highlighter methods ----------------------------------------------
+
+	/**
+	 * Renders the highlights.
+	 *
+	 * @param g the graphics context
+	 */
+	public void paint(Graphics g) {
+		// PENDING(prinz) - should cull ranges not visible
+		int len = highlights.size();
+		for (int i = 0; i < len; i++) {
+			HighlightInfo info = highlights.elementAt(i);
+			if (!(info instanceof LayeredHighlightInfo)) {
+				// Avoid allocing unless we need it.
+				Rectangle a = component.getBounds();
+				Insets insets = component.getInsets();
+				a.x = insets.left;
+				a.y = insets.top;
+				a.width -= insets.left + insets.right;
+				a.height -= insets.top + insets.bottom;
+				for (; i < len; i++) {
+					info = highlights.elementAt(i);
+					if (!(info instanceof LayeredHighlightInfo)) {
+						Highlighter.HighlightPainter p = info.getPainter();
+						p.paint(g, info.getStartOffset(), info.getEndOffset(),
+								a, component);
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Called when the UI is being installed into the
+	 * interface of a JTextComponent.  Installs the editor, and
+	 * removes any existing highlights.
+	 *
+	 * @param c the editor component
+	 * @see Highlighter#install
+	 */
+	public void install(JTextComponent c) {
+		component = c;
+		removeAllHighlights();
+	}
+
+	/**
+	 * Called when the UI is being removed from the interface of
+	 * a JTextComponent.
+	 *
+	 * @param c the component
+	 * @see Highlighter#deinstall
+	 */
+	public void deinstall(JTextComponent c) {
+		component = null;
+	}
+
+	/**
+	 * Adds a highlight to the view.  Returns a tag that can be used
+	 * to refer to the highlight.
+	 *
+	 * @param p0   the start offset of the range to highlight &gt;= 0
+	 * @param p1   the end offset of the range to highlight &gt;= p0
+	 * @param p    the painter to use to actually render the highlight
+	 * @return     an object that can be used as a tag
+	 *   to refer to the highlight
+	 * @exception BadLocationException if the specified location is invalid
+	 */
+	public Object addHighlight(int p0, int p1, Highlighter.HighlightPainter p) throws BadLocationException {
+		if (p0 < 0) {
+			throw new BadLocationException("Invalid start offset", p0);
+		}
+
+		if (p1 < p0) {
+			throw new BadLocationException("Invalid end offset", p1);
+		}
+
+		Document doc = component.getDocument();
+		HighlightInfo i = (getDrawsLayeredHighlights() &&
+						   (p instanceof LayeredHighlighter.LayerPainter)) ?
+						  new LayeredHighlightInfo() : new HighlightInfo();
+		i.painter = p;
+		i.p0 = doc.createPosition(p0);
+		i.p1 = doc.createPosition(p1);
+		highlights.addElement(i);
+		safeDamageRange(p0, p1);
+		return i;
+	}
+
+	/**
+	 * Removes a highlight from the view.
+	 *
+	 * @param tag the reference to the highlight
+	 */
+	public void removeHighlight(Object tag) {
+		if (tag instanceof LayeredHighlightInfo) {
+			LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
+			if (lhi.width > 0 && lhi.height > 0) {
+				component.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
+			}
+		}
+		else {
+			HighlightInfo info = (HighlightInfo) tag;
+			safeDamageRange(info.p0, info.p1);
+		}
+		highlights.removeElement(tag);
+	}
+
+	/**
+	 * Removes all highlights.
+	 */
+	public void removeAllHighlights() {
+		TextUI mapper = component.getUI();
+		if (getDrawsLayeredHighlights()) {
+			int len = highlights.size();
+			if (len != 0) {
+				int minX = 0;
+				int minY = 0;
+				int maxX = 0;
+				int maxY = 0;
+				int p0 = -1;
+				int p1 = -1;
+				for (int i = 0; i < len; i++) {
+					HighlightInfo hi = highlights.elementAt(i);
+					if (hi instanceof LayeredHighlightInfo) {
+						LayeredHighlightInfo info = (LayeredHighlightInfo)hi;
+						minX = Math.min(minX, info.x);
+						minY = Math.min(minY, info.y);
+						maxX = Math.max(maxX, info.x + info.width);
+						maxY = Math.max(maxY, info.y + info.height);
+					}
+					else {
+						if (p0 == -1) {
+							p0 = hi.p0.getOffset();
+							p1 = hi.p1.getOffset();
+						}
+						else {
+							p0 = Math.min(p0, hi.p0.getOffset());
+							p1 = Math.max(p1, hi.p1.getOffset());
+						}
+					}
+				}
+				if (minX != maxX && minY != maxY) {
+					component.repaint(minX, minY, maxX - minX, maxY - minY);
+				}
+				if (p0 != -1) {
+					try {
+						safeDamageRange(p0, p1);
+					} catch (BadLocationException e) {}
+				}
+				highlights.removeAllElements();
+			}
+		}
+		else if (mapper != null) {
+			int len = highlights.size();
+			if (len != 0) {
+				int p0 = Integer.MAX_VALUE;
+				int p1 = 0;
+				for (int i = 0; i < len; i++) {
+					HighlightInfo info = highlights.elementAt(i);
+					p0 = Math.min(p0, info.p0.getOffset());
+					p1 = Math.max(p1, info.p1.getOffset());
+				}
+				try {
+					safeDamageRange(p0, p1);
+				} catch (BadLocationException e) {}
+
+				highlights.removeAllElements();
+			}
+		}
+	}
+
+	/**
+	 * Changes a highlight.
+	 *
+	 * @param tag the highlight tag
+	 * @param p0 the beginning of the range &gt;= 0
+	 * @param p1 the end of the range &gt;= p0
+	 * @exception BadLocationException if the specified location is invalid
+	 */
+	public void changeHighlight(Object tag, int p0, int p1) throws BadLocationException {
+		if (p0 < 0) {
+			throw new BadLocationException("Invalid beginning of the range", p0);
+		}
+
+		if (p1 < p0) {
+			throw new BadLocationException("Invalid end of the range", p1);
+		}
+
+		Document doc = component.getDocument();
+		if (tag instanceof LayeredHighlightInfo) {
+			LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
+			if (lhi.width > 0 && lhi.height > 0) {
+				component.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
+			}
+			// Mark the highlights region as invalid, it will reset itself
+			// next time asked to paint.
+			lhi.width = lhi.height = 0;
+			lhi.p0 = doc.createPosition(p0);
+			lhi.p1 = doc.createPosition(p1);
+			safeDamageRange(Math.min(p0, p1), Math.max(p0, p1));
+		}
+		else {
+			HighlightInfo info = (HighlightInfo) tag;
+			int oldP0 = info.p0.getOffset();
+			int oldP1 = info.p1.getOffset();
+			if (p0 == oldP0) {
+				safeDamageRange(Math.min(oldP1, p1),
+								   Math.max(oldP1, p1));
+			} else if (p1 == oldP1) {
+				safeDamageRange(Math.min(p0, oldP0),
+								   Math.max(p0, oldP0));
+			} else {
+				safeDamageRange(oldP0, oldP1);
+				safeDamageRange(p0, p1);
+			}
+			info.p0 = doc.createPosition(p0);
+			info.p1 = doc.createPosition(p1);
+		}
+	}
+
+	/**
+	 * Makes a copy of the highlights.  Does not actually clone each highlight,
+	 * but only makes references to them.
+	 *
+	 * @return the copy
+	 * @see Highlighter#getHighlights
+	 */
+	public Highlighter.Highlight[] getHighlights() {
+		int size = highlights.size();
+		if (size == 0) {
+			return noHighlights;
+		}
+		Highlighter.Highlight[] h = new Highlighter.Highlight[size];
+		highlights.copyInto(h);
+		return h;
+	}
+
+	/**
+	 * When leaf Views (such as LabelView) are rendering they should
+	 * call into this method. If a highlight is in the given region it will
+	 * be drawn immediately.
+	 *
+	 * @param g Graphics used to draw
+	 * @param p0 starting offset of view
+	 * @param p1 ending offset of view
+	 * @param viewBounds Bounds of View
+	 * @param editor JTextComponent
+	 * @param view View instance being rendered
+	 */
+	public void paintLayeredHighlights(Graphics g, int p0, int p1,
+									   Shape viewBounds,
+									   JTextComponent editor, View view) {
+		for (int counter = highlights.size() - 1; counter >= 0; counter--) {
+			HighlightInfo tag = highlights.elementAt(counter);
+			if (tag instanceof LayeredHighlightInfo) {
+				LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
+				int start = lhi.getStartOffset();
+				int end = lhi.getEndOffset();
+				if ((p0 < start && p1 > start) ||
+					(p0 >= start && p0 < end)) {
+					lhi.paintLayeredHighlights(g, p0, p1, viewBounds,
+											   editor, view);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Queues damageRange() call into event dispatch thread
+	 * to be sure that views are in consistent state.
+	 */
+	private void safeDamageRange(final Position p0, final Position p1) {
+		safeDamager.damageRange(p0, p1);
+	}
+
+	/**
+	 * Queues damageRange() call into event dispatch thread
+	 * to be sure that views are in consistent state.
+	 */
+	private void safeDamageRange(int a0, int a1) throws BadLocationException {
+		Document doc = component.getDocument();
+		safeDamageRange(doc.createPosition(a0), doc.createPosition(a1));
+	}
+
+	/**
+	 * If true, highlights are drawn as the Views draw the text. That is
+	 * the Views will call into <code>paintLayeredHighlight</code> which
+	 * will result in a rectangle being drawn before the text is drawn
+	 * (if the offsets are in a highlighted region that is). For this to
+	 * work the painter supplied must be an instance of
+	 * LayeredHighlightPainter.
+	 */
+	public void setDrawsLayeredHighlights(boolean newValue) {
+		drawsLayeredHighlights = newValue;
+	}
+
+	public boolean getDrawsLayeredHighlights() {
+		return drawsLayeredHighlights;
+	}
+
+	// ---- member variables --------------------------------------------
+
+	private final static Highlighter.Highlight[] noHighlights =
+			new Highlighter.Highlight[0];
+	private Vector<HighlightInfo> highlights = new Vector<HighlightInfo>();
+	private JTextComponent component;
+	private boolean drawsLayeredHighlights;
+	private SafeDamager safeDamager = new SafeDamager();
+
+
+	/**
+	 * Default implementation of LayeredHighlighter.LayerPainter that can
+	 * be used for painting highlights.
+	 * <p>
+	 * As of 1.4 this field is final.
+	 */
+	public static final LayeredHighlighter.LayerPainter DefaultPainter = new DefaultHighlightPainter(null);
+
+
+	/**
+	 * Simple highlight painter that fills a highlighted area with
+	 * a solid color.
+	 */
+	public static class DefaultHighlightPainter extends LayeredHighlighter.LayerPainter {
+
+		/**
+		 * Constructs a new highlight painter. If <code>c</code> is null,
+		 * the JTextComponent will be queried for its selection color.
+		 *
+		 * @param c the color for the highlight
+		 */
+		public DefaultHighlightPainter(Color c) {
+			color = c;
+		}
+
+		/**
+		 * Returns the color of the highlight.
+		 *
+		 * @return the color
+		 */
+		public Color getColor() {
+			return color;
+		}
+
+		// --- HighlightPainter methods ---------------------------------------
+
+		/**
+		 * Paints a highlight.
+		 *
+		 * @param g the graphics context
+		 * @param offs0 the starting model offset &gt;= 0
+		 * @param offs1 the ending model offset &gt;= offs1
+		 * @param bounds the bounding box for the highlight
+		 * @param c the editor
+		 */
+		public void paint(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
+			Rectangle alloc = bounds.getBounds();
+			try {
+				// --- determine locations ---
+				TextUI mapper = c.getUI();
+				Rectangle p0 = mapper.modelToView(c, offs0);
+				Rectangle p1 = mapper.modelToView(c, offs1);
+
+				// --- render ---
+				Color color = getColor();
+
+				if (color == null) {
+					g.setColor(c.getSelectionColor());
+				}
+				else {
+					g.setColor(color);
+				}
+				if (p0.y == p1.y) {
+					// same line, render a rectangle
+					Rectangle r = p0.union(p1);
+					g.fillRect(r.x, r.y, r.width, r.height);
+				} else {
+					// different lines
+					int p0ToMarginWidth = alloc.x + alloc.width - p0.x;
+					g.fillRect(p0.x, p0.y, p0ToMarginWidth, p0.height);
+					if ((p0.y + p0.height) != p1.y) {
+						g.fillRect(alloc.x, p0.y + p0.height, alloc.width,
+								   p1.y - (p0.y + p0.height));
+					}
+					g.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height);
+				}
+			} catch (BadLocationException e) {
+				// can't render
+			}
+		}
+
+		// --- LayerPainter methods ----------------------------
+		/**
+		 * Paints a portion of a highlight.
+		 *
+		 * @param g the graphics context
+		 * @param offs0 the starting model offset &gt;= 0
+		 * @param offs1 the ending model offset &gt;= offs1
+		 * @param bounds the bounding box of the view, which is not
+		 *        necessarily the region to paint.
+		 * @param c the editor
+		 * @param view View painting for
+		 * @return region drawing occurred in
+		 */
+		public Shape paintLayer(Graphics g, int offs0, int offs1,
+								Shape bounds, JTextComponent c, View view) {
+			Color color = getColor();
+
+			if (color == null) {
+				g.setColor(c.getSelectionColor());
+			}
+			else {
+				g.setColor(color);
+			}
+
+			Rectangle r;
+
+			if (offs0 == view.getStartOffset() &&
+				offs1 == view.getEndOffset()) {
+				// Contained in view, can just use bounds.
+				if (bounds instanceof Rectangle) {
+					r = (Rectangle) bounds;
+				}
+				else {
+					r = bounds.getBounds();
+				}
+			}
+			else {
+				// Should only render part of View.
+				try {
+					// --- determine locations ---
+					Shape shape = view.modelToView(offs0, Position.Bias.Forward,
+												   offs1,Position.Bias.Backward,
+												   bounds);
+					r = (shape instanceof Rectangle) ?
+								  (Rectangle)shape : shape.getBounds();
+					// view.modelToView is broken crap, so hack a fix - fschmidt
+					int height = c.getFontMetrics(c.getFont()).getHeight();
+					if( offs0 == offs1 && r.height >= 2*height ) {
+						r.y = r.y + r.height - height;
+						r.height = height;
+						r.width = 1;
+					}
+					// logger.info("offs0="+offs0+" offs1="+offs1+" shape="+shape+" bounds="+bounds);
+					// end hack
+				} catch (BadLocationException e) {
+					// can't render
+					r = null;
+				}
+			}
+
+			if (r != null) {
+				// If we are asked to highlight, we should draw something even
+				// if the model-to-view projection is of zero width (6340106).
+				r.width = Math.max(r.width, 1);
+
+				g.fillRect(r.x, r.y, r.width, r.height);
+			}
+
+			return r;
+		}
+
+		private Color color;
+
+	}
+
+
+	class HighlightInfo implements Highlighter.Highlight {
+
+		public int getStartOffset() {
+			return p0.getOffset();
+		}
+
+		public int getEndOffset() {
+			return p1.getOffset();
+		}
+
+		public Highlighter.HighlightPainter getPainter() {
+			return painter;
+		}
+
+		Position p0;
+		Position p1;
+		Highlighter.HighlightPainter painter;
+	}
+
+
+	/**
+	 * LayeredHighlightPainter is used when a drawsLayeredHighlights is
+	 * true. It maintains a rectangle of the region to paint.
+	 */
+	class LayeredHighlightInfo extends HighlightInfo {
+
+		void union(Shape bounds) {
+			if (bounds == null)
+				return;
+
+			Rectangle alloc;
+			if (bounds instanceof Rectangle) {
+				alloc = (Rectangle)bounds;
+			}
+			else {
+				alloc = bounds.getBounds();
+			}
+			if (width == 0 || height == 0) {
+				x = alloc.x;
+				y = alloc.y;
+				width = alloc.width;
+				height = alloc.height;
+			}
+			else {
+				width = Math.max(x + width, alloc.x + alloc.width);
+				height = Math.max(y + height, alloc.y + alloc.height);
+				x = Math.min(x, alloc.x);
+				width -= x;
+				y = Math.min(y, alloc.y);
+				height -= y;
+			}
+		}
+
+		/**
+		 * Restricts the region based on the receivers offsets and messages
+		 * the painter to paint the region.
+		 */
+		void paintLayeredHighlights(Graphics g, int p0, int p1,
+									Shape viewBounds, JTextComponent editor,
+									View view) {
+			int start = getStartOffset();
+			int end = getEndOffset();
+			// Restrict the region to what we represent
+			p0 = Math.max(start, p0);
+			p1 = Math.min(end, p1);
+			// Paint the appropriate region using the painter and union
+			// the effected region with our bounds.
+			union(((LayeredHighlighter.LayerPainter)painter).paintLayer
+				  (g, p0, p1, viewBounds, editor, view));
+		}
+
+		int x;
+		int y;
+		int width;
+		int height;
+	}
+
+	/**
+	 * This class invokes <code>mapper.damageRange</code> in
+	 * EventDispatchThread. The only one instance per Highlighter
+	 * is cretaed. When a number of ranges should be damaged
+	 * it collects them into queue and damages
+	 * them in consecutive order in <code>run</code>
+	 * call.
+	 */
+	class SafeDamager implements Runnable {
+		private Vector<Position> p0 = new Vector<Position>(10);
+		private Vector<Position> p1 = new Vector<Position>(10);
+		private Document lastDoc = null;
+
+		/**
+		 * Executes range(s) damage and cleans range queue.
+		 */
+		public synchronized void run() {
+			if (component != null) {
+				TextUI mapper = component.getUI();
+				if (mapper != null && lastDoc == component.getDocument()) {
+					// the Document should be the same to properly
+					// display highlights
+					int len = p0.size();
+					for (int i = 0; i < len; i++){
+						mapper.damageRange(component,
+								p0.get(i).getOffset(),
+								p1.get(i).getOffset());
+					}
+				}
+			}
+			p0.clear();
+			p1.clear();
+
+			// release reference
+			lastDoc = null;
+		}
+
+		/**
+		 * Adds the range to be damaged into the range queue. If the
+		 * range queue is empty (the first call or run() was already
+		 * invoked) then adds this class instance into EventDispatch
+		 * queue.
+		 *
+		 * The method also tracks if the current document changed or
+		 * component is null. In this case it removes all ranges added
+		 * before from range queue.
+		 */
+		public synchronized void damageRange(Position pos0, Position pos1) {
+			if (component == null) {
+				p0.clear();
+				lastDoc = null;
+				return;
+			}
+
+			boolean addToQueue = p0.isEmpty();
+			Document curDoc = component.getDocument();
+			if (curDoc != lastDoc) {
+				if (!p0.isEmpty()) {
+					p0.clear();
+					p1.clear();
+				}
+				lastDoc = curDoc;
+			}
+			p0.add(pos0);
+			p1.add(pos1);
+
+			if (addToQueue) {
+				SwingUtilities.invokeLater(this);
+			}
+		}
+	}
+}
--- a/src/luan/modules/swing/TextAreaLuan.java	Fri Jun 06 19:19:36 2025 -0600
+++ b/src/luan/modules/swing/TextAreaLuan.java	Sat Jun 07 23:53:55 2025 -0600
@@ -19,6 +19,7 @@
 import javax.swing.text.WrappedPlainView;
 import javax.swing.text.Segment;
 import javax.swing.text.DefaultEditorKit;
+import javax.swing.text.Highlighter;
 import javax.swing.event.DocumentEvent;
 import goodjava.logging.Logger;
 import goodjava.logging.LoggerFactory;
@@ -44,6 +45,10 @@
 			blinkTimer.stop();
 			setVisible(true);
 		}
+
+		@Override protected Highlighter.HighlightPainter getSelectionPainter() {
+			return DefaultHighlighter.DefaultPainter;
+		}
 	};
 
 	public static class Range {
@@ -111,6 +116,7 @@
 	public TextAreaLuan() {
 		super();
 		//logger.info(""+getFont().getSize());
+		setHighlighter(new DefaultHighlighter());
 		if( UIManager.getLookAndFeel().getName().startsWith("FlatLaf") ) {
 			setCaret(flatLafCaret);
 		}