1. /*
  2. * @(#)PlainView.java 1.74 04/04/15
  3. *
  4. * Copyright 2004 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.74 04/15/04
  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(this, attr, g, x, y,
  83. p0-elem.getStartOffset(),
  84. p1-elem.getStartOffset());
  85. } else {
  86. if (sel0 == sel1 || selected == unselected) {
  87. // no selection, or it is invisible
  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(this, 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(this, 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. Highlighter h = host.getHighlighter();
  223. g.setFont(host.getFont());
  224. sel0 = host.getSelectionStart();
  225. sel1 = host.getSelectionEnd();
  226. unselected = (host.isEnabled()) ?
  227. host.getForeground() : host.getDisabledTextColor();
  228. Caret c = host.getCaret();
  229. selected = c.isSelectionVisible() && h != null ?
  230. host.getSelectedTextColor() : unselected;
  231. updateMetrics();
  232. // If the lines are clipped then we don't expend the effort to
  233. // try and paint them. Since all of the lines are the same height
  234. // with this object, determination of what lines need to be repainted
  235. // is quick.
  236. Rectangle clip = g.getClipBounds();
  237. int fontHeight = metrics.getHeight();
  238. int heightBelow = (alloc.y + alloc.height) - (clip.y + clip.height);
  239. int linesBelow = Math.max(0, heightBelow / fontHeight);
  240. int heightAbove = clip.y - alloc.y;
  241. int linesAbove = Math.max(0, heightAbove / fontHeight);
  242. int linesTotal = alloc.height / fontHeight;
  243. if (alloc.height % fontHeight != 0) {
  244. linesTotal++;
  245. }
  246. // update the visible lines
  247. Rectangle lineArea = lineToRect(a, linesAbove);
  248. int y = lineArea.y + metrics.getAscent();
  249. int x = lineArea.x;
  250. Element map = getElement();
  251. int lineCount = map.getElementCount();
  252. int endLine = Math.min(lineCount, linesTotal - linesBelow);
  253. lineCount--;
  254. LayeredHighlighter dh = (h instanceof LayeredHighlighter) ?
  255. (LayeredHighlighter)h : null;
  256. for (int line = linesAbove; line < endLine; line++) {
  257. if (dh != null) {
  258. Element lineElement = map.getElement(line);
  259. if (line == lineCount) {
  260. dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
  261. lineElement.getEndOffset(),
  262. originalA, host, this);
  263. }
  264. else {
  265. dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
  266. lineElement.getEndOffset() - 1,
  267. originalA, host, this);
  268. }
  269. }
  270. drawLine(line, g, x, y);
  271. y += fontHeight;
  272. if (line == 0) {
  273. // This should never really happen, in so far as if
  274. // firstLineOffset is non 0, there should only be one
  275. // line of text.
  276. x -= firstLineOffset;
  277. }
  278. }
  279. }
  280. /**
  281. * Should return a shape ideal for painting based on the passed in
  282. * Shape <code>a</code>. This is useful if painting in a different
  283. * region. The default implementation returns <code>a</code>.
  284. */
  285. Shape adjustPaintRegion(Shape a) {
  286. return a;
  287. }
  288. /**
  289. * Provides a mapping from the document model coordinate space
  290. * to the coordinate space of the view mapped to it.
  291. *
  292. * @param pos the position to convert >= 0
  293. * @param a the allocated region to render into
  294. * @return the bounding box of the given position
  295. * @exception BadLocationException if the given position does not
  296. * represent a valid location in the associated document
  297. * @see View#modelToView
  298. */
  299. public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
  300. // line coordinates
  301. Document doc = getDocument();
  302. Element map = getElement();
  303. int lineIndex = map.getElementIndex(pos);
  304. Rectangle lineArea = lineToRect(a, lineIndex);
  305. // determine span from the start of the line
  306. tabBase = lineArea.x;
  307. Element line = map.getElement(lineIndex);
  308. int p0 = line.getStartOffset();
  309. Segment s = SegmentCache.getSharedSegment();
  310. doc.getText(p0, pos - p0, s);
  311. int xOffs = Utilities.getTabbedTextWidth(s, metrics, tabBase, this,p0);
  312. SegmentCache.releaseSharedSegment(s);
  313. // fill in the results and return
  314. lineArea.x += xOffs;
  315. lineArea.width = 1;
  316. lineArea.height = metrics.getHeight();
  317. return lineArea;
  318. }
  319. /**
  320. * Provides a mapping from the view coordinate space to the logical
  321. * coordinate space of the model.
  322. *
  323. * @param fx the X coordinate >= 0
  324. * @param fy the Y coordinate >= 0
  325. * @param a the allocated region to render into
  326. * @return the location within the model that best represents the
  327. * given point in the view >= 0
  328. * @see View#viewToModel
  329. */
  330. public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
  331. // PENDING(prinz) properly calculate bias
  332. bias[0] = Position.Bias.Forward;
  333. Rectangle alloc = a.getBounds();
  334. Document doc = getDocument();
  335. int x = (int) fx;
  336. int y = (int) fy;
  337. if (y < alloc.y) {
  338. // above the area covered by this icon, so the the position
  339. // is assumed to be the start of the coverage for this view.
  340. return getStartOffset();
  341. } else if (y > alloc.y + alloc.height) {
  342. // below the area covered by this icon, so the the position
  343. // is assumed to be the end of the coverage for this view.
  344. return getEndOffset() - 1;
  345. } else {
  346. // positioned within the coverage of this view vertically,
  347. // so we figure out which line the point corresponds to.
  348. // if the line is greater than the number of lines contained, then
  349. // simply use the last line as it represents the last possible place
  350. // we can position to.
  351. Element map = doc.getDefaultRootElement();
  352. int lineIndex = Math.abs((y - alloc.y) / metrics.getHeight() );
  353. if (lineIndex >= map.getElementCount()) {
  354. return getEndOffset() - 1;
  355. }
  356. Element line = map.getElement(lineIndex);
  357. int dx = 0;
  358. if (lineIndex == 0) {
  359. alloc.x += firstLineOffset;
  360. alloc.width -= firstLineOffset;
  361. }
  362. if (x < alloc.x) {
  363. // point is to the left of the line
  364. return line.getStartOffset();
  365. } else if (x > alloc.x + alloc.width) {
  366. // point is to the right of the line
  367. return line.getEndOffset() - 1;
  368. } else {
  369. // Determine the offset into the text
  370. try {
  371. int p0 = line.getStartOffset();
  372. int p1 = line.getEndOffset() - 1;
  373. Segment s = SegmentCache.getSharedSegment();
  374. doc.getText(p0, p1 - p0, s);
  375. tabBase = alloc.x;
  376. int offs = p0 + Utilities.getTabbedTextOffset(s, metrics,
  377. tabBase, x, this, p0);
  378. SegmentCache.releaseSharedSegment(s);
  379. return offs;
  380. } catch (BadLocationException e) {
  381. // should not happen
  382. return -1;
  383. }
  384. }
  385. }
  386. }
  387. /**
  388. * Gives notification that something was inserted into the document
  389. * in a location that this view is responsible for.
  390. *
  391. * @param changes the change information from the associated document
  392. * @param a the current allocation of the view
  393. * @param f the factory to use to rebuild if the view has children
  394. * @see View#insertUpdate
  395. */
  396. public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
  397. updateDamage(changes, a, f);
  398. }
  399. /**
  400. * Gives notification that something was removed from the document
  401. * in a location that this view is responsible for.
  402. *
  403. * @param changes the change information from the associated document
  404. * @param a the current allocation of the view
  405. * @param f the factory to use to rebuild if the view has children
  406. * @see View#removeUpdate
  407. */
  408. public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
  409. updateDamage(changes, a, f);
  410. }
  411. /**
  412. * Gives notification from the document that attributes were changed
  413. * in a location that this view is responsible for.
  414. *
  415. * @param changes the change information from the associated document
  416. * @param a the current allocation of the view
  417. * @param f the factory to use to rebuild if the view has children
  418. * @see View#changedUpdate
  419. */
  420. public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
  421. updateDamage(changes, a, f);
  422. }
  423. /**
  424. * Sets the size of the view. This should cause
  425. * layout of the view along the given axis, if it
  426. * has any layout duties.
  427. *
  428. * @param width the width >= 0
  429. * @param height the height >= 0
  430. */
  431. public void setSize(float width, float height) {
  432. super.setSize(width, height);
  433. updateMetrics();
  434. }
  435. // --- TabExpander methods ------------------------------------------
  436. /**
  437. * Returns the next tab stop position after a given reference position.
  438. * This implementation does not support things like centering so it
  439. * ignores the tabOffset argument.
  440. *
  441. * @param x the current position >= 0
  442. * @param tabOffset the position within the text stream
  443. * that the tab occurred at >= 0.
  444. * @return the tab stop, measured in points >= 0
  445. */
  446. public float nextTabStop(float x, int tabOffset) {
  447. if (tabSize == 0) {
  448. return x;
  449. }
  450. int ntabs = (((int) x) - tabBase) / tabSize;
  451. return tabBase + ((ntabs + 1) * tabSize);
  452. }
  453. // --- local methods ------------------------------------------------
  454. /*
  455. * Repaint the region of change covered by the given document
  456. * event. Damages the line that begins the range to cover
  457. * the case when the insert/remove is only on one line.
  458. * If lines are added or removed, damages the whole
  459. * view. The longest line is checked to see if it has
  460. * changed.
  461. *
  462. * @since 1.4
  463. */
  464. protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) {
  465. Component host = getContainer();
  466. updateMetrics();
  467. Element elem = getElement();
  468. DocumentEvent.ElementChange ec = changes.getChange(elem);
  469. Element[] added = (ec != null) ? ec.getChildrenAdded() : null;
  470. Element[] removed = (ec != null) ? ec.getChildrenRemoved() : null;
  471. if (((added != null) && (added.length > 0)) ||
  472. ((removed != null) && (removed.length > 0))) {
  473. // lines were added or removed...
  474. if (added != null) {
  475. int currWide = getLineWidth(longLine);
  476. for (int i = 0; i < added.length; i++) {
  477. int w = getLineWidth(added[i]);
  478. if (w > currWide) {
  479. currWide = w;
  480. longLine = added[i];
  481. }
  482. }
  483. }
  484. if (removed != null) {
  485. for (int i = 0; i < removed.length; i++) {
  486. if (removed[i] == longLine) {
  487. calculateLongestLine();
  488. break;
  489. }
  490. }
  491. }
  492. preferenceChanged(null, true, true);
  493. host.repaint();
  494. } else {
  495. Element map = getElement();
  496. int line = map.getElementIndex(changes.getOffset());
  497. damageLineRange(line, line, a, host);
  498. if (changes.getType() == DocumentEvent.EventType.INSERT) {
  499. // check to see if the line is longer than current
  500. // longest line.
  501. int w = getLineWidth(longLine);
  502. Element e = map.getElement(line);
  503. if (e == longLine) {
  504. preferenceChanged(null, true, false);
  505. } else if (getLineWidth(e) > w) {
  506. longLine = e;
  507. preferenceChanged(null, true, false);
  508. }
  509. } else if (changes.getType() == DocumentEvent.EventType.REMOVE) {
  510. if (map.getElement(line) == longLine) {
  511. // removed from longest line... recalc
  512. calculateLongestLine();
  513. preferenceChanged(null, true, false);
  514. }
  515. }
  516. }
  517. }
  518. /**
  519. * Repaint the given line range.
  520. *
  521. * @param host the component hosting the view (used to call repaint)
  522. * @param a the region allocated for the view to render into
  523. * @param line0 the starting line number to repaint. This must
  524. * be a valid line number in the model.
  525. * @param line1 the ending line number to repaint. This must
  526. * be a valid line number in the model.
  527. * @since 1.4
  528. */
  529. protected void damageLineRange(int line0, int line1, Shape a, Component host) {
  530. if (a != null) {
  531. Rectangle area0 = lineToRect(a, line0);
  532. Rectangle area1 = lineToRect(a, line1);
  533. if ((area0 != null) && (area1 != null)) {
  534. Rectangle damage = area0.union(area1);
  535. host.repaint(damage.x, damage.y, damage.width, damage.height);
  536. } else {
  537. host.repaint();
  538. }
  539. }
  540. }
  541. /**
  542. * Determine the rectangle that represents the given line.
  543. *
  544. * @param a the region allocated for the view to render into
  545. * @param line the line number to find the region of. This must
  546. * be a valid line number in the model.
  547. * @since 1.4
  548. */
  549. protected Rectangle lineToRect(Shape a, int line) {
  550. Rectangle r = null;
  551. updateMetrics();
  552. if (metrics != null) {
  553. Rectangle alloc = a.getBounds();
  554. if (line == 0) {
  555. alloc.x += firstLineOffset;
  556. alloc.width -= firstLineOffset;
  557. }
  558. r = new Rectangle(alloc.x, alloc.y + (line * metrics.getHeight()),
  559. alloc.width, metrics.getHeight());
  560. }
  561. return r;
  562. }
  563. /**
  564. * Iterate over the lines represented by the child elements
  565. * of the element this view represents, looking for the line
  566. * that is the longest. The <em>longLine</em> variable is updated to
  567. * represent the longest line contained. The <em>font</em> variable
  568. * is updated to indicate the font used to calculate the
  569. * longest line.
  570. */
  571. private void calculateLongestLine() {
  572. Component c = getContainer();
  573. font = c.getFont();
  574. metrics = c.getFontMetrics(font);
  575. Document doc = getDocument();
  576. Element lines = getElement();
  577. int n = lines.getElementCount();
  578. int maxWidth = -1;
  579. for (int i = 0; i < n; i++) {
  580. Element line = lines.getElement(i);
  581. int w = getLineWidth(line);
  582. if (w > maxWidth) {
  583. maxWidth = w;
  584. longLine = line;
  585. }
  586. }
  587. }
  588. /**
  589. * Calculate the width of the line represented by
  590. * the given element. It is assumed that the font
  591. * and font metrics are up-to-date.
  592. */
  593. private int getLineWidth(Element line) {
  594. int p0 = line.getStartOffset();
  595. int p1 = line.getEndOffset();
  596. int w;
  597. Segment s = SegmentCache.getSharedSegment();
  598. try {
  599. line.getDocument().getText(p0, p1 - p0, s);
  600. w = Utilities.getTabbedTextWidth(s, metrics, tabBase, this, p0);
  601. } catch (BadLocationException ble) {
  602. w = 0;
  603. }
  604. SegmentCache.releaseSharedSegment(s);
  605. return w;
  606. }
  607. // --- member variables -----------------------------------------------
  608. /**
  609. * Font metrics for the current font.
  610. */
  611. protected FontMetrics metrics;
  612. /**
  613. * The current longest line. This is used to calculate
  614. * the preferred width of the view. Since the calculation
  615. * is potentially expensive we try to avoid it by stashing
  616. * which line is currently the longest.
  617. */
  618. Element longLine;
  619. /**
  620. * Font used to calculate the longest line... if this
  621. * changes we need to recalculate the longest line
  622. */
  623. Font font;
  624. Segment lineBuffer;
  625. int tabSize;
  626. int tabBase;
  627. int sel0;
  628. int sel1;
  629. Color unselected;
  630. Color selected;
  631. /**
  632. * Offset of where to draw the first character on the first line.
  633. * This is a hack and temporary until we can better address the problem
  634. * of text measuring. This field is actually never set directly in
  635. * PlainView, but by FieldView.
  636. */
  637. int firstLineOffset;
  638. }