| /* 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; | |
| /** | |
| * The base offset for tab calculations. | |
| */ | |
| private int tabBase; | |
| /** | |
| * The tab size. | |
| */ | |
| private int tabSize; | |
| 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); | |
| tabSize = getTabSize() * metrics.charWidth('m'); | |
| } | |
| } | |
| /** | |
| * @since 1.4 | |
| */ | |
| protected Rectangle lineToRect(Shape a, int line) | |
| { | |
| // Ensure metrics are up-to-date. | |
| updateMetrics(); | |
| Rectangle rect = a instanceof Rectangle ? (Rectangle) a : 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); | |
| tabBase = rect.x; | |
| // 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, tabBase, | |
| 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 instanceof Rectangle ? (Rectangle) s : s.getBounds(); | |
| tabBase = rect.x; | |
| // FIXME: Text may be scrolled. | |
| Document document = textComponent.getDocument(); | |
| Element root = getElement(); | |
| int height = metrics.getHeight(); | |
| // For layered highlighters we need to paint the layered highlights | |
| // before painting any text. | |
| LayeredHighlighter hl = null; | |
| Highlighter h = textComponent.getHighlighter(); | |
| if (h instanceof LayeredHighlighter) | |
| hl = (LayeredHighlighter) h; | |
| int count = root.getElementCount(); | |
| // Determine first and last line inside the clip. | |
| Rectangle clip = g.getClipBounds(); | |
| SwingUtilities.computeIntersection(rect.x, rect.y, rect.width, rect.height, | |
| clip); | |
| int line0 = (clip.y - rect.y) / height; | |
| line0 = Math.max(0, Math.min(line0, count - 1)); | |
| int line1 = (clip.y + clip.height - rect.y) / height; | |
| line1 = Math.max(0, Math.min(line1, count - 1)); | |
| int y = rect.y + metrics.getAscent() + height * line0; | |
| for (int i = line0; i <= line1; i++) | |
| { | |
| if (hl != null) | |
| { | |
| Element lineEl = root.getElement(i); | |
| // Exclude the trailing newline from beeing highlighted. | |
| if (i == count) | |
| hl.paintLayeredHighlights(g, lineEl.getStartOffset(), | |
| lineEl.getEndOffset(), s, textComponent, | |
| this); | |
| else | |
| hl.paintLayeredHighlights(g, lineEl.getStartOffset(), | |
| lineEl.getEndOffset() - 1, s, | |
| textComponent, this); | |
| } | |
| 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 next = x; | |
| if (tabSize != 0) | |
| { | |
| int numTabs = (((int) x) - tabBase) / tabSize; | |
| next = tabBase + (numTabs + 1) * tabSize; | |
| } | |
| return next; | |
| } | |
| /** | |
| * 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 instanceof Rectangle ? (Rectangle) a : a.getBounds(); | |
| tabBase = rec.x; | |
| int pos; | |
| if ((int) y < rec.y) | |
| // Above our area vertically. Return start offset. | |
| pos = getStartOffset(); | |
| else if ((int) y > rec.y + rec.height) | |
| // Below our area vertically. Return end offset. | |
| pos = getEndOffset() - 1; | |
| else | |
| { | |
| // Inside the allocation vertically. Determine line and X offset. | |
| Document doc = getDocument(); | |
| Element root = doc.getDefaultRootElement(); | |
| int line = Math.abs(((int) y - rec.y) / metrics.getHeight()); | |
| if (line >= root.getElementCount()) | |
| pos = getEndOffset() - 1; | |
| else | |
| { | |
| Element lineEl = root.getElement(line); | |
| if (x < rec.x) | |
| // To the left of the allocation area. | |
| pos = lineEl.getStartOffset(); | |
| else if (x > rec.x + rec.width) | |
| // To the right of the allocation area. | |
| pos = lineEl.getEndOffset() - 1; | |
| else | |
| { | |
| try | |
| { | |
| int p0 = lineEl.getStartOffset(); | |
| int p1 = lineEl.getEndOffset(); | |
| Segment s = new Segment(); | |
| doc.getText(p0, p1 - p0, s); | |
| tabBase = rec.x; | |
| pos = p0 + Utilities.getTabbedTextOffset(s, metrics, | |
| tabBase, (int) x, | |
| this, p0); | |
| } | |
| catch (BadLocationException ex) | |
| { | |
| // Should not happen. | |
| pos = -1; | |
| } | |
| } | |
| } | |
| } | |
| // Bias is always forward. | |
| b[0] = Position.Bias.Forward; | |
| return 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, tabBase, this, | |
| lineEl.getStartOffset()); | |
| } | |
| } |