2021년 11월 21일 일요일

Java Swing Lined Text API

 1. Java Swing Lined Text API

package com.home.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.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;

public class JSText {

	public JTextPane textPane;
	public JScrollPane ctrl;
	public LineNumber lineNumber;

	public JSText() {
		textPane = new JTextPane();
		lineNumber = new LineNumber(textPane);
		ctrl = new JScrollPane(textPane);
		ctrl.setRowHeaderView( lineNumber );
	}
	public JSText(String fontName, int fontSize) {
		this();
		textPane.setFont(new Font( fontName, Font.PLAIN, fontSize)); //Font.BOLD
	}

	public JTextPane getTextPane() { return textPane; }
	public JScrollPane getScrollPane() { return ctrl; }
	
	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();
				}
			}
		}
	}	
}

댓글 없음:

댓글 쓰기