package com.wordpress.tips4java;
import com.igormaznitsa.prol.easygui.AbstractProlEditor;
import java.awt.*;
import java.beans.*;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.UndoManager;
/**
* The class implements the source editor pane of the IDE.
* As the Base for the class I have used the open sources class developed by Rob Camick and placed at http://tips4java.wordpress.com/2009/05/23/text-component-line-number/
*/
public class TextLineNumber extends AbstractProlEditor {
private static final long serialVersionUID = -2223529439306867844L;
/**
* Inside logger, the logger id = "PROL_NOTE_PAD"
*/
protected static final Logger LOG = Logger.getLogger("PROL_NOTE_PAD");
/**
* The component allows to add the number line counter in the editor
*/
private static final class LineNumberComponent extends JPanel implements CaretListener, DocumentListener, PropertyChangeListener {
private static final long serialVersionUID = -981371742373160068L;
public final static float LEFT = 0.0f;
public final static float CENTER = 0.5f;
public final static float RIGHT = 1.0f;
private final static Border OUTER = new MatteBorder(0, 0, 0, 2, Color.GRAY);
private final static int HGHT = 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 LineNumberComponent(JTextComponent component) {
this(component, 3);
}
/**
* Create a line number component for a text component.
*
* @param component the related text component
* @param minimumDisplayDigits the number of digits used to calculate the
* minimum width of the component
*/
public LineNumberComponent(JTextComponent component, int minimumDisplayDigits) {
this.component = component;
setBackground(Color.LIGHT_GRAY);
setFont(component.getFont());
setBorderGap(5);
setCurrentLineForeground(Color.YELLOW);
setForeground(Color.DARK_GRAY);
setDigitAlignment(RIGHT);
setMinimumDisplayDigits(minimumDisplayDigits);
component.getDocument().addDocumentListener(this);
component.addCaretListener(this);
component.addPropertyChangeListener("font", this);
}
/**
* Gets the update font property
*
* @return the update font property
*/
public boolean getUpdateFont() {
return updateFont;
}
/**
* Set the update font property. Indicates whether this Font should be
* updated automatically when the Font of the related text component is
* changed.
*
* @param updateFont when true update the Font and repaint the line numbers,
* otherwise just repaint the line numbers.
*/
public void setUpdateFont(boolean updateFont) {
this.updateFont = updateFont;
}
/**
* Gets the border gap
*
* @return the border gap in pixels
*/
public int getBorderGap() {
return borderGap;
}
/**
* The border gap is used in calculating the left and right insets of the
* border. Default value is 5.
*
* @param borderGap the gap in pixels
*/
public void setBorderGap(int borderGap) {
this.borderGap = borderGap;
Border inner = new EmptyBorder(0, borderGap, 0, borderGap);
setBorder(new CompoundBorder(OUTER, inner));
lastDigits = 0;
setPreferredWidth();
}
/**
* Gets the current line rendering Color
*
* @return the Color used to render the current line number
*/
public Color getCurrentLineForeground() {
return currentLineForeground == null ? getForeground() : currentLineForeground;
}
/**
* The Color used to render the current line digits. Default is Coolor.RED.
*
* @param currentLineForeground the Color used to render the current line
*/
public void setCurrentLineForeground(Color currentLineForeground) {
this.currentLineForeground = currentLineForeground;
}
/**
* Gets the digit alignment
*
* @return the alignment of the painted digits
*/
public float getDigitAlignment() {
return digitAlignment;
}
/**
* Specify the horizontal alignment of the digits within the component.
* Common values would be:
* <ul>
* <li>TextLineNumber.LEFT
* <li>TextLineNumber.CENTER
* <li>TextLineNumber.RIGHT (default)
* </ul>
*
* @param currentLineForeground the Color used to render the current line
*/
public void setDigitAlignment(float digitAlignment) {
this.digitAlignment = digitAlignment > 1.0f ? 1.0f : digitAlignment < 0.0f ? -1.0f : digitAlignment;
}
/**
* Gets the minimum display digits
*
* @return the minimum display digits
*/
public int getMinimumDisplayDigits() {
return minimumDisplayDigits;
}
/**
* Specify the minimum number of digits used to calculate the preferred
* width of the component. Default is 3.
*
* @param minimumDisplayDigits the number digits used in the preferred width
* calculation
*/
public void setMinimumDisplayDigits(int minimumDisplayDigits) {
this.minimumDisplayDigits = minimumDisplayDigits;
setPreferredWidth();
}
/**
* Calculate the width needed to display the maximum line number
*/
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, HGHT);
setPreferredSize(d);
setSize(d);
}
}
/**
* Draw the line numbers
*/
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Determine the width of the space available to draw the line number
FontMetrics fontMetrics = component.getFontMetrics(component.getFont());
Insets insets = getInsets();
int availableWidth = getSize().width - insets.left - insets.right;
// Determine the rows to draw within the clipped bounds.
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());
}
// Get the line number as a string and then determine the
// "X" and "Y" offsets for drawing the string.
final String lineNumber = getTextLineNumber(rowStartOffset);
final int stringWidth = fontMetrics.stringWidth(lineNumber);
final int x = getOffsetX(availableWidth, stringWidth) + insets.left;
final int y = getOffsetY(rowStartOffset, fontMetrics);
if (y < 0) {
break;
}
g.drawString(lineNumber, x, y);
// Move to the next row
rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1;
}
catch (final Exception e) {
LOG.log(Level.SEVERE, "Exception at textLineNumber", e);
break;
}
}
}
/*
* We need to know if the caret is currently positioned on the line we
* are about to paint so the line number can be highlighted.
*/
private boolean isCurrentLine(int rowStartOffset) {
int caretPosition = component.getCaretPosition();
Element root = component.getDocument().getDefaultRootElement();
return root.getElementIndex(rowStartOffset) == root.getElementIndex(caretPosition);
}
/*
* Get the line number to be drawn. The empty string will be returned
* when a line of text has wrapped.
*/
protected final String getTextLineNumber(final int rowStartOffset) {
final Element root = component.getDocument().getDefaultRootElement();
final int index = root.getElementIndex(rowStartOffset);
final Element line = root.getElement(index);
if (line.getStartOffset() == rowStartOffset) {
return String.valueOf(index + 1);
}
else {
return "";
}
}
/*
* Determine the X offset to properly align the line number when drawn
*/
private int getOffsetX(final int availableWidth, final int stringWidth) {
return (int) ((availableWidth - stringWidth) * digitAlignment);
}
/**
* Determine the Y offset for the current row
* @return coordinate or -1 if component has not size
*/
private int getOffsetY(final int rowStartOffset, final FontMetrics fontMetrics)
throws BadLocationException {
// Get the bounding rectangle of the row
final Rectangle r = component.modelToView(rowStartOffset);
if (r == null) {
return -1;
}
final int lineHeight = fontMetrics.getHeight();
final int y = r.y + r.height;
int descent = 0;
// The text needs to be positioned above the bottom of the bounding
// rectangle based on the descent of the font(s) contained on the row.
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>();
}
final Element root = component.getDocument().getDefaultRootElement();
final int index = root.getElementIndex(rowStartOffset);
final Element line = root.getElement(index);
for (int i = 0; i < line.getElementCount(); i++) {
final Element child = line.getElement(i);
final AttributeSet attrset = child.getAttributes();
final String fontFamily = (String) attrset.getAttribute(StyleConstants.FontFamily);
final Integer fontSize = (Integer) attrset.getAttribute(StyleConstants.FontSize);
final String key = fontFamily + fontSize;
FontMetrics fntmtrcs = fonts.get(key);
if (fntmtrcs == null) {
final Font font = new Font(fontFamily, Font.PLAIN, fontSize);
fntmtrcs = component.getFontMetrics(font);
fonts.put(key, fntmtrcs);
}
descent = Math.max(descent, fntmtrcs.getDescent());
}
}
return y - descent;
}
//
// Implement CaretListener interface
//
@Override
public final void caretUpdate(final CaretEvent e) {
// Get the line the caret is positioned on
final int caretPosition = component.getCaretPosition();
final Element root = component.getDocument().getDefaultRootElement();
final int currentLine = root.getElementIndex(caretPosition);
// Need to repaint so the correct line number can be highlighted
if (lastLine != currentLine) {
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();
}
/*
* A document change may affect the number of displayed lines of text.
* Therefore the lines numbers will also change.
*/
private void documentChanged() {
// Preferred size of the component has not been updated at the time
// the DocumentEvent is fired
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
int preferredHeight = component.getPreferredSize().height;
// Document change has caused a change in the number of lines.
// Repaint to reflect the new line numbers
if (lastHeight != preferredHeight) {
setPreferredWidth();
repaint();
lastHeight = preferredHeight;
}
}
});
}
//
// Implement PropertyChangeListener interface
//
@Override
public void propertyChange(final PropertyChangeEvent evt) {
if (evt.getNewValue() instanceof Font) {
if (updateFont) {
final Font newFont = (Font) evt.getNewValue();
setFont(newFont);
lastDigits = 0;
setPreferredWidth();
}
else {
repaint();
}
}
}
}
protected UndoManager undoManager;
public synchronized String getText() {
return editor.getText();
}
public synchronized UndoManager getUndoManager() {
return undoManager;
}
public synchronized void addUndoableEditListener(final UndoableEditListener listener) {
editor.getDocument().addUndoableEditListener(listener);
}
public synchronized void addDocumentListener(final DocumentListener listener) {
editor.getDocument().addDocumentListener(listener);
}
public TextLineNumber() {
super("Editor");
editor.setContentType("text/prol");
removePropertyFromList("EdWordWrap");
editor.setForeground(Color.BLACK);
editor.setBackground(Color.WHITE);
editor.setCaretColor(Color.BLACK);
editor.setFont(new Font("Courier", Font.BOLD, 14));
editor.setVisible(true);
setEnabled(true);
undoManager = new UndoManager();
final LineNumberComponent lineNumerator = new LineNumberComponent(editor);
lineNumerator.setUpdateFont(true);
scrollPane.setRowHeaderView(lineNumerator);
}
public synchronized int getCaretPosition() {
return this.editor.getCaretPosition();
}
public synchronized void setCaretPosition(final int pos){
this.editor.setCaretPosition(pos);
this.editor.getCaret().setVisible(true);
}
public synchronized void setCaretPosition(final int line, final int pos) {
try {
final Element rootelement = editor.getDocument().getDefaultRootElement();
final Element element = rootelement.getElement(line - 1);
final int offset = element.getStartOffset() + pos - 1;
editor.setCaretPosition(offset);
editor.requestFocus();
}
catch (Exception ex) {
LOG.throwing(this.getClass().getCanonicalName(), "setCaretPosition()", ex);
}
}
@Override
public void loadPreferences(final Preferences prefs) {
final Color backColor = extractColor(prefs, "sourceedbackcolor");
final Color caretColor = extractColor(prefs, "sourcecaretcolor");
final Color fgColor = extractColor(prefs, "sourceforegroundcolor");
if (backColor != null) {
setEdBackground(backColor);
}
if (caretColor != null) {
setEdCaretColor(caretColor);
}
if (fgColor != null) {
setEdForeground(fgColor);
}
setEdWordWrap(prefs.getBoolean("sourcewordwrap", true));
setEdFont(loadFontFromPrefs(prefs, "sourcefont"));
}
@Override
public void savePreferences(final Preferences prefs) {
prefs.putInt("sourceedbackcolor", getEdBackground().getRGB());
prefs.putInt("sourcecaretcolor", getEdCaretColor().getRGB());
prefs.putInt("sourceforegroundcolor", getEdForeground().getRGB());
prefs.putBoolean("sourcewordwrap", getEdWordWrap());
saveFontToPrefs(prefs, "sourcefont", editor.getFont());
}
public boolean uncommentSelectedLines() {
if (editor.getDocument().getLength() == 0) {
return false;
}
int selectionStart = editor.getSelectionStart();
int selectionEnd = editor.getSelectionEnd();
if (selectionStart < 0 || selectionEnd < 0) {
selectionStart = editor.getCaretPosition();
selectionEnd = selectionStart;
}
final Element root = editor.getDocument().getDefaultRootElement();
final int startElement = root.getElementIndex(selectionStart);
final int endElement = root.getElementIndex(selectionEnd);
boolean result = false;
for (int i = startElement; i <= endElement; i++) {
final Element elem = root.getElement(i);
try {
final String elementtext = elem.getDocument().getText(elem.getStartOffset(), elem.getEndOffset() - elem.getStartOffset());
if (elementtext.trim().startsWith("%")) {
final int indexofcomment = elementtext.indexOf('%');
if (indexofcomment >= 0) {
elem.getDocument().remove(elem.getStartOffset() + indexofcomment, 1);
result = true;
}
}
}
catch (BadLocationException ex) {
LOG.throwing(this.getClass().getCanonicalName(), "uncommentSelectedLines()", ex);
}
}
editor.revalidate();
return result;
}
public boolean commentSelectedLines() {
if (editor.getDocument().getLength() == 0) {
return false;
}
int selectionStart = editor.getSelectionStart();
int selectionEnd = editor.getSelectionEnd();
if (selectionStart < 0 || selectionEnd < 0) {
selectionStart = editor.getCaretPosition();
selectionEnd = selectionStart;
}
final Element root = editor.getDocument().getDefaultRootElement();
final int startElement = root.getElementIndex(selectionStart);
final int endElement = root.getElementIndex(selectionEnd);
boolean result = false;
for (int i = startElement; i <= endElement; i++) {
final Element elem = root.getElement(i);
try {
final String elementtext = elem.getDocument().getText(elem.getStartOffset(), elem.getEndOffset() - elem.getStartOffset());
if (!elementtext.trim().startsWith("%")) {
elem.getDocument().insertString(elem.getStartOffset(), "%", null);
result = true;
}
}
catch (BadLocationException ex) {
LOG.throwing(this.getClass().getCanonicalName(), "commentSelectedLines()", ex);
}
}
editor.revalidate();
return result;
}
@Override
public boolean doesSupportTextCut() {
return true;
}
@Override
public boolean doesSupportTextPaste() {
return true;
}
}