1. /*
  2. * @(#)PlainView.java 1.71 03/01/23
  3. *
  4. * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package javax.swing.text;
  8. import java.util.Vector;
  9. import java.util.Properties;
  10. import java.awt.*;
  11. import javax.swing.event.*;
  12. /**
  13. * Implements View interface for a simple multi-line text view
  14. * that has text in one font and color. The view represents each
  15. * child element as a line of text.
  16. *
  17. * @author Timothy Prinzing
  18. * @version 1.71 01/23/03
  19. * @see View
  20. */
  21. public class PlainView extends View implements TabExpander {
  22. /**
  23. * Constructs a new PlainView wrapped on an element.
  24. *
  25. * @param elem the element
  26. */
  27. public PlainView(Element elem) {
  28. super(elem);
  29. }
  30. /**
  31. * Returns the tab size set for the document, defaulting to 8.
  32. *
  33. * @return the tab size
  34. */
  35. protected int getTabSize() {
  36. Integer i = (Integer) getDocument().getProperty(PlainDocument.tabSizeAttribute);
  37. int size = (i != null) ? i.intValue() : 8;
  38. return size;
  39. }
  40. /**
  41. * Renders a line of text, suppressing whitespace at the end
  42. * and expanding any tabs. This is implemented to make calls
  43. * to the methods <code>drawUnselectedText</code> and
  44. * <code>drawSelectedText</code> so that the way selected and
  45. * unselected text are rendered can be customized.
  46. *
  47. * @param lineIndex the line to draw >= 0
  48. * @param g the <code>Graphics</code> context
  49. * @param x the starting X position >= 0
  50. * @param y the starting Y position >= 0
  51. * @see #drawUnselectedText
  52. * @see #drawSelectedText
  53. */
  54. protected void drawLine(int lineIndex, Graphics g, int x, int y) {
  55. Element line = getElement().getElement(lineIndex);
  56. Element elem;
  57. try {
  58. if (line.isLeaf()) {
  59. drawElement(lineIndex, line, g, x, y);
  60. } else {
  61. // this line contains the composed text.
  62. int count = line.getElementCount();
  63. for(int i = 0; i < count; i++) {
  64. elem = line.getElement(i);
  65. x = drawElement(lineIndex, elem, g, x, y);
  66. }
  67. }
  68. } catch (BadLocationException e) {
  69. throw new StateInvariantError("Can't render line: " + lineIndex);
  70. }
  71. }
  72. private int drawElement(int lineIndex, Element elem, Graphics g, int x, int y) throws BadLocationException {
  73. int p0 = elem.getStartOffset();
  74. int p1 = elem.getEndOffset();
  75. p1 = Math.min(getDocument().getLength(), p1);
  76. if (lineIndex == 0) {
  77. x += firstLineOffset;
  78. }
  79. AttributeSet attr = elem.getAttributes();
  80. if (Utilities.isComposedTextAttributeDefined(attr)) {
  81. g.setColor(unselected);
  82. x = Utilities.drawComposedText(attr, g, x, y,
  83. p0-elem.getStartOffset(),
  84. p1-elem.getStartOffset());
  85. } else {
  86. if (sel0 == sel1) {
  87. // no selection
  88. x = drawUnselectedText(g, x, y, p0, p1);
  89. } else if ((p0 >= sel0 && p0 <= sel1) && (p1 >= sel0 && p1 <= sel1)) {
  90. x = drawSelectedText(g, x, y, p0, p1);
  91. } else if (sel0 >= p0 && sel0 <= p1) {
  92. if (sel1 >= p0 && sel1 <= p1) {
  93. x = drawUnselectedText(g, x, y, p0, sel0);
  94. x = drawSelectedText(g, x, y, sel0, sel1);
  95. x = drawUnselectedText(g, x, y, sel1, p1);
  96. } else {
  97. x = drawUnselectedText(g, x, y, p0, sel0);
  98. x = drawSelectedText(g, x, y, sel0, p1);
  99. }
  100. } else if (sel1 >= p0 && sel1 <= p1) {
  101. x = drawSelectedText(g, x, y, p0, sel1);
  102. x = drawUnselectedText(g, x, y, sel1, p1);
  103. } else {
  104. x = drawUnselectedText(g, x, y, p0, p1);
  105. }
  106. }
  107. return x;
  108. }
  109. /**
  110. * Renders the given range in the model as normal unselected
  111. * text. Uses the foreground or disabled color to render the text.
  112. *
  113. * @param g the graphics context
  114. * @param x the starting X coordinate >= 0
  115. * @param y the starting Y coordinate >= 0
  116. * @param p0 the beginning position in the model >= 0
  117. * @param p1 the ending position in the model >= 0
  118. * @return the X location of the end of the range >= 0
  119. * @exception BadLocationException if the range is invalid
  120. */
  121. protected int drawUnselectedText(Graphics g, int x, int y,
  122. int p0, int p1) throws BadLocationException {
  123. g.setColor(unselected);
  124. Document doc = getDocument();
  125. Segment s = SegmentCache.getSharedSegment();
  126. doc.getText(p0, p1 - p0, s);
  127. int ret = Utilities.drawTabbedText(s, x, y, g, this, p0);
  128. SegmentCache.releaseSharedSegment(s);
  129. return ret;
  130. }
  131. /**
  132. * Renders the given range in the model as selected text. This
  133. * is implemented to render the text in the color specified in
  134. * the hosting component. It assumes the highlighter will render
  135. * the selected background.
  136. *
  137. * @param g the graphics context
  138. * @param x the starting X coordinate >= 0
  139. * @param y the starting Y coordinate >= 0
  140. * @param p0 the beginning position in the model >= 0
  141. * @param p1 the ending position in the model >= 0
  142. * @return the location of the end of the range
  143. * @exception BadLocationException if the range is invalid
  144. */
  145. protected int drawSelectedText(Graphics g, int x,
  146. int y, int p0, int p1) throws BadLocationException {
  147. g.setColor(selected);
  148. Document doc = getDocument();
  149. Segment s = SegmentCache.getSharedSegment();
  150. doc.getText(p0, p1 - p0, s);
  151. int ret = Utilities.drawTabbedText(s, x, y, g, this, p0);
  152. SegmentCache.releaseSharedSegment(s);
  153. return ret;
  154. }
  155. /**
  156. * Gives access to a buffer that can be used to fetch
  157. * text from the associated document.
  158. *
  159. * @return the buffer
  160. */
  161. protected final Segment getLineBuffer() {
  162. if (lineBuffer == null) {
  163. lineBuffer = new Segment();
  164. }
  165. return lineBuffer;
  166. }
  167. /**
  168. * Checks to see if the font metrics and longest line
  169. * are up-to-date.
  170. *
  171. * @since 1.4
  172. */
  173. protected void updateMetrics() {
  174. Component host = getContainer();
  175. Font f = host.getFont();
  176. if (font != f) {
  177. // The font changed, we need to recalculate the
  178. // longest line.
  179. calculateLongestLine();
  180. tabSize = getTabSize() * metrics.charWidth('m');
  181. }
  182. }
  183. // ---- View methods ----------------------------------------------------
  184. /**
  185. * Determines the preferred span for this view along an
  186. * axis.
  187. *
  188. * @param axis may be either View.X_AXIS or View.Y_AXIS
  189. * @return the span the view would like to be rendered into >= 0.
  190. * Typically the view is told to render into the span
  191. * that is returned, although there is no guarantee.
  192. * The parent may choose to resize or break the view.
  193. * @exception IllegalArgumentException for an invalid axis
  194. */
  195. public float getPreferredSpan(int axis) {
  196. updateMetrics();
  197. switch (axis) {
  198. case View.X_AXIS:
  199. return getLineWidth(longLine);
  200. case View.Y_AXIS:
  201. return getElement().getElementCount() * metrics.getHeight();
  202. default:
  203. throw new IllegalArgumentException("Invalid axis: " + axis);
  204. }
  205. }
  206. /**
  207. * Renders using the given rendering surface and area on that surface.
  208. * The view may need to do layout and create child views to enable
  209. * itself to render into the given allocation.
  210. *
  211. * @param g the rendering surface to use
  212. * @param a the allocated region to render into
  213. *
  214. * @see View#paint
  215. */
  216. public void paint(Graphics g, Shape a) {
  217. Shape originalA = a;
  218. a = adjustPaintRegion(a);
  219. Rectangle alloc = (Rectangle) a;
  220. tabBase = alloc.x;
  221. JTextComponent host = (JTextComponent) getContainer();
  222. g.setFont(host.getFont());
  223. sel0 = host.getSelectionStart();
  224. sel1 = host.getSelectionEnd();
  225. unselected = (host.isEnabled()) ?
  226. host.getForeground() : host.getDisabledTextColor();
  227. Caret c = host.getCaret();
  228. selected = c.isSelectionVisible() ? host.getSelectedTextColor() : unselected;
  229. updateMetrics();
  230. // If the lines are clipped then we don't expend the effort to
  231. // try and paint them. Since all of the lines are the same height
  232. // with this object, determination of what lines need to be repainted
  233. // is quick.
  234. Rectangle clip = g.getClipBounds();
  235. int fontHeight = metrics.getHeight();
  236. int heightBelow = (alloc.y + alloc.height) - (clip.y + clip.height);
  237. int linesBelow = Math.max(0, heightBelow / fontHeight);
  238. int heightAbove = clip.y - alloc.y;
  239. int linesAbove = Math.max(0, heightAbove / fontHeight);
  240. int linesTotal = alloc.height / fontHeight;
  241. if (alloc.height % fontHeight != 0) {
  242. linesTotal++;
  243. }
  244. // update the visible lines
  245. Rectangle lineArea = lineToRect(a, linesAbove);
  246. int y = lineArea.y + metrics.getAscent();
  247. int x = lineArea.x;
  248. Element map = getElement();
  249. int lineCount = map.getElementCount();
  250. int endLine = Math.min(lineCount, linesTotal - linesBelow);
  251. lineCount--;
  252. Highlighter h = host.getHighlighter();
  253. LayeredHighlighter dh = (h instanceof LayeredHighlighter) ?
  254. (LayeredHighlighter)h : null;
  255. for (int line = linesAbove; line < endLine; line++) {
  256. if (dh != null) {
  257. Element lineElement = map.getElement(line);
  258. if (line == lineCount) {
  259. dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
  260. lineElement.getEndOffset(),
  261. originalA, host, this);
  262. }
  263. else {
  264. dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
  265. lineElement.getEndOffset() - 1,
  266. originalA, host, this);
  267. }
  268. }
  269. drawLine(line, g, x, y);
  270. y += fontHeight;
  271. if (line == 0) {
  272. // This should never really happen, in so far as if
  273. // firstLineOffset is non 0, there should only be one
  274. // line of text.
  275. x -= firstLineOffset;
  276. }
  277. }
  278. }
  279. /**
  280. * Should return a shape ideal for painting based on the passed in
  281. * Shape <code>a</code>. This is useful if painting in a different
  282. * region. The default implementation returns <code>a</code>.
  283. */
  284. Shape adjustPaintRegion(Shape a) {
  285. return a;
  286. }
  287. /**
  288. * Provides a mapping from the document model coordinate space
  289. * to the coordinate space of the view mapped to it.
  290. *
  291. * @param pos the position to convert >= 0
  292. * @param a the allocated region to render into
  293. * @return the bounding box of the given position
  294. * @exception BadLocationException if the given position does not
  295. * represent a valid location in the associated document
  296. * @see View#modelToView
  297. */
  298. public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
  299. // line coordinates
  300. Document doc = getDocument();
  301. Element map = getElement();
  302. int lineIndex = map.getElementIndex(pos);
  303. Rectangle lineArea = lineToRect(a, lineIndex);
  304. // determine span from the start of the line
  305. tabBase = lineArea.x;
  306. Element line = map.getElement(lineIndex);
  307. int p0 = line.getStartOffset();
  308. Segment s = SegmentCache.getSharedSegment();
  309. doc.getText(p0, pos - p0, s);
  310. int xOffs = Utilities.getTabbedTextWidth(s, metrics, tabBase, this,p0);
  311. SegmentCache.releaseSharedSegment(s);
  312. // fill in the results and return
  313. lineArea.x += xOffs;
  314. lineArea.width = 1;
  315. lineArea.height = metrics.getHeight();
  316. return lineArea;
  317. }
  318. /**
  319. * Provides a mapping from the view coordinate space to the logical
  320. * coordinate space of the model.
  321. *
  322. * @param fx the X coordinate >= 0
  323. * @param fy the Y coordinate >= 0
  324. * @param a the allocated region to render into
  325. * @return the location within the model that best represents the
  326. * given point in the view >= 0
  327. * @see View#viewToModel
  328. */
  329. public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
  330. // PENDING(prinz) properly calculate bias
  331. bias[0] = Position.Bias.Forward;
  332. Rectangle alloc = a.getBounds();
  333. Document doc = getDocument();
  334. int x = (int) fx;
  335. int y = (int) fy;
  336. if (y < alloc.y) {
  337. // above the area covered by this icon, so the the position
  338. // is assumed to be the start of the coverage for this view.
  339. return getStartOffset();
  340. } else if (y > alloc.y + alloc.height) {
  341. // below the area covered by this icon, so the the position
  342. // is assumed to be the end of the coverage for this view.
  343. return getEndOffset() - 1;
  344. } else {
  345. // positioned within the coverage of this view vertically,
  346. // so we figure out which line the point corresponds to.
  347. // if the line is greater than the number of lines contained, then
  348. // simply use the last line as it represents the last possible place
  349. // we can position to.
  350. Element map = doc.getDefaultRootElement();
  351. int lineIndex = Math.abs((y - alloc.y) / metrics.getHeight() );
  352. if (lineIndex >= map.getElementCount()) {
  353. return getEndOffset() - 1;
  354. }
  355. Element line = map.getElement(lineIndex);
  356. int dx = 0;
  357. if (lineIndex == 0) {
  358. alloc.x += firstLineOffset;
  359. alloc.width -= firstLineOffset;
  360. }
  361. if (x < alloc.x) {
  362. // point is to the left of the line
  363. return line.getStartOffset();
  364. } else if (x > alloc.x + alloc.width) {
  365. // point is to the right of the line
  366. return line.getEndOffset() - 1;
  367. } else {
  368. // Determine the offset into the text
  369. try {
  370. int p0 = line.getStartOffset();
  371. int p1 = line.getEndOffset() - 1;
  372. Segment s = SegmentCache.getSharedSegment();
  373. doc.getText(p0, p1 - p0, s);
  374. tabBase = alloc.x;
  375. int offs = p0 + Utilities.getTabbedTextOffset(s, metrics,
  376. tabBase, x, this, p0);
  377. SegmentCache.releaseSharedSegment(s);
  378. return offs;
  379. } catch (BadLocationException e) {
  380. // should not happen
  381. return -1;
  382. }
  383. }
  384. }
  385. }
  386. /**
  387. * Gives notification that something was inserted into the document
  388. * in a location that this view is responsible for.
  389. *
  390. * @param changes the change information from the associated document
  391. * @param a the current allocation of the view
  392. * @param f the factory to use to rebuild if the view has children
  393. * @see View#insertUpdate
  394. */
  395. public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
  396. updateDamage(changes, a, f);
  397. }
  398. /**
  399. * Gives notification that something was removed from the document
  400. * in a location that this view is responsible for.
  401. *
  402. * @param changes the change information from the associated document
  403. * @param a the current allocation of the view
  404. * @param f the factory to use to rebuild if the view has children
  405. * @see View#removeUpdate
  406. */
  407. public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
  408. updateDamage(changes, a, f);
  409. }
  410. /**
  411. * Gives notification from the document that attributes were changed
  412. * in a location that this view is responsible for.
  413. *
  414. * @param changes the change information from the associated document
  415. * @param a the current allocation of the view
  416. * @param f the factory to use to rebuild if the view has children
  417. * @see View#changedUpdate
  418. */
  419. public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
  420. updateDamage(changes, a, f);
  421. }
  422. /**
  423. * Sets the size of the view. This should cause
  424. * layout of the view along the given axis, if it
  425. * has any layout duties.
  426. *
  427. * @param width the width >= 0
  428. * @param height the height >= 0
  429. */
  430. public void setSize(float width, float height) {
  431. super.setSize(width, height);
  432. updateMetrics();
  433. }
  434. // --- TabExpander methods ------------------------------------------
  435. /**
  436. * Returns the next tab stop position after a given reference position.
  437. * This implementation does not support things like centering so it
  438. * ignores the tabOffset argument.
  439. *
  440. * @param x the current position >= 0
  441. * @param tabOffset the position within the text stream
  442. * that the tab occurred at >= 0.
  443. * @return the tab stop, measured in points >= 0
  444. */
  445. public float nextTabStop(float x, int tabOffset) {
  446. if (tabSize == 0) {
  447. return x;
  448. }
  449. int ntabs = (((int) x) - tabBase) / tabSize;
  450. return tabBase + ((ntabs + 1) * tabSize);
  451. }
  452. // --- local methods ------------------------------------------------
  453. /*
  454. * Repaint the region of change covered by the given document
  455. * event. Damages the line that begins the range to cover
  456. * the case when the insert/remove is only on one line.
  457. * If lines are added or removed, damages the whole
  458. * view. The longest line is checked to see if it has
  459. * changed.
  460. *
  461. * @since 1.4
  462. */
  463. protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) {
  464. Component host = getContainer();
  465. updateMetrics();
  466. Element elem = getElement();
  467. DocumentEvent.ElementChange ec = changes.getChange(elem);
  468. Element[] added = (ec != null) ? ec.getChildrenAdded() : null;
  469. Element[] removed = (ec != null) ? ec.getChildrenRemoved() : null;
  470. if (((added != null) && (added.length > 0)) ||
  471. ((removed != null) && (removed.length > 0))) {
  472. // lines were added or removed...
  473. if (added != null) {
  474. int currWide = getLineWidth(longLine);
  475. for (int i = 0; i < added.length; i++) {
  476. int w = getLineWidth(added[i]);
  477. if (w > currWide) {
  478. currWide = w;
  479. longLine = added[i];
  480. }
  481. }
  482. }
  483. if (removed != null) {
  484. for (int i = 0; i < removed.length; i++) {
  485. if (removed[i] == longLine) {
  486. calculateLongestLine();
  487. break;
  488. }
  489. }
  490. }
  491. preferenceChanged(null, true, true);
  492. host.repaint();
  493. } else {
  494. Element map = getElement();
  495. int line = map.getElementIndex(changes.getOffset());
  496. damageLineRange(line, line, a, host);
  497. if (changes.getType() == DocumentEvent.EventType.INSERT) {
  498. // check to see if the line is longer than current
  499. // longest line.
  500. int w = getLineWidth(longLine);
  501. Element e = map.getElement(line);
  502. if (e == longLine) {
  503. preferenceChanged(null, true, false);
  504. } else if (getLineWidth(e) > w) {
  505. longLine = e;
  506. preferenceChanged(null, true, false);
  507. }
  508. } else if (changes.getType() == DocumentEvent.EventType.REMOVE) {
  509. if (map.getElement(line) == longLine) {
  510. // removed from longest line... recalc
  511. calculateLongestLine();
  512. preferenceChanged(null, true, false);
  513. }
  514. }
  515. }
  516. }
  517. /**
  518. * Repaint the given line range.
  519. *
  520. * @param host the component hosting the view (used to call repaint)
  521. * @param a the region allocated for the view to render into
  522. * @param line0 the starting line number to repaint. This must
  523. * be a valid line number in the model.
  524. * @param line1 the ending line number to repaint. This must
  525. * be a valid line number in the model.
  526. * @since 1.4
  527. */
  528. protected void damageLineRange(int line0, int line1, Shape a, Component host) {
  529. if (a != null) {
  530. Rectangle area0 = lineToRect(a, line0);
  531. Rectangle area1 = lineToRect(a, line1);
  532. if ((area0 != null) && (area1 != null)) {
  533. Rectangle damage = area0.union(area1);
  534. host.repaint(damage.x, damage.y, damage.width, damage.height);
  535. } else {
  536. host.repaint();
  537. }
  538. }
  539. }
  540. /**
  541. * Determine the rectangle that represents the given line.
  542. *
  543. * @param a the region allocated for the view to render into
  544. * @param line the line number to find the region of. This must
  545. * be a valid line number in the model.
  546. * @since 1.4
  547. */
  548. protected Rectangle lineToRect(Shape a, int line) {
  549. Rectangle r = null;
  550. updateMetrics();
  551. if (metrics != null) {
  552. Rectangle alloc = a.getBounds();
  553. if (line == 0) {
  554. alloc.x += firstLineOffset;
  555. alloc.width -= firstLineOffset;
  556. }
  557. r = new Rectangle(alloc.x, alloc.y + (line * metrics.getHeight()),
  558. alloc.width, metrics.getHeight());
  559. }
  560. return r;
  561. }
  562. /**
  563. * Iterate over the lines represented by the child elements
  564. * of the element this view represents, looking for the line
  565. * that is the longest. The <em>longLine</em> variable is updated to
  566. * represent the longest line contained. The <em>font</em> variable
  567. * is updated to indicate the font used to calculate the
  568. * longest line.
  569. */
  570. private void calculateLongestLine() {
  571. Component c = getContainer();
  572. font = c.getFont();
  573. metrics = c.getFontMetrics(font);
  574. Document doc = getDocument();
  575. Element lines = getElement();
  576. int n = lines.getElementCount();
  577. int maxWidth = -1;
  578. for (int i = 0; i < n; i++) {
  579. Element line = lines.getElement(i);
  580. int w = getLineWidth(line);
  581. if (w > maxWidth) {
  582. maxWidth = w;
  583. longLine = line;
  584. }
  585. }
  586. }
  587. /**
  588. * Calculate the width of the line represented by
  589. * the given element. It is assumed that the font
  590. * and font metrics are up-to-date.
  591. */
  592. private int getLineWidth(Element line) {
  593. int p0 = line.getStartOffset();
  594. int p1 = line.getEndOffset();
  595. int w;
  596. Segment s = SegmentCache.getSharedSegment();
  597. try {
  598. line.getDocument().getText(p0, p1 - p0, s);
  599. w = Utilities.getTabbedTextWidth(s, metrics, tabBase, this, p0);
  600. } catch (BadLocationException ble) {
  601. w = 0;
  602. }
  603. SegmentCache.releaseSharedSegment(s);
  604. return w;
  605. }
  606. // --- member variables -----------------------------------------------
  607. /**
  608. * Font metrics for the current font.
  609. */
  610. protected FontMetrics metrics;
  611. /**
  612. * The current longest line. This is used to calculate
  613. * the preferred width of the view. Since the calculation
  614. * is potentially expensive we try to avoid it by stashing
  615. * which line is currently the longest.
  616. */
  617. Element longLine;
  618. /**
  619. * Font used to calculate the longest line... if this
  620. * changes we need to recalculate the longest line
  621. */
  622. Font font;
  623. Segment lineBuffer;
  624. int tabSize;
  625. int tabBase;
  626. int sel0;
  627. int sel1;
  628. Color unselected;
  629. Color selected;
  630. /**
  631. * Offset of where to draw the first character on the first line.
  632. * This is a hack and temporary until we can better address the problem
  633. * of text measuring. This field is actually never set directly in
  634. * PlainView, but by FieldView.
  635. */
  636. int firstLineOffset;
  637. }