Java Swing - TextView
package jlib5.swing;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import javax.swing.undo.UndoManager;
public class TextView {
public JTextPane textPane;
public JScrollPane ctrl;
public LineNumber lineNumber;
JPopupMenu menu;
public TextView() {
textPane = new JTextPane();
lineNumber = new LineNumber(textPane);
ctrl = new JScrollPane(textPane);
ctrl.setRowHeaderView(lineNumber);
setContextMenu();
}
public TextView(String fontName, int fontSize) {
this();
textPane.setFont(new Font( fontName, Font.PLAIN, fontSize)); //Font.BOLD
}
public JScrollPane getLayout() { return ctrl; }
public JTextPane getCtrl() { return textPane; }
public void addContextMenu(String name, Runnable handler) {
if( name == null ) {
menu.addSeparator();
} else {
JMenuItem item = new JMenuItem(name);
item.addActionListener( (e) -> handler.run() );
if( this.menu == null ) {
this.menu = new JPopupMenu();
this.menu.add(item);
textPane.setComponentPopupMenu(this.menu);
} else {
menu.add(item);
}
}
}
public void pasteHtml() {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
if( clipboard != null ) {
try {
Object htmlText = clipboard.getData(DataFlavor.allHtmlFlavor);
clipboard.setContents(new StringSelection(htmlText.toString()), null);
textPane.paste();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void setContextMenu() {
addContextMenu( "Clear", () -> textPane.setText("") );
addContextMenu( "Cut", () -> textPane.cut() );
addContextMenu( "Copy", () -> textPane.copy() );
addContextMenu( "Paste", () -> textPane.paste() );
addContextMenu( "Paste Html", this::pasteHtml );
addContextMenu( null, null );
}
public JTextPane getTextPane() { return textPane; }
public JScrollPane getScrollPane() { return ctrl; }
public void clear() {
textPane.setText("");
}
public void disableEdit() { textPane.setEditable(false); }
public void enableEdit() { textPane.setEditable(true); }
public void select( int start, int end ) { textPane.select(start, end); }
public void append(String text) throws BadLocationException {
StyledDocument doc = textPane.getStyledDocument();
doc.insertString(doc.getLength(), text, null);
}
public void scrollToTop() {
Swing.runLater(() -> getScrollPane().getVerticalScrollBar().setValue(0));
}
public void scrollToBottom() {
StyledDocument doc = textPane.getStyledDocument();
int pos = doc.getLength();
Swing.runLater(() -> getScrollPane().getVerticalScrollBar().setValue(pos-1));
}
public static class LineNumber extends JPanel
implements CaretListener, DocumentListener, PropertyChangeListener
{
private static final long serialVersionUID = 1L;
public final static float LEFT = 0.0f;
public final static float CENTER = 0.5f;
public final static float RIGHT = 1.0f;
private final Border OUTER = new MatteBorder(0, 0, 0, 2, Color.GRAY);
private final static int HEIGHT = Integer.MAX_VALUE - 1000000;
// Text component this TextTextLineNumber component is in sync with
private JTextComponent component;
// Properties that can be changed
private boolean updateFont;
private int borderGap;
private Color currentLineForeground;
private float digitAlignment;
private int minimumDisplayDigits;
// Keep history information to reduce the number of times the component
// needs to be repainted
private int lastDigits;
private int lastHeight;
private int lastLine;
private HashMap<String, FontMetrics> fonts;
/**
* Create a line number component for a text component. This minimum
* display width will be based on 3 digits.
*
* @param component the related text component
*/
public LineNumber(JTextComponent component)
{
this(component, 3);
}
public LineNumber(JTextComponent component, int minimumDisplayDigits)
{
this.component = component;
setFont( component.getFont() );
setBorderGap( 5 );
setCurrentLineForeground( Color.RED );
setDigitAlignment( RIGHT );
setMinimumDisplayDigits( minimumDisplayDigits );
component.getDocument().addDocumentListener(this);
component.addCaretListener( this );
component.addPropertyChangeListener("font", this);
}
public boolean getUpdateFont()
{
return updateFont;
}
public void setUpdateFont(boolean updateFont)
{
this.updateFont = updateFont;
}
public int getBorderGap()
{
return borderGap;
}
public void setBorderGap(int borderGap)
{
this.borderGap = borderGap;
Border inner = new EmptyBorder(0, borderGap, 0, borderGap);
setBorder( new CompoundBorder(OUTER, inner) );
lastDigits = 0;
setPreferredWidth();
}
public Color getCurrentLineForeground()
{
return currentLineForeground == null ? getForeground() : currentLineForeground;
}
public void setCurrentLineForeground(Color currentLineForeground)
{
this.currentLineForeground = currentLineForeground;
}
public float getDigitAlignment()
{
return digitAlignment;
}
public void setDigitAlignment(float digitAlignment)
{
this.digitAlignment =
digitAlignment > 1.0f ? 1.0f : digitAlignment < 0.0f ? -1.0f : digitAlignment;
}
public int getMinimumDisplayDigits()
{
return minimumDisplayDigits;
}
public void setMinimumDisplayDigits(int minimumDisplayDigits)
{
this.minimumDisplayDigits = minimumDisplayDigits;
setPreferredWidth();
}
private void setPreferredWidth()
{
Element root = component.getDocument().getDefaultRootElement();
int lines = root.getElementCount();
int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits);
// Update sizes when number of digits in the line number changes
if (lastDigits != digits)
{
lastDigits = digits;
FontMetrics fontMetrics = getFontMetrics( getFont() );
int width = fontMetrics.charWidth( '0' ) * digits;
Insets insets = getInsets();
int preferredWidth = insets.left + insets.right + width;
Dimension d = getPreferredSize();
d.setSize(preferredWidth, HEIGHT);
setPreferredSize( d );
setSize( d );
}
}
/**
* Draw the line numbers
*/
@SuppressWarnings("deprecation")
@Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
FontMetrics fontMetrics = component.getFontMetrics( component.getFont() );
Insets insets = getInsets();
int availableWidth = getSize().width - insets.left - insets.right;
Rectangle clip = g.getClipBounds();
int rowStartOffset = component.viewToModel( new Point(0, clip.y) );
int endOffset = component.viewToModel( new Point(0, clip.y + clip.height) );
while (rowStartOffset <= endOffset)
{
try
{
if (isCurrentLine(rowStartOffset))
g.setColor( getCurrentLineForeground() );
else
g.setColor( getForeground() );
String lineNumber = getTextLineNumber(rowStartOffset);
int stringWidth = fontMetrics.stringWidth( lineNumber );
int x = getOffsetX(availableWidth, stringWidth) + insets.left;
int y = getOffsetY(rowStartOffset, fontMetrics);
g.drawString(lineNumber, x, y);
rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1;
}
catch(Exception e) {break;}
}
}
private boolean isCurrentLine(int rowStartOffset)
{
int caretPosition = component.getCaretPosition();
Element root = component.getDocument().getDefaultRootElement();
if (root.getElementIndex( rowStartOffset ) == root.getElementIndex(caretPosition))
return true;
else
return false;
}
protected String getTextLineNumber(int rowStartOffset)
{
Element root = component.getDocument().getDefaultRootElement();
int index = root.getElementIndex( rowStartOffset );
Element line = root.getElement( index );
if (line.getStartOffset() == rowStartOffset)
return String.valueOf(index + 1);
else
return "";
}
private int getOffsetX(int availableWidth, int stringWidth)
{
return (int)((availableWidth - stringWidth) * digitAlignment);
}
private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics)
throws BadLocationException
{
Rectangle r = component.modelToView( rowStartOffset );
int lineHeight = fontMetrics.getHeight();
int y = r.y + r.height;
int descent = 0;
if (r.height == lineHeight) // default font is being used
{
descent = fontMetrics.getDescent();
}
else // We need to check all the attributes for font changes
{
if (fonts == null)
fonts = new HashMap<String, FontMetrics>();
Element root = component.getDocument().getDefaultRootElement();
int index = root.getElementIndex( rowStartOffset );
Element line = root.getElement( index );
for (int i = 0; i < line.getElementCount(); i++)
{
Element child = line.getElement(i);
AttributeSet as = child.getAttributes();
String fontFamily = (String)as.getAttribute(StyleConstants.FontFamily);
Integer fontSize = (Integer)as.getAttribute(StyleConstants.FontSize);
String key = fontFamily + fontSize;
FontMetrics fm = fonts.get( key );
if (fm == null)
{
Font font = new Font(fontFamily, Font.PLAIN, fontSize);
fm = component.getFontMetrics( font );
fonts.put(key, fm);
}
descent = Math.max(descent, fm.getDescent());
}
}
return y - descent;
}
//
// Implement CaretListener interface
//
@Override
public void caretUpdate(CaretEvent e)
{
int caretPosition = component.getCaretPosition();
Element root = component.getDocument().getDefaultRootElement();
int currentLine = root.getElementIndex( caretPosition );
if (lastLine != currentLine)
{
// repaint();
getParent().repaint();
lastLine = currentLine;
}
}
//
// Implement DocumentListener interface
//
@Override
public void changedUpdate(DocumentEvent e)
{
documentChanged();
}
@Override
public void insertUpdate(DocumentEvent e)
{
documentChanged();
}
@Override
public void removeUpdate(DocumentEvent e)
{
documentChanged();
}
private void documentChanged()
{
SwingUtilities.invokeLater(new Runnable()
{
@SuppressWarnings("deprecation")
@Override
public void run()
{
try
{
int endPos = component.getDocument().getLength();
Rectangle rect = component.modelToView(endPos);
if (rect != null && rect.y != lastHeight)
{
setPreferredWidth();
// repaint();
getParent().repaint();
lastHeight = rect.y;
}
}
catch (BadLocationException ex) { /* nothing to do */ }
}
});
}
@Override
public void propertyChange(PropertyChangeEvent evt)
{
if (evt.getNewValue() instanceof Font)
{
if (updateFont)
{
Font newFont = (Font) evt.getNewValue();
setFont(newFont);
lastDigits = 0;
setPreferredWidth();
}
else
{
// repaint();
getParent().repaint();
}
}
}
}
}