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 36KB

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