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