2022년 2월 19일 토요일

Java Swing - TextView

 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();
				}
			}
		}
	}	
}

댓글 없음:

댓글 쓰기