You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TextPaneCanvas.java 35KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. /*
  2. * Copyright (c) 2006-2014 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.addons.ui_swing.textpane;
  23. import com.dmdirc.addons.ui_swing.UIUtilities;
  24. import com.dmdirc.interfaces.config.AggregateConfigProvider;
  25. import com.dmdirc.interfaces.config.ConfigChangeListener;
  26. import com.dmdirc.ui.messages.IRCDocument;
  27. import com.dmdirc.ui.messages.IRCTextAttribute;
  28. import com.dmdirc.ui.messages.LinePosition;
  29. import com.dmdirc.util.collections.ListenerList;
  30. import java.awt.Cursor;
  31. import java.awt.Graphics;
  32. import java.awt.Graphics2D;
  33. import java.awt.Point;
  34. import java.awt.Rectangle;
  35. import java.awt.Shape;
  36. import java.awt.Toolkit;
  37. import java.awt.event.AdjustmentEvent;
  38. import java.awt.event.AdjustmentListener;
  39. import java.awt.event.ComponentEvent;
  40. import java.awt.event.ComponentListener;
  41. import java.awt.event.MouseEvent;
  42. import java.awt.font.LineBreakMeasurer;
  43. import java.awt.font.TextAttribute;
  44. import java.awt.font.TextHitInfo;
  45. import java.awt.font.TextLayout;
  46. import java.text.AttributedCharacterIterator;
  47. import java.text.AttributedString;
  48. import java.util.HashMap;
  49. import java.util.Map;
  50. import javax.swing.JPanel;
  51. import javax.swing.SwingUtilities;
  52. import javax.swing.ToolTipManager;
  53. import javax.swing.event.MouseInputListener;
  54. /** Canvas object to draw text. */
  55. class TextPaneCanvas extends JPanel implements MouseInputListener,
  56. ComponentListener, AdjustmentListener, ConfigChangeListener {
  57. /**
  58. * A version number for this class. It should be changed whenever the class structure is changed
  59. * (or anything else that would prevent serialized objects being unserialized with the new
  60. * class).
  61. */
  62. private static final long serialVersionUID = 8;
  63. /** Hand cursor. */
  64. private static final Cursor HAND_CURSOR = new Cursor(Cursor.HAND_CURSOR);
  65. /** Single Side padding for textpane. */
  66. private static final int SINGLE_SIDE_PADDING = 3;
  67. /** Both Side padding for textpane. */
  68. private static final int DOUBLE_SIDE_PADDING = SINGLE_SIDE_PADDING * 2;
  69. /** Padding to add to line height. */
  70. private static final double LINE_PADDING = 0.2;
  71. /** IRCDocument. */
  72. private final IRCDocument document;
  73. /** parent textpane. */
  74. private final TextPane textPane;
  75. /** Position -> TextLayout. */
  76. private final Map<Rectangle, TextLayout> positions;
  77. /** TextLayout -> Line numbers. */
  78. private final Map<TextLayout, LineInfo> textLayouts;
  79. /** Start line. */
  80. private int startLine;
  81. /** Selection. */
  82. private LinePosition selection;
  83. /** First visible line (from the top). */
  84. private int firstVisibleLine;
  85. /** Last visible line (from the top). */
  86. private int lastVisibleLine;
  87. /** Config Manager. */
  88. private final AggregateConfigProvider manager;
  89. /** Quick copy? */
  90. private boolean quickCopy;
  91. /** Mouse click listeners. */
  92. private final ListenerList listeners = new ListenerList();
  93. /**
  94. * Creates a new text pane canvas.
  95. *
  96. * @param parent parent text pane for the canvas
  97. * @param document IRCDocument to be displayed
  98. */
  99. public TextPaneCanvas(final TextPane parent, final IRCDocument document) {
  100. super();
  101. this.document = document;
  102. textPane = parent;
  103. this.manager = parent.getWindow().getContainer().getConfigManager();
  104. startLine = 0;
  105. setDoubleBuffered(true);
  106. setOpaque(true);
  107. textLayouts = new HashMap<>();
  108. positions = new HashMap<>();
  109. selection = new LinePosition(-1, -1, -1, -1);
  110. addMouseListener(this);
  111. addMouseMotionListener(this);
  112. addComponentListener(this);
  113. manager.addChangeListener("ui", "quickCopy", this);
  114. updateCachedSettings();
  115. ToolTipManager.sharedInstance().registerComponent(this);
  116. }
  117. /**
  118. * Paints the text onto the canvas.
  119. *
  120. * @param graphics graphics object to draw onto
  121. */
  122. @Override
  123. public void paintComponent(final Graphics graphics) {
  124. final Graphics2D g = (Graphics2D) graphics;
  125. final Map<?, ?> desktopHints = (Map<?, ?>) Toolkit.getDefaultToolkit().
  126. getDesktopProperty("awt.font.desktophints");
  127. if (desktopHints != null) {
  128. g.addRenderingHints(desktopHints);
  129. }
  130. paintOntoGraphics(g);
  131. }
  132. /**
  133. * Re calculates positions of lines and repaints if required.
  134. */
  135. protected void recalc() {
  136. if (isVisible()) {
  137. repaint();
  138. }
  139. }
  140. /**
  141. * Updates cached config settings.
  142. */
  143. private void updateCachedSettings() {
  144. quickCopy = manager.getOptionBool("ui", "quickCopy");
  145. UIUtilities.invokeLater(new Runnable() {
  146. /** {@inheritDoc} */
  147. @Override
  148. public void run() {
  149. recalc();
  150. }
  151. });
  152. }
  153. private void paintOntoGraphics(final Graphics2D g) {
  154. final float formatWidth = getWidth() - DOUBLE_SIDE_PADDING;
  155. final float formatHeight = getHeight();
  156. float drawPosY = formatHeight;
  157. int paragraphStart;
  158. int paragraphEnd;
  159. LineBreakMeasurer lineMeasurer;
  160. textLayouts.clear();
  161. positions.clear();
  162. //check theres something to draw and theres some space to draw in
  163. if (document.getNumLines() == 0 || formatWidth < 1) {
  164. setCursor(Cursor.getDefaultCursor());
  165. return;
  166. }
  167. // Check the start line is in range
  168. if (startLine >= document.getNumLines() - 1) {
  169. startLine = document.getNumLines() - 1;
  170. }
  171. if (startLine < 0) {
  172. startLine = 0;
  173. }
  174. //sets the last visible line
  175. lastVisibleLine = startLine;
  176. firstVisibleLine = startLine;
  177. // Iterate through the lines
  178. for (int line = startLine; line >= 0; line--) {
  179. float drawPosX;
  180. final AttributedCharacterIterator iterator = document.getStyledLine(
  181. line);
  182. int lineHeight = document.getLineHeight(line);
  183. lineHeight += lineHeight * LINE_PADDING;
  184. paragraphStart = iterator.getBeginIndex();
  185. paragraphEnd = iterator.getEndIndex();
  186. lineMeasurer = new LineBreakMeasurer(iterator,
  187. g.getFontRenderContext());
  188. lineMeasurer.setPosition(paragraphStart);
  189. final int wrappedLine = getNumWrappedLines(lineMeasurer,
  190. paragraphStart, paragraphEnd,
  191. formatWidth);
  192. if (wrappedLine > 1) {
  193. drawPosY -= lineHeight * wrappedLine;
  194. }
  195. if (line == startLine) {
  196. drawPosY += DOUBLE_SIDE_PADDING;
  197. }
  198. int numberOfWraps = 0;
  199. int chars = 0;
  200. // Loop through each wrapped line
  201. while (lineMeasurer.getPosition() < paragraphEnd) {
  202. final TextLayout layout = lineMeasurer.nextLayout(formatWidth);
  203. // Calculate the Y offset
  204. if (wrappedLine == 1) {
  205. drawPosY -= lineHeight;
  206. } else if (numberOfWraps != 0) {
  207. drawPosY += lineHeight;
  208. }
  209. // Calculate the initial X position
  210. if (layout.isLeftToRight()) {
  211. drawPosX = SINGLE_SIDE_PADDING;
  212. } else {
  213. drawPosX = formatWidth - layout.getAdvance();
  214. }
  215. // Check if the target is in range
  216. if (drawPosY >= 0 || drawPosY <= formatHeight) {
  217. g.setColor(textPane.getForeground());
  218. layout.draw(g, drawPosX, drawPosY + layout.getDescent());
  219. doHighlight(line, chars, layout, g, drawPosY, drawPosX);
  220. firstVisibleLine = line;
  221. textLayouts.put(layout, new LineInfo(line, numberOfWraps));
  222. positions.put(new Rectangle(0,
  223. (int) (drawPosY + 1.5 - layout.getAscent()
  224. + layout.getDescent()),
  225. (int) formatWidth + DOUBLE_SIDE_PADDING,
  226. (int) (layout.getAscent() + layout.getDescent())),
  227. layout);
  228. }
  229. numberOfWraps++;
  230. chars += layout.getCharacterCount();
  231. }
  232. if (numberOfWraps > 1) {
  233. drawPosY -= lineHeight * (wrappedLine - 1);
  234. }
  235. if (drawPosY <= 0) {
  236. break;
  237. }
  238. }
  239. checkForLink();
  240. }
  241. /**
  242. * Returns the number of times a line will wrap.
  243. *
  244. * @param lineMeasurer LineBreakMeasurer to work out wrapping for
  245. * @param paragraphStart Start index of the paragraph
  246. * @param paragraphEnd End index of the paragraph
  247. * @param formatWidth Width to wrap at
  248. *
  249. * @return Number of times the line wraps
  250. */
  251. private int getNumWrappedLines(final LineBreakMeasurer lineMeasurer,
  252. final int paragraphStart,
  253. final int paragraphEnd,
  254. final float formatWidth) {
  255. int wrappedLine = 0;
  256. while (lineMeasurer.getPosition() < paragraphEnd) {
  257. lineMeasurer.nextLayout(formatWidth);
  258. wrappedLine++;
  259. }
  260. lineMeasurer.setPosition(paragraphStart);
  261. return wrappedLine;
  262. }
  263. /**
  264. * Redraws the text that has been highlighted.
  265. *
  266. * @param line Line number
  267. * @param chars Number of characters so far in the line
  268. * @param layout Current line textlayout
  269. * @param g Graphics surface to draw highlight on
  270. * @param drawPosY current y location of the line
  271. * @param drawPosX current x location of the line
  272. */
  273. private void doHighlight(final int line, final int chars,
  274. final TextLayout layout, final Graphics2D g,
  275. final float drawPosY, final float drawPosX) {
  276. int selectionStartLine;
  277. int selectionStartChar;
  278. int selectionEndLine;
  279. int selectionEndChar;
  280. if (selection.getStartLine() > selection.getEndLine()) {
  281. // Swap both
  282. selectionStartLine = selection.getEndLine();
  283. selectionStartChar = selection.getEndPos();
  284. selectionEndLine = selection.getStartLine();
  285. selectionEndChar = selection.getStartPos();
  286. } else if (selection.getStartLine() == selection.getEndLine()
  287. && selection.getStartPos() > selection.getEndPos()) {
  288. // Just swap the chars
  289. selectionStartLine = selection.getStartLine();
  290. selectionStartChar = selection.getEndPos();
  291. selectionEndLine = selection.getEndLine();
  292. selectionEndChar = selection.getStartPos();
  293. } else {
  294. // Swap nothing
  295. selectionStartLine = selection.getStartLine();
  296. selectionStartChar = selection.getStartPos();
  297. selectionEndLine = selection.getEndLine();
  298. selectionEndChar = selection.getEndPos();
  299. }
  300. //Does this line need highlighting?
  301. if (selectionStartLine <= line && selectionEndLine >= line) {
  302. int firstChar;
  303. int lastChar;
  304. // Determine the first char we care about
  305. if (selectionStartLine < line || selectionStartChar < chars) {
  306. firstChar = chars;
  307. } else {
  308. firstChar = selectionStartChar;
  309. }
  310. // ... And the last
  311. if (selectionEndLine > line || selectionEndChar > chars + layout.
  312. getCharacterCount()) {
  313. lastChar = chars + layout.getCharacterCount();
  314. } else {
  315. lastChar = selectionEndChar;
  316. }
  317. // If the selection includes the chars we're showing
  318. if (lastChar > chars && firstChar < chars
  319. + layout.getCharacterCount()) {
  320. String text = document.getLine(line).getText();
  321. if (firstChar >= 0 && text.length() > lastChar) {
  322. text = text.substring(firstChar, lastChar);
  323. }
  324. if (text.isEmpty()) {
  325. return;
  326. }
  327. final AttributedCharacterIterator iterator = document.
  328. getStyledLine(line);
  329. if (iterator.getEndIndex() == iterator.getBeginIndex()) {
  330. return;
  331. }
  332. final AttributedString as = new AttributedString(iterator,
  333. firstChar, lastChar);
  334. as.addAttribute(TextAttribute.FOREGROUND,
  335. textPane.getBackground());
  336. as.addAttribute(TextAttribute.BACKGROUND,
  337. textPane.getForeground());
  338. final TextLayout newLayout = new TextLayout(as.getIterator(),
  339. g.getFontRenderContext());
  340. final Shape shape = layout.getLogicalHighlightShape(firstChar
  341. - chars,
  342. lastChar - chars);
  343. final int trans = (int) (newLayout.getDescent() + drawPosY);
  344. if (firstChar != 0) {
  345. g.translate(shape.getBounds().getX(), 0);
  346. }
  347. newLayout.draw(g, drawPosX, trans);
  348. if (firstChar != 0) {
  349. g.translate(-1 * shape.getBounds().getX(), 0);
  350. }
  351. }
  352. }
  353. }
  354. /**
  355. * {@inheritDoc}
  356. *
  357. * @param e Adjustment event
  358. */
  359. @Override
  360. public void adjustmentValueChanged(final AdjustmentEvent e) {
  361. if (startLine != e.getValue()) {
  362. startLine = e.getValue();
  363. recalc();
  364. }
  365. }
  366. /**
  367. * {@inheritDoc}
  368. *
  369. * @param e Mouse event
  370. */
  371. @Override
  372. public void mouseClicked(final MouseEvent e) {
  373. String clickedText = "";
  374. final int start;
  375. final int end;
  376. final LineInfo lineInfo = getClickPosition(getMousePosition(), true);
  377. fireMouseEvents(getClickType(lineInfo), MouseEventType.CLICK, e);
  378. if (lineInfo.getLine() != -1) {
  379. clickedText = document.getLine(lineInfo.getLine()).getText();
  380. if (lineInfo.getIndex() == -1) {
  381. start = -1;
  382. end = -1;
  383. } else {
  384. final int[] extent =
  385. getSurroundingWordIndexes(clickedText,
  386. lineInfo.getIndex());
  387. start = extent[0];
  388. end = extent[1];
  389. }
  390. if (e.getClickCount() == 2) {
  391. selection.setStartLine(lineInfo.getLine());
  392. selection.setEndLine(lineInfo.getLine());
  393. selection.setStartPos(start);
  394. selection.setEndPos(end);
  395. if (quickCopy) {
  396. textPane.copy(e.isShiftDown());
  397. clearSelection();
  398. }
  399. } else if (e.getClickCount() == 3) {
  400. selection.setStartLine(lineInfo.getLine());
  401. selection.setEndLine(lineInfo.getLine());
  402. selection.setStartPos(0);
  403. selection.setEndPos(clickedText.length());
  404. if (quickCopy) {
  405. textPane.copy(e.isShiftDown());
  406. clearSelection();
  407. }
  408. }
  409. }
  410. }
  411. /**
  412. * Returns the type of text this click represents.
  413. *
  414. * @param lineInfo Line info of click.
  415. *
  416. * @return Click type for specified position
  417. */
  418. public ClickTypeValue getClickType(final LineInfo lineInfo) {
  419. if (lineInfo.getLine() != -1) {
  420. final AttributedCharacterIterator iterator = document.getStyledLine(
  421. lineInfo.getLine());
  422. final int index = lineInfo.getIndex();
  423. if (index >= iterator.getBeginIndex() && index <= iterator.
  424. getEndIndex()) {
  425. iterator.setIndex(lineInfo.getIndex());
  426. Object linkattr =
  427. iterator.getAttributes().get(
  428. IRCTextAttribute.HYPERLINK);
  429. if (linkattr instanceof String) {
  430. return new ClickTypeValue(ClickType.HYPERLINK,
  431. (String) linkattr);
  432. }
  433. linkattr =
  434. iterator.getAttributes().get(IRCTextAttribute.CHANNEL);
  435. if (linkattr instanceof String) {
  436. return new ClickTypeValue(ClickType.CHANNEL,
  437. (String) linkattr);
  438. }
  439. linkattr = iterator.getAttributes().get(
  440. IRCTextAttribute.NICKNAME);
  441. if (linkattr instanceof String) {
  442. return new ClickTypeValue(ClickType.NICKNAME,
  443. (String) linkattr);
  444. }
  445. } else {
  446. return new ClickTypeValue(ClickType.NORMAL, "");
  447. }
  448. }
  449. return new ClickTypeValue(ClickType.NORMAL, "");
  450. }
  451. /**
  452. * Returns the indexes for the word surrounding the index in the specified string.
  453. *
  454. * @param text Text to get word from
  455. * @param index Index to get surrounding word
  456. *
  457. * @return Indexes of the word surrounding the index (start, end)
  458. */
  459. protected int[] getSurroundingWordIndexes(final String text,
  460. final int index) {
  461. final int start = getSurroundingWordStart(text, index);
  462. final int end = getSurroundingWordEnd(text, index);
  463. if (start < 0 || end > text.length() || start > end) {
  464. return new int[]{0, 0};
  465. }
  466. return new int[]{start, end};
  467. }
  468. /**
  469. * Returns the start index for the word surrounding the index in the specified string.
  470. *
  471. * @param text Text to get word from
  472. * @param index Index to get surrounding word
  473. *
  474. * @return Start index of the word surrounding the index
  475. */
  476. private int getSurroundingWordStart(final String text, final int index) {
  477. int start = index;
  478. // Traverse backwards
  479. while (start > 0 && start < text.length()
  480. && text.charAt(start) != ' ') {
  481. start--;
  482. }
  483. if (start + 1 < text.length() && text.charAt(start) == ' ') {
  484. start++;
  485. }
  486. return start;
  487. }
  488. /**
  489. * Returns the end index for the word surrounding the index in the specified string.
  490. *
  491. * @param text Text to get word from
  492. * @param index Index to get surrounding word
  493. *
  494. * @return End index of the word surrounding the index
  495. */
  496. private int getSurroundingWordEnd(final String text, final int index) {
  497. int end = index;
  498. // And forwards
  499. while (end < text.length() && end > 0 && text.charAt(end) != ' ') {
  500. end++;
  501. }
  502. return end;
  503. }
  504. /**
  505. * {@inheritDoc}
  506. *
  507. * @param e Mouse event
  508. */
  509. @Override
  510. public void mousePressed(final MouseEvent e) {
  511. fireMouseEvents(getClickType(getClickPosition(e.getPoint(), false)),
  512. MouseEventType.PRESSED, e);
  513. if (e.getButton() == MouseEvent.BUTTON1) {
  514. highlightEvent(MouseEventType.CLICK, e);
  515. }
  516. }
  517. /**
  518. * {@inheritDoc}
  519. *
  520. * @param e Mouse event
  521. */
  522. @Override
  523. public void mouseReleased(final MouseEvent e) {
  524. fireMouseEvents(getClickType(getClickPosition(e.getPoint(), false)),
  525. MouseEventType.RELEASED, e);
  526. if (quickCopy) {
  527. textPane.copy((e.getModifiers() & MouseEvent.CTRL_MASK)
  528. == MouseEvent.CTRL_MASK);
  529. SwingUtilities.invokeLater(new Runnable() {
  530. /** {@inheritDoc} */
  531. @Override
  532. public void run() {
  533. clearSelection();
  534. }
  535. });
  536. }
  537. if (e.getButton() == MouseEvent.BUTTON1) {
  538. highlightEvent(MouseEventType.RELEASED, e);
  539. }
  540. }
  541. /**
  542. * {@inheritDoc}
  543. *
  544. * @param e Mouse event
  545. */
  546. @Override
  547. public void mouseDragged(final MouseEvent e) {
  548. if (e.getModifiersEx() == MouseEvent.BUTTON1_DOWN_MASK) {
  549. highlightEvent(MouseEventType.DRAG, e);
  550. }
  551. }
  552. /**
  553. * {@inheritDoc}
  554. *
  555. * @param e Mouse event
  556. */
  557. @Override
  558. public void mouseEntered(final MouseEvent e) {
  559. //Ignore
  560. }
  561. /**
  562. * {@inheritDoc}
  563. *
  564. * @param e Mouse event
  565. */
  566. @Override
  567. public void mouseExited(final MouseEvent e) {
  568. //Ignore
  569. }
  570. /**
  571. * {@inheritDoc}
  572. *
  573. * @param e Mouse event
  574. */
  575. @Override
  576. public void mouseMoved(final MouseEvent e) {
  577. checkForLink();
  578. }
  579. /** Checks for a link under the cursor and sets appropriately. */
  580. private void checkForLink() {
  581. final AttributedCharacterIterator iterator =
  582. getIterator(getMousePosition());
  583. if (iterator != null
  584. && (iterator.getAttribute(IRCTextAttribute.HYPERLINK) != null
  585. || iterator.getAttribute(IRCTextAttribute.CHANNEL) != null
  586. || iterator.getAttribute(IRCTextAttribute.NICKNAME) != null)) {
  587. setCursor(HAND_CURSOR);
  588. return;
  589. }
  590. if (getCursor() == HAND_CURSOR) {
  591. setCursor(Cursor.getDefaultCursor());
  592. }
  593. }
  594. /**
  595. * Retrieves a character iterator for the text at the specified mouse position.
  596. *
  597. * @since 0.6.4
  598. * @param mousePosition The mouse position to retrieve text for
  599. *
  600. * @return A corresponding character iterator, or null if the specified mouse position doesn't
  601. * correspond to any text
  602. */
  603. private AttributedCharacterIterator getIterator(final Point mousePosition) {
  604. final LineInfo lineInfo = getClickPosition(mousePosition, false);
  605. if (lineInfo.getLine() != -1
  606. && document.getLine(lineInfo.getLine()) != null) {
  607. final AttributedCharacterIterator iterator = document.getStyledLine(lineInfo.getLine());
  608. if (lineInfo.getIndex() < iterator.getBeginIndex()
  609. || lineInfo.getIndex() > iterator.getEndIndex()) {
  610. return null;
  611. }
  612. iterator.setIndex(lineInfo.getIndex());
  613. return iterator;
  614. }
  615. return null;
  616. }
  617. /**
  618. * Sets the selection for the given event.
  619. *
  620. * @param type mouse event type
  621. * @param e responsible mouse event
  622. */
  623. protected void highlightEvent(final MouseEventType type,
  624. final MouseEvent e) {
  625. if (isVisible()) {
  626. final Point point = e.getLocationOnScreen();
  627. SwingUtilities.convertPointFromScreen(point, this);
  628. if (!contains(point)) {
  629. final Rectangle bounds = getBounds();
  630. final Point mousePos = e.getPoint();
  631. if (mousePos.getX() < bounds.getX()) {
  632. point.setLocation(bounds.getX() + SINGLE_SIDE_PADDING,
  633. point.getY());
  634. } else if (mousePos.getX() > (bounds.getX() + bounds.
  635. getWidth())) {
  636. point.setLocation(bounds.getX() + bounds.getWidth()
  637. - SINGLE_SIDE_PADDING,
  638. point.getY());
  639. }
  640. if (mousePos.getY() < bounds.getY()) {
  641. point.setLocation(point.getX(), bounds.getY()
  642. + DOUBLE_SIDE_PADDING);
  643. } else if (mousePos.getY() > (bounds.getY() + bounds.
  644. getHeight())) {
  645. point.setLocation(bounds.getX() + bounds.getWidth()
  646. - SINGLE_SIDE_PADDING, bounds.getY() + bounds.
  647. getHeight() - DOUBLE_SIDE_PADDING - 1);
  648. }
  649. }
  650. LineInfo info = getClickPosition(point, true);
  651. final Rectangle first = getFirstLineRectangle();
  652. final Rectangle last = getLastLineRectangle();
  653. if (info.getLine() == -1 && info.getPart() == -1 && contains(point)
  654. && document.getNumLines() != 0 && first != null
  655. && last != null) {
  656. if (first.getY() >= point.getY()) {
  657. info = getFirstLineInfo();
  658. } else if (last.getY() <= point.getY()) {
  659. info = getLastLineInfo();
  660. }
  661. }
  662. if (info.getLine() != -1 && info.getPart() != -1) {
  663. if (type == MouseEventType.CLICK) {
  664. selection.setStartLine(info.getLine());
  665. selection.setStartPos(info.getIndex());
  666. }
  667. selection.setEndLine(info.getLine());
  668. selection.setEndPos(info.getIndex());
  669. recalc();
  670. }
  671. }
  672. }
  673. /**
  674. * Returns the visible rectangle of the first line.
  675. *
  676. * @return First line's rectangle
  677. */
  678. private Rectangle getFirstLineRectangle() {
  679. TextLayout firstLineLayout = null;
  680. for (Map.Entry<TextLayout, LineInfo> entry : textLayouts.entrySet()) {
  681. if (entry.getValue().getLine() == firstVisibleLine) {
  682. firstLineLayout = entry.getKey();
  683. }
  684. }
  685. if (firstLineLayout == null) {
  686. return null;
  687. }
  688. for (Map.Entry<Rectangle, TextLayout> entry : positions.entrySet()) {
  689. if (entry.getValue() == firstLineLayout) {
  690. return entry.getKey();
  691. }
  692. }
  693. return null;
  694. }
  695. /**
  696. * Returns the last line's visible rectangle.
  697. *
  698. * @return Last line's rectangle
  699. */
  700. private Rectangle getLastLineRectangle() {
  701. TextLayout lastLineLayout = null;
  702. for (Map.Entry<TextLayout, LineInfo> entry : textLayouts.entrySet()) {
  703. if (entry.getValue().getLine() == lastVisibleLine) {
  704. lastLineLayout = entry.getKey();
  705. }
  706. }
  707. if (lastLineLayout == null) {
  708. return null;
  709. }
  710. for (Map.Entry<Rectangle, TextLayout> entry : positions.entrySet()) {
  711. if (entry.getValue() == lastLineLayout) {
  712. return entry.getKey();
  713. }
  714. }
  715. return null;
  716. }
  717. /**
  718. * Returns the LineInfo for the first visible line.
  719. *
  720. * @return First line's line info
  721. */
  722. private LineInfo getFirstLineInfo() {
  723. int firstLineParts = Integer.MAX_VALUE;
  724. for (Map.Entry<TextLayout, LineInfo> entry : textLayouts.entrySet()) {
  725. if (entry.getValue().getLine() == firstVisibleLine
  726. && entry.getValue().getPart() < firstLineParts) {
  727. firstLineParts = entry.getValue().getPart();
  728. }
  729. }
  730. return new LineInfo(firstVisibleLine, firstLineParts);
  731. }
  732. /**
  733. * Returns the LineInfo for the last visible line.
  734. *
  735. * @return Last line's line info
  736. */
  737. private LineInfo getLastLineInfo() {
  738. int lastLineParts = -1;
  739. for (Map.Entry<TextLayout, LineInfo> entry : textLayouts.entrySet()) {
  740. if (entry.getValue().getLine() == lastVisibleLine
  741. && entry.getValue().getPart() > lastLineParts) {
  742. lastLineParts = entry.getValue().getPart();
  743. }
  744. }
  745. return new LineInfo(lastVisibleLine + 1, lastLineParts);
  746. }
  747. /**
  748. *
  749. * Returns the line information from a mouse click inside the textpane.
  750. *
  751. * @param point mouse position
  752. * @param selection Are we selecting text?
  753. *
  754. * @return line number, line part, position in whole line
  755. */
  756. public LineInfo getClickPosition(final Point point,
  757. final boolean selection) {
  758. int lineNumber = -1;
  759. int linePart = -1;
  760. int pos = 0;
  761. if (point != null) {
  762. for (Map.Entry<Rectangle, TextLayout> entry
  763. : positions.entrySet()) {
  764. if (entry.getKey().contains(point)) {
  765. lineNumber = textLayouts.get(entry.getValue()).getLine();
  766. linePart = textLayouts.get(entry.getValue()).getPart();
  767. }
  768. }
  769. pos = getHitPosition(lineNumber, linePart, (int) point.getX(),
  770. (int) point.getY(), selection);
  771. }
  772. return new LineInfo(lineNumber, linePart, pos);
  773. }
  774. /**
  775. * Returns the character index for a specified line and part for a specific hit position.
  776. *
  777. * @param lineNumber Line number
  778. * @param linePart Line part
  779. * @param x X position
  780. * @param y Y position
  781. *
  782. * @return Hit position
  783. */
  784. private int getHitPosition(final int lineNumber, final int linePart,
  785. final int x, final int y, final boolean selection) {
  786. int pos = 0;
  787. for (Map.Entry<Rectangle, TextLayout> entry : positions.entrySet()) {
  788. if (textLayouts.get(entry.getValue()).getLine() == lineNumber) {
  789. if (textLayouts.get(entry.getValue()).getPart() < linePart) {
  790. pos += entry.getValue().getCharacterCount();
  791. } else if (textLayouts.get(entry.getValue()).getPart()
  792. == linePart) {
  793. final TextHitInfo hit = entry.getValue().hitTestChar(x
  794. - DOUBLE_SIDE_PADDING, y);
  795. if (selection || x > entry.getValue().getBounds().getX()) {
  796. pos += hit.getInsertionIndex();
  797. } else {
  798. pos += hit.getCharIndex();
  799. }
  800. }
  801. }
  802. }
  803. return pos;
  804. }
  805. /**
  806. * Returns the selected range info.
  807. *
  808. * @return Selected range info
  809. */
  810. protected LinePosition getSelectedRange() {
  811. if (selection.getStartLine() > selection.getEndLine()) {
  812. // Swap both
  813. return new LinePosition(selection.getEndLine(),
  814. selection.getEndPos(), selection.getStartLine(),
  815. selection.getStartPos());
  816. } else if (selection.getStartLine() == selection.getEndLine()
  817. && selection.getStartPos() > selection.getEndPos()) {
  818. // Just swap the chars
  819. return new LinePosition(selection.getStartLine(), selection.
  820. getEndPos(), selection.getEndLine(),
  821. selection.getStartPos());
  822. } else {
  823. // Swap nothing
  824. return new LinePosition(selection.getStartLine(), selection.
  825. getStartPos(), selection.getEndLine(),
  826. selection.getEndPos());
  827. }
  828. }
  829. /** Clears the selection. */
  830. protected void clearSelection() {
  831. selection.setEndLine(selection.getStartLine());
  832. selection.setEndPos(selection.getStartPos());
  833. recalc();
  834. }
  835. /**
  836. * Selects the specified region of text.
  837. *
  838. * @param position Line position
  839. */
  840. public void setSelectedRange(final LinePosition position) {
  841. selection = new LinePosition(position);
  842. recalc();
  843. }
  844. /**
  845. * Returns the first visible line.
  846. *
  847. * @return the line number of the first visible line
  848. */
  849. public int getFirstVisibleLine() {
  850. return firstVisibleLine;
  851. }
  852. /**
  853. * Returns the last visible line.
  854. *
  855. * @return the line number of the last visible line
  856. */
  857. public int getLastVisibleLine() {
  858. return lastVisibleLine;
  859. }
  860. /**
  861. * Returns the number of visible lines.
  862. *
  863. * @return Number of visible lines
  864. */
  865. public int getNumVisibleLines() {
  866. return lastVisibleLine - firstVisibleLine;
  867. }
  868. /**
  869. * {@inheritDoc}
  870. *
  871. * @param e Component event
  872. */
  873. @Override
  874. public void componentResized(final ComponentEvent e) {
  875. recalc();
  876. }
  877. /**
  878. * {@inheritDoc}
  879. *
  880. * @param e Component event
  881. */
  882. @Override
  883. public void componentMoved(final ComponentEvent e) {
  884. //Ignore
  885. }
  886. /**
  887. * {@inheritDoc}
  888. *
  889. * @param e Component event
  890. */
  891. @Override
  892. public void componentShown(final ComponentEvent e) {
  893. //Ignore
  894. }
  895. /**
  896. * {@inheritDoc}
  897. *
  898. * @param e Component event
  899. */
  900. @Override
  901. public void componentHidden(final ComponentEvent e) {
  902. //Ignore
  903. }
  904. /** {@inheritDoc} */
  905. @Override
  906. public void configChanged(final String domain, final String key) {
  907. updateCachedSettings();
  908. }
  909. /** {@inheritDoc} */
  910. @Override
  911. public String getToolTipText(final MouseEvent event) {
  912. final AttributedCharacterIterator iterator = getIterator(
  913. event.getPoint());
  914. if (iterator != null
  915. && iterator.getAttribute(IRCTextAttribute.TOOLTIP) != null) {
  916. return iterator.getAttribute(IRCTextAttribute.TOOLTIP).toString();
  917. }
  918. return super.getToolTipText(event);
  919. }
  920. /**
  921. * Fires mouse clicked events with the associated values.
  922. *
  923. * @param clickType Click type
  924. * @param eventType Mouse event type
  925. * @param event Triggering mouse event
  926. */
  927. private void fireMouseEvents(final ClickTypeValue clickType,
  928. final MouseEventType eventType, final MouseEvent event) {
  929. for (TextPaneListener listener
  930. : listeners.get(TextPaneListener.class)) {
  931. listener.mouseClicked(clickType, eventType, event);
  932. }
  933. }
  934. /**
  935. * Adds a textpane listener.
  936. *
  937. * @param listener Listener to add
  938. */
  939. public void addTextPaneListener(final TextPaneListener listener) {
  940. listeners.add(TextPaneListener.class, listener);
  941. }
  942. /**
  943. * Removes a textpane listener.
  944. *
  945. * @param listener Listener to remove
  946. */
  947. public void removeTextPaneListener(final TextPaneListener listener) {
  948. listeners.remove(TextPaneListener.class, listener);
  949. }
  950. }