1. /*
  2. * @(#)Utilities.java 1.22 01/11/29
  3. *
  4. * Copyright 2002 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.lang.reflect.Method;
  9. import java.awt.Rectangle;
  10. import java.awt.Graphics;
  11. import java.awt.FontMetrics;
  12. import java.awt.Toolkit;
  13. import java.text.*;
  14. import java.awt.Graphics2D;
  15. import java.awt.font.FontRenderContext;
  16. import java.awt.font.TextLayout;
  17. import java.awt.font.TextAttribute;
  18. import java.text.AttributedString;
  19. /**
  20. * A collection of methods to deal with various text
  21. * related activities.
  22. *
  23. * @author Timothy Prinzing
  24. * @version 1.22 11/29/01
  25. */
  26. public class Utilities {
  27. /**
  28. * Draws the given text, expanding any tabs that are contained
  29. * using the given tab expansion technique. This particular
  30. * implementation renders in a 1.1 style coordinate system
  31. * where ints are used and 72dpi is assumed.
  32. *
  33. * @param s the source of the text
  34. * @param x the X origin >= 0
  35. * @param y the Y origin >= 0
  36. * @param g the graphics context
  37. * @param e how to expand the tabs. If this value is null,
  38. * tabs will be expanded as a space character.
  39. * @param startOffset starting offset of the text in the document >= 0
  40. * @returns the X location at the end of the rendered text
  41. */
  42. public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
  43. TabExpander e, int startOffset) {
  44. FontMetrics metrics = g.getFontMetrics();
  45. int nextX = x;
  46. char[] txt = s.array;
  47. int flushLen = 0;
  48. int flushIndex = s.offset;
  49. int n = s.offset + s.count;
  50. for (int i = s.offset; i < n; i++) {
  51. if (txt[i] == '\t') {
  52. if (flushLen > 0) {
  53. g.drawChars(txt, flushIndex, flushLen, x, y);
  54. flushLen = 0;
  55. }
  56. flushIndex = i + 1;
  57. if (e != null) {
  58. nextX = (int) e.nextTabStop((float) nextX, startOffset + i - s.offset);
  59. } else {
  60. nextX += metrics.charWidth(' ');
  61. }
  62. x = nextX;
  63. } else if ((txt[i] == '\n') || (txt[i] == '\r')) {
  64. if (flushLen > 0) {
  65. g.drawChars(txt, flushIndex, flushLen, x, y);
  66. flushLen = 0;
  67. }
  68. flushIndex = i + 1;
  69. x = nextX;
  70. } else {
  71. flushLen += 1;
  72. nextX += metrics.charWidth(txt[i]);
  73. }
  74. }
  75. if (flushLen > 0) {
  76. g.drawChars(txt, flushIndex, flushLen, x, y);
  77. }
  78. return nextX;
  79. }
  80. /**
  81. * Determines the width of the given segment of text taking tabs
  82. * into consideration. This is implemented in a 1.1 style coordinate
  83. * system where ints are used and 72dpi is assumed.
  84. *
  85. * @param s the source of the text
  86. * @param metrics the font metrics to use for the calculation
  87. * @param x the X origin >= 0
  88. * @param e how to expand the tabs. If this value is null,
  89. * tabs will be expanded as a space character.
  90. * @param startOffset starting offset of the text in the document >= 0
  91. * @returns the width of the text
  92. */
  93. public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x,
  94. TabExpander e, int startOffset) {
  95. int nextX = x;
  96. char[] txt = s.array;
  97. int n = s.offset + s.count;
  98. for (int i = s.offset; i < n; i++) {
  99. if (txt[i] == '\t') {
  100. if (e != null) {
  101. nextX = (int) e.nextTabStop((float) nextX,
  102. startOffset + i - s.offset);
  103. } else {
  104. nextX += metrics.charWidth(' ');
  105. }
  106. } else if(txt[i] != '\n') {
  107. nextX += metrics.charWidth(txt[i]);
  108. }
  109. // Ignore newlines, they take up space and we shouldn't be
  110. // counting them.
  111. }
  112. return nextX - x;
  113. }
  114. /**
  115. * Determines the relative offset into the given text that
  116. * best represents the given span in the view coordinate
  117. * system. This is implemented in a 1.1 style coordinate
  118. * system where ints are used and 72dpi is assumed.
  119. *
  120. * @param s the source of the text
  121. * @param metrics the font metrics to use for the calculation
  122. * @param x0 the starting view location representing the start
  123. * of the given text >= 0.
  124. * @param x the target view location to translate to an
  125. * offset into the text >= 0.
  126. * @param e how to expand the tabs. If this value is null,
  127. * tabs will be expanded as a space character.
  128. * @param startOffset starting offset of the text in the document >= 0
  129. * @returns the offset into the text >= 0
  130. */
  131. public static final int getTabbedTextOffset(Segment s, FontMetrics metrics,
  132. int x0, int x, TabExpander e,
  133. int startOffset) {
  134. return getTabbedTextOffset(s, metrics, x0, x, e, startOffset, true);
  135. }
  136. public static final int getTabbedTextOffset(Segment s,
  137. FontMetrics metrics,
  138. int x0, int x, TabExpander e,
  139. int startOffset,
  140. boolean round) {
  141. int currX = x0;
  142. int nextX = currX;
  143. char[] txt = s.array;
  144. int n = s.offset + s.count;
  145. for (int i = s.offset; i < n; i++) {
  146. if (txt[i] == '\t') {
  147. if (e != null) {
  148. nextX = (int) e.nextTabStop((float) nextX,
  149. startOffset + i - s.offset);
  150. } else {
  151. nextX += metrics.charWidth(' ');
  152. }
  153. } else {
  154. nextX += metrics.charWidth(txt[i]);
  155. }
  156. if ((x >= currX) && (x < nextX)) {
  157. // found the hit position... return the appropriate side
  158. if ((round == false) || ((x - currX) < (nextX - x))) {
  159. return i - s.offset;
  160. } else {
  161. return i + 1 - s.offset;
  162. }
  163. }
  164. currX = nextX;
  165. }
  166. // didn't find, return end offset
  167. return s.count;
  168. }
  169. /**
  170. * Determine where to break the given text to fit
  171. * within the the given span. This trys to find a
  172. * whitespace boundry.
  173. * @param s the source of the text
  174. * @param metrics the font metrics to use for the calculation
  175. * @param x0 the starting view location representing the start
  176. * of the given text.
  177. * @param x the target view location to translate to an
  178. * offset into the text.
  179. * @param e how to expand the tabs. If this value is null,
  180. * tabs will be expanded as a space character.
  181. * @param startOffset starting offset in the document of the text
  182. * @returns the offset into the given text.
  183. */
  184. public static final int getBreakLocation(Segment s, FontMetrics metrics,
  185. int x0, int x, TabExpander e,
  186. int startOffset) {
  187. int index = Utilities.getTabbedTextOffset(s, metrics, x0, x,
  188. e, startOffset, false);
  189. for (int i = s.offset + Math.min(index, s.count - 1);
  190. i >= s.offset; i--) {
  191. char ch = s.array[i];
  192. if (Character.isWhitespace(ch)) {
  193. // found whitespace, break here
  194. index = i - s.offset + 1;
  195. break;
  196. }
  197. }
  198. return index;
  199. }
  200. /**
  201. * Determines the starting row model position of the row that contains
  202. * the specified model position. Assumes the row(s) are currently
  203. * displayed in a view.
  204. *
  205. * @param c the editor
  206. * @param offs the offset in the document >= 0
  207. * @return the position >= 0
  208. * @exception BadLocationException if the offset is out of range
  209. */
  210. public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException {
  211. Rectangle r = c.modelToView(offs);
  212. int lastOffs = offs;
  213. int y = r.y;
  214. while ((r != null) && (y == r.y)) {
  215. offs = lastOffs;
  216. lastOffs -= 1;
  217. r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
  218. }
  219. return offs;
  220. }
  221. /**
  222. * Determines the ending row model position of the row that contains
  223. * the specified model position. Assumes the row(s) are currently
  224. * displayed in a view.
  225. *
  226. * @param c the editor
  227. * @param offs the offset in the document >= 0
  228. * @return the position >= 0
  229. * @exception BadLocationException if the offset is out of range
  230. */
  231. public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException {
  232. Rectangle r = c.modelToView(offs);
  233. int n = c.getDocument().getLength();
  234. int lastOffs = offs;
  235. int y = r.y;
  236. while ((r != null) && (y == r.y)) {
  237. offs = lastOffs;
  238. lastOffs += 1;
  239. r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
  240. }
  241. return offs;
  242. }
  243. /**
  244. * Determines the position in the model that is closest to the given
  245. * view location in the row above.
  246. *
  247. * @param c the editor
  248. * @param offs the offset in the document >= 0
  249. * @param x the X coordinate >= 0
  250. * @return the model position >= 0
  251. * @exception BadLocationException if the offset is out of range
  252. */
  253. public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException {
  254. int lastOffs = getRowStart(c, offs) - 1;
  255. int bestSpan = Short.MAX_VALUE;
  256. int y = 0;
  257. Rectangle r = null;
  258. if (lastOffs >= 0) {
  259. r = c.modelToView(lastOffs);
  260. y = r.y;
  261. }
  262. while ((r != null) && (y == r.y)) {
  263. int span = Math.abs(r.x - x);
  264. if (span < bestSpan) {
  265. offs = lastOffs;
  266. bestSpan = span;
  267. }
  268. lastOffs -= 1;
  269. r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
  270. }
  271. return offs;
  272. }
  273. /**
  274. * Determines the position in the model that is closest to the given
  275. * view location in the row below.
  276. *
  277. * @param c the editor
  278. * @param offs the offset in the document >= 0
  279. * @param x the X coordinate >= 0
  280. * @return the model position >= 0
  281. * @exception BadLocationException if the offset is out of range
  282. */
  283. public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException {
  284. int lastOffs = getRowEnd(c, offs) + 1;
  285. int bestSpan = Short.MAX_VALUE;
  286. int n = c.getDocument().getLength();
  287. int y = 0;
  288. Rectangle r = null;
  289. if (lastOffs <= n) {
  290. r = c.modelToView(lastOffs);
  291. y = r.y;
  292. }
  293. while ((r != null) && (y == r.y)) {
  294. int span = Math.abs(x - r.x);
  295. if (span < bestSpan) {
  296. offs = lastOffs;
  297. bestSpan = span;
  298. }
  299. lastOffs += 1;
  300. r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
  301. }
  302. return offs;
  303. }
  304. /**
  305. * Determines the start of a word for the given model location.
  306. * Uses BreakIterator.getWordInstance() to actually get the words.
  307. *
  308. * @param c the editor
  309. * @param offs the offset in the document >= 0
  310. * @returns the location in the model of the word start >= 0.
  311. * @exception BadLocationException if the offset is out of range
  312. */
  313. public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException {
  314. Document doc = c.getDocument();
  315. Element line = getParagraphElement(c, offs);
  316. int lineStart = line.getStartOffset();
  317. int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
  318. String s = doc.getText(lineStart, lineEnd - lineStart);
  319. if(s != null && s.length() > 0) {
  320. BreakIterator words = BreakIterator.getWordInstance();
  321. words.setText(s);
  322. int wordPosition = offs - lineStart;
  323. if(wordPosition >= words.last()) {
  324. wordPosition = words.last() - 1;
  325. }
  326. words.following(wordPosition);
  327. offs = lineStart + words.previous();
  328. }
  329. return offs;
  330. }
  331. /**
  332. * Determines the end of a word for the given location.
  333. * Uses BreakIterator.getWordInstance() to actually get the words.
  334. *
  335. * @param c the editor
  336. * @param offs the offset in the document >= 0
  337. * @returns the location in the model of the word end >= 0.
  338. * @exception BadLocationException if the offset is out of range
  339. */
  340. public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException {
  341. Document doc = c.getDocument();
  342. Element line = getParagraphElement(c, offs);
  343. int lineStart = line.getStartOffset();
  344. int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
  345. String s = doc.getText(lineStart, lineEnd - lineStart);
  346. if(s != null && s.length() > 0) {
  347. BreakIterator words = BreakIterator.getWordInstance();
  348. words.setText(s);
  349. int wordPosition = offs - lineStart;
  350. if(wordPosition >= words.last()) {
  351. wordPosition = words.last() - 1;
  352. }
  353. offs = lineStart + words.following(wordPosition);
  354. }
  355. return offs;
  356. }
  357. /**
  358. * Determines the start of the next word for the given location.
  359. * Uses BreakIterator.getWordInstance() to actually get the words.
  360. *
  361. * @param c the editor
  362. * @param offs the offset in the document >= 0
  363. * @returns the location in the model of the word start >= 0.
  364. * @exception BadLocationException if the offset is out of range
  365. */
  366. public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException {
  367. int nextWord;
  368. Element line = getParagraphElement(c, offs);
  369. for (nextWord = getNextWordInParagraph(line, offs, false);
  370. nextWord == BreakIterator.DONE;
  371. nextWord = getNextWordInParagraph(line, offs, true)) {
  372. // didn't find in this line, try the next line
  373. offs = line.getEndOffset();
  374. line = getParagraphElement(c, offs);
  375. }
  376. return nextWord;
  377. }
  378. /**
  379. * Finds the next word in the given elements text. The first
  380. * parameter allows searching multiple paragraphs where even
  381. * the first offset is desired.
  382. * Returns the offset of the next word, or BreakIterator.DONE
  383. * if there are no more words in the element.
  384. */
  385. static int getNextWordInParagraph(Element line, int offs, boolean first) throws BadLocationException {
  386. if (line == null) {
  387. throw new BadLocationException("No more words", offs);
  388. }
  389. Document doc = line.getDocument();
  390. int lineStart = line.getStartOffset();
  391. int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
  392. if ((offs >= lineEnd) || (offs < lineStart)) {
  393. throw new BadLocationException("No more words", offs);
  394. }
  395. String s = doc.getText(lineStart, lineEnd - lineStart);
  396. BreakIterator words = BreakIterator.getWordInstance();
  397. words.setText(s);
  398. if ((first && (words.first() == (offs - lineStart))) &&
  399. (! Character.isWhitespace(s.charAt(words.first())))) {
  400. return offs;
  401. }
  402. int wordPosition = words.following(offs - lineStart);
  403. if ((wordPosition == BreakIterator.DONE) ||
  404. (wordPosition >= s.length())) {
  405. // there are no more words on this line.
  406. return BreakIterator.DONE;
  407. }
  408. // if we haven't shot past the end... check to
  409. // see if the current boundary represents whitespace.
  410. // if so, we need to try again
  411. char ch = s.charAt(wordPosition);
  412. if (! Character.isWhitespace(ch)) {
  413. return lineStart + wordPosition;
  414. }
  415. // it was whitespace, try again. The assumption
  416. // is that it must be a word start if the last
  417. // one had whitespace following it.
  418. wordPosition = words.next();
  419. if (wordPosition != BreakIterator.DONE) {
  420. offs = lineStart + wordPosition;
  421. if (offs != lineEnd) {
  422. return offs;
  423. }
  424. }
  425. return BreakIterator.DONE;
  426. }
  427. /**
  428. * Determine the start of the prev word for the given location.
  429. * Uses BreakIterator.getWordInstance() to actually get the words.
  430. *
  431. * @param c the editor
  432. * @param offs the offset in the document >= 0
  433. * @returns the location in the model of the word start >= 0.
  434. * @exception BadLocationException if the offset is out of range
  435. */
  436. public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException {
  437. int prevWord;
  438. Element line = getParagraphElement(c, offs);
  439. for (prevWord = getPrevWordInParagraph(line, offs);
  440. prevWord == BreakIterator.DONE;
  441. prevWord = getPrevWordInParagraph(line, offs)) {
  442. // didn't find in this line, try the prev line
  443. offs = line.getStartOffset() - 1;
  444. line = getParagraphElement(c, offs);
  445. }
  446. return prevWord;
  447. }
  448. /**
  449. * Finds the previous word in the given elements text. The first
  450. * parameter allows searching multiple paragraphs where even
  451. * the first offset is desired.
  452. * Returns the offset of the next word, or BreakIterator.DONE
  453. * if there are no more words in the element.
  454. */
  455. static int getPrevWordInParagraph(Element line, int offs) throws BadLocationException {
  456. if (line == null) {
  457. throw new BadLocationException("No more words", offs);
  458. }
  459. Document doc = line.getDocument();
  460. int lineStart = line.getStartOffset();
  461. int lineEnd = line.getEndOffset();
  462. if ((offs > lineEnd) || (offs < lineStart)) {
  463. throw new BadLocationException("No more words", offs);
  464. }
  465. String s = doc.getText(lineStart, lineEnd - lineStart);
  466. BreakIterator words = BreakIterator.getWordInstance();
  467. words.setText(s);
  468. if (words.following(offs - lineStart) == BreakIterator.DONE) {
  469. words.last();
  470. }
  471. int wordPosition = words.previous();
  472. if (wordPosition == (offs - lineStart)) {
  473. wordPosition = words.previous();
  474. }
  475. if (wordPosition == BreakIterator.DONE) {
  476. // there are no more words on this line.
  477. return BreakIterator.DONE;
  478. }
  479. // if we haven't shot past the end... check to
  480. // see if the current boundary represents whitespace.
  481. // if so, we need to try again
  482. char ch = s.charAt(wordPosition);
  483. if (! Character.isWhitespace(ch)) {
  484. return lineStart + wordPosition;
  485. }
  486. // it was whitespace, try again. The assumption
  487. // is that it must be a word start if the last
  488. // one had whitespace following it.
  489. wordPosition = words.previous();
  490. if (wordPosition != BreakIterator.DONE) {
  491. return lineStart + wordPosition;
  492. }
  493. return BreakIterator.DONE;
  494. }
  495. /**
  496. * Determines the element to use for a paragraph/line.
  497. *
  498. * @param c the editor
  499. * @param offs the starting offset in the document >= 0
  500. * @return the element
  501. */
  502. public static final Element getParagraphElement(JTextComponent c, int offs) {
  503. Document doc = c.getDocument();
  504. if (doc instanceof StyledDocument) {
  505. return ((StyledDocument)doc).getParagraphElement(offs);
  506. }
  507. Element map = doc.getDefaultRootElement();
  508. int index = map.getElementIndex(offs);
  509. Element paragraph = map.getElement(index);
  510. if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) {
  511. return paragraph;
  512. }
  513. return null;
  514. }
  515. static boolean isComposedTextElement(Element elem) {
  516. AttributeSet as = elem.getAttributes();
  517. return isComposedTextAttributeDefined(as);
  518. }
  519. static boolean isComposedTextAttributeDefined(AttributeSet as) {
  520. return ((as != null) &&
  521. (as.isDefined(StyleConstants.ComposedTextAttribute)));
  522. }
  523. /**
  524. * Draws the given composed text passed from an input method.
  525. *
  526. * @param attr the attributes containing the composed text
  527. * @param g the graphics context
  528. * @param x the X origin
  529. * @param y the Y origin
  530. * @param p0 starting offset in the composed text to be rendered
  531. * @param p1 ending offset in the composed text to be rendered
  532. * @returns the new insertion position
  533. */
  534. static int drawComposedText(AttributeSet attr, Graphics g, int x, int y,
  535. int p0, int p1) throws BadLocationException {
  536. Graphics2D g2d = (Graphics2D)g;
  537. AttributedString as = (AttributedString)attr.getAttribute(
  538. StyleConstants.ComposedTextAttribute);
  539. as.addAttribute(TextAttribute.FONT, g.getFont());
  540. if (p0 >= p1)
  541. return x;
  542. AttributedCharacterIterator aci = as.getIterator(null, p0, p1);
  543. // Create text layout
  544. TextLayout layout = new TextLayout(aci, g2d.getFontRenderContext());
  545. // draw
  546. layout.draw(g2d, x, y);
  547. return x + (int)layout.getAdvance();
  548. }
  549. /**
  550. * Indicates whether or not the package is being used
  551. * in a 1.2 environment.
  552. */
  553. static boolean is1dot2;
  554. static {
  555. is1dot2 = false;
  556. try {
  557. // Test if method introduced in 1.2 is available.
  558. Method m = Toolkit.class.getMethod("getMaximumCursorColors", null);
  559. is1dot2 = (m != null);
  560. } catch (NoSuchMethodException e) {
  561. is1dot2 = false;
  562. }
  563. // Warn if running wrong version of this class for this JDK.
  564. if (!is1dot2) {
  565. System.err.println("warning: running 1.2 version of Utilities");
  566. }
  567. }
  568. }