/*
* Gutter.java
* :tabSize=8:indentSize=8:noTabs=false:
* :folding=explicit:collapseFolds=1:
*
* Copyright (C) 1999, 2000 mike dillon
* Portions copyright (C) 2001, 2002 Slava Pestov
* minor features added for use with Wiring. [barragan]
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package processing.app.syntax;
import processing.app.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
//import org.gjt.sp.jedit.*;
import processing.app.syntax.im.InputMethodSupport;
import processing.core.PApplet;
public class Gutter extends JComponent implements SwingConstants
{
public Gutter(JEditTextArea textArea, TextAreaDefaults defaults)
{
this.textArea = textArea;
setGutterWidth(defaults.gutterWidth);
setCollapsed(defaults.gutterCollapsed);
setLineNumberingEnabled(defaults.gutterLineNumbers);
setHighlightInterval(defaults.gutterHighlightInterval);
setCurrentLineHighlightEnabled(defaults.currentLineHighlightEnabled);
setBackground(defaults.gutterbgcolor);
setForeground(defaults.gutterfgcolor);
setHighlightedForeground(defaults.gutterHighlightColor);
//setMarkerHighlightColor(defaults.gutterMarkerColor);
//setRegisterHighlightColor(defaults.gutterRegisterColor);
setCurrentLineForeground(defaults.gutterCurrentLineColor);
String alignment = defaults.gutterNumberAlignment;
if ("right".equals(alignment)) {
setLineNumberAlignment(Gutter.RIGHT);
} else if ("center".equals(alignment)) {
setLineNumberAlignment(Gutter.CENTER);
} else // left == default case
{
setLineNumberAlignment(Gutter.LEFT);
}
setBorder(defaults.gutterBorderWidth, defaults.gutterFocusBorderColor, defaults.gutterNoFocusBorderColor, defaults.bgcolor);
setFont(defaults.gutterFont);
setDoubleBuffered(true);
MouseHandler ml = new MouseHandler();
addMouseListener(ml);
addMouseMotionListener(ml);
}
public void paintComponent(Graphics gfx)
{
if (!collapsed)
{
// fill the background
Rectangle r = gfx.getClipBounds();
gfx.setColor(getBackground());
gfx.fillRect(r.x, r.y, r.width, r.height);
// paint custom highlights, if there are any
if (highlights != null) paintCustomHighlights(gfx);
// paint line numbers, if they are enabled
if (lineNumberingEnabled) paintLineNumbers(gfx);
}
}
private void paintLineNumbers(Graphics gfx)
{
FontMetrics pfm = textArea.getPainter().getFontMetrics();
int lineHeight = pfm.getHeight();
// XXX this is a hack to resize the gutter so the numbers fill in
int lineCount = textArea.getLineCount();
int digitsCount = (Integer.toString(lineCount)).length();
setGutterWidth(3 + fm.charWidth('w') * ((digitsCount < 2) ? 2 : digitsCount));
Rectangle clip = gfx.getClipBounds();
int baseline = (int) (clip.y - clip.y % lineHeight) + (int) Math.round(
(this.baseline + lineHeight - pfm.getDescent()) / 2.0);
int firstLine = clip.y / lineHeight + textArea.getFirstLine() + 1;
int lastLine = firstLine + clip.height / lineHeight;
int caretLine = textArea.getCaretLine() + 1;
int firstValidLine = firstLine > 1 ? firstLine : 1;
int lastValidLine = (lastLine > textArea.getLineCount())
? textArea.getLineCount() : lastLine;
boolean highlightCurrentLine = currentLineHighlightEnabled;
//&& (textArea.getSelectionStart() == textArea.getSelectionStop());
gfx.setFont(getFont());
Color fg = getForeground();
Color hfg = getHighlightedForeground();
Color clfg = getCurrentLineForeground();
String number;
int offset;
for (int line = firstLine; line <= lastLine;
line++, baseline += lineHeight)
{
// only print numbers for valid lines
if (line < firstValidLine || line > lastValidLine)
continue;
number = Integer.toString(line);
switch (alignment)
{
case RIGHT:
//offset = gutterSize.width - collapsedSize.width
offset = gutterSize.width - 2
- (fm.stringWidth(number) + 1);
break;
case CENTER:
//offset = ((gutterSize.width - collapsedSize.width)
offset = ((gutterSize.width - 2)
- fm.stringWidth(number)) / 2;
break;
case LEFT: default:
offset = 1;
}
if (line == caretLine && highlightCurrentLine)
{
gfx.setColor(clfg);
}
else if (interval > 1 && line % interval == 0)
{
gfx.setColor(hfg);
}
else
{
gfx.setColor(fg);
}
gfx.drawString(number, ileft + offset, baseline);
}
}
private void paintCustomHighlights(Graphics gfx)
{
int lineHeight = textArea.getPainter().getFontMetrics()
.getHeight();
int firstLine = textArea.getFirstLine();
int lastLine = firstLine + (getHeight() / lineHeight);
int y = 0;
for (int line = firstLine; line < lastLine;
line++, y += lineHeight)
{
highlights.paintHighlight(gfx, line, y);
}
}
/**
* Marks a line as needing a repaint.
* @param line The line to invalidate
*/
public final void invalidateLine(int line)
{
if(collapsed)
return;
FontMetrics pfm = textArea.getPainter().getFontMetrics();
repaint(0,textArea.lineToY(line) + pfm.getDescent() + pfm.getLeading(),
getWidth(),pfm.getHeight());
}
/**
* Marks a range of lines as needing a repaint.
* @param firstLine The first line to invalidate
* @param lastLine The last line to invalidate
*/
public final void invalidateLineRange(int firstLine, int lastLine)
{
if(collapsed)
return;
FontMetrics pfm = textArea.getPainter().getFontMetrics();
repaint(0,textArea.lineToY(firstLine) + pfm.getDescent() + pfm.getLeading(),
getWidth(),(lastLine - firstLine + 1) * pfm.getHeight());
}
/**
* Adds a custom highlight painter.
* @param highlight The highlight
*/
public void addCustomHighlight(TextAreaHighlight highlight)
{
highlight.init(textArea, highlights);
highlights = highlight;
}
/**
* Convenience method for setting a default matte border on the right
* with the specified border width and color
* @param width The border width (in pixels)
* @param color1 The focused border color
* @param color2 The unfocused border color
* @param color3 The gutter/text area gap color
*/
public void setBorder(int width, Color color1, Color color2, Color color3)
{
this.borderWidth = width;
focusBorder = new CompoundBorder(new MatteBorder(0,0,0,width,color3),
new MatteBorder(0,0,0,width,color1));
noFocusBorder = new CompoundBorder(new MatteBorder(0,0,0,width,color3),
new MatteBorder(0,0,0,width,color2));
updateBorder();
}
/**
* Sets the border differently if the text area has focus or not.
*/
public void updateBorder()
{
// because we are called from the text area's focus handler,
// we do an invokeLater() so that the view's focus handler
// has a chance to execute and set the edit pane properly
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
/*if(view.getEditPane() == null)
return;
if(view.getEditPane().getTextArea() == textArea)
setBorder(focusBorder);
else*/
setBorder(noFocusBorder);
}
});
}
/*
* JComponent.setBorder(Border) is overridden here to cache the left
* inset of the border (if any) to avoid having to fetch it during every
* repaint.
*/
public void setBorder(Border border)
{
super.setBorder(border);
if (border == null)
{
ileft = 0;
collapsedSize.width = 0;
collapsedSize.height = 0;
}
else
{
Insets insets = border.getBorderInsets(this);
ileft = insets.left;
collapsedSize.width = 5; //insets.left + insets.right;
collapsedSize.height = insets.top + insets.bottom;
}
}
/*
* JComponent.setFont(Font) is overridden here to cache the baseline for
* the font. This avoids having to get the font metrics during every
* repaint.
*/
public void setFont(Font font)
{
super.setFont(font);
fm = getFontMetrics(font);
baseline = fm.getAscent();
}
/**
* Get the foreground color for highlighted line numbers
* @return The highlight color
*/
public Color getHighlightedForeground()
{
return intervalHighlight;
}
public void setHighlightedForeground(Color highlight)
{
intervalHighlight = highlight;
}
public Color getCurrentLineForeground()
{
return currentLineHighlight;
}
public void setCurrentLineForeground(Color highlight)
{
currentLineHighlight = highlight;
}
/**
* Set the width of the expanded gutter
* @param width The gutter width
*/
public void setGutterWidth(int width)
{
if (width < collapsedSize.width) width = collapsedSize.width;
gutterSize.width = width;
// if the gutter is expanded, ask the text area to revalidate
// the layout to resize the gutter
if (!collapsed) textArea.revalidate();
}
/**
* Get the width of the expanded gutter
* @return The gutter width
*/
public int getGutterWidth()
{
return gutterSize.width;
}
/*
* Component.getPreferredSize() is overridden here to support the
* collapsing behavior.
*/
public Dimension getPreferredSize()
{
if (collapsed)
{
return collapsedSize;
}
else
{
return gutterSize;
}
}
public Dimension getMinimumSize()
{
return getPreferredSize();
}
public String getToolTipText(MouseEvent evt)
{
return (highlights == null) ? null :
highlights.getToolTipText(evt);
}
/**
* Identifies whether or not the line numbers are drawn in the gutter
* @return true if the line numbers are drawn, false otherwise
*/
public boolean isLineNumberingEnabled()
{
return lineNumberingEnabled;
}
/**
* Turns the line numbering on or off and causes the gutter to be
* repainted.
* @param enabled true if line numbers are drawn, false otherwise
*/
public void setLineNumberingEnabled(boolean enabled)
{
if (lineNumberingEnabled == enabled) return;
lineNumberingEnabled = enabled;
repaint();
}
/**
* Toggles line numbering.
* @param enabled true if line numbers are drawn, false otherwise
*/
public void toggleLineNumberingEnabled()
{
setLineNumberingEnabled(!lineNumberingEnabled);
}
/**
* Identifies whether the horizontal alignment of the line numbers.
* @return Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
*/
public int getLineNumberAlignment()
{
return alignment;
}
/**
* Sets the horizontal alignment of the line numbers.
* @param alignment Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
*/
public void setLineNumberAlignment(int alignment)
{
if (this.alignment == alignment) return;
this.alignment = alignment;
repaint();
}
/**
* Identifies whether the gutter is collapsed or expanded.
* @return true if the gutter is collapsed, false if it is expanded
*/
public boolean isCollapsed()
{
return collapsed;
}
/**
* Sets whether the gutter is collapsed or expanded and force the text
* area to update its layout if there is a change.
* @param collapsed true if the gutter is collapsed,
* false if it is expanded
*/
public void setCollapsed(boolean collapsed)
{
if (this.collapsed == collapsed) return;
this.collapsed = collapsed;
textArea.revalidate();
}
/**
* Toggles whether the gutter is collapsed or expanded.
*/
public void toggleCollapsed()
{
setCollapsed(!collapsed);
}
/**
* Makes the gutter's current size the default for future sessions.
* @since jEdit 2.7pre2
*/
public void saveGutterSize()
{
/*jEdit.setProperty("view.gutter.width", Integer.toString(
gutterSize.width));
*/}
/**
* Sets the number of lines between highlighted line numbers.
* @return The number of lines between highlighted line numbers or
* zero if highlighting is disabled
*/
public int getHighlightInterval()
{
return interval;
}
/**
* Sets the number of lines between highlighted line numbers. Any value
* less than or equal to one will result in highlighting being disabled.
* @param interval The number of lines between highlighted line numbers
*/
public void setHighlightInterval(int interval)
{
if (interval <= 1) interval = 0;
this.interval = interval;
repaint();
}
public boolean isCurrentLineHighlightEnabled()
{
return currentLineHighlightEnabled;
}
public void setCurrentLineHighlightEnabled(boolean enabled)
{
if (currentLineHighlightEnabled == enabled) return;
currentLineHighlightEnabled = enabled;
repaint();
}
public JPopupMenu getContextMenu()
{
return context;
}
public void setContextMenu(JPopupMenu context)
{
this.context = context;
}
// private members
//private View view;
private JEditTextArea textArea;
private JPopupMenu context;
private TextAreaHighlight highlights;
private int baseline = 0;
private int ileft = 0;
private Dimension gutterSize = new Dimension(0,0);
private Dimension collapsedSize = new Dimension(5,0);
private Color intervalHighlight;
private Color currentLineHighlight;
private FontMetrics fm;
private int alignment;
private int interval = 0;
private boolean lineNumberingEnabled = true;
private boolean currentLineHighlightEnabled = false;
private boolean collapsed = false;
private int borderWidth;
private Border focusBorder, noFocusBorder;
class MouseHandler extends MouseAdapter implements MouseMotionListener
{
public void mousePressed(MouseEvent e)
{
if(e.getX() >= getWidth() - borderWidth)
{
e.translatePoint(-getWidth(),0);
textArea.mouseHandler.mousePressed(e);
//return;
}
else if(context != null && (e.getModifiers()
& InputEvent.BUTTON3_MASK) != 0)
{
if(context.isVisible())
context.setVisible(false);
else
{
//XXX this is a hack to make sure the
//XXX actions get the right text area
textArea.requestFocus();
context.show(Gutter.this,
e.getX()+1, e.getY()+1);
}
}
else
if(e.getClickCount() == 2)
toggleCollapsed();
else
{
dragStart = e.getPoint();
startWidth = gutterSize.width;
}
}
public void mouseDragged(MouseEvent e)
{
if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
return;
if(collapsed || e.getX() >= getWidth() - borderWidth)
{
e.translatePoint(-getWidth(),0);
textArea.mouseHandler.mouseDragged(e);
return;
}
if (dragStart == null) return;
// gutterSize.width = startWidth + e.getX() - dragStart.x;
// if (gutterSize.width < collapsedSize.width)
// gutterSize.width = startWidth;
textArea.revalidate();
}
/* public void mouseExited(MouseEvent e)
{
if (dragStart != null && dragStart.x > e.getPoint().x)
{
setCollapsed(true);
gutterSize.width = startWidth;
textArea.revalidate();
}
//dragStart = null;
} */
public void mouseMoved(MouseEvent e) {}
public void mouseReleased(MouseEvent e)
{
if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
return;
if(collapsed || e.getX() >= getWidth() - borderWidth)
{
e.translatePoint(-getWidth(),0);
textArea.mouseHandler.mouseReleased(e);
return;
}
dragStart = null;
}
private Point dragStart = null;
private int startWidth = 0;
}
}