view src/luan/modules/swing/TextAreaLuan.java @ 1957:269e78ad8a85

swing for windows
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 29 May 2025 22:29:15 -0600
parents b785eff96faf
children
line wrap: on
line source

package luan.modules.swing;

import java.util.List;
import java.util.Collections;
import java.awt.Rectangle;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.event.FocusListener;
import java.awt.event.FocusEvent;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.Timer;
import javax.swing.text.DefaultCaret;
import javax.swing.text.BadLocationException;
import javax.swing.text.View;
import javax.swing.text.Element;
import javax.swing.text.WrappedPlainView;
import javax.swing.text.Segment;
import javax.swing.text.DefaultEditorKit;
import javax.swing.event.DocumentEvent;
import goodjava.logging.Logger;
import goodjava.logging.LoggerFactory;


public class TextAreaLuan extends JTextArea implements DocumentUpdateListener {
	private static final Logger logger = LoggerFactory.getLogger(TextAreaLuan.class);

	private final DefaultCaret flatLafCaret = new DefaultCaret() {
		private final Timer blinkTimer = new Timer(500, new ActionListener() {
			@Override public void actionPerformed(ActionEvent evt) {
				setVisible(!isVisible());
			}
		});

		@Override public void focusGained(FocusEvent e) {
			super.focusGained(e);
			blinkTimer.start();
		}

		@Override public void focusLost(FocusEvent e) {
			// Don't call super — we want to keep caret visible
			blinkTimer.stop();
			setVisible(true);
		}
	};

	public static class Range {
		private final int start;
		private final int end;

		public Range(int start,int end) {
			this.start = start;
			this.end = end;
		}
	}

	static class CustomWrappedPlainView extends WrappedPlainView {
		public CustomWrappedPlainView(Element elem) {
			super(elem,false);
		}

		@Override protected int calculateBreakPosition(int p0, int p1) {
			try {
				int candidate = super.calculateBreakPosition(p0, p1);
				if (candidate == p1)
					return p1; // Everything fits — don't wrap early
				Segment segment = new Segment();
				getDocument().getText(p0, candidate - p0 + 1, segment);
				char[] a = segment.array;
				int start = segment.offset;
				int i = start + segment.count - 1;
				if( !SwingLuan.isWordChar(a[i]) )
					return candidate;
				do {
					if( --i < start )
						return candidate;
				} while( SwingLuan.isWordChar(a[i]) );
				int breakPos = i + 1 - start + p0;
				do {
					if( --i < start )
						return candidate;
				} while( !SwingLuan.isWordChar(a[i]) );
				return breakPos;
			} catch (BadLocationException e) {
				throw new RuntimeException(e);
			}
		}
	}


	private boolean showWhitespace = false;
	private List<Range> highlights = Collections.emptyList();
	private static final Color highlightColor = new Color(0x2dada3);
	private final LuanDocumentListener ldl = new LuanDocumentListener(this);

	public TextAreaLuan() {
		super();
		//logger.info(""+getFont().getSize());
		if( UIManager.getLookAndFeel().getName().startsWith("FlatLaf") ) {
			setCaret(flatLafCaret);
		}
		getDocument().addDocumentListener(new WeakDocumentListener(ldl));
		getActionMap().put( DefaultEditorKit.selectWordAction, SwingLuan.selectWordAction );
		getActionMap().put( DefaultEditorKit.selectLineAction, SwingLuan.selectWordAndDotsAction );
	}

	@Override public void updateUI() {
		super.updateUI();
		if (UIManager.getLookAndFeel().getName().startsWith("FlatLaf")) {
			setUI(new com.formdev.flatlaf.ui.FlatTextAreaUI() {
				@Override public View create(Element elem) {
					if (getLineWrap() && getWrapStyleWord())
						return new CustomWrappedPlainView(elem);
					return super.create(elem);
				}
			});
		}
	}

	@Override public int getRowHeight() {
		return super.getRowHeight();
	}

	public int getLineHeight(int line) throws BadLocationException {
		if( !getLineWrap() )
			return getRowHeight();
		int startOffset = modelToView(getLineStartOffset(line)).y;
		int endOffset = modelToView(getLineEndOffset(line)).y;
		int height = endOffset - startOffset;
		if( height == 0 )
			height = getRowHeight();
		return height;
	}

	public int getLineRows(int line) throws BadLocationException {
		return getLineHeight(line) / getRowHeight();
	}

	public boolean isWhitespaceVisible() {
		return showWhitespace;
	}

	public void setWhitespaceVisible(boolean showWhitespace) {
		this.showWhitespace = showWhitespace;
		repaint();
	}

	public void setHightlights(List<Range> highlights) {
		this.highlights = highlights;
		repaint();
	}

	public void clearHighlights() {
		if( highlights.size() > 0 )
			setHightlights(Collections.emptyList());
	}

	@Override public void updated(DocumentEvent event) {
		clearHighlights();
	}

	@Override protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		try {
			if( showWhitespace ) {
				g.setColor(Color.LIGHT_GRAY);
				String text = getText();
				int ascent = g.getFontMetrics().getAscent();
				Rectangle visible = getVisibleRect();
				for( int pos=0; pos<text.length(); pos++ ) {
					char ch = text.charAt(pos);
					String symbol;
					switch(ch) {
						case ' ': symbol = "·"; break;
						case '\t': symbol = "→"; break;
						case '\n': symbol = "¶"; break;
						default: continue;
					}
					Rectangle r = modelToView(pos);
					if( visible.contains(r)	)
						g.drawString( symbol, r.x, r.y + ascent );
				}
			}
			g.setColor(highlightColor);
			for( Range range : highlights ) {
				Rectangle r1 = modelToView(range.start);
				Rectangle r2 = modelToView(range.end);
				if( r1.y == r2.y ) {
					g.drawRect( r1.x, r1.y, r2.x - r1.x, r1.height );
				} else {
					g.drawLine( r1.x, r1.y, r1.x, r1.y+r1.height );
					g.drawLine( r2.x, r2.y, r2.x, r2.y+r2.height );
					for( int i=range.start; i<range.end; i++ ) {
						Rectangle r = modelToView(i);
						if( r1.y == r.y ) {
							r2 = r;
						} else {
							int x1 = r1.x;
							int x2 = r2.x + r2.width;
							int y = r1.y;
							g.drawLine( x1, y, x2, y );
							y += r1.height;
							g.drawLine( x1, y, x2, y );
							r1 = r;
							r2 = r;
						}
					}
					r2 = modelToView(range.end);
					if( r2.x > r1.x ) {
						int x1 = r1.x;
						int x2 = r2.x;
						int y = r1.y;
						g.drawLine( x1, y, x2, y );
						y += r1.height;
						g.drawLine( x1, y, x2, y );
					}
				}
			}
		} catch(BadLocationException e) {
			throw new RuntimeException(e);
		}
	}

	private UndoManagerLuan getUndoManagerLuan() {
		return (UndoManagerLuan)getDocument().getProperty("undo");
	}

	@Override public void paste() {
		UndoManagerLuan undo = getUndoManagerLuan();
		undo.beginTransaction();
		try {
			super.paste();
		} finally {
			undo.endTransaction();
		}
	}
}