| /* PlainView.java -- |
| Copyright (C) 2004, 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.Component; |
| import java.awt.Font; |
| import java.awt.FontMetrics; |
| import java.awt.Graphics; |
| import java.awt.Rectangle; |
| import java.awt.Shape; |
| |
| import javax.swing.SwingUtilities; |
| import javax.swing.event.DocumentEvent; |
| import javax.swing.event.DocumentEvent.ElementChange; |
| |
| public class PlainView extends View implements TabExpander |
| { |
| Color selectedColor; |
| Color unselectedColor; |
| |
| /** |
| * The color that is used to draw disabled text fields. |
| */ |
| Color disabledColor; |
| |
| /** |
| * While painting this is the textcomponent's current start index |
| * of the selection. |
| */ |
| int selectionStart; |
| |
| /** |
| * While painting this is the textcomponent's current end index |
| * of the selection. |
| */ |
| int selectionEnd; |
| |
| Font font; |
| |
| /** The length of the longest line in the Document **/ |
| float maxLineLength = -1; |
| |
| /** The longest line in the Document **/ |
| Element longestLine = null; |
| |
| protected FontMetrics metrics; |
| |
| /** |
| * The instance returned by {@link #getLineBuffer()}. |
| */ |
| private transient Segment lineBuffer; |
| |
| public PlainView(Element elem) |
| { |
| super(elem); |
| } |
| |
| /** |
| * @since 1.4 |
| */ |
| protected void updateMetrics() |
| { |
| Component component = getContainer(); |
| Font font = component.getFont(); |
| |
| if (this.font != font) |
| { |
| this.font = font; |
| metrics = component.getFontMetrics(font); |
| } |
| } |
| |
| /** |
| * @since 1.4 |
| */ |
| protected Rectangle lineToRect(Shape a, int line) |
| { |
| // Ensure metrics are up-to-date. |
| updateMetrics(); |
| |
| Rectangle rect = a.getBounds(); |
| int fontHeight = metrics.getHeight(); |
| return new Rectangle(rect.x, rect.y + (line * fontHeight), |
| rect.width, fontHeight); |
| } |
| |
| public Shape modelToView(int position, Shape a, Position.Bias b) |
| throws BadLocationException |
| { |
| // Ensure metrics are up-to-date. |
| updateMetrics(); |
| |
| Document document = getDocument(); |
| |
| // Get rectangle of the line containing position. |
| int lineIndex = getElement().getElementIndex(position); |
| Rectangle rect = lineToRect(a, lineIndex); |
| |
| // Get the rectangle for position. |
| Element line = getElement().getElement(lineIndex); |
| int lineStart = line.getStartOffset(); |
| Segment segment = getLineBuffer(); |
| document.getText(lineStart, position - lineStart, segment); |
| int xoffset = Utilities.getTabbedTextWidth(segment, metrics, rect.x, |
| this, lineStart); |
| |
| // Calc the real rectangle. |
| rect.x += xoffset; |
| rect.width = 1; |
| rect.height = metrics.getHeight(); |
| |
| return rect; |
| } |
| |
| /** |
| * Draws a line of text. The X and Y coordinates specify the start of |
| * the <em>baseline</em> of the line. |
| * |
| * @param lineIndex the index of the line |
| * @param g the graphics to use for drawing the text |
| * @param x the X coordinate of the baseline |
| * @param y the Y coordinate of the baseline |
| */ |
| protected void drawLine(int lineIndex, Graphics g, int x, int y) |
| { |
| try |
| { |
| Element line = getElement().getElement(lineIndex); |
| int startOffset = line.getStartOffset(); |
| int endOffset = line.getEndOffset() - 1; |
| |
| if (selectionStart <= startOffset) |
| // Selection starts before the line ... |
| if (selectionEnd <= startOffset) |
| { |
| // end ends before the line: Draw completely unselected text. |
| drawUnselectedText(g, x, y, startOffset, endOffset); |
| } |
| else if (selectionEnd <= endOffset) |
| { |
| // and ends within the line: First part is selected, |
| // second is not. |
| x = drawSelectedText(g, x, y, startOffset, selectionEnd); |
| drawUnselectedText(g, x, y, selectionEnd, endOffset); |
| } |
| else |
| // and ends behind the line: Draw completely selected text. |
| drawSelectedText(g, x, y, startOffset, endOffset); |
| else if (selectionStart < endOffset) |
| // Selection starts within the line .. |
| if (selectionEnd < endOffset) |
| { |
| // and ends within it: First part unselected, second part |
| // selected, third part unselected. |
| x = drawUnselectedText(g, x, y, startOffset, selectionStart); |
| x = drawSelectedText(g, x, y, selectionStart, selectionEnd); |
| drawUnselectedText(g, x, y, selectionEnd, endOffset); |
| } |
| else |
| { |
| // and ends behind the line: First part unselected, second |
| // part selected. |
| x = drawUnselectedText(g, x, y, startOffset, selectionStart); |
| drawSelectedText(g, x, y, selectionStart, endOffset); |
| } |
| else |
| // Selection is behind this line: Draw completely unselected text. |
| drawUnselectedText(g, x, y, startOffset, endOffset); |
| } |
| catch (BadLocationException e) |
| { |
| AssertionError ae = new AssertionError("Unexpected bad location"); |
| ae.initCause(e); |
| throw ae; |
| } |
| } |
| |
| 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, segment.offset); |
| } |
| |
| /** |
| * Draws a chunk of unselected text. |
| * |
| * @param g the graphics to use for drawing the text |
| * @param x the X coordinate of the baseline |
| * @param y the Y coordinate of the baseline |
| * @param p0 the start position in the text model |
| * @param p1 the end position in the text model |
| * |
| * @return the X location of the end of the range |
| * |
| * @throws BadLocationException if <code>p0</code> or <code>p1</code> are |
| * 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, segment.offset); |
| } |
| |
| public void paint(Graphics g, Shape s) |
| { |
| // Ensure metrics are up-to-date. |
| updateMetrics(); |
| |
| JTextComponent textComponent = (JTextComponent) getContainer(); |
| |
| selectedColor = textComponent.getSelectedTextColor(); |
| unselectedColor = textComponent.getForeground(); |
| disabledColor = textComponent.getDisabledTextColor(); |
| selectionStart = textComponent.getSelectionStart(); |
| selectionEnd = textComponent.getSelectionEnd(); |
| |
| Rectangle rect = s.getBounds(); |
| |
| // FIXME: Text may be scrolled. |
| Document document = textComponent.getDocument(); |
| Element root = document.getDefaultRootElement(); |
| int y = rect.y + metrics.getAscent(); |
| int height = metrics.getHeight(); |
| |
| int count = root.getElementCount(); |
| for (int i = 0; i < count; i++) |
| { |
| drawLine(i, g, rect.x, y); |
| y += height; |
| } |
| } |
| |
| /** |
| * Returns the tab size of a tab. Checks the Document's |
| * properties for PlainDocument.tabSizeAttribute and returns it if it is |
| * defined, otherwise returns 8. |
| * |
| * @return the tab size. |
| */ |
| protected int getTabSize() |
| { |
| Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute); |
| if (tabSize == null) |
| return 8; |
| return ((Integer)tabSize).intValue(); |
| } |
| |
| /** |
| * 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) |
| { |
| float tabSizePixels = getTabSize() * metrics.charWidth('m'); |
| return (float) (Math.floor(x / tabSizePixels) + 1) * tabSizePixels; |
| } |
| |
| /** |
| * Returns the length of the longest line, used for getting the span |
| * @return the length of the longest line |
| */ |
| float determineMaxLineLength() |
| { |
| // if the longest line is cached, return the cached value |
| if (maxLineLength != -1) |
| return maxLineLength; |
| |
| // otherwise we have to go through all the lines and find it |
| Element el = getElement(); |
| Segment seg = getLineBuffer(); |
| float span = 0; |
| for (int i = 0; i < el.getElementCount(); i++) |
| { |
| Element child = el.getElement(i); |
| int start = child.getStartOffset(); |
| int end = child.getEndOffset() - 1; |
| try |
| { |
| el.getDocument().getText(start, end - start, seg); |
| } |
| catch (BadLocationException ex) |
| { |
| AssertionError ae = new AssertionError("Unexpected bad location"); |
| ae.initCause(ex); |
| throw ae; |
| } |
| |
| if (seg == null || seg.array == null || seg.count == 0) |
| continue; |
| |
| int width = metrics.charsWidth(seg.array, seg.offset, seg.count); |
| if (width > span) |
| { |
| longestLine = child; |
| span = width; |
| } |
| } |
| maxLineLength = span; |
| return maxLineLength; |
| } |
| |
| public float getPreferredSpan(int axis) |
| { |
| if (axis != X_AXIS && axis != Y_AXIS) |
| throw new IllegalArgumentException(); |
| |
| // make sure we have the metrics |
| updateMetrics(); |
| |
| Element el = getElement(); |
| float span; |
| |
| switch (axis) |
| { |
| case X_AXIS: |
| span = determineMaxLineLength(); |
| break; |
| case Y_AXIS: |
| default: |
| span = metrics.getHeight() * el.getElementCount(); |
| break; |
| } |
| |
| return span; |
| } |
| |
| /** |
| * Maps coordinates from the <code>View</code>'s space into a position |
| * in the document model. |
| * |
| * @param x the x coordinate in the view space |
| * @param y the y coordinate in the view space |
| * @param a the allocation of this <code>View</code> |
| * @param b the bias to use |
| * |
| * @return the position in the document that corresponds to the screen |
| * coordinates <code>x, y</code> |
| */ |
| public int viewToModel(float x, float y, Shape a, Position.Bias[] b) |
| { |
| Rectangle rec = a.getBounds(); |
| Document doc = getDocument(); |
| Element root = doc.getDefaultRootElement(); |
| |
| // PlainView doesn't support line-wrapping so we can find out which |
| // Element was clicked on just by the y-position. |
| // Since the coordinates may be outside of the coordinate space |
| // of the allocation area (e.g. user dragged mouse outside |
| // the component) we have to limit the values. |
| // This has the nice effect that the user can drag the |
| // mouse above or below the component and it will still |
| // react to the x values (e.g. when selecting). |
| int lineClicked |
| = Math.min(Math.max((int) (y - rec.y) / metrics.getHeight(), 0), |
| root.getElementCount() - 1); |
| |
| Element line = root.getElement(lineClicked); |
| |
| Segment s = getLineBuffer(); |
| int start = line.getStartOffset(); |
| // We don't want the \n at the end of the line. |
| int end = line.getEndOffset() - 1; |
| try |
| { |
| doc.getText(start, end - start, s); |
| } |
| catch (BadLocationException ble) |
| { |
| AssertionError ae = new AssertionError("Unexpected bad location"); |
| ae.initCause(ble); |
| throw ae; |
| } |
| |
| int pos = Utilities.getTabbedTextOffset(s, metrics, rec.x, (int)x, this, start); |
| return Math.max (0, pos); |
| } |
| |
| /** |
| * Since insertUpdate and removeUpdate each deal with children |
| * Elements being both added and removed, they both have to perform |
| * the same checks. So they both simply call this method. |
| * @param changes the DocumentEvent for the changes to the Document. |
| * @param a the allocation of the View. |
| * @param f the ViewFactory to use for rebuilding. |
| */ |
| protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) |
| { |
| // This happens during initialization. |
| if (metrics == null) |
| { |
| updateMetrics(); |
| preferenceChanged(null, true, true); |
| return; |
| } |
| |
| Element element = getElement(); |
| |
| // Find longest line if it hasn't been initialized yet. |
| if (longestLine == null) |
| findLongestLine(0, element.getElementCount() - 1); |
| |
| ElementChange change = changes.getChange(element); |
| if (changes.getType() == DocumentEvent.EventType.INSERT) |
| { |
| // Handles character/line insertion. |
| |
| // Determine if lines have been added. In this case we repaint |
| // differently. |
| boolean linesAdded = true; |
| if (change == null) |
| linesAdded = false; |
| |
| // Determine the start line. |
| int start; |
| if (linesAdded) |
| start = change.getIndex(); |
| else |
| start = element.getElementIndex(changes.getOffset()); |
| |
| // Determine the length of the updated region. |
| int length = 0; |
| if (linesAdded) |
| length = change.getChildrenAdded().length - 1; |
| |
| // Update the longest line and length. |
| int oldMaxLength = (int) maxLineLength; |
| if (longestLine.getEndOffset() < changes.getOffset() |
| || longestLine.getStartOffset() > changes.getOffset() |
| + changes.getLength()) |
| { |
| findLongestLine(start, start + length); |
| } |
| else |
| { |
| findLongestLine(0, element.getElementCount() - 1); |
| } |
| |
| // Trigger a preference change so that the layout gets updated |
| // correctly. |
| preferenceChanged(null, maxLineLength != oldMaxLength, linesAdded); |
| |
| // Damage the updated line range. |
| int endLine = start; |
| if (linesAdded) |
| endLine = element.getElementCount() - 1; |
| damageLineRange(start, endLine, a, getContainer()); |
| |
| } |
| else |
| { |
| // Handles character/lines removals. |
| |
| // Update the longest line and length and trigger preference changed. |
| int oldMaxLength = (int) maxLineLength; |
| if (change != null) |
| { |
| // Line(s) have been removed. |
| findLongestLine(0, element.getElementCount() - 1); |
| preferenceChanged(null, maxLineLength != oldMaxLength, true); |
| } |
| else |
| { |
| // No line has been removed. |
| int lineNo = getElement().getElementIndex(changes.getOffset()); |
| Element line = getElement().getElement(lineNo); |
| if (longestLine == line) |
| { |
| findLongestLine(0, element.getElementCount() - 1); |
| preferenceChanged(null, maxLineLength != oldMaxLength, false); |
| } |
| damageLineRange(lineNo, lineNo, a, getContainer()); |
| } |
| } |
| } |
| |
| /** |
| * 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(changes, a, f); |
| } |
| |
| /** |
| * 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) |
| { |
| updateDamage(changes, a, f); |
| } |
| |
| /** |
| * This method is called when attributes were changed in the |
| * Document in a location that this view is responsible for. |
| */ |
| public void changedUpdate (DocumentEvent changes, Shape a, ViewFactory f) |
| { |
| updateDamage(changes, a, f); |
| } |
| |
| /** |
| * Repaint the given line range. This is called from insertUpdate, |
| * changedUpdate, and removeUpdate when no new lines were added |
| * and no lines were removed, to repaint the line that was |
| * modified. |
| * |
| * @param line0 the start of the range |
| * @param line1 the end of the range |
| * @param a the rendering region of the host |
| * @param host the Component that uses this View (used to call repaint |
| * on that Component) |
| * |
| * @since 1.4 |
| */ |
| protected void damageLineRange (int line0, int line1, Shape a, Component host) |
| { |
| if (a == null) |
| return; |
| |
| Rectangle rec0 = lineToRect(a, line0); |
| Rectangle rec1 = lineToRect(a, line1); |
| |
| if (rec0 == null || rec1 == null) |
| // something went wrong, repaint the entire host to be safe |
| host.repaint(); |
| else |
| { |
| Rectangle repaintRec = SwingUtilities.computeUnion(rec0.x, rec0.y, |
| rec0.width, |
| rec0.height, rec1); |
| host.repaint(repaintRec.x, repaintRec.y, repaintRec.width, |
| repaintRec.height); |
| } |
| } |
| |
| /** |
| * Provides a {@link Segment} object, that can be used to fetch text from |
| * the document. |
| * |
| * @returna {@link Segment} object, that can be used to fetch text from |
| * the document |
| */ |
| protected final Segment getLineBuffer() |
| { |
| if (lineBuffer == null) |
| lineBuffer = new Segment(); |
| return lineBuffer; |
| } |
| |
| /** |
| * Finds and updates the longest line in the view inside an interval of |
| * lines. |
| * |
| * @param start the start of the search interval |
| * @param end the end of the search interval |
| */ |
| private void findLongestLine(int start, int end) |
| { |
| for (int i = start; i <= end; i++) |
| { |
| int w = getLineLength(i); |
| if (w > maxLineLength) |
| { |
| maxLineLength = w; |
| longestLine = getElement().getElement(i); |
| } |
| } |
| } |
| |
| /** |
| * Determines the length of the specified line. |
| * |
| * @param line the number of the line |
| * |
| * @return the length of the line in pixels |
| */ |
| private int getLineLength(int line) |
| { |
| Element lineEl = getElement().getElement(line); |
| Segment buffer = getLineBuffer(); |
| try |
| { |
| Document doc = getDocument(); |
| doc.getText(lineEl.getStartOffset(), |
| lineEl.getEndOffset() - lineEl.getStartOffset() - 1, |
| buffer); |
| } |
| catch (BadLocationException ex) |
| { |
| AssertionError err = new AssertionError("Unexpected bad location"); |
| err.initCause(ex); |
| throw err; |
| } |
| |
| return Utilities.getTabbedTextWidth(buffer, metrics, 0, this, |
| lineEl.getStartOffset()); |
| } |
| } |
| |