| /* WrappedPlainView.java -- |
| Copyright (C) 2005, 2006 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath 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, or (at your option) |
| any later version. |
| |
| GNU Classpath 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 GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package javax.swing.text; |
| |
| import java.awt.Color; |
| import java.awt.Container; |
| import java.awt.FontMetrics; |
| import java.awt.Graphics; |
| import java.awt.Rectangle; |
| import java.awt.Shape; |
| |
| import javax.swing.event.DocumentEvent; |
| import javax.swing.text.Position.Bias; |
| |
| /** |
| * @author Anthony Balkissoon abalkiss at redhat dot com |
| * |
| */ |
| public class WrappedPlainView extends BoxView implements TabExpander |
| { |
| /** The color for selected text **/ |
| Color selectedColor; |
| |
| /** The color for unselected text **/ |
| Color unselectedColor; |
| |
| /** The color for disabled components **/ |
| Color disabledColor; |
| |
| /** |
| * Stores the font metrics. This is package private to avoid synthetic |
| * accessor method. |
| */ |
| FontMetrics metrics; |
| |
| /** Whether or not to wrap on word boundaries **/ |
| boolean wordWrap; |
| |
| /** A ViewFactory that creates WrappedLines **/ |
| ViewFactory viewFactory = new WrappedLineCreator(); |
| |
| /** The start of the selected text **/ |
| int selectionStart; |
| |
| /** The end of the selected text **/ |
| int selectionEnd; |
| |
| /** The height of the line (used while painting) **/ |
| int lineHeight; |
| |
| /** |
| * The instance returned by {@link #getLineBuffer()}. |
| */ |
| private transient Segment lineBuffer; |
| |
| public WrappedPlainView (Element elem) |
| { |
| this (elem, false); |
| } |
| |
| public WrappedPlainView (Element elem, boolean wordWrap) |
| { |
| super (elem, Y_AXIS); |
| this.wordWrap = wordWrap; |
| } |
| |
| /** |
| * Provides access to the Segment used for retrievals from the Document. |
| * @return the Segment. |
| */ |
| protected final Segment getLineBuffer() |
| { |
| if (lineBuffer == null) |
| lineBuffer = new Segment(); |
| return lineBuffer; |
| } |
| |
| /** |
| * Returns the next tab stop position after a given reference position. |
| * |
| * This implementation ignores the <code>tabStop</code> argument. |
| * |
| * @param x the current x position in pixels |
| * @param tabStop the position within the text stream that the tab occured at |
| */ |
| public float nextTabStop(float x, int tabStop) |
| { |
| JTextComponent host = (JTextComponent)getContainer(); |
| float tabSizePixels = getTabSize() |
| * host.getFontMetrics(host.getFont()).charWidth('m'); |
| return (float) (Math.floor(x / tabSizePixels) + 1) * tabSizePixels; |
| } |
| |
| /** |
| * Returns the tab size for the Document based on |
| * PlainDocument.tabSizeAttribute, defaulting to 8 if this property is |
| * not defined |
| * |
| * @return the tab size. |
| */ |
| protected int getTabSize() |
| { |
| Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute); |
| if (tabSize == null) |
| return 8; |
| return ((Integer)tabSize).intValue(); |
| } |
| |
| /** |
| * Draws a line of text, suppressing white space at the end and expanding |
| * tabs. Calls drawSelectedText and drawUnselectedText. |
| * @param p0 starting document position to use |
| * @param p1 ending document position to use |
| * @param g graphics context |
| * @param x starting x position |
| * @param y starting y position |
| */ |
| protected void drawLine(int p0, int p1, Graphics g, int x, int y) |
| { |
| try |
| { |
| // We have to draw both selected and unselected text. There are |
| // several cases: |
| // - entire range is unselected |
| // - entire range is selected |
| // - start of range is selected, end of range is unselected |
| // - start of range is unselected, end of range is selected |
| // - middle of range is selected, start and end of range is unselected |
| |
| // entire range unselected: |
| if ((selectionStart == selectionEnd) || |
| (p0 > selectionEnd || p1 < selectionStart)) |
| drawUnselectedText(g, x, y, p0, p1); |
| |
| // entire range selected |
| else if (p0 >= selectionStart && p1 <= selectionEnd) |
| drawSelectedText(g, x, y, p0, p1); |
| |
| // start of range selected, end of range unselected |
| else if (p0 >= selectionStart) |
| { |
| x = drawSelectedText(g, x, y, p0, selectionEnd); |
| drawUnselectedText(g, x, y, selectionEnd, p1); |
| } |
| |
| // start of range unselected, end of range selected |
| else if (selectionStart > p0 && selectionEnd > p1) |
| { |
| x = drawUnselectedText(g, x, y, p0, selectionStart); |
| drawSelectedText(g, x, y, selectionStart, p1); |
| } |
| |
| // middle of range selected |
| else if (selectionStart > p0) |
| { |
| x = drawUnselectedText(g, x, y, p0, selectionStart); |
| x = drawSelectedText(g, x, y, selectionStart, selectionEnd); |
| drawUnselectedText(g, x, y, selectionEnd, p1); |
| } |
| } |
| catch (BadLocationException ble) |
| { |
| // shouldn't happen |
| } |
| } |
| |
| /** |
| * Renders the range of text as selected text. Just paints the text |
| * in the color specified by the host component. Assumes the highlighter |
| * will render the selected background. |
| * @param g the graphics context |
| * @param x the starting X coordinate |
| * @param y the starting Y coordinate |
| * @param p0 the starting model location |
| * @param p1 the ending model location |
| * @return the X coordinate of the end of the text |
| * @throws BadLocationException if the given range is invalid |
| */ |
| protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1) |
| throws BadLocationException |
| { |
| g.setColor(selectedColor); |
| Segment segment = getLineBuffer(); |
| getDocument().getText(p0, p1 - p0, segment); |
| return Utilities.drawTabbedText(segment, x, y, g, this, p0); |
| } |
| |
| /** |
| * Renders the range of text as normal unhighlighted text. |
| * @param g the graphics context |
| * @param x the starting X coordinate |
| * @param y the starting Y coordinate |
| * @param p0 the starting model location |
| * @param p1 the end model location |
| * @return the X location of the end off the range |
| * @throws BadLocationException if the range given is invalid |
| */ |
| protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1) |
| throws BadLocationException |
| { |
| JTextComponent textComponent = (JTextComponent) getContainer(); |
| if (textComponent.isEnabled()) |
| g.setColor(unselectedColor); |
| else |
| g.setColor(disabledColor); |
| |
| Segment segment = getLineBuffer(); |
| getDocument().getText(p0, p1 - p0, segment); |
| return Utilities.drawTabbedText(segment, x, y, g, this, p0); |
| } |
| |
| /** |
| * Loads the children to initiate the view. Called by setParent. |
| * Creates a WrappedLine for each child Element. |
| */ |
| protected void loadChildren (ViewFactory f) |
| { |
| Element root = getElement(); |
| int numChildren = root.getElementCount(); |
| if (numChildren == 0) |
| return; |
| |
| View[] children = new View[numChildren]; |
| for (int i = 0; i < numChildren; i++) |
| children[i] = new WrappedLine(root.getElement(i)); |
| replace(0, 0, children); |
| } |
| |
| /** |
| * Calculates the break position for the text between model positions |
| * p0 and p1. Will break on word boundaries or character boundaries |
| * depending on the break argument given in construction of this |
| * WrappedPlainView. Used by the nested WrappedLine class to determine |
| * when to start the next logical line. |
| * @param p0 the start model position |
| * @param p1 the end model position |
| * @return the model position at which to break the text |
| */ |
| protected int calculateBreakPosition(int p0, int p1) |
| { |
| Container c = getContainer(); |
| |
| int li = getLeftInset(); |
| int ti = getTopInset(); |
| |
| Rectangle alloc = new Rectangle(li, ti, |
| getWidth()-getRightInset()-li, |
| getHeight()-getBottomInset()-ti); |
| |
| // Mimic a behavior observed in the RI. |
| if (alloc.isEmpty()) |
| return 0; |
| |
| updateMetrics(); |
| |
| try |
| { |
| getDocument().getText(p0, p1 - p0, getLineBuffer()); |
| } |
| catch (BadLocationException ble) |
| { |
| // this shouldn't happen |
| throw new InternalError("Invalid offsets p0: " + p0 + " - p1: " + p1); |
| } |
| |
| if (wordWrap) |
| return Utilities.getBreakLocation(lineBuffer, metrics, alloc.x, |
| alloc.x + alloc.width, this, p0); |
| else |
| return p0 + Utilities.getTabbedTextOffset(lineBuffer, metrics, alloc.x, |
| alloc.x + alloc.width, this, 0, |
| true); |
| } |
| |
| void updateMetrics() |
| { |
| Container component = getContainer(); |
| metrics = component.getFontMetrics(component.getFont()); |
| } |
| |
| /** |
| * Determines the preferred span along the given axis. Implemented to |
| * cache the font metrics and then call the super classes method. |
| */ |
| public float getPreferredSpan (int axis) |
| { |
| updateMetrics(); |
| return super.getPreferredSpan(axis); |
| } |
| |
| /** |
| * Determines the minimum span along the given axis. Implemented to |
| * cache the font metrics and then call the super classes method. |
| */ |
| public float getMinimumSpan (int axis) |
| { |
| updateMetrics(); |
| return super.getMinimumSpan(axis); |
| } |
| |
| /** |
| * Determines the maximum span along the given axis. Implemented to |
| * cache the font metrics and then call the super classes method. |
| */ |
| public float getMaximumSpan (int axis) |
| { |
| updateMetrics(); |
| return super.getMaximumSpan(axis); |
| } |
| |
| /** |
| * Called when something was inserted. Overridden so that |
| * the view factory creates WrappedLine views. |
| */ |
| public void insertUpdate (DocumentEvent e, Shape a, ViewFactory f) |
| { |
| super.insertUpdate(e, a, viewFactory); |
| |
| // No repaint needed, as this is done by the WrappedLine instances. |
| } |
| |
| /** |
| * Called when something is removed. Overridden so that |
| * the view factory creates WrappedLine views. |
| */ |
| public void removeUpdate (DocumentEvent e, Shape a, ViewFactory f) |
| { |
| super.removeUpdate(e, a, viewFactory); |
| |
| // No repaint needed, as this is done by the WrappedLine instances. |
| } |
| |
| /** |
| * Called when the portion of the Document that this View is responsible |
| * for changes. Overridden so that the view factory creates |
| * WrappedLine views. |
| */ |
| public void changedUpdate (DocumentEvent e, Shape a, ViewFactory f) |
| { |
| super.changedUpdate(e, a, viewFactory); |
| |
| // No repaint needed, as this is done by the WrappedLine instances. |
| } |
| |
| class WrappedLineCreator implements ViewFactory |
| { |
| // Creates a new WrappedLine |
| public View create(Element elem) |
| { |
| return new WrappedLine(elem); |
| } |
| } |
| |
| /** |
| * Renders the <code>Element</code> that is associated with this |
| * <code>View</code>. Caches the metrics and then calls |
| * super.paint to paint all the child views. |
| * |
| * @param g the <code>Graphics</code> context to render to |
| * @param a the allocated region for the <code>Element</code> |
| */ |
| public void paint(Graphics g, Shape a) |
| { |
| JTextComponent comp = (JTextComponent)getContainer(); |
| // Ensure metrics are up-to-date. |
| updateMetrics(); |
| |
| selectionStart = comp.getSelectionStart(); |
| selectionEnd = comp.getSelectionEnd(); |
| |
| selectedColor = comp.getSelectedTextColor(); |
| unselectedColor = comp.getForeground(); |
| disabledColor = comp.getDisabledTextColor(); |
| selectedColor = comp.getSelectedTextColor(); |
| lineHeight = metrics.getHeight(); |
| g.setFont(comp.getFont()); |
| |
| super.paint(g, a); |
| } |
| |
| /** |
| * Sets the size of the View. Implemented to update the metrics |
| * and then call super method. |
| */ |
| public void setSize (float width, float height) |
| { |
| updateMetrics(); |
| if (width != getWidth()) |
| preferenceChanged(null, true, true); |
| super.setSize(width, height); |
| } |
| |
| class WrappedLine extends View |
| { |
| /** Used to cache the number of lines for this View **/ |
| int numLines = 1; |
| |
| public WrappedLine(Element elem) |
| { |
| super(elem); |
| determineNumLines(); |
| } |
| |
| /** |
| * Renders this (possibly wrapped) line using the given Graphics object |
| * and on the given rendering surface. |
| */ |
| public void paint(Graphics g, Shape s) |
| { |
| Rectangle rect = s.getBounds(); |
| |
| int end = getEndOffset(); |
| int currStart = getStartOffset(); |
| int currEnd; |
| int count = 0; |
| while (currStart < end) |
| { |
| currEnd = calculateBreakPosition(currStart, end); |
| |
| drawLine(currStart, currEnd, g, rect.x, rect.y + metrics.getAscent()); |
| |
| rect.y += lineHeight; |
| if (currEnd == currStart) |
| currStart ++; |
| else |
| currStart = currEnd; |
| |
| count++; |
| |
| } |
| |
| if (count != numLines) |
| { |
| numLines = count; |
| preferenceChanged(this, false, true); |
| } |
| |
| } |
| |
| /** |
| * Calculates the number of logical lines that the Element |
| * needs to be displayed and updates the variable numLines |
| * accordingly. |
| */ |
| void determineNumLines() |
| { |
| numLines = 0; |
| int end = getEndOffset(); |
| if (end == 0) |
| return; |
| |
| int breakPoint; |
| for (int i = getStartOffset(); i < end;) |
| { |
| numLines ++; |
| // careful: check that there's no off-by-one problem here |
| // depending on which position calculateBreakPosition returns |
| breakPoint = calculateBreakPosition(i, end); |
| |
| if (breakPoint == 0) |
| return; |
| |
| // If breakPoint is equal to the current index no further |
| // line is needed and we can end the loop. |
| if (breakPoint == i) |
| break; |
| else |
| i = breakPoint; |
| } |
| } |
| |
| /** |
| * Determines the preferred span for this view along the given axis. |
| * |
| * @param axis the axis (either X_AXIS or Y_AXIS) |
| * |
| * @return the preferred span along the given axis. |
| * @throws IllegalArgumentException if axis is not X_AXIS or Y_AXIS |
| */ |
| public float getPreferredSpan(int axis) |
| { |
| if (axis == X_AXIS) |
| return getWidth(); |
| else if (axis == Y_AXIS) |
| { |
| if (metrics == null) |
| updateMetrics(); |
| return numLines * metrics.getHeight(); |
| } |
| |
| throw new IllegalArgumentException("Invalid axis for getPreferredSpan: " |
| + axis); |
| } |
| |
| /** |
| * Provides a mapping from model space to view space. |
| * |
| * @param pos the position in the model |
| * @param a the region into which the view is rendered |
| * @param b the position bias (forward or backward) |
| * |
| * @return a box in view space that represents the given position |
| * in model space |
| * @throws BadLocationException if the given model position is invalid |
| */ |
| public Shape modelToView(int pos, Shape a, Bias b) |
| throws BadLocationException |
| { |
| Rectangle rect = a.getBounds(); |
| |
| // Throwing a BadLocationException is an observed behavior of the RI. |
| if (rect.isEmpty()) |
| throw new BadLocationException("Unable to calculate view coordinates " |
| + "when allocation area is empty.", 5); |
| |
| Segment s = getLineBuffer(); |
| int lineHeight = metrics.getHeight(); |
| |
| // Return a rectangle with width 1 and height equal to the height |
| // of the text |
| rect.height = lineHeight; |
| rect.width = 1; |
| |
| int currLineStart = getStartOffset(); |
| int end = getEndOffset(); |
| |
| if (pos < currLineStart || pos >= end) |
| throw new BadLocationException("invalid offset", pos); |
| |
| while (true) |
| { |
| int currLineEnd = calculateBreakPosition(currLineStart, end); |
| // If pos is between currLineStart and currLineEnd then just find |
| // the width of the text from currLineStart to pos and add that |
| // to rect.x |
| if (pos >= currLineStart && pos < currLineEnd) |
| { |
| try |
| { |
| getDocument().getText(currLineStart, pos - currLineStart, s); |
| } |
| catch (BadLocationException ble) |
| { |
| // Shouldn't happen |
| } |
| rect.x += Utilities.getTabbedTextWidth(s, metrics, rect.x, |
| WrappedPlainView.this, |
| currLineStart); |
| return rect; |
| } |
| // Increment rect.y so we're checking the next logical line |
| rect.y += lineHeight; |
| |
| // Increment currLineStart to the model position of the start |
| // of the next logical line |
| if (currLineEnd == currLineStart) |
| currLineStart = end; |
| else |
| currLineStart = currLineEnd; |
| } |
| |
| } |
| |
| /** |
| * Provides a mapping from view space to model space. |
| * |
| * @param x the x coordinate in view space |
| * @param y the y coordinate in view space |
| * @param a the region into which the view is rendered |
| * @param b the position bias (forward or backward) |
| * |
| * @return the location in the model that best represents the |
| * given point in view space |
| */ |
| public int viewToModel(float x, float y, Shape a, Bias[] b) |
| { |
| Segment s = getLineBuffer(); |
| Rectangle rect = a.getBounds(); |
| int currLineStart = getStartOffset(); |
| |
| // Although calling modelToView with the last possible offset will |
| // cause a BadLocationException in CompositeView it is allowed |
| // to return that offset in viewToModel. |
| int end = getEndOffset(); |
| |
| int lineHeight = metrics.getHeight(); |
| if (y < rect.y) |
| return currLineStart; |
| |
| if (y > rect.y + rect.height) |
| return end; |
| |
| // Note: rect.x and rect.width do not represent the width of painted |
| // text but the area where text *may* be painted. This means the width |
| // is most of the time identical to the component's width. |
| |
| while (currLineStart != end) |
| { |
| int currLineEnd = calculateBreakPosition(currLineStart, end); |
| |
| // If we're at the right y-position that means we're on the right |
| // logical line and we should look for the character |
| if (y >= rect.y && y < rect.y + lineHeight) |
| { |
| try |
| { |
| getDocument().getText(currLineStart, currLineEnd - currLineStart, s); |
| } |
| catch (BadLocationException ble) |
| { |
| // Shouldn't happen |
| } |
| |
| int offset = Utilities.getTabbedTextOffset(s, metrics, rect.x, |
| (int) x, |
| WrappedPlainView.this, |
| currLineStart); |
| // If the calculated offset is the end of the line (in the |
| // document (= start of the next line) return the preceding |
| // offset instead. This makes sure that clicking right besides |
| // the last character in a line positions the cursor after the |
| // last character and not in the beginning of the next line. |
| return (offset == currLineEnd) ? offset - 1 : offset; |
| } |
| // Increment rect.y so we're checking the next logical line |
| rect.y += lineHeight; |
| |
| // Increment currLineStart to the model position of the start |
| // of the next logical line. |
| currLineStart = currLineEnd; |
| |
| } |
| |
| return end; |
| } |
| |
| /** |
| * <p>This method is called from insertUpdate and removeUpdate.</p> |
| * |
| * <p>If the number of lines in the document has changed, just repaint |
| * the whole thing (note, could improve performance by not repainting |
| * anything above the changes). If the number of lines hasn't changed, |
| * just repaint the given Rectangle.</p> |
| * |
| * <p>Note that the <code>Rectangle</code> argument may be <code>null</code> |
| * when the allocation area is empty.</code> |
| * |
| * @param a the Rectangle to repaint if the number of lines hasn't changed |
| */ |
| void updateDamage (Rectangle a) |
| { |
| // If the allocation area is empty we can't do anything useful. |
| // As determining the number of lines is impossible in that state we |
| // reset it to an invalid value which can then be recalculated at a |
| // later point. |
| if (a == null || a.isEmpty()) |
| { |
| numLines = 1; |
| return; |
| } |
| |
| int oldNumLines = numLines; |
| determineNumLines(); |
| |
| if (numLines != oldNumLines) |
| preferenceChanged(this, false, true); |
| else |
| getContainer().repaint(a.x, a.y, a.width, a.height); |
| } |
| |
| /** |
| * This method is called when something is inserted into the Document |
| * that this View is displaying. |
| * |
| * @param changes the DocumentEvent for the changes. |
| * @param a the allocation of the View |
| * @param f the ViewFactory used to rebuild |
| */ |
| public void insertUpdate (DocumentEvent changes, Shape a, ViewFactory f) |
| { |
| updateDamage((Rectangle)a); |
| } |
| |
| /** |
| * This method is called when something is removed from the Document |
| * that this View is displaying. |
| * |
| * @param changes the DocumentEvent for the changes. |
| * @param a the allocation of the View |
| * @param f the ViewFactory used to rebuild |
| */ |
| public void removeUpdate (DocumentEvent changes, Shape a, ViewFactory f) |
| { |
| // Note: This method is not called when characters from the |
| // end of the document are removed. The reason for this |
| // can be found in the implementation of View.forwardUpdate: |
| // The document event will denote offsets which do not exist |
| // any more, getViewIndex() will therefore return -1 and this |
| // makes View.forwardUpdate() skip this method call. |
| // However this seems to cause no trouble and as it reduces the |
| // number of method calls it can stay this way. |
| |
| updateDamage((Rectangle)a); |
| } |
| } |
| } |