CaRMtl/eric/JSprogram/LineNumberView.java

330 lines
9.2 KiB
Java

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package eric.JSprogram;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.SizeSequence;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
//import eric.JSprogram.JSConsole;
/**
* LineNumberView is a simple line-number gutter that works correctly
* even when lines are wrapped in the associated text component. This
* is meant to be used as the RowHeaderView in a JScrollPane that
* contains the associated text component. Example usage:
*<pre>
* JTextArea ta = new JTextArea();
* ta.setLineWrap(true);
* ta.setWrapStyleWord(true);
* JScrollPane sp = new JScrollPane(ta);
* sp.setRowHeaderView(new LineNumberView(ta));
*</pre>
*
* @author Alan Moore
*/
public class LineNumberView extends JComponent
{
// This is for the border to the right of the line numbers.
// There's probably a UIDefaults value that could be used for this.
private static final Color BORDER_COLOR = Color.GRAY;
private final static Color DEFAULT_BACKGROUND = new Color(214, 221, 229);
private final static Color DEFAULT_FOREGROUND = Color.black;
private static final int WIDTH_TEMPLATE = 99999;
private static final int MARGIN = 2;
private FontMetrics viewFontMetrics;
private int maxNumberWidth;
private int componentWidth;
private int textTopInset;
private int textFontAscent;
private int textFontHeight;
private JTextComponent text;
private SizeSequence sizes;
private int startLine = 0;
private boolean structureChanged = true;
/**
* Construct a LineNumberView and attach it to the given text component.
* The LineNumberView will listen for certain kinds of events from the
* text component and update itself accordingly.
*
* @param startLine the line that changed, if there's only one
* @param structureChanged if <tt>true</tt>, ignore the line number and
* update all the line heights.
*/
public LineNumberView(JTextComponent text)
{
if (text == null)
{
throw new IllegalArgumentException("Text component cannot be null");
}
this.text = text;
updateCachedMetrics();
UpdateHandler handler = new UpdateHandler();
text.getDocument().addDocumentListener(handler);
text.addPropertyChangeListener(handler);
text.addComponentListener(handler);
// setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, BORDER_COLOR));
setBorder(BorderFactory.createEmptyBorder());
setBackground(DEFAULT_BACKGROUND);
setForeground(DEFAULT_FOREGROUND) ;
}
/**
* Schedule a repaint because one or more line heights may have changed.
*
* @param startLine the line that changed, if there's only one
* @param structureChanged if <tt>true</tt>, ignore the line number and
* update all the line heights.
*/
private void viewChanged(int startLine, boolean structureChanged)
{
this.startLine = startLine;
this.structureChanged = structureChanged;
revalidate();
repaint();
}
/** Update the line heights as needed. */
private void updateSizes()
{
if (startLine < 0)
{
return;
}
if (structureChanged)
{
int count = getAdjustedLineCount();
sizes = new SizeSequence(count);
for (int i = 0; i < count; i++)
{
sizes.setSize(i, getLineHeight(i));
}
structureChanged = false;
}
else
{
sizes.setSize(startLine, getLineHeight(startLine));
}
startLine = -1;
}
/* Copied from javax.swing.text.PlainDocument */
private int getAdjustedLineCount()
{
// There is an implicit break being modeled at the end of the
// document to deal with boundary conditions at the end. This
// is not desired in the line count, so we detect it and remove
// its effect if throwing off the count.
Element map = text.getDocument().getDefaultRootElement();
int n = map.getElementCount();
Element lastLine = map.getElement(n - 1);
if ((lastLine.getEndOffset() - lastLine.getStartOffset()) > 1)
{
return n;
}
return n - 1;
}
/**
* Get the height of a line from the JTextComponent.
*
* @param index the line number
* @param the height, in pixels
*/
private int getLineHeight(int index)
{
int lastPos = sizes.getPosition(index) + textTopInset;
int height = textFontHeight;
try
{
Element map = text.getDocument().getDefaultRootElement();
int lastChar = map.getElement(index).getEndOffset() - 1;
Rectangle r = text.modelToView(lastChar);
height = (r.y - lastPos) + r.height;
}
catch (BadLocationException ex)
{
ex.printStackTrace();
}
return height;
}
/**
* Cache some values that are used a lot in painting or size
* calculations. Also ensures that the line-number font is not
* larger than the text component's font (by point-size, anyway).
*/
private void updateCachedMetrics()
{
Font textFont = text.getFont();
FontMetrics fm = getFontMetrics(textFont);
textFontHeight = Math.max(fm.getHeight(),10);
textFontHeight = JSEditor.TailleTexte; //private donc inaccessible ici, dommage...
textFontAscent = fm.getAscent();
textTopInset = text.getInsets().top;
Font viewFont = getFont();
boolean changed = false;
if (viewFont == null)
{
viewFont = UIManager.getFont("Label.font");
changed = true;
}
if (viewFont.getSize() != textFont.getSize())
{
viewFont = viewFont.deriveFont(textFont.getSize2D());
changed = true;
}
viewFontMetrics = getFontMetrics(viewFont);
maxNumberWidth = (viewFontMetrics.stringWidth(String.valueOf(WIDTH_TEMPLATE))+10)/2;
componentWidth = 2 * MARGIN + maxNumberWidth;
if (changed)
{
super.setFont(viewFont);
}
}
public Dimension getPreferredSize()
{
return new Dimension(componentWidth, text.getHeight());
}
public void setFont(Font font)
{
super.setFont(font);
updateCachedMetrics();
}
public void paintComponent(Graphics g)
{
updateSizes();
Rectangle clip = g.getClipBounds();
g.setColor(getBackground());
g.fillRect(clip.x, clip.y, clip.width, clip.height);
g.setColor(getForeground());
int base = clip.y - textTopInset;
int first = sizes.getIndex(base);
int last = sizes.getIndex(base + clip.height);
String text = "";
for (int i = first; i <= last; i++)
{
text = String.valueOf(i+1);
int x = MARGIN + maxNumberWidth - viewFontMetrics.stringWidth(text);
int y = sizes.getPosition(i) + textFontAscent + textTopInset;
g.drawString(text, x, y);
}
}
class UpdateHandler extends ComponentAdapter
implements PropertyChangeListener, DocumentListener
{
/**
* The text component was resized. 'Nuff said.
*/
public void componentResized(ComponentEvent evt)
{
viewChanged(0, true);
}
/**
* A bound property was changed on the text component. Properties
* like the font, border, and tab size affect the layout of the
* whole document, so we invalidate all the line heights here.
*/
public void propertyChange(PropertyChangeEvent evt)
{
Object oldValue = evt.getOldValue();
Object newValue = evt.getNewValue();
String propertyName = evt.getPropertyName();
if ("document".equals(propertyName))
{
if (oldValue != null && oldValue instanceof Document)
{
((Document)oldValue).removeDocumentListener(this);
}
if (newValue != null && newValue instanceof Document)
{
((Document)newValue).addDocumentListener(this);
}
}
updateCachedMetrics();
viewChanged(0, true);
}
/**
* Text was inserted into the document.
*/
public void insertUpdate(DocumentEvent evt)
{
update(evt);
}
/**
* Text was removed from the document.
*/
public void removeUpdate(DocumentEvent evt)
{
update(evt);
}
/**
* Text attributes were changed. In a source-code editor based on
* StyledDocument, attribute changes should be applied automatically
* in response to inserts and removals. Since we're already
* listening for those, this method should be redundant, but YMMV.
*/
public void changedUpdate(DocumentEvent evt)
{
// update(evt);
}
/**
* If the edit was confined to a single line, invalidate that
* line's height. Otherwise, invalidate them all.
*/
private void update(DocumentEvent evt)
{
Element map = text.getDocument().getDefaultRootElement();
int line = map.getElementIndex(evt.getOffset());
DocumentEvent.ElementChange ec = evt.getChange(map);
viewChanged(line, ec != null);
}
}
}