/* GeoGebra - Dynamic Mathematics for Everyone http://www.geogebra.org This file is part of GeoGebra. This code has been written initially for Scilab (http://www.scilab.org/). 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. */ package org.geogebra.desktop.gui.editor; import java.awt.Color; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.Shape; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultHighlighter; import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.Highlighter; import javax.swing.text.JTextComponent; import javax.swing.text.Position; import javax.swing.text.View; import org.geogebra.common.util.debug.Log; /** * Useful to match opening and closing keywords from left to right or from right * to left * * @author Calixte DENIZET */ public class MatchingBlockManager { private Document doc; private GeoGebraEditorPane pane; private MatchingBlockScanner scanner; private Highlighter highlighter; private MatchingBlockScanner.MatchingPositions smpos; private Highlighter.HighlightPainter ocPainter; private Object first; private Object second; private boolean insideOc; private boolean insideKw; private boolean ocIncluded; private boolean kwIncluded; private boolean lr; private MouseOverMatcher mouseover; /** * Constructor * * @param doc * the doc to highlight * @param pane * the GeoGebraEditorPane associated with this Manager * @param lr * if true the matching is from left to right * @param highlighter * the highlighter to use */ public MatchingBlockManager(Document doc, GeoGebraEditorPane pane, boolean lr, Highlighter highlighter) { this.doc = doc; this.pane = pane; this.scanner = new MatchingBlockScanner(doc); this.highlighter = highlighter; this.lr = lr; } /** * @return the scanner used by thhis manager */ public MatchingBlockScanner getScanner() { return scanner; } /** * Set the painter for the matching open/close keywords (such as '('/')' or * '['/']'). The contents between the matchings is highlighted. * * @param filled * true if a filled rectangle must be used to highlight * @param color * the color of the painter **/ public void setPainterForOpenClose(boolean filled, boolean included, Color color) { this.insideOc = true; this.ocIncluded = included; update(); ocPainter = new InsideLinePainter(filled, false, color); } /** * Set the painter for the matching open/close keywords (such as '('/')' or * '['/']'). The matchings are highlighted. * * @param type * one of the three values : GeoGebraKeywordsPainter.FILLED * GeoGebraKeywordsPainter.UNDERLINED * GeoGebraKeywordsPainter.FRAMED * @param color * the color of the painter **/ public void setPainterForOpenClose(int type, Color color) { this.insideOc = false; update(); ocPainter = new GeoGebraKeywordsPainter(color, type); } /** * Set the painter for the matching open/close keywords (such as '('/')' or * '['/']'). Properties are found in the file scinotesConfiguration.xml The * contents between the matchings is highlighted. **/ public void setPainterForOpenClose() { Parameters param = new Parameters(Color.decode("#40e0d0"), false, false, true, GeoGebraKeywordsPainter.FILLED, true); if (param.inside) { boolean b = param.type == GeoGebraKeywordsPainter.FILLED; setPainterForOpenClose(b, param.included, param.color); } else { setPainterForOpenClose(param.type, param.color); } if (param.onmouseover) { activateMouseOver(); } } /** * Set the defaults from scinotesConfiguration.xml */ public void setDefaults() { setPainterForOpenClose(); } /** * Activate this MatchingBlockManager to listen to the KeywordEvent generate * by a MouseOver. */ public void activateMouseOver() { if (mouseover == null) { mouseover = new MouseOverMatcher(); } pane.addKeywordListener(mouseover); } /** * Desactivate this MatchingBlockManager to listen to the KeywordEvent * generate by a MouseOver. */ public void desactivateMouseOver() { if (mouseover != null) { pane.removeKeywordListener(mouseover); mouseover = null; } } /** * Remove the highlights if they exist. */ private void update() { if (first != null) { highlighter.removeHighlight(first); first = null; } if (second != null) { highlighter.removeHighlight(second); second = null; } } /** * Search the matching keywords * * @param tok * the type of the token at the position pos in the document * @param pos * the position in the doc */ public void searchMatchingBlock(int tok, int pos) { MatchingBlockScanner.MatchingPositions mpos = null; if (tok == GeoGebraLexerConstants.OPENCLOSE) { mpos = scanner.getMatchingBlock(pos, lr); } if (mpos != this.smpos) { this.smpos = mpos; if (first != null) { highlighter.removeHighlight(first); if (second != null) { highlighter.removeHighlight(second); } } if (mpos != null && tok == GeoGebraLexerConstants.OPENCLOSE) { createHighlights(mpos, insideOc, ocIncluded, ocPainter); } else if (mpos != null) { createHighlights(mpos, insideKw, kwIncluded, null); } } } /** * Create the highlights * * @param mpos * the position of the matching keywords * @param inside * true if we look at the contents between the keywords * @param hp * the painter to use */ private void createHighlights(MatchingBlockScanner.MatchingPositions mpos, boolean inside, boolean included, Highlighter.HighlightPainter hp) { try { if (!inside) { first = highlighter.addHighlight(mpos.firstB, mpos.firstE, hp); second = highlighter.addHighlight(mpos.secondB, mpos.secondE, hp); } else { if (lr) { if (included) { first = highlighter.addHighlight(mpos.firstB, mpos.secondE, hp); } else { first = highlighter.addHighlight(mpos.firstE, mpos.secondB, hp); } } else { if (included) { first = highlighter.addHighlight(mpos.secondB, mpos.firstE, hp); } else { first = highlighter.addHighlight(mpos.secondE, mpos.firstB, hp); } } } } catch (BadLocationException e) { Log.error(e.getMessage()); } } /** * Inner class used to retriev infos from scinotesConfiguration.xml */ public static class Parameters { /** * The color */ public Color color; /** * Inside or not */ public boolean inside; /** * Strict or not */ public boolean strict; /** * Included or not */ public boolean included; /** * The type */ public int type; /** * The onmouseover */ public boolean onmouseover; /** * Constructor * * @param color * the color * @param inside * inside or not * @param type * the type * @param onmouseover * a boolean */ public Parameters(Color color, boolean inside, boolean strict, boolean included, int type, boolean onmouseover) { this.color = color; this.inside = inside; this.type = type; this.strict = strict; this.included = included; this.onmouseover = onmouseover; } } /** * Inner class to highlight matching keywords */ public class GeoGebraKeywordsPainter extends DefaultHighlighter.DefaultHighlightPainter { /** * FILLED */ public static final int FILLED = 0; /** * UNDERLINED */ public static final int UNDERLINED = 1; /** * FRAMED */ public static final int FRAMED = 2; private Color color; private int type; /** * Constructor * * @param color * the color to paint * @param type * must be FILLED, UNDERLINED or FRAMED */ public GeoGebraKeywordsPainter(Color color, int type) { super(color); this.color = color; this.type = type; } /** * paintLayer * * @param g * Graphics * @param offs0 * the beginning * @param offs1 * the end * @param bounds * the bounds * @param c * the text component where to paint * @param view * the view to use * @return the shape containg the highlighted text */ @Override public Shape paintLayer(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c, View view) { try { Rectangle r = (Rectangle) view.modelToView(offs0, Position.Bias.Forward, offs1, Position.Bias.Backward, bounds); g.setColor(color); switch (type) { case UNDERLINED: g.drawLine(r.x, r.y + r.height - 1, r.x + r.width - 1, r.y + r.height - 1); return r; case FRAMED: g.drawRect(r.x, r.y, r.width - 1, r.height - 1); return r; case FILLED: default: g.fillRect(r.x, r.y, r.width, r.height); return r; } } catch (BadLocationException e) { return null; } } } /** * Inner class to highlight the content inside two keywords. The highlight * depends on the position of the content. */ class InsideLinePainter implements Highlighter.HighlightPainter { private boolean filled; private boolean strict; private Color color; /** * Constructor * * @param filled * if the highlighted rectangle must be filled * @param color * the color to paint */ protected InsideLinePainter(boolean filled, boolean strict, Color color) { this.filled = filled; this.strict = strict; this.color = color; } /** * Implements a strategy to render contents depending on the position of * these * * @param g * Graphics * @param pos0 * the beginning * @param pos1 * the end * @param bounds * the bounds * @param c * the text component where to paint */ @Override public void paint(Graphics g, int pos0, int pos1, Shape bounds, JTextComponent c) { try { Rectangle alloc = bounds.getBounds(); Rectangle p0 = c.modelToView(pos0); Rectangle p1 = c.modelToView(pos1); g.setColor(color); if (p0.y == p1.y) { Rectangle r = p0.union(p1); if (filled) { g.fillRect(r.x, r.y, r.width, r.height); } else { g.drawRect(r.x, r.y, r.width - 1, r.height - 1); } } else { Element root = doc.getDefaultRootElement(); int line0 = root.getElementIndex(pos0); int line1 = root.getElementIndex(pos1); Rectangle r0 = c .modelToView(root.getElement(line0).getEndOffset()); Rectangle r1 = c.modelToView( root.getElement(line1).getStartOffset()); if (line0 != line1) { if (!strict) { if (filled) { g.fillRect(p0.x, p0.y, alloc.width, p0.height); g.fillRect(alloc.x, p0.y + p0.height, alloc.width, r0.y - p0.y - p0.height); if (r1.y != p1.y) { g.fillRect(r1.x, r1.y, alloc.width, r1.height); } g.fillRect(r1.x, p1.y, p1.x, r1.height); } else { g.drawRect(p0.x, p0.y, alloc.width - 1, p0.height - 1); g.drawRect(alloc.x, p0.y + p0.height, alloc.width - 1, r0.y - p0.y - p0.height - 1); if (r1.y != p1.y) { g.drawRect(r1.x, r1.y, alloc.width, r1.height - 1); } g.drawRect(r1.x, p1.y, p1.x, r1.height - 1); } } if (filled) { g.fillRect(alloc.x, r0.y, alloc.width, r1.y - r0.y); } else { g.drawRect(alloc.x, r0.y, alloc.width - 1, r1.y - r0.y - 1); } } else { /* * This part of the code has been copied (for the * filling) from DefaultHighlighter.java */ int w = alloc.x + alloc.width - p0.x; if (filled) { g.fillRect(p0.x, p0.y, w, p0.height); if ((p0.y + p0.height) != p1.y) { g.fillRect(alloc.x, p0.y + p0.height, alloc.width, p1.y - (p0.y + p0.height)); } g.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height); } else { g.drawRect(p0.x, p0.y, w - 1, p0.height - 1); if ((p0.y + p0.height) != p1.y) { g.drawRect(alloc.x, p0.y + p0.height, alloc.width - 1, p1.y - (p0.y + p0.height) - 1); } g.drawRect(alloc.x, p1.y, (p1.x - alloc.x) - 1, p1.height - 1); } } } } catch (BadLocationException e) { } } } /** * Inner class to highlight on a KeywordEvent generated by a ONMOUSEOVER */ class MouseOverMatcher extends KeywordAdapter.MouseOverAdapter { /** * What to do when the event occured * * @param e * the event */ @Override public void caughtKeyword(KeywordEvent e) { if (lr) { searchMatchingBlock(e.getType(), e.getStart()); } else { searchMatchingBlock(e.getType(), e.getStart() + e.getLength()); } } } }