/*
 * Gutter.java
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999, 2000 mike dillon
 * Portions copyright (C) 2001, 2002 Slava Pestov
 *
 * 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 org.gjt.sp.jedit.textarea;

//{{{ Imports
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

import org.gjt.sp.jedit.Registers;
import org.gjt.sp.jedit.buffer.BufferAdapter;
import org.gjt.sp.jedit.buffer.BufferListener;
import org.gjt.sp.jedit.buffer.JEditBuffer;
import org.gjt.sp.util.Log;
//}}}

/**
 * The gutter is the component that displays folding indicators and line
 * numbers to the left of the text area. The only methods in this class
 * that should be called by plugins are those for adding and removing
 * text area extensions.
 *
 * @see #addExtension(TextAreaExtension)
 * @see #addExtension(int,TextAreaExtension)
 * @see #removeExtension(TextAreaExtension)
 * @see TextAreaExtension
 * @see TextArea
 *
 * @author Mike Dillon and Slava Pestov
 * @version $Id: Gutter.java 25604 2022-09-13 11:01:01Z kpouer $
 */
public class Gutter extends JComponent implements SwingConstants
{
	//{{{ Layers
	/**
	 * The lowest possible layer.
	 * @see #addExtension(int,TextAreaExtension)
	 * @since jEdit 4.0pre4
	 */
	public static final int LOWEST_LAYER = Integer.MIN_VALUE;

	/**
	 * Default extension layer. This is above the wrap guide but below the
	 * bracket highlight.
	 * @since jEdit 4.0pre4
	 */
	public static final int DEFAULT_LAYER = 0;

	/**
	 * Highest possible layer.
	 * @since jEdit 4.0pre4
	 */
	public static final int HIGHEST_LAYER = Integer.MAX_VALUE;
	//}}}

	//{{{ Fold painters
	/**
	 * Fold painter service.
	 * @since jEdit 4.3pre16
	 */
	public static final String FOLD_PAINTER_PROPERTY = "foldPainter";
	public static final String FOLD_PAINTER_SERVICE = "org.gjt.sp.jedit.textarea.FoldPainter";
	public static final String DEFAULT_FOLD_PAINTER_SERVICE = "Triangle";

	//{{{ setFolderPainter() method
	public void setFoldPainter(FoldPainter painter)
	{
		if (painter == null)
			foldPainter = new TriangleFoldPainter();
		else
			foldPainter = painter;
	}
	//}}}
	
	//}}} Fold painters
	
	//{{{ Gutter constructor
	public Gutter(TextArea textArea)
	{
		this.textArea = textArea;
		enabled = true;
		selectionAreaEnabled = true;
		selectionAreaWidth = SELECTION_GUTTER_WIDTH;

		setAutoscrolls(true);
		setOpaque(true);
		setRequestFocusEnabled(false);

		extensionMgr = new ExtensionManager();

		mouseHandler = new MouseHandler();
		addMouseListener(mouseHandler);
		addMouseMotionListener(mouseHandler);

		bufferListener = new BufferAdapter()
		{
			@Override
			public void bufferLoaded(JEditBuffer buffer)
			{
				updateLineNumberWidth();
			}

			@Override
			public void contentInserted(JEditBuffer buffer, int startLine,
					int offset, int numLines, int length)
			{
				if (numLines != 0)
					updateLineNumberWidth();
			}

			@Override
			public void contentRemoved(JEditBuffer buffer, int startLine,
					int offset, int numLines, int length) 
			{
				if (numLines != 0)
					updateLineNumberWidth();
			}
		};

		updateBorder();
		setFoldPainter(textArea.getFoldPainter());
	} //}}}

	//{{{ paintComponent() method
	@Override
	public void paintComponent(Graphics _gfx)
	{
		Graphics2D gfx = (Graphics2D)_gfx;
		gfx.setRenderingHints(textArea.getPainter().renderingHints);
		// fill the background
		Rectangle clip = gfx.getClipBounds();
		gfx.setColor(getBackground());
		int bgColorWidth = isSelectionAreaEnabled() ? FOLD_MARKER_SIZE :
			clip.width; 
		gfx.fillRect(clip.x, clip.y, bgColorWidth, clip.height);
		if (isSelectionAreaEnabled())
		{
			if (selectionAreaBgColor == null)
				selectionAreaBgColor = getBackground();
			gfx.setColor(selectionAreaBgColor);
			gfx.fillRect(clip.x + FOLD_MARKER_SIZE, clip.y,
				clip.width - FOLD_MARKER_SIZE, clip.height);
		}
		// if buffer is loading, don't paint anything
		if (textArea.getBuffer().isLoading())
			return;

		int lineHeight = textArea.getPainter().getLineHeight();

		if(lineHeight == 0)
			return;

		int firstLine = clip.y / lineHeight;
		int lastLine = (clip.y + clip.height - 1) / lineHeight;

		if(lastLine - firstLine > textArea.getVisibleLines())
		{
			Log.log(Log.ERROR,this,"BUG: firstLine=" + firstLine);
			Log.log(Log.ERROR,this,"     lastLine=" + lastLine);
			Log.log(Log.ERROR,this,"     visibleLines=" + textArea.getVisibleLines());
			Log.log(Log.ERROR,this,"     height=" + getHeight());
			Log.log(Log.ERROR,this,"     painter.height=" + lineHeight);
			Log.log(Log.ERROR,this,"     clip.y=" + clip.y);
			Log.log(Log.ERROR,this,"     clip.height=" + clip.height);
			Log.log(Log.ERROR,this,"     lineHeight=" + lineHeight);
		}
	
		int y = clip.y - clip.y % lineHeight + textArea.getPainter().getLineExtraSpacing();

		extensionMgr.paintScreenLineRange(textArea,gfx,
			firstLine,lastLine,y,lineHeight);

		for (int line = firstLine; line <= lastLine;
			line++, y += lineHeight)
		{
			paintLine(gfx,line,y);
		}
	} //}}}

	//{{{ addExtension() method
	/**
	 * Adds a text area extension, which can perform custom painting and
	 * tool tip handling.
	 * @param extension The extension
	 * @since jEdit 4.0pre4
	 */
	public void addExtension(TextAreaExtension extension)
	{
		extensionMgr.addExtension(DEFAULT_LAYER,extension);
		repaint();
	} //}}}

	//{{{ addExtension() method
	/**
	 * Adds a text area extension, which can perform custom painting and
	 * tool tip handling.
	 * @param layer The layer to add the extension to. Note that more than
	 * extension can share the same layer.
	 * @param extension The extension
	 * @since jEdit 4.0pre4
	 */
	public void addExtension(int layer, TextAreaExtension extension)
	{
		extensionMgr.addExtension(layer,extension);
		repaint();
	} //}}}

	//{{{ removeExtension() method
	/**
	 * Removes a text area extension. It will no longer be asked to
	 * perform custom painting and tool tip handling.
	 * @param extension The extension
	 * @since jEdit 4.0pre4
	 */
	public void removeExtension(TextAreaExtension extension)
	{
		extensionMgr.removeExtension(extension);
		repaint();
	} //}}}

	//{{{ getExtensions() method
	/**
	 * Returns an array of registered text area extensions. Useful for
	 * debugging purposes.
	 * @since jEdit 4.1pre5
	 */
	public TextAreaExtension[] getExtensions()
	{
		return extensionMgr.getExtensions();
	} //}}}

	//{{{ getToolTipText() method
	/**
	 * Returns the tool tip to display at the specified location.
	 * @param evt The mouse event
	 */
	@Override
	public String getToolTipText(MouseEvent evt)
	{
		if(textArea.getBuffer().isLoading())
			return null;

		return extensionMgr.getToolTipText(evt.getX(),evt.getY());
	} //}}}

	//{{{ setBorder() method
	/**
	 * 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)
	{
		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();
	} //}}}

	//{{{ updateBorder() method
	/**
	 * Sets the border differently if the text area has focus or not.
	 */
	public void updateBorder()
	{
		if (textArea.hasFocus())
			setBorder(focusBorder);
		else
			setBorder(noFocusBorder);
	} //}}}

	//{{{ setBorder() method
	/*
	 * 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.
	 */
	@Override
	public void setBorder(Border border)
	{
		super.setBorder(border);

		if (border == null)
		{
			collapsedSize.width = 0;
			collapsedSize.height = 0;
		}
		else
		{
			Insets insets = border.getBorderInsets(this);
			collapsedSize.width = FOLD_MARKER_SIZE + insets.right;
			if (isSelectionAreaEnabled())
				 collapsedSize.width += selectionAreaWidth;
			collapsedSize.height = gutterSize.height
				= insets.top + insets.bottom;
			lineNumberWidth = fm.charWidth('5') * getLineNumberDigitCount(); 
			gutterSize.width = FOLD_MARKER_SIZE + insets.right
				+ lineNumberWidth;
		}

		revalidate();
	} //}}}

	//{{{ setMinLineNumberDigitCount() method
	public void setMinLineNumberDigitCount(int min)
	{
		if (min == minLineNumberDigits)
			return;
		minLineNumberDigits = min;
		if (textArea.getBuffer() != null)
			updateLineNumberWidth();
	} //}}}

	//{{{ getMinLineNumberDigitCount() method
	private int getMinLineNumberDigitCount()
	{
		return minLineNumberDigits;
	} //}}}

	//{{{ getLineNumberDigitCount() method
	private int getLineNumberDigitCount()
	{
		JEditBuffer buf = textArea.getBuffer();
		int minDigits = getMinLineNumberDigitCount();
		if (buf == null)
			return minDigits;
		int count = buf.getLineCount();
		int digits;
		for (digits = 0; count > 0; digits++)
			count /= 10;
		return Math.max(digits, minDigits);
	} //}}}

	//{{{ setBuffer() method
	void setBuffer(JEditBuffer newBuffer)
	{
		if (buffer != null)
			buffer.removeBufferListener(bufferListener);
		buffer = newBuffer;
		if (buffer != null)
			buffer.addBufferListener(bufferListener);
		updateLineNumberWidth();
	} //}}}

	//{{{ updateLineNumberWidth() method
	private void updateLineNumberWidth()
	{
		Font f = getFont();
		if (f != null)
			setFont(getFont());
	} //}}}

	//{{{ dispose() method
	void dispose()
	{
		if (buffer != null)
		{
			buffer.removeBufferListener(bufferListener);
			buffer = null;
		}
	} //}}}

	//{{{ setFont() method
	/*
	 * JComponent.setFont(Font) is overridden here to cache the font
	 * metrics for the font. This avoids having to get the font metrics
	 * during every repaint.
	 */
	@Override
	public void setFont(Font font)
	{
		super.setFont(font);

		fm = getFontMetrics(font);

		Border border = getBorder();
		if(border != null)
		{
			lineNumberWidth = fm.charWidth('5') * getLineNumberDigitCount(); 
			gutterSize.width = FOLD_MARKER_SIZE
				+ border.getBorderInsets(this).right
				+ lineNumberWidth;
			revalidate();
		}
	} //}}}

	//{{{ Getters and setters

	//{{{ setGutterEnabled() method
	/* Enables showing or hiding the gutter. */
	public void setGutterEnabled(boolean enabled)
	{
		this.enabled = enabled;
		revalidate();
	} //}}}

	//{{{ isSelectionAreaEnabled() method
	public boolean isSelectionAreaEnabled()
	{
		return selectionAreaEnabled;
	} //}}}

	//{{{ setSelectionAreaEnabled() method
	public void setSelectionAreaEnabled(boolean enabled)
	{
		if (isSelectionAreaEnabled() == enabled)
			return;
		selectionAreaEnabled = enabled;
		if (enabled)
			collapsedSize.width += selectionAreaWidth;
		else
			collapsedSize.width -= selectionAreaWidth;
		revalidate();
	} //}}}

	//{{{ setSelectionAreaBackground() method
	public void setSelectionAreaBackground(Color bgColor)
	{
		selectionAreaBgColor = bgColor;
		repaint();
	} //}}}

	//{{{ setSelectionAreaWidth() method
	public void setSelectionAreaWidth(int width)
	{
		selectionAreaWidth = width;
		revalidate();
	} //}}}

	//{{{ getHighlightedForeground() method
	/**
	 * Get the foreground color for highlighted line numbers
	 * @return The highlight color
	 */
	public Color getHighlightedForeground()
	{
		return intervalHighlight;
	} //}}}

	//{{{ setHighlightedForeground() method
	public void setHighlightedForeground(Color highlight)
	{
		intervalHighlight = highlight;
	} //}}}

	//{{{ getCurrentLineForeground() method
	public Color getCurrentLineForeground()
 	{
		return currentLineHighlight;
	} //}}}

	//{{{ setCurrentLineForeground() method
	public void setCurrentLineForeground(Color highlight)
	{
		currentLineHighlight = highlight;
 	} //}}}

	//{{{ getFoldColor() method
	public Color getFoldColor()
 	{
		return foldColor;
	} //}}}

	//{{{ setFoldColor() method
	public void setFoldColor(Color foldColor)
	{
		this.foldColor = foldColor;
 	} //}}}

	//{{{ getPreferredSize() method
	/*
	 * Component.getPreferredSize() is overridden here to support the
	 * collapsing behavior.
	 */
	@Override
	public Dimension getPreferredSize()
	{
		if (! enabled)
			return disabledSize;
		if (expanded)
			return gutterSize;
		else
			return collapsedSize;
	} //}}}

	//{{{ getMinimumSize() method
	@Override
	public Dimension getMinimumSize()
	{
		return getPreferredSize();
	} //}}}

	//{{{ getLineNumberAlignment() method
	/**
	 * Identifies whether the horizontal alignment of the line numbers.
	 * @return Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
	 */
	public int getLineNumberAlignment()
	{
		return alignment;
	} //}}}

	//{{{ setLineNumberAlignment() method
	/**
	 * 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();
	} //}}}

	//{{{ isExpanded() method
	/**
	 * Identifies whether the gutter is collapsed or expanded.
	 * @return true if the gutter is expanded, false if it is collapsed
	 */
	public boolean isExpanded()
	{
		return expanded;
	} //}}}

	//{{{ setExpanded() method
	/**
	 * Sets whether the gutter is collapsed or expanded and force the text
	 * area to update its layout if there is a change.
	 * @param expanded true if the gutter is expanded,
	 *                   false if it is collapsed
	 */
	public void setExpanded(boolean expanded)
	{
		if (this.expanded == expanded) return;

		this.expanded = expanded;

		textArea.revalidate();
	} //}}}

	//{{{ toggleExpanded() method
	/**
	 * Toggles whether the gutter is collapsed or expanded.
	 */
	public void toggleExpanded()
	{
		setExpanded(!expanded);
	} //}}}

	//{{{ getHighlightInterval() method
	/**
	 * 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;
	} //}}}

	//{{{ setHighlightInterval() method
	/**
	 * 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();
	} //}}}

	//{{{ isCurrentLineHighlightEnabled() method
	public boolean isCurrentLineHighlightEnabled()
	{
		return currentLineHighlightEnabled;
	} //}}}

	//{{{ setCurrentLineHighlightEnabled() method
	public void setCurrentLineHighlightEnabled(boolean enabled)
	{
		if (currentLineHighlightEnabled == enabled) return;

		currentLineHighlightEnabled = enabled;

		repaint();
	} //}}}

	//{{{ getStructureHighlightColor() method
	/**
	 * Returns the structure highlight color.
	 * @since jEdit 4.2pre3
	 */
	public final Color getStructureHighlightColor()
	{
		return structureHighlightColor;
	} //}}}

	//{{{ setStructureHighlightColor() method
	/**
	 * Sets the structure highlight color.
	 * @param structureHighlightColor The structure highlight color
	 * @since jEdit 4.2pre3
	 */
	public final void setStructureHighlightColor(Color structureHighlightColor)
	{
		this.structureHighlightColor = structureHighlightColor;
		repaint();
	} //}}}

	//{{{ isStructureHighlightEnabled() method
	/**
	 * Returns true if structure highlighting is enabled, false otherwise.
	 * @since jEdit 4.2pre3
	 */
	public final boolean isStructureHighlightEnabled()
	{
		return structureHighlight;
	} //}}}

	//{{{ setStructureHighlightEnabled() method
	/**
	 * Enables or disables structure highlighting.
	 * @param structureHighlight True if structure highlighting should be
	 * enabled, false otherwise
	 * @since jEdit 4.2pre3
	 */
	public final void setStructureHighlightEnabled(boolean structureHighlight)
	{
		this.structureHighlight = structureHighlight;
		repaint();
	} //}}}

	public void setSelectionPopupHandler(GutterPopupHandler handler)
	{
		mouseHandler.selectionPopupHandler = handler;
	}

	public GutterPopupHandler getSelectionPopupHandler()
	{
		return mouseHandler.selectionPopupHandler;
	}

	public void setMouseActionsProvider(MouseActionsProvider mouseActionsProvider)
	{
		mouseHandler.mouseActions = mouseActionsProvider;
	}
	//}}}

	//{{{ Private members

	//{{{ Instance variables
	private static final int FOLD_MARKER_SIZE = 12;
	private static final int SELECTION_GUTTER_WIDTH = 12;
		// The selection gutter exists only if the gutter is not expanded

	private boolean enabled;
	private final TextArea textArea;
	private final MouseHandler mouseHandler;
	private final ExtensionManager extensionMgr;

	private final Dimension gutterSize = new Dimension(0,0);
	private final Dimension collapsedSize = new Dimension(0,0);
	private int lineNumberWidth;
	private final Dimension disabledSize = new Dimension(0,0);

	private Color intervalHighlight;
	private Color currentLineHighlight;
	private Color foldColor;
	private Color selectionAreaBgColor;

	private FontMetrics fm;

	private int alignment;

	private int interval;
	private boolean currentLineHighlightEnabled;
	private boolean expanded;
	private boolean selectionAreaEnabled;

	private boolean structureHighlight;
	private Color structureHighlightColor;

	private int borderWidth;
	private Border focusBorder, noFocusBorder;
	
	private FoldPainter foldPainter;
	private JEditBuffer buffer;
	private final BufferListener bufferListener;
	private int minLineNumberDigits;
	private int selectionAreaWidth;
	//}}}

	//{{{ paintLine() method
	private void paintLine(Graphics2D gfx, int line, int y)
	{
		JEditBuffer buffer = textArea.getBuffer();
		if(buffer.isLoading())
			return;

		FontMetrics textAreaFm = textArea.getPainter().getFontMetrics();
		int lineHeight = textArea.getPainter().getLineHeight();
		int baseline = textAreaFm.getAscent() + textAreaFm.getLeading();
		
		ChunkCache.LineInfo info = textArea.chunkCache.getLineInfo(line);
		int physicalLine = info.physicalLine;

		// Skip lines beyond EOF
		if(physicalLine == -1)
			return;

		boolean drawFoldMiddle = true;
		//{{{ Paint fold start and end indicators
		if(info.firstSubregion && buffer.isFoldStart(physicalLine))
		{
			drawFoldMiddle = false;
			foldPainter.paintFoldStart(this, gfx, line, physicalLine,
					textArea.displayManager.isLineVisible(physicalLine+1),
					y - textArea.getPainter().getLineExtraSpacing(), lineHeight, buffer);
		}
		else if(info.lastSubregion && buffer.isFoldEnd(physicalLine))
		{
			drawFoldMiddle = false;
			foldPainter.paintFoldEnd(this, gfx, line, physicalLine, y - textArea.getPainter().getLineExtraSpacing(),
					lineHeight, buffer);
		} //}}}
		//{{{ Paint bracket scope
		else if(structureHighlight)
		{
			StructureMatcher.Match match = textArea.getStructureMatch();
			int caretLine = textArea.getCaretLine();

			if(textArea.isStructureHighlightVisible()
				&& physicalLine >= Math.min(caretLine,match.startLine)
				&& physicalLine <= Math.max(caretLine,match.startLine))
			{
				int caretScreenLine;
				if(caretLine > textArea.getLastPhysicalLine())
					caretScreenLine = Integer.MAX_VALUE;
				else if(textArea.displayManager.isLineVisible(
						textArea.getCaretLine()))
				{
					caretScreenLine = textArea
						.getScreenLineOfOffset(
						textArea.getCaretPosition());
				}
				else
				{
					caretScreenLine = -1;
				}

				int structScreenLine;
				if(match.startLine > textArea.getLastPhysicalLine())
					structScreenLine = Integer.MAX_VALUE;
				else if(textArea.displayManager.isLineVisible(
						match.startLine))
				{
					structScreenLine = textArea
						.getScreenLineOfOffset(
						match.start);
				}
				else
				{
					structScreenLine = -1;
				}

				if(caretScreenLine > structScreenLine)
				{
					int tmp = caretScreenLine;
					caretScreenLine = structScreenLine;
					structScreenLine = tmp;
				}

				gfx.setColor(structureHighlightColor);
				drawFoldMiddle = false;
				if(structScreenLine == caretScreenLine)
				{
					// do nothing
					drawFoldMiddle = true;
				}
				// draw |^
				else if(line == caretScreenLine)
				{
					gfx.fillRect(5,
						y
						+ lineHeight / 2,
						5,
						2);
					gfx.fillRect(5,
						y
						+ lineHeight / 2,
						2,
						lineHeight - lineHeight / 2);
				}
				// draw |_
				else if(line == structScreenLine)
				{
					gfx.fillRect(5,
						y,
						2,
						lineHeight / 2);
					gfx.fillRect(5,
						y + lineHeight / 2,
						5,
						2);
				}
				// draw |
				else if(line > caretScreenLine
					&& line < structScreenLine)
				{
					gfx.fillRect(5,
						y,
						2,
						lineHeight);
				}
			}
		} //}}}
		if(drawFoldMiddle && buffer.getFoldLevel(physicalLine) > 0)
		{
			foldPainter.paintFoldMiddle(this, gfx, line, physicalLine,
					y - textArea.getPainter().getLineExtraSpacing(), lineHeight, buffer);
		}

		//{{{ Paint line numbers
		if(info.firstSubregion && expanded)
		{
			String number = Integer.toString(physicalLine + 1);

			int offset;
			switch (alignment)
			{
			case RIGHT:
				offset = lineNumberWidth - (fm.stringWidth(number) + 1);
				break;
			case CENTER:
				offset = (lineNumberWidth - fm.stringWidth(number)) / 2;
				break;
			case LEFT: default:
				offset = 0;
				break;
			}

			if (physicalLine == textArea.getCaretLine() && currentLineHighlightEnabled)
			{
				gfx.setColor(currentLineHighlight);
			}
			else if (interval > 1 && (physicalLine + 1) % interval == 0)
				gfx.setColor(intervalHighlight);
			else
				gfx.setColor(getForeground());

			gfx.drawString(number, FOLD_MARKER_SIZE + offset,
				baseline + y);
		} //}}}
	} //}}}

	//}}}

	//{{{ MouseHandler class
	class MouseHandler extends MouseInputAdapter
	{
		MouseActionsProvider mouseActions;
		boolean drag;
		int toolTipInitialDelay, toolTipReshowDelay;
		int selectionStart;
		boolean selectLines;
		int selAnchorLine;
		GutterPopupHandler selectionPopupHandler;

		//{{{ mouseEntered() method
		@Override
		public void mouseEntered(MouseEvent e)
		{
			ToolTipManager ttm = ToolTipManager.sharedInstance();
			toolTipInitialDelay = ttm.getInitialDelay();
			toolTipReshowDelay = ttm.getReshowDelay();
			ttm.setInitialDelay(0);
			ttm.setReshowDelay(0);
		} //}}}

		//{{{ mouseExited() method
		@Override
		public void mouseExited(MouseEvent evt)
		{
			ToolTipManager ttm = ToolTipManager.sharedInstance();
			ttm.setInitialDelay(toolTipInitialDelay);
			ttm.setReshowDelay(toolTipReshowDelay);
		} //}}}

		//{{{ mousePressed() method
		@Override
		public void mousePressed(MouseEvent e)
		{
			textArea.requestFocus();

			boolean outsideGutter =
				(e.getX() >= getWidth() - borderWidth * 2);
			if(TextAreaMouseHandler.isPopupTrigger(e) || outsideGutter)
			{
				if ((selectionPopupHandler != null) &&
					(! outsideGutter) &&
					(e.getX() > FOLD_MARKER_SIZE))
				{
					int screenLine = e.getY() / textArea.getPainter().getLineHeight();
					int line = textArea.chunkCache.getLineInfo(screenLine)
						.physicalLine;
					if (line >= 0)
					{
						selectionPopupHandler.handlePopup(
							e.getX(), e.getY(), line);
						return;
					}
				}
				e.translatePoint(-getWidth(),0);
				textArea.mouseHandler.mousePressed(e);
				drag = true;
			}
			else
			{
				JEditBuffer buffer = textArea.getBuffer();

				int screenLine = e.getY() / textArea.getPainter().getLineHeight();

				int line = textArea.chunkCache.getLineInfo(screenLine)
					.physicalLine;

				if(line == -1)
					return;

				if (e.getX() >= FOLD_MARKER_SIZE)
				{
					selectionStart = textArea.getLineStartOffset(line);
					int selectionEnd = getFoldEndOffset(line);
					Selection s = new Selection.Range(
						selectionStart, selectionEnd);
					if(textArea.isMultipleSelectionEnabled())
						textArea.addToSelection(s);
					else
						textArea.setSelection(s);
					textArea.moveCaretPosition(selectionEnd, false);
					selectLines = true;
					selAnchorLine = line;
					return;
				}

				//{{{ Determine action
				String defaultAction;
				String variant;
				if(buffer.isFoldStart(line))
				{
					defaultAction = "toggle-fold";
					variant = "fold";
				}
				else if(structureHighlight
					&& textArea.isStructureHighlightVisible()
					&& textArea.lineInStructureScope(line))
				{
					defaultAction = "match-struct";
					variant = "struct";
				}
				else
					return;

				String action = null;

				if (mouseActions != null)
					action = mouseActions.getActionForEvent(
						e,variant);

				if(action == null)
					action = defaultAction;
				//}}}

				//{{{ Handle actions
				StructureMatcher.Match match = textArea
					.getStructureMatch();

				if(action.equals("select-fold"))
				{
					textArea.displayManager.expandFold(line,true);
					textArea.selectFold(line);
				}
				else if(action.equals("narrow-fold"))
				{
					int[] lines = buffer.getFoldAtLine(line);
					textArea.displayManager.narrow(lines[0],lines[1]);
				}
				else if(action.startsWith("toggle-fold"))
				{
					if(textArea.displayManager
						.isLineVisible(line + 1))
					{
						textArea.collapseFold(line);
					}
					else
					{
						if(action.endsWith("-fully"))
						{
							textArea.displayManager
								.expandFold(line,
								true);
						}
						else
						{
							textArea.displayManager
								.expandFold(line,
								false);
						}
					}
				}
				else if(action.equals("match-struct"))
				{
					if(match != null)
						textArea.setCaretPosition(match.end);
				}
				else if(action.equals("select-struct"))
				{
					if(match != null)
					{
						match.matcher.selectMatch(
							textArea);
					}
				}
				else if(action.equals("narrow-struct"))
				{
					if(match != null)
					{
						int start = Math.min(
							match.startLine,
							textArea.getCaretLine());
						int end = Math.max(
							match.endLine,
							textArea.getCaretLine());
						textArea.displayManager.narrow(start,end);
					}
				} //}}}
			}
		} //}}}

		//{{{ mouseDragged() method
		@Override
		public void mouseDragged(MouseEvent e)
		{
			if(drag /* && e.getX() >= getWidth() - borderWidth * 2 */)
			{
				e.translatePoint(-getWidth(),0);
				textArea.mouseHandler.mouseDragged(e);
			}
			else if(selectLines)
			{
				int screenLine = e.getY() / textArea.getPainter().getLineHeight();
				int line;
				if(e.getY() < 0)
				{
					textArea.scrollUpLine();
					line = textArea.getFirstPhysicalLine();
				}
				else if(e.getY() >= getHeight())
				{
					textArea.scrollDownLine();
					line = textArea.getLastPhysicalLine();
				}
				else
					line = textArea.chunkCache.getLineInfo(screenLine)
						.physicalLine;

				int selStart, selEnd;
				if(line < selAnchorLine)
				{
					selStart = textArea.getLineStartOffset(line);
					selEnd = getFoldEndOffset(selAnchorLine);
					textArea.moveCaretPosition(selStart, false);
				}
				else
				{
					selStart = textArea.getLineStartOffset(selAnchorLine);
					selEnd = getFoldEndOffset(line);
					textArea.moveCaretPosition(selEnd, false);
				}

				textArea.resizeSelection(selStart, selEnd, 0, false);
			}
		} //}}}

		//{{{ getFoldEndOffset() method
		private int getFoldEndOffset(int line)
		{
			JEditBuffer buffer = textArea.getBuffer();
			int endLine;
			if ((line == buffer.getLineCount() - 1) ||
				(textArea.displayManager.isLineVisible(line + 1)))
			{
				endLine = line;
			}
			else
			{
				int[] lines = buffer.getFoldAtLine(line);
				endLine = lines[1];
			}

			if(endLine == buffer.getLineCount() - 1)
				return buffer.getLineEndOffset(endLine) - 1;
			else
				return buffer.getLineEndOffset(endLine);
		} //}}}

		//{{{ mouseReleased() method
		@Override
		public void mouseReleased(MouseEvent e)
		{
			if(drag && e.getX() >= getWidth() - borderWidth * 2)
			{
				e.translatePoint(-getWidth(),0);
				textArea.mouseHandler.mouseReleased(e);
			}
			if (selectLines)
			{
				Selection sel = textArea.getSelectionAtOffset(selectionStart);
				if(sel != null)
					Registers.setRegister('%', textArea.getSelectedText(sel));
			}

			drag = false;
			selectLines = false;
		} //}}}
	} //}}}
}
