/**
* Copyright (c) 2000-2006 Liferay, Inc. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.mypsycho.swing;
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.*;
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 javax.swing.UIManager;
/**
* 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
*/
@SuppressWarnings("serial")
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;
// The minimum width of the gutter, in number of digits
private static final int MIN_DIGITS = 3;
private static final int MARGIN = 5;
private FontMetrics viewFontMetrics;
private int digitWidth;
private int maxDigits;
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 text
* the associated text component
*/
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));
}
/**
* 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;
if (structureChanged) {
maxDigits = 0;
componentWidth = 0;
}
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;
}
private int getMaxDigits() {
if (maxDigits <= 0) {
int lineCount = getAdjustedLineCount();
maxDigits = Math.max(MIN_DIGITS, String.valueOf(lineCount).length());
}
return maxDigits;
}
private int getComponentWidth() {
if (componentWidth <= 0) {
componentWidth = getMaxDigits() * digitWidth + MARGIN * 2;
}
return componentWidth;
}
/* 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 = fm.getHeight();
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);
digitWidth = viewFontMetrics.stringWidth("0");
if (changed) {
super.setFont(viewFont);
}
}
public Dimension getPreferredSize() {
return new Dimension(getComponentWidth(), 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 = getComponentWidth() - MARGIN - 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.
*/
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);
}
}
}