/*
* Copyright (c) 2010-2016, Sikuli.org, sikulix.com
* Released under the MIT License.
*
*/
package org.sikuli.ide;
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import org.sikuli.basics.Debug;
public class EditorLineNumberView extends JComponent implements MouseListener {
private static ImageIcon ERROR_ICON = SikuliIDE.getIconResource("/icons/error_icon.gif");
// 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 = new Color(155, 155, 155);
private static Color FG_COLOR = Color.GRAY;
private static Color BG_COLOR = new Color(241, 241, 241);
private static Color selBG_COLOR = new Color(220, 220, 220);
private static final int WIDTH_TEMPLATE = 999;
private static final int MARGIN = 5;
private FontMetrics viewFontMetrics;
private int maxNumberWidth;
private int componentWidth;
private int textTopInset;
private int textFontAscent;
private int textFontHeight;
private EditorPane text;
private SizeSequence sizes;
private int startLine = 0;
private boolean structureChanged = true;
private Set<Integer> errLines = new HashSet<Integer>();
private int line;
private SikuliIDEPopUpMenu popMenuLineNumber = null;
private boolean wasPopup = false;
public EditorLineNumberView(JTextComponent text) {
/**
* 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.
*/
if (text == null) {
throw new IllegalArgumentException("Text component required! Cannot be null!");
}
this.text = (EditorPane) 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));
setForeground(FG_COLOR);
setBackground(BG_COLOR);
init();
}
private void init() {
addMouseListener(this);
setToolTipText("RightClick for options - left to select the line");
popMenuLineNumber = new SikuliIDEPopUpMenu("POP_LINE", this);
if (!popMenuLineNumber.isValidMenu()) {
popMenuLineNumber = null;
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(componentWidth, text.getHeight());
}
@Override
public void setFont(Font font) {
super.setFont(font);
updateCachedMetrics();
}
private void updateCachedMetrics() {
// Cache some values that are used a lot in painting or size calculations.
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");
viewFont = viewFont.deriveFont(Font.PLAIN);
changed = true;
}
if (viewFont.getSize() > textFont.getSize()) {
viewFont = viewFont.deriveFont(textFont.getSize2D());
changed = true;
}
viewFontMetrics = getFontMetrics(viewFont);
maxNumberWidth = viewFontMetrics.stringWidth(String.valueOf(WIDTH_TEMPLATE));
componentWidth = 2 * MARGIN + maxNumberWidth;
if (changed) {
super.setFont(viewFont);
}
}
@Override
public void paintComponent(Graphics g) {
updateSizes();
Rectangle clip = g.getClipBounds();
g.setColor(getBackground());
g.fillRect(clip.x, clip.y, clip.width, clip.height);
if (sizes == null) {
return;
}
// draw line numbers
g.setColor(getForeground());
int base = clip.y - textTopInset;
int first = sizes.getIndex(base);
int last = sizes.getIndex(base + clip.height);
String lnum;
lnum = "";
for (int i = first; i < last; i++) {
lnum = String.valueOf(i + 1);
int x = MARGIN + maxNumberWidth - viewFontMetrics.stringWidth(lnum);
int y = (sizes.getPosition(i) + sizes.getPosition(i + 1)) / 2 + textFontAscent / 2 + textTopInset / 2;
if (errLines.contains(i + 1)) {
final int h = 12;
g.drawImage(ERROR_ICON.getImage(), 0, y - h + 1, h, h, null);
g.setColor(Color.RED);
} else {
g.setColor(getForeground());
}
g.drawString(lnum, x, y);
}
}
private void updateSizes() {
// Update the line heights as needed.
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 {
if (sizes != null) {
sizes.setSize(startLine, getLineHeight(startLine));
}
}
startLine = -1;
}
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 root = text.getDocument().getDefaultRootElement();
int n = root.getElementCount();
Element lastLine = root.getElement(n - 1);
if ((lastLine.getEndOffset() - lastLine.getStartOffset()) >= 1) {
return n;
}
return n - 1;
}
private int getLineHeight(int index) {
// Get the height of a line from the JTextComponent.
Element e;
int lastPos = sizes.getPosition(index) + textTopInset;
Element l = text.getDocument().getDefaultRootElement().getElement(index);
Rectangle r = null;
Rectangle r1;
int h = textFontHeight;
int max_h = 0;
try {
if (l.getElementCount() < 2) {
r = text.modelToView(l.getEndOffset() - 1);
} else {
for (int i = 0; i < l.getElementCount(); i++) {
e = l.getElement(i);
if ("component".equals(e.getName())) {
r1 = text.modelToView(e.getStartOffset());
if (max_h < r1.height) {
max_h = r1.height;
r = r1;
}
}
}
}
if (r == null) {
r = text.modelToView(l.getEndOffset() - 1);
}
h = (r.y - lastPos) + r.height;
} catch (Exception ex) {
}
return h;
}
public void addErrorMark(int line) {
errLines.add(line);
}
public void resetErrorMark() {
errLines.clear();
}
//<editor-fold defaultstate="collapsed" desc="mouse actions">
@Override
public void mouseEntered(MouseEvent me) {
setBackground(selBG_COLOR);
}
@Override
public void mouseExited(MouseEvent me) {
setBackground(BG_COLOR);
}
@Override
public void mouseClicked(MouseEvent me) {
if (wasPopup) {
wasPopup = false;
return;
}
((EditorPane) text).jumpTo(sizes.getIndex(me.getY()) + 1);
if (me.getClickCount() == 2) {
((EditorPane) text).getDocument();
}
}
@Override
public void mousePressed(MouseEvent me) {
checkPopup(me);
}
@Override
public void mouseReleased(MouseEvent me) {
checkPopup(me);
}
private void checkPopup(MouseEvent me) {
if (me.isPopupTrigger()) {
if (popMenuLineNumber != null) {
wasPopup = true;
popMenuLineNumber.show(this, me.getX(), me.getY());
}
return;
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="UpdateHandler">
private void viewChanged(int startLine, boolean structureChanged) {
// Schedule a repaint because one or more line heights may have changed.
// triggered by UpdateHandler
this.startLine = startLine;
this.structureChanged |= structureChanged;
revalidate();
repaint();
}
class UpdateHandler extends ComponentAdapter implements PropertyChangeListener, DocumentListener {
@Override
public void componentResized(ComponentEvent evt) {
// all lines invalidated
viewChanged(0, true);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
// a doc prop changed - invalidate all lines
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);
}
@Override
public void insertUpdate(DocumentEvent evt) {
update(evt, "insert"); // Text was inserted into the document.
}
@Override
public void removeUpdate(DocumentEvent evt) {
update(evt, "remove"); //Text was removed from the document.
}
@Override
public void changedUpdate(DocumentEvent evt) {
// update(evt); //done by insert / remove already
}
private void update(DocumentEvent evt, String msg) {
// invalidate one or all lines
Element map = text.getDocument().getDefaultRootElement();
int line = map.getElementIndex(evt.getOffset());
DocumentEvent.ElementChange ec = evt.getChange(map);
Debug.log(6, "LineNumbers: " + msg + " update - struct changed: " + (ec != null));
viewChanged(line, ec != null);
}
}
//</editor-fold>
}