1 /* 2 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package javax.swing.text.html; 26 27 import sun.awt.AppContext; 28 29 import java.awt.*; 30 import java.awt.event.*; 31 import java.io.*; 32 import java.net.MalformedURLException; 33 import java.net.URL; 34 import javax.swing.text.*; 35 import javax.swing.*; 36 import javax.swing.event.*; 37 import javax.swing.plaf.TextUI; 38 import java.util.*; 39 import javax.accessibility.*; 40 import java.lang.ref.*; 41 import java.security.AccessController; 42 import java.security.PrivilegedAction; 43 44 /** 45 * The Swing JEditorPane text component supports different kinds 46 * of content via a plug-in mechanism called an EditorKit. Because 47 * HTML is a very popular format of content, some support is provided 48 * by default. The default support is provided by this class, which 49 * supports HTML version 3.2 (with some extensions), and is migrating 50 * toward version 4.0. 51 * The <applet> tag is not supported, but some support is provided 52 * for the <object> tag. 53 * <p> 54 * There are several goals of the HTML EditorKit provided, that have 55 * an effect upon the way that HTML is modeled. These 56 * have influenced its design in a substantial way. 57 * <dl> 58 * <dt> 59 * Support editing 60 * <dd> 61 * It might seem fairly obvious that a plug-in for JEditorPane 62 * should provide editing support, but that fact has several 63 * design considerations. There are a substantial number of HTML 64 * documents that don't properly conform to an HTML specification. 65 * These must be normalized somewhat into a correct form if one 66 * is to edit them. Additionally, users don't like to be presented 67 * with an excessive amount of structure editing, so using traditional 68 * text editing gestures is preferred over using the HTML structure 69 * exactly as defined in the HTML document. 70 * <p> 71 * The modeling of HTML is provided by the class <code>HTMLDocument</code>. 72 * Its documentation describes the details of how the HTML is modeled. 73 * The editing support leverages heavily off of the text package. 74 * 75 * <dt> 76 * Extendable/Scalable 77 * <dd> 78 * To maximize the usefulness of this kit, a great deal of effort 79 * has gone into making it extendable. These are some of the 80 * features. 81 * <ol> 82 * <li> 83 * The parser is replaceable. The default parser is the Hot Java 84 * parser which is DTD based. A different DTD can be used, or an 85 * entirely different parser can be used. To change the parser, 86 * reimplement the getParser method. The default parser is 87 * dynamically loaded when first asked for, so the class files 88 * will never be loaded if an alternative parser is used. The 89 * default parser is in a separate package called parser below 90 * this package. 91 * <li> 92 * The parser drives the ParserCallback, which is provided by 93 * HTMLDocument. To change the callback, subclass HTMLDocument 94 * and reimplement the createDefaultDocument method to return 95 * document that produces a different reader. The reader controls 96 * how the document is structured. Although the Document provides 97 * HTML support by default, there is nothing preventing support of 98 * non-HTML tags that result in alternative element structures. 99 * <li> 100 * The default view of the models are provided as a hierarchy of 101 * View implementations, so one can easily customize how a particular 102 * element is displayed or add capabilities for new kinds of elements 103 * by providing new View implementations. The default set of views 104 * are provided by the <code>HTMLFactory</code> class. This can 105 * be easily changed by subclassing or replacing the HTMLFactory 106 * and reimplementing the getViewFactory method to return the alternative 107 * factory. 108 * <li> 109 * The View implementations work primarily off of CSS attributes, 110 * which are kept in the views. This makes it possible to have 111 * multiple views mapped over the same model that appear substantially 112 * different. This can be especially useful for printing. For 113 * most HTML attributes, the HTML attributes are converted to CSS 114 * attributes for display. This helps make the View implementations 115 * more general purpose 116 * </ol> 117 * 118 * <dt> 119 * Asynchronous Loading 120 * <dd> 121 * Larger documents involve a lot of parsing and take some time 122 * to load. By default, this kit produces documents that will be 123 * loaded asynchronously if loaded using <code>JEditorPane.setPage</code>. 124 * This is controlled by a property on the document. The method 125 * {@link #createDefaultDocument createDefaultDocument} can 126 * be overriden to change this. The batching of work is done 127 * by the <code>HTMLDocument.HTMLReader</code> class. The actual 128 * work is done by the <code>DefaultStyledDocument</code> and 129 * <code>AbstractDocument</code> classes in the text package. 130 * 131 * <dt> 132 * Customization from current LAF 133 * <dd> 134 * HTML provides a well known set of features without exactly 135 * specifying the display characteristics. Swing has a theme 136 * mechanism for its look-and-feel implementations. It is desirable 137 * for the look-and-feel to feed display characteristics into the 138 * HTML views. An user with poor vision for example would want 139 * high contrast and larger than typical fonts. 140 * <p> 141 * The support for this is provided by the <code>StyleSheet</code> 142 * class. The presentation of the HTML can be heavily influenced 143 * by the setting of the StyleSheet property on the EditorKit. 144 * 145 * <dt> 146 * Not lossy 147 * <dd> 148 * An EditorKit has the ability to be read and save documents. 149 * It is generally the most pleasing to users if there is no loss 150 * of data between the two operation. The policy of the HTMLEditorKit 151 * will be to store things not recognized or not necessarily visible 152 * so they can be subsequently written out. The model of the HTML document 153 * should therefore contain all information discovered while reading the 154 * document. This is constrained in some ways by the need to support 155 * editing (i.e. incorrect documents sometimes must be normalized). 156 * The guiding principle is that information shouldn't be lost, but 157 * some might be synthesized to produce a more correct model or it might 158 * be rearranged. 159 * </dl> 160 * 161 * @author Timothy Prinzing 162 */ 163 public class HTMLEditorKit extends StyledEditorKit implements Accessible { 164 165 private JEditorPane theEditor; 166 167 /** 168 * Constructs an HTMLEditorKit, creates a StyleContext, 169 * and loads the style sheet. 170 */ HTMLEditorKit()171 public HTMLEditorKit() { 172 173 } 174 175 /** 176 * Get the MIME type of the data that this 177 * kit represents support for. This kit supports 178 * the type <code>text/html</code>. 179 * 180 * @return the type 181 */ getContentType()182 public String getContentType() { 183 return "text/html"; 184 } 185 186 /** 187 * Fetch a factory that is suitable for producing 188 * views of any models that are produced by this 189 * kit. 190 * 191 * @return the factory 192 */ getViewFactory()193 public ViewFactory getViewFactory() { 194 return defaultFactory; 195 } 196 197 /** 198 * Create an uninitialized text storage model 199 * that is appropriate for this type of editor. 200 * 201 * @return the model 202 */ createDefaultDocument()203 public Document createDefaultDocument() { 204 StyleSheet styles = getStyleSheet(); 205 StyleSheet ss = new StyleSheet(); 206 207 ss.addStyleSheet(styles); 208 209 HTMLDocument doc = new HTMLDocument(ss); 210 doc.setParser(getParser()); 211 doc.setAsynchronousLoadPriority(4); 212 doc.setTokenThreshold(100); 213 return doc; 214 } 215 216 /** 217 * Try to get an HTML parser from the document. If no parser is set for 218 * the document, return the editor kit's default parser. It is an error 219 * if no parser could be obtained from the editor kit. 220 */ ensureParser(HTMLDocument doc)221 private Parser ensureParser(HTMLDocument doc) throws IOException { 222 Parser p = doc.getParser(); 223 if (p == null) { 224 p = getParser(); 225 } 226 if (p == null) { 227 throw new IOException("Can't load parser"); 228 } 229 return p; 230 } 231 232 /** 233 * Inserts content from the given stream. If <code>doc</code> is 234 * an instance of HTMLDocument, this will read 235 * HTML 3.2 text. Inserting HTML into a non-empty document must be inside 236 * the body Element, if you do not insert into the body an exception will 237 * be thrown. When inserting into a non-empty document all tags outside 238 * of the body (head, title) will be dropped. 239 * 240 * @param in the stream to read from 241 * @param doc the destination for the insertion 242 * @param pos the location in the document to place the 243 * content 244 * @exception IOException on any I/O error 245 * @exception BadLocationException if pos represents an invalid 246 * location within the document 247 * @exception RuntimeException (will eventually be a BadLocationException) 248 * if pos is invalid 249 */ read(Reader in, Document doc, int pos)250 public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException { 251 252 if (doc instanceof HTMLDocument) { 253 HTMLDocument hdoc = (HTMLDocument) doc; 254 if (pos > doc.getLength()) { 255 throw new BadLocationException("Invalid location", pos); 256 } 257 258 Parser p = ensureParser(hdoc); 259 ParserCallback receiver = hdoc.getReader(pos); 260 Boolean ignoreCharset = (Boolean)doc.getProperty("IgnoreCharsetDirective"); 261 p.parse(in, receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue()); 262 receiver.flush(); 263 } else { 264 super.read(in, doc, pos); 265 } 266 } 267 268 /** 269 * Inserts HTML into an existing document. 270 * 271 * @param doc the document to insert into 272 * @param offset the offset to insert HTML at 273 * @param popDepth the number of ElementSpec.EndTagTypes to generate before 274 * inserting 275 * @param pushDepth the number of ElementSpec.StartTagTypes with a direction 276 * of ElementSpec.JoinNextDirection that should be generated 277 * before inserting, but after the end tags have been generated 278 * @param insertTag the first tag to start inserting into document 279 * @exception RuntimeException (will eventually be a BadLocationException) 280 * if pos is invalid 281 */ insertHTML(HTMLDocument doc, int offset, String html, int popDepth, int pushDepth, HTML.Tag insertTag)282 public void insertHTML(HTMLDocument doc, int offset, String html, 283 int popDepth, int pushDepth, 284 HTML.Tag insertTag) throws 285 BadLocationException, IOException { 286 if (offset > doc.getLength()) { 287 throw new BadLocationException("Invalid location", offset); 288 } 289 290 Parser p = ensureParser(doc); 291 ParserCallback receiver = doc.getReader(offset, popDepth, pushDepth, 292 insertTag); 293 Boolean ignoreCharset = (Boolean)doc.getProperty 294 ("IgnoreCharsetDirective"); 295 p.parse(new StringReader(html), receiver, (ignoreCharset == null) ? 296 false : ignoreCharset.booleanValue()); 297 receiver.flush(); 298 } 299 300 /** 301 * Write content from a document to the given stream 302 * in a format appropriate for this kind of content handler. 303 * 304 * @param out the stream to write to 305 * @param doc the source for the write 306 * @param pos the location in the document to fetch the 307 * content 308 * @param len the amount to write out 309 * @exception IOException on any I/O error 310 * @exception BadLocationException if pos represents an invalid 311 * location within the document 312 */ write(Writer out, Document doc, int pos, int len)313 public void write(Writer out, Document doc, int pos, int len) 314 throws IOException, BadLocationException { 315 316 if (doc instanceof HTMLDocument) { 317 HTMLWriter w = new HTMLWriter(out, (HTMLDocument)doc, pos, len); 318 w.write(); 319 } else if (doc instanceof StyledDocument) { 320 MinimalHTMLWriter w = new MinimalHTMLWriter(out, (StyledDocument)doc, pos, len); 321 w.write(); 322 } else { 323 super.write(out, doc, pos, len); 324 } 325 } 326 327 /** 328 * Called when the kit is being installed into the 329 * a JEditorPane. 330 * 331 * @param c the JEditorPane 332 */ install(JEditorPane c)333 public void install(JEditorPane c) { 334 c.addMouseListener(linkHandler); 335 c.addMouseMotionListener(linkHandler); 336 c.addCaretListener(nextLinkAction); 337 super.install(c); 338 theEditor = c; 339 } 340 341 /** 342 * Called when the kit is being removed from the 343 * JEditorPane. This is used to unregister any 344 * listeners that were attached. 345 * 346 * @param c the JEditorPane 347 */ deinstall(JEditorPane c)348 public void deinstall(JEditorPane c) { 349 c.removeMouseListener(linkHandler); 350 c.removeMouseMotionListener(linkHandler); 351 c.removeCaretListener(nextLinkAction); 352 super.deinstall(c); 353 theEditor = null; 354 } 355 356 /** 357 * Default Cascading Style Sheet file that sets 358 * up the tag views. 359 */ 360 public static final String DEFAULT_CSS = "default.css"; 361 362 /** 363 * Set the set of styles to be used to render the various 364 * HTML elements. These styles are specified in terms of 365 * CSS specifications. Each document produced by the kit 366 * will have a copy of the sheet which it can add the 367 * document specific styles to. By default, the StyleSheet 368 * specified is shared by all HTMLEditorKit instances. 369 * This should be reimplemented to provide a finer granularity 370 * if desired. 371 */ setStyleSheet(StyleSheet s)372 public void setStyleSheet(StyleSheet s) { 373 if (s == null) { 374 AppContext.getAppContext().remove(DEFAULT_STYLES_KEY); 375 } else { 376 AppContext.getAppContext().put(DEFAULT_STYLES_KEY, s); 377 } 378 } 379 380 /** 381 * Get the set of styles currently being used to render the 382 * HTML elements. By default the resource specified by 383 * DEFAULT_CSS gets loaded, and is shared by all HTMLEditorKit 384 * instances. 385 */ getStyleSheet()386 public StyleSheet getStyleSheet() { 387 AppContext appContext = AppContext.getAppContext(); 388 StyleSheet defaultStyles = (StyleSheet) appContext.get(DEFAULT_STYLES_KEY); 389 390 if (defaultStyles == null) { 391 defaultStyles = new StyleSheet(); 392 appContext.put(DEFAULT_STYLES_KEY, defaultStyles); 393 try { 394 InputStream is = HTMLEditorKit.getResourceAsStream(DEFAULT_CSS); 395 Reader r = new BufferedReader( 396 new InputStreamReader(is, "ISO-8859-1")); 397 defaultStyles.loadRules(r, null); 398 r.close(); 399 } catch (Throwable e) { 400 // on error we simply have no styles... the html 401 // will look mighty wrong but still function. 402 } 403 } 404 return defaultStyles; 405 } 406 407 /** 408 * Fetch a resource relative to the HTMLEditorKit classfile. 409 * If this is called on 1.2 the loading will occur under the 410 * protection of a doPrivileged call to allow the HTMLEditorKit 411 * to function when used in an applet. 412 * 413 * @param name the name of the resource, relative to the 414 * HTMLEditorKit class 415 * @return a stream representing the resource 416 */ getResourceAsStream(final String name)417 static InputStream getResourceAsStream(final String name) { 418 return AccessController.doPrivileged( 419 new PrivilegedAction<InputStream>() { 420 public InputStream run() { 421 return HTMLEditorKit.class.getResourceAsStream(name); 422 } 423 }); 424 } 425 426 /** 427 * Fetches the command list for the editor. This is 428 * the list of commands supported by the superclass 429 * augmented by the collection of commands defined 430 * locally for style operations. 431 * 432 * @return the command list 433 */ 434 public Action[] getActions() { 435 return TextAction.augmentList(super.getActions(), this.defaultActions); 436 } 437 438 /** 439 * Copies the key/values in <code>element</code>s AttributeSet into 440 * <code>set</code>. This does not copy component, icon, or element 441 * names attributes. Subclasses may wish to refine what is and what 442 * isn't copied here. But be sure to first remove all the attributes that 443 * are in <code>set</code>.<p> 444 * This is called anytime the caret moves over a different location. 445 * 446 */ 447 protected void createInputAttributes(Element element, 448 MutableAttributeSet set) { 449 set.removeAttributes(set); 450 set.addAttributes(element.getAttributes()); 451 set.removeAttribute(StyleConstants.ComposedTextAttribute); 452 453 Object o = set.getAttribute(StyleConstants.NameAttribute); 454 if (o instanceof HTML.Tag) { 455 HTML.Tag tag = (HTML.Tag)o; 456 // PENDING: we need a better way to express what shouldn't be 457 // copied when editing... 458 if(tag == HTML.Tag.IMG) { 459 // Remove the related image attributes, src, width, height 460 set.removeAttribute(HTML.Attribute.SRC); 461 set.removeAttribute(HTML.Attribute.HEIGHT); 462 set.removeAttribute(HTML.Attribute.WIDTH); 463 set.addAttribute(StyleConstants.NameAttribute, 464 HTML.Tag.CONTENT); 465 } 466 else if (tag == HTML.Tag.HR || tag == HTML.Tag.BR) { 467 // Don't copy HRs or BRs either. 468 set.addAttribute(StyleConstants.NameAttribute, 469 HTML.Tag.CONTENT); 470 } 471 else if (tag == HTML.Tag.COMMENT) { 472 // Don't copy COMMENTs either 473 set.addAttribute(StyleConstants.NameAttribute, 474 HTML.Tag.CONTENT); 475 set.removeAttribute(HTML.Attribute.COMMENT); 476 } 477 else if (tag == HTML.Tag.INPUT) { 478 // or INPUT either 479 set.addAttribute(StyleConstants.NameAttribute, 480 HTML.Tag.CONTENT); 481 set.removeAttribute(HTML.Tag.INPUT); 482 } 483 else if (tag instanceof HTML.UnknownTag) { 484 // Don't copy unknowns either:( 485 set.addAttribute(StyleConstants.NameAttribute, 486 HTML.Tag.CONTENT); 487 set.removeAttribute(HTML.Attribute.ENDTAG); 488 } 489 } 490 } 491 492 /** 493 * Gets the input attributes used for the styled 494 * editing actions. 495 * 496 * @return the attribute set 497 */ 498 public MutableAttributeSet getInputAttributes() { 499 if (input == null) { 500 input = getStyleSheet().addStyle(null, null); 501 } 502 return input; 503 } 504 505 /** 506 * Sets the default cursor. 507 * 508 * @since 1.3 509 */ 510 public void setDefaultCursor(Cursor cursor) { 511 defaultCursor = cursor; 512 } 513 514 /** 515 * Returns the default cursor. 516 * 517 * @since 1.3 518 */ 519 public Cursor getDefaultCursor() { 520 return defaultCursor; 521 } 522 523 /** 524 * Sets the cursor to use over links. 525 * 526 * @since 1.3 527 */ 528 public void setLinkCursor(Cursor cursor) { 529 linkCursor = cursor; 530 } 531 532 /** 533 * Returns the cursor to use over hyper links. 534 * @since 1.3 535 */ 536 public Cursor getLinkCursor() { 537 return linkCursor; 538 } 539 540 /** 541 * Indicates whether an html form submission is processed automatically 542 * or only <code>FormSubmitEvent</code> is fired. 543 * 544 * @return true if html form submission is processed automatically, 545 * false otherwise. 546 * 547 * @see #setAutoFormSubmission 548 * @since 1.5 549 */ 550 public boolean isAutoFormSubmission() { 551 return isAutoFormSubmission; 552 } 553 554 /** 555 * Specifies if an html form submission is processed 556 * automatically or only <code>FormSubmitEvent</code> is fired. 557 * By default it is set to true. 558 * 559 * @see #isAutoFormSubmission() 560 * @see FormSubmitEvent 561 * @since 1.5 562 */ 563 public void setAutoFormSubmission(boolean isAuto) { 564 isAutoFormSubmission = isAuto; 565 } 566 567 /** 568 * Creates a copy of the editor kit. 569 * 570 * @return the copy 571 */ 572 public Object clone() { 573 HTMLEditorKit o = (HTMLEditorKit)super.clone(); 574 if (o != null) { 575 o.input = null; 576 o.linkHandler = new LinkController(); 577 } 578 return o; 579 } 580 581 /** 582 * Fetch the parser to use for reading HTML streams. 583 * This can be reimplemented to provide a different 584 * parser. The default implementation is loaded dynamically 585 * to avoid the overhead of loading the default parser if 586 * it's not used. The default parser is the HotJava parser 587 * using an HTML 3.2 DTD. 588 */ 589 protected Parser getParser() { 590 if (defaultParser == null) { 591 try { 592 Class c = Class.forName("javax.swing.text.html.parser.ParserDelegator"); 593 defaultParser = (Parser) c.newInstance(); 594 } catch (Throwable e) { 595 } 596 } 597 return defaultParser; 598 } 599 600 // ----- Accessibility support ----- 601 private AccessibleContext accessibleContext; 602 603 /** 604 * returns the AccessibleContext associated with this editor kit 605 * 606 * @return the AccessibleContext associated with this editor kit 607 * @since 1.4 608 */ 609 public AccessibleContext getAccessibleContext() { 610 if (theEditor == null) { 611 return null; 612 } 613 if (accessibleContext == null) { 614 AccessibleHTML a = new AccessibleHTML(theEditor); 615 accessibleContext = a.getAccessibleContext(); 616 } 617 return accessibleContext; 618 } 619 620 // --- variables ------------------------------------------ 621 622 private static final Cursor MoveCursor = Cursor.getPredefinedCursor 623 (Cursor.HAND_CURSOR); 624 private static final Cursor DefaultCursor = Cursor.getPredefinedCursor 625 (Cursor.DEFAULT_CURSOR); 626 627 /** Shared factory for creating HTML Views. */ 628 private static final ViewFactory defaultFactory = new HTMLFactory(); 629 630 MutableAttributeSet input; 631 private static final Object DEFAULT_STYLES_KEY = new Object(); 632 private LinkController linkHandler = new LinkController(); 633 private static Parser defaultParser = null; 634 private Cursor defaultCursor = DefaultCursor; 635 private Cursor linkCursor = MoveCursor; 636 private boolean isAutoFormSubmission = true; 637 638 /** 639 * Class to watch the associated component and fire 640 * hyperlink events on it when appropriate. 641 */ 642 public static class LinkController extends MouseAdapter implements MouseMotionListener, Serializable { 643 private Element curElem = null; 644 /** 645 * If true, the current element (curElem) represents an image. 646 */ 647 private boolean curElemImage = false; 648 private String href = null; 649 /** This is used by viewToModel to avoid allocing a new array each 650 * time. */ 651 private transient Position.Bias[] bias = new Position.Bias[1]; 652 /** 653 * Current offset. 654 */ 655 private int curOffset; 656 657 /** 658 * Called for a mouse click event. 659 * If the component is read-only (ie a browser) then 660 * the clicked event is used to drive an attempt to 661 * follow the reference specified by a link. 662 * 663 * @param e the mouse event 664 * @see MouseListener#mouseClicked 665 */ 666 public void mouseClicked(MouseEvent e) { 667 JEditorPane editor = (JEditorPane) e.getSource(); 668 669 if (! editor.isEditable() && editor.isEnabled() && 670 SwingUtilities.isLeftMouseButton(e)) { 671 Point pt = new Point(e.getX(), e.getY()); 672 int pos = editor.viewToModel(pt); 673 if (pos >= 0) { 674 activateLink(pos, editor, e); 675 } 676 } 677 } 678 679 // ignore the drags 680 public void mouseDragged(MouseEvent e) { 681 } 682 683 // track the moving of the mouse. 684 public void mouseMoved(MouseEvent e) { 685 JEditorPane editor = (JEditorPane) e.getSource(); 686 if (!editor.isEnabled()) { 687 return; 688 } 689 690 HTMLEditorKit kit = (HTMLEditorKit)editor.getEditorKit(); 691 boolean adjustCursor = true; 692 Cursor newCursor = kit.getDefaultCursor(); 693 if (!editor.isEditable()) { 694 Point pt = new Point(e.getX(), e.getY()); 695 int pos = editor.getUI().viewToModel(editor, pt, bias); 696 if (bias[0] == Position.Bias.Backward && pos > 0) { 697 pos--; 698 } 699 if (pos >= 0 &&(editor.getDocument() instanceof HTMLDocument)){ 700 HTMLDocument hdoc = (HTMLDocument)editor.getDocument(); 701 Element elem = hdoc.getCharacterElement(pos); 702 if (!doesElementContainLocation(editor, elem, pos, 703 e.getX(), e.getY())) { 704 elem = null; 705 } 706 if (curElem != elem || curElemImage) { 707 Element lastElem = curElem; 708 curElem = elem; 709 String href = null; 710 curElemImage = false; 711 if (elem != null) { 712 AttributeSet a = elem.getAttributes(); 713 AttributeSet anchor = (AttributeSet)a. 714 getAttribute(HTML.Tag.A); 715 if (anchor == null) { 716 curElemImage = (a.getAttribute(StyleConstants. 717 NameAttribute) == HTML.Tag.IMG); 718 if (curElemImage) { 719 href = getMapHREF(editor, hdoc, elem, a, 720 pos, e.getX(), e.getY()); 721 } 722 } 723 else { 724 href = (String)anchor.getAttribute 725 (HTML.Attribute.HREF); 726 } 727 } 728 729 if (href != this.href) { 730 // reference changed, fire event(s) 731 fireEvents(editor, hdoc, href, lastElem, e); 732 this.href = href; 733 if (href != null) { 734 newCursor = kit.getLinkCursor(); 735 } 736 } 737 else { 738 adjustCursor = false; 739 } 740 } 741 else { 742 adjustCursor = false; 743 } 744 curOffset = pos; 745 } 746 } 747 if (adjustCursor && editor.getCursor() != newCursor) { 748 editor.setCursor(newCursor); 749 } 750 } 751 752 /** 753 * Returns a string anchor if the passed in element has a 754 * USEMAP that contains the passed in location. 755 */ 756 private String getMapHREF(JEditorPane html, HTMLDocument hdoc, 757 Element elem, AttributeSet attr, int offset, 758 int x, int y) { 759 Object useMap = attr.getAttribute(HTML.Attribute.USEMAP); 760 if (useMap != null && (useMap instanceof String)) { 761 Map m = hdoc.getMap((String)useMap); 762 if (m != null && offset < hdoc.getLength()) { 763 Rectangle bounds; 764 TextUI ui = html.getUI(); 765 try { 766 Shape lBounds = ui.modelToView(html, offset, 767 Position.Bias.Forward); 768 Shape rBounds = ui.modelToView(html, offset + 1, 769 Position.Bias.Backward); 770 bounds = lBounds.getBounds(); 771 bounds.add((rBounds instanceof Rectangle) ? 772 (Rectangle)rBounds : rBounds.getBounds()); 773 } catch (BadLocationException ble) { 774 bounds = null; 775 } 776 if (bounds != null) { 777 AttributeSet area = m.getArea(x - bounds.x, 778 y - bounds.y, 779 bounds.width, 780 bounds.height); 781 if (area != null) { 782 return (String)area.getAttribute(HTML.Attribute. 783 HREF); 784 } 785 } 786 } 787 } 788 return null; 789 } 790 791 /** 792 * Returns true if the View representing <code>e</code> contains 793 * the location <code>x</code>, <code>y</code>. <code>offset</code> 794 * gives the offset into the Document to check for. 795 */ 796 private boolean doesElementContainLocation(JEditorPane editor, 797 Element e, int offset, 798 int x, int y) { 799 if (e != null && offset > 0 && e.getStartOffset() == offset) { 800 try { 801 TextUI ui = editor.getUI(); 802 Shape s1 = ui.modelToView(editor, offset, 803 Position.Bias.Forward); 804 if (s1 == null) { 805 return false; 806 } 807 Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle)s1 : 808 s1.getBounds(); 809 Shape s2 = ui.modelToView(editor, e.getEndOffset(), 810 Position.Bias.Backward); 811 if (s2 != null) { 812 Rectangle r2 = (s2 instanceof Rectangle) ? (Rectangle)s2 : 813 s2.getBounds(); 814 r1.add(r2); 815 } 816 return r1.contains(x, y); 817 } catch (BadLocationException ble) { 818 } 819 } 820 return true; 821 } 822 823 /** 824 * Calls linkActivated on the associated JEditorPane 825 * if the given position represents a link.<p>This is implemented 826 * to forward to the method with the same name, but with the following 827 * args both == -1. 828 * 829 * @param pos the position 830 * @param editor the editor pane 831 */ 832 protected void activateLink(int pos, JEditorPane editor) { 833 activateLink(pos, editor, null); 834 } 835 836 /** 837 * Calls linkActivated on the associated JEditorPane 838 * if the given position represents a link. If this was the result 839 * of a mouse click, <code>x</code> and 840 * <code>y</code> will give the location of the mouse, otherwise 841 * they will be {@literal <} 0. 842 * 843 * @param pos the position 844 * @param html the editor pane 845 */ 846 void activateLink(int pos, JEditorPane html, MouseEvent mouseEvent) { 847 Document doc = html.getDocument(); 848 if (doc instanceof HTMLDocument) { 849 HTMLDocument hdoc = (HTMLDocument) doc; 850 Element e = hdoc.getCharacterElement(pos); 851 AttributeSet a = e.getAttributes(); 852 AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A); 853 HyperlinkEvent linkEvent = null; 854 String description; 855 int x = -1; 856 int y = -1; 857 858 if (mouseEvent != null) { 859 x = mouseEvent.getX(); 860 y = mouseEvent.getY(); 861 } 862 863 if (anchor == null) { 864 href = getMapHREF(html, hdoc, e, a, pos, x, y); 865 } 866 else { 867 href = (String)anchor.getAttribute(HTML.Attribute.HREF); 868 } 869 870 if (href != null) { 871 linkEvent = createHyperlinkEvent(html, hdoc, href, anchor, 872 e, mouseEvent); 873 } 874 if (linkEvent != null) { 875 html.fireHyperlinkUpdate(linkEvent); 876 } 877 } 878 } 879 880 /** 881 * Creates and returns a new instance of HyperlinkEvent. If 882 * <code>hdoc</code> is a frame document a HTMLFrameHyperlinkEvent 883 * will be created. 884 */ 885 HyperlinkEvent createHyperlinkEvent(JEditorPane html, 886 HTMLDocument hdoc, String href, 887 AttributeSet anchor, 888 Element element, 889 MouseEvent mouseEvent) { 890 URL u; 891 try { 892 URL base = hdoc.getBase(); 893 u = new URL(base, href); 894 // Following is a workaround for 1.2, in which 895 // new URL("file://...", "#...") causes the filename to 896 // be lost. 897 if (href != null && "file".equals(u.getProtocol()) && 898 href.startsWith("#")) { 899 String baseFile = base.getFile(); 900 String newFile = u.getFile(); 901 if (baseFile != null && newFile != null && 902 !newFile.startsWith(baseFile)) { 903 u = new URL(base, baseFile + href); 904 } 905 } 906 } catch (MalformedURLException m) { 907 u = null; 908 } 909 HyperlinkEvent linkEvent; 910 911 if (!hdoc.isFrameDocument()) { 912 linkEvent = new HyperlinkEvent( 913 html, HyperlinkEvent.EventType.ACTIVATED, u, href, 914 element, mouseEvent); 915 } else { 916 String target = (anchor != null) ? 917 (String)anchor.getAttribute(HTML.Attribute.TARGET) : null; 918 if ((target == null) || (target.equals(""))) { 919 target = hdoc.getBaseTarget(); 920 } 921 if ((target == null) || (target.equals(""))) { 922 target = "_self"; 923 } 924 linkEvent = new HTMLFrameHyperlinkEvent( 925 html, HyperlinkEvent.EventType.ACTIVATED, u, href, 926 element, mouseEvent, target); 927 } 928 return linkEvent; 929 } 930 931 void fireEvents(JEditorPane editor, HTMLDocument doc, String href, 932 Element lastElem, MouseEvent mouseEvent) { 933 if (this.href != null) { 934 // fire an exited event on the old link 935 URL u; 936 try { 937 u = new URL(doc.getBase(), this.href); 938 } catch (MalformedURLException m) { 939 u = null; 940 } 941 HyperlinkEvent exit = new HyperlinkEvent(editor, 942 HyperlinkEvent.EventType.EXITED, u, this.href, 943 lastElem, mouseEvent); 944 editor.fireHyperlinkUpdate(exit); 945 } 946 if (href != null) { 947 // fire an entered event on the new link 948 URL u; 949 try { 950 u = new URL(doc.getBase(), href); 951 } catch (MalformedURLException m) { 952 u = null; 953 } 954 HyperlinkEvent entered = new HyperlinkEvent(editor, 955 HyperlinkEvent.EventType.ENTERED, 956 u, href, curElem, mouseEvent); 957 editor.fireHyperlinkUpdate(entered); 958 } 959 } 960 } 961 962 /** 963 * Interface to be supported by the parser. This enables 964 * providing a different parser while reusing some of the 965 * implementation provided by this editor kit. 966 */ 967 public static abstract class Parser { 968 /** 969 * Parse the given stream and drive the given callback 970 * with the results of the parse. This method should 971 * be implemented to be thread-safe. 972 */ 973 public abstract void parse(Reader r, ParserCallback cb, boolean ignoreCharSet) throws IOException; 974 975 } 976 977 /** 978 * The result of parsing drives these callback methods. 979 * The open and close actions should be balanced. The 980 * <code>flush</code> method will be the last method 981 * called, to give the receiver a chance to flush any 982 * pending data into the document. 983 * <p>Refer to DocumentParser, the default parser used, for further 984 * information on the contents of the AttributeSets, the positions, and 985 * other info. 986 * 987 * @see javax.swing.text.html.parser.DocumentParser 988 */ 989 public static class ParserCallback { 990 /** 991 * This is passed as an attribute in the attributeset to indicate 992 * the element is implied eg, the string '<>foo<\t>' 993 * contains an implied html element and an implied body element. 994 * 995 * @since 1.3 996 */ 997 public static final Object IMPLIED = "_implied_"; 998 999 1000 public void flush() throws BadLocationException { 1001 } 1002 1003 public void handleText(char[] data, int pos) { 1004 } 1005 1006 public void handleComment(char[] data, int pos) { 1007 } 1008 1009 public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) { 1010 } 1011 1012 public void handleEndTag(HTML.Tag t, int pos) { 1013 } 1014 1015 public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) { 1016 } 1017 1018 public void handleError(String errorMsg, int pos){ 1019 } 1020 1021 /** 1022 * This is invoked after the stream has been parsed, but before 1023 * <code>flush</code>. <code>eol</code> will be one of \n, \r 1024 * or \r\n, which ever is encountered the most in parsing the 1025 * stream. 1026 * 1027 * @since 1.3 1028 */ 1029 public void handleEndOfLineString(String eol) { 1030 } 1031 } 1032 1033 /** 1034 * A factory to build views for HTML. The following 1035 * table describes what this factory will build by 1036 * default. 1037 * 1038 * <table summary="Describes the tag and view created by this factory by default"> 1039 * <tr> 1040 * <th align=left>Tag<th align=left>View created 1041 * </tr><tr> 1042 * <td>HTML.Tag.CONTENT<td>InlineView 1043 * </tr><tr> 1044 * <td>HTML.Tag.IMPLIED<td>javax.swing.text.html.ParagraphView 1045 * </tr><tr> 1046 * <td>HTML.Tag.P<td>javax.swing.text.html.ParagraphView 1047 * </tr><tr> 1048 * <td>HTML.Tag.H1<td>javax.swing.text.html.ParagraphView 1049 * </tr><tr> 1050 * <td>HTML.Tag.H2<td>javax.swing.text.html.ParagraphView 1051 * </tr><tr> 1052 * <td>HTML.Tag.H3<td>javax.swing.text.html.ParagraphView 1053 * </tr><tr> 1054 * <td>HTML.Tag.H4<td>javax.swing.text.html.ParagraphView 1055 * </tr><tr> 1056 * <td>HTML.Tag.H5<td>javax.swing.text.html.ParagraphView 1057 * </tr><tr> 1058 * <td>HTML.Tag.H6<td>javax.swing.text.html.ParagraphView 1059 * </tr><tr> 1060 * <td>HTML.Tag.DT<td>javax.swing.text.html.ParagraphView 1061 * </tr><tr> 1062 * <td>HTML.Tag.MENU<td>ListView 1063 * </tr><tr> 1064 * <td>HTML.Tag.DIR<td>ListView 1065 * </tr><tr> 1066 * <td>HTML.Tag.UL<td>ListView 1067 * </tr><tr> 1068 * <td>HTML.Tag.OL<td>ListView 1069 * </tr><tr> 1070 * <td>HTML.Tag.LI<td>BlockView 1071 * </tr><tr> 1072 * <td>HTML.Tag.DL<td>BlockView 1073 * </tr><tr> 1074 * <td>HTML.Tag.DD<td>BlockView 1075 * </tr><tr> 1076 * <td>HTML.Tag.BODY<td>BlockView 1077 * </tr><tr> 1078 * <td>HTML.Tag.HTML<td>BlockView 1079 * </tr><tr> 1080 * <td>HTML.Tag.CENTER<td>BlockView 1081 * </tr><tr> 1082 * <td>HTML.Tag.DIV<td>BlockView 1083 * </tr><tr> 1084 * <td>HTML.Tag.BLOCKQUOTE<td>BlockView 1085 * </tr><tr> 1086 * <td>HTML.Tag.PRE<td>BlockView 1087 * </tr><tr> 1088 * <td>HTML.Tag.BLOCKQUOTE<td>BlockView 1089 * </tr><tr> 1090 * <td>HTML.Tag.PRE<td>BlockView 1091 * </tr><tr> 1092 * <td>HTML.Tag.IMG<td>ImageView 1093 * </tr><tr> 1094 * <td>HTML.Tag.HR<td>HRuleView 1095 * </tr><tr> 1096 * <td>HTML.Tag.BR<td>BRView 1097 * </tr><tr> 1098 * <td>HTML.Tag.TABLE<td>javax.swing.text.html.TableView 1099 * </tr><tr> 1100 * <td>HTML.Tag.INPUT<td>FormView 1101 * </tr><tr> 1102 * <td>HTML.Tag.SELECT<td>FormView 1103 * </tr><tr> 1104 * <td>HTML.Tag.TEXTAREA<td>FormView 1105 * </tr><tr> 1106 * <td>HTML.Tag.OBJECT<td>ObjectView 1107 * </tr><tr> 1108 * <td>HTML.Tag.FRAMESET<td>FrameSetView 1109 * </tr><tr> 1110 * <td>HTML.Tag.FRAME<td>FrameView 1111 * </tr> 1112 * </table> 1113 */ 1114 public static class HTMLFactory implements ViewFactory { 1115 1116 /** 1117 * Creates a view from an element. 1118 * 1119 * @param elem the element 1120 * @return the view 1121 */ 1122 public View create(Element elem) { 1123 AttributeSet attrs = elem.getAttributes(); 1124 Object elementName = 1125 attrs.getAttribute(AbstractDocument.ElementNameAttribute); 1126 Object o = (elementName != null) ? 1127 null : attrs.getAttribute(StyleConstants.NameAttribute); 1128 if (o instanceof HTML.Tag) { 1129 HTML.Tag kind = (HTML.Tag) o; 1130 if (kind == HTML.Tag.CONTENT) { 1131 return new InlineView(elem); 1132 } else if (kind == HTML.Tag.IMPLIED) { 1133 String ws = (String) elem.getAttributes().getAttribute( 1134 CSS.Attribute.WHITE_SPACE); 1135 if ((ws != null) && ws.equals("pre")) { 1136 return new LineView(elem); 1137 } 1138 return new javax.swing.text.html.ParagraphView(elem); 1139 } else if ((kind == HTML.Tag.P) || 1140 (kind == HTML.Tag.H1) || 1141 (kind == HTML.Tag.H2) || 1142 (kind == HTML.Tag.H3) || 1143 (kind == HTML.Tag.H4) || 1144 (kind == HTML.Tag.H5) || 1145 (kind == HTML.Tag.H6) || 1146 (kind == HTML.Tag.DT)) { 1147 // paragraph 1148 return new javax.swing.text.html.ParagraphView(elem); 1149 } else if ((kind == HTML.Tag.MENU) || 1150 (kind == HTML.Tag.DIR) || 1151 (kind == HTML.Tag.UL) || 1152 (kind == HTML.Tag.OL)) { 1153 return new ListView(elem); 1154 } else if (kind == HTML.Tag.BODY) { 1155 return new BodyBlockView(elem); 1156 } else if (kind == HTML.Tag.HTML) { 1157 return new BlockView(elem, View.Y_AXIS); 1158 } else if ((kind == HTML.Tag.LI) || 1159 (kind == HTML.Tag.CENTER) || 1160 (kind == HTML.Tag.DL) || 1161 (kind == HTML.Tag.DD) || 1162 (kind == HTML.Tag.DIV) || 1163 (kind == HTML.Tag.BLOCKQUOTE) || 1164 (kind == HTML.Tag.PRE) || 1165 (kind == HTML.Tag.FORM)) { 1166 // vertical box 1167 return new BlockView(elem, View.Y_AXIS); 1168 } else if (kind == HTML.Tag.NOFRAMES) { 1169 return new NoFramesView(elem, View.Y_AXIS); 1170 } else if (kind==HTML.Tag.IMG) { 1171 return new ImageView(elem); 1172 } else if (kind == HTML.Tag.ISINDEX) { 1173 return new IsindexView(elem); 1174 } else if (kind == HTML.Tag.HR) { 1175 return new HRuleView(elem); 1176 } else if (kind == HTML.Tag.BR) { 1177 return new BRView(elem); 1178 } else if (kind == HTML.Tag.TABLE) { 1179 return new javax.swing.text.html.TableView(elem); 1180 } else if ((kind == HTML.Tag.INPUT) || 1181 (kind == HTML.Tag.SELECT) || 1182 (kind == HTML.Tag.TEXTAREA)) { 1183 return new FormView(elem); 1184 } else if (kind == HTML.Tag.OBJECT) { 1185 return new ObjectView(elem); 1186 } else if (kind == HTML.Tag.FRAMESET) { 1187 if (elem.getAttributes().isDefined(HTML.Attribute.ROWS)) { 1188 return new FrameSetView(elem, View.Y_AXIS); 1189 } else if (elem.getAttributes().isDefined(HTML.Attribute.COLS)) { 1190 return new FrameSetView(elem, View.X_AXIS); 1191 } 1192 throw new RuntimeException("Can't build a" + kind + ", " + elem + ":" + 1193 "no ROWS or COLS defined."); 1194 } else if (kind == HTML.Tag.FRAME) { 1195 return new FrameView(elem); 1196 } else if (kind instanceof HTML.UnknownTag) { 1197 return new HiddenTagView(elem); 1198 } else if (kind == HTML.Tag.COMMENT) { 1199 return new CommentView(elem); 1200 } else if (kind == HTML.Tag.HEAD) { 1201 // Make the head never visible, and never load its 1202 // children. For Cursor positioning, 1203 // getNextVisualPositionFrom is overriden to always return 1204 // the end offset of the element. 1205 return new BlockView(elem, View.X_AXIS) { 1206 public float getPreferredSpan(int axis) { 1207 return 0; 1208 } 1209 public float getMinimumSpan(int axis) { 1210 return 0; 1211 } 1212 public float getMaximumSpan(int axis) { 1213 return 0; 1214 } 1215 protected void loadChildren(ViewFactory f) { 1216 } 1217 public Shape modelToView(int pos, Shape a, 1218 Position.Bias b) throws BadLocationException { 1219 return a; 1220 } 1221 public int getNextVisualPositionFrom(int pos, 1222 Position.Bias b, Shape a, 1223 int direction, Position.Bias[] biasRet) { 1224 return getElement().getEndOffset(); 1225 } 1226 }; 1227 } else if ((kind == HTML.Tag.TITLE) || 1228 (kind == HTML.Tag.META) || 1229 (kind == HTML.Tag.LINK) || 1230 (kind == HTML.Tag.STYLE) || 1231 (kind == HTML.Tag.SCRIPT) || 1232 (kind == HTML.Tag.AREA) || 1233 (kind == HTML.Tag.MAP) || 1234 (kind == HTML.Tag.PARAM) || 1235 (kind == HTML.Tag.APPLET)) { 1236 return new HiddenTagView(elem); 1237 } 1238 } 1239 // If we get here, it's either an element we don't know about 1240 // or something from StyledDocument that doesn't have a mapping to HTML. 1241 String nm = (elementName != null) ? (String)elementName : 1242 elem.getName(); 1243 if (nm != null) { 1244 if (nm.equals(AbstractDocument.ContentElementName)) { 1245 return new LabelView(elem); 1246 } else if (nm.equals(AbstractDocument.ParagraphElementName)) { 1247 return new ParagraphView(elem); 1248 } else if (nm.equals(AbstractDocument.SectionElementName)) { 1249 return new BoxView(elem, View.Y_AXIS); 1250 } else if (nm.equals(StyleConstants.ComponentElementName)) { 1251 return new ComponentView(elem); 1252 } else if (nm.equals(StyleConstants.IconElementName)) { 1253 return new IconView(elem); 1254 } 1255 } 1256 1257 // default to text display 1258 return new LabelView(elem); 1259 } 1260 1261 static class BodyBlockView extends BlockView implements ComponentListener { 1262 public BodyBlockView(Element elem) { 1263 super(elem,View.Y_AXIS); 1264 } 1265 // reimplement major axis requirements to indicate that the 1266 // block is flexible for the body element... so that it can 1267 // be stretched to fill the background properly. 1268 protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) { 1269 r = super.calculateMajorAxisRequirements(axis, r); 1270 r.maximum = Integer.MAX_VALUE; 1271 return r; 1272 } 1273 1274 protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { 1275 Container container = getContainer(); 1276 Container parentContainer; 1277 if (container != null 1278 && (container instanceof javax.swing.JEditorPane) 1279 && (parentContainer = container.getParent()) != null 1280 && (parentContainer instanceof javax.swing.JViewport)) { 1281 JViewport viewPort = (JViewport)parentContainer; 1282 if (cachedViewPort != null) { 1283 JViewport cachedObject = cachedViewPort.get(); 1284 if (cachedObject != null) { 1285 if (cachedObject != viewPort) { 1286 cachedObject.removeComponentListener(this); 1287 } 1288 } else { 1289 cachedViewPort = null; 1290 } 1291 } 1292 if (cachedViewPort == null) { 1293 viewPort.addComponentListener(this); 1294 cachedViewPort = new WeakReference<JViewport>(viewPort); 1295 } 1296 1297 componentVisibleWidth = viewPort.getExtentSize().width; 1298 if (componentVisibleWidth > 0) { 1299 Insets insets = container.getInsets(); 1300 viewVisibleWidth = componentVisibleWidth - insets.left - getLeftInset(); 1301 //try to use viewVisibleWidth if it is smaller than targetSpan 1302 targetSpan = Math.min(targetSpan, viewVisibleWidth); 1303 } 1304 } else { 1305 if (cachedViewPort != null) { 1306 JViewport cachedObject = cachedViewPort.get(); 1307 if (cachedObject != null) { 1308 cachedObject.removeComponentListener(this); 1309 } 1310 cachedViewPort = null; 1311 } 1312 } 1313 super.layoutMinorAxis(targetSpan, axis, offsets, spans); 1314 } 1315 1316 public void setParent(View parent) { 1317 //if parent == null unregister component listener 1318 if (parent == null) { 1319 if (cachedViewPort != null) { 1320 Object cachedObject; 1321 if ((cachedObject = cachedViewPort.get()) != null) { 1322 ((JComponent)cachedObject).removeComponentListener(this); 1323 } 1324 cachedViewPort = null; 1325 } 1326 } 1327 super.setParent(parent); 1328 } 1329 1330 public void componentResized(ComponentEvent e) { 1331 if ( !(e.getSource() instanceof JViewport) ) { 1332 return; 1333 } 1334 JViewport viewPort = (JViewport)e.getSource(); 1335 if (componentVisibleWidth != viewPort.getExtentSize().width) { 1336 Document doc = getDocument(); 1337 if (doc instanceof AbstractDocument) { 1338 AbstractDocument document = (AbstractDocument)getDocument(); 1339 document.readLock(); 1340 try { 1341 layoutChanged(X_AXIS); 1342 preferenceChanged(null, true, true); 1343 } finally { 1344 document.readUnlock(); 1345 } 1346 1347 } 1348 } 1349 } 1350 public void componentHidden(ComponentEvent e) { 1351 } 1352 public void componentMoved(ComponentEvent e) { 1353 } 1354 public void componentShown(ComponentEvent e) { 1355 } 1356 /* 1357 * we keep weak reference to viewPort if and only if BodyBoxView is listening for ComponentEvents 1358 * only in that case cachedViewPort is not equal to null. 1359 * we need to keep this reference in order to remove BodyBoxView from viewPort listeners. 1360 * 1361 */ 1362 private Reference<JViewport> cachedViewPort = null; 1363 private boolean isListening = false; 1364 private int viewVisibleWidth = Integer.MAX_VALUE; 1365 private int componentVisibleWidth = Integer.MAX_VALUE; 1366 } 1367 1368 } 1369 1370 // --- Action implementations ------------------------------ 1371 1372 /** The bold action identifier 1373 */ 1374 public static final String BOLD_ACTION = "html-bold-action"; 1375 /** The italic action identifier 1376 */ 1377 public static final String ITALIC_ACTION = "html-italic-action"; 1378 /** The paragraph left indent action identifier 1379 */ 1380 public static final String PARA_INDENT_LEFT = "html-para-indent-left"; 1381 /** The paragraph right indent action identifier 1382 */ 1383 public static final String PARA_INDENT_RIGHT = "html-para-indent-right"; 1384 /** The font size increase to next value action identifier 1385 */ 1386 public static final String FONT_CHANGE_BIGGER = "html-font-bigger"; 1387 /** The font size decrease to next value action identifier 1388 */ 1389 public static final String FONT_CHANGE_SMALLER = "html-font-smaller"; 1390 /** The Color choice action identifier 1391 The color is passed as an argument 1392 */ 1393 public static final String COLOR_ACTION = "html-color-action"; 1394 /** The logical style choice action identifier 1395 The logical style is passed in as an argument 1396 */ 1397 public static final String LOGICAL_STYLE_ACTION = "html-logical-style-action"; 1398 /** 1399 * Align images at the top. 1400 */ 1401 public static final String IMG_ALIGN_TOP = "html-image-align-top"; 1402 1403 /** 1404 * Align images in the middle. 1405 */ 1406 public static final String IMG_ALIGN_MIDDLE = "html-image-align-middle"; 1407 1408 /** 1409 * Align images at the bottom. 1410 */ 1411 public static final String IMG_ALIGN_BOTTOM = "html-image-align-bottom"; 1412 1413 /** 1414 * Align images at the border. 1415 */ 1416 public static final String IMG_BORDER = "html-image-border"; 1417 1418 1419 /** HTML used when inserting tables. */ 1420 private static final String INSERT_TABLE_HTML = "<table border=1><tr><td></td></tr></table>"; 1421 1422 /** HTML used when inserting unordered lists. */ 1423 private static final String INSERT_UL_HTML = "<ul><li></li></ul>"; 1424 1425 /** HTML used when inserting ordered lists. */ 1426 private static final String INSERT_OL_HTML = "<ol><li></li></ol>"; 1427 1428 /** HTML used when inserting hr. */ 1429 private static final String INSERT_HR_HTML = "<hr>"; 1430 1431 /** HTML used when inserting pre. */ 1432 private static final String INSERT_PRE_HTML = "<pre></pre>"; 1433 1434 private static final NavigateLinkAction nextLinkAction = 1435 new NavigateLinkAction("next-link-action"); 1436 1437 private static final NavigateLinkAction previousLinkAction = 1438 new NavigateLinkAction("previous-link-action"); 1439 1440 private static final ActivateLinkAction activateLinkAction = 1441 new ActivateLinkAction("activate-link-action"); 1442 1443 private static final Action[] defaultActions = { 1444 new InsertHTMLTextAction("InsertTable", INSERT_TABLE_HTML, 1445 HTML.Tag.BODY, HTML.Tag.TABLE), 1446 new InsertHTMLTextAction("InsertTableRow", INSERT_TABLE_HTML, 1447 HTML.Tag.TABLE, HTML.Tag.TR, 1448 HTML.Tag.BODY, HTML.Tag.TABLE), 1449 new InsertHTMLTextAction("InsertTableDataCell", INSERT_TABLE_HTML, 1450 HTML.Tag.TR, HTML.Tag.TD, 1451 HTML.Tag.BODY, HTML.Tag.TABLE), 1452 new InsertHTMLTextAction("InsertUnorderedList", INSERT_UL_HTML, 1453 HTML.Tag.BODY, HTML.Tag.UL), 1454 new InsertHTMLTextAction("InsertUnorderedListItem", INSERT_UL_HTML, 1455 HTML.Tag.UL, HTML.Tag.LI, 1456 HTML.Tag.BODY, HTML.Tag.UL), 1457 new InsertHTMLTextAction("InsertOrderedList", INSERT_OL_HTML, 1458 HTML.Tag.BODY, HTML.Tag.OL), 1459 new InsertHTMLTextAction("InsertOrderedListItem", INSERT_OL_HTML, 1460 HTML.Tag.OL, HTML.Tag.LI, 1461 HTML.Tag.BODY, HTML.Tag.OL), 1462 new InsertHRAction(), 1463 new InsertHTMLTextAction("InsertPre", INSERT_PRE_HTML, 1464 HTML.Tag.BODY, HTML.Tag.PRE), 1465 nextLinkAction, previousLinkAction, activateLinkAction, 1466 1467 new BeginAction(beginAction, false), 1468 new BeginAction(selectionBeginAction, true) 1469 }; 1470 1471 // link navigation support 1472 private boolean foundLink = false; 1473 private int prevHypertextOffset = -1; 1474 private Object linkNavigationTag; 1475 1476 1477 /** 1478 * An abstract Action providing some convenience methods that may 1479 * be useful in inserting HTML into an existing document. 1480 * <p>NOTE: None of the convenience methods obtain a lock on the 1481 * document. If you have another thread modifying the text these 1482 * methods may have inconsistent behavior, or return the wrong thing. 1483 */ 1484 public static abstract class HTMLTextAction extends StyledTextAction { 1485 public HTMLTextAction(String name) { 1486 super(name); 1487 } 1488 1489 /** 1490 * @return HTMLDocument of <code>e</code>. 1491 */ 1492 protected HTMLDocument getHTMLDocument(JEditorPane e) { 1493 Document d = e.getDocument(); 1494 if (d instanceof HTMLDocument) { 1495 return (HTMLDocument) d; 1496 } 1497 throw new IllegalArgumentException("document must be HTMLDocument"); 1498 } 1499 1500 /** 1501 * @return HTMLEditorKit for <code>e</code>. 1502 */ 1503 protected HTMLEditorKit getHTMLEditorKit(JEditorPane e) { 1504 EditorKit k = e.getEditorKit(); 1505 if (k instanceof HTMLEditorKit) { 1506 return (HTMLEditorKit) k; 1507 } 1508 throw new IllegalArgumentException("EditorKit must be HTMLEditorKit"); 1509 } 1510 1511 /** 1512 * Returns an array of the Elements that contain <code>offset</code>. 1513 * The first elements corresponds to the root. 1514 */ 1515 protected Element[] getElementsAt(HTMLDocument doc, int offset) { 1516 return getElementsAt(doc.getDefaultRootElement(), offset, 0); 1517 } 1518 1519 /** 1520 * Recursive method used by getElementsAt. 1521 */ 1522 private Element[] getElementsAt(Element parent, int offset, 1523 int depth) { 1524 if (parent.isLeaf()) { 1525 Element[] retValue = new Element[depth + 1]; 1526 retValue[depth] = parent; 1527 return retValue; 1528 } 1529 Element[] retValue = getElementsAt(parent.getElement 1530 (parent.getElementIndex(offset)), offset, depth + 1); 1531 retValue[depth] = parent; 1532 return retValue; 1533 } 1534 1535 /** 1536 * Returns number of elements, starting at the deepest leaf, needed 1537 * to get to an element representing <code>tag</code>. This will 1538 * return -1 if no elements is found representing <code>tag</code>, 1539 * or 0 if the parent of the leaf at <code>offset</code> represents 1540 * <code>tag</code>. 1541 */ 1542 protected int elementCountToTag(HTMLDocument doc, int offset, 1543 HTML.Tag tag) { 1544 int depth = -1; 1545 Element e = doc.getCharacterElement(offset); 1546 while (e != null && e.getAttributes().getAttribute 1547 (StyleConstants.NameAttribute) != tag) { 1548 e = e.getParentElement(); 1549 depth++; 1550 } 1551 if (e == null) { 1552 return -1; 1553 } 1554 return depth; 1555 } 1556 1557 /** 1558 * Returns the deepest element at <code>offset</code> matching 1559 * <code>tag</code>. 1560 */ 1561 protected Element findElementMatchingTag(HTMLDocument doc, int offset, 1562 HTML.Tag tag) { 1563 Element e = doc.getDefaultRootElement(); 1564 Element lastMatch = null; 1565 while (e != null) { 1566 if (e.getAttributes().getAttribute 1567 (StyleConstants.NameAttribute) == tag) { 1568 lastMatch = e; 1569 } 1570 e = e.getElement(e.getElementIndex(offset)); 1571 } 1572 return lastMatch; 1573 } 1574 } 1575 1576 1577 /** 1578 * InsertHTMLTextAction can be used to insert an arbitrary string of HTML 1579 * into an existing HTML document. At least two HTML.Tags need to be 1580 * supplied. The first Tag, parentTag, identifies the parent in 1581 * the document to add the elements to. The second tag, addTag, 1582 * identifies the first tag that should be added to the document as 1583 * seen in the HTML string. One important thing to remember, is that 1584 * the parser is going to generate all the appropriate tags, even if 1585 * they aren't in the HTML string passed in.<p> 1586 * For example, lets say you wanted to create an action to insert 1587 * a table into the body. The parentTag would be HTML.Tag.BODY, 1588 * addTag would be HTML.Tag.TABLE, and the string could be something 1589 * like <table><tr><td></td></tr></table>. 1590 * <p>There is also an option to supply an alternate parentTag and 1591 * addTag. These will be checked for if there is no parentTag at 1592 * offset. 1593 */ 1594 public static class InsertHTMLTextAction extends HTMLTextAction { 1595 public InsertHTMLTextAction(String name, String html, 1596 HTML.Tag parentTag, HTML.Tag addTag) { 1597 this(name, html, parentTag, addTag, null, null); 1598 } 1599 1600 public InsertHTMLTextAction(String name, String html, 1601 HTML.Tag parentTag, 1602 HTML.Tag addTag, 1603 HTML.Tag alternateParentTag, 1604 HTML.Tag alternateAddTag) { 1605 this(name, html, parentTag, addTag, alternateParentTag, 1606 alternateAddTag, true); 1607 } 1608 1609 /* public */ 1610 InsertHTMLTextAction(String name, String html, 1611 HTML.Tag parentTag, 1612 HTML.Tag addTag, 1613 HTML.Tag alternateParentTag, 1614 HTML.Tag alternateAddTag, 1615 boolean adjustSelection) { 1616 super(name); 1617 this.html = html; 1618 this.parentTag = parentTag; 1619 this.addTag = addTag; 1620 this.alternateParentTag = alternateParentTag; 1621 this.alternateAddTag = alternateAddTag; 1622 this.adjustSelection = adjustSelection; 1623 } 1624 1625 /** 1626 * A cover for HTMLEditorKit.insertHTML. If an exception it 1627 * thrown it is wrapped in a RuntimeException and thrown. 1628 */ 1629 protected void insertHTML(JEditorPane editor, HTMLDocument doc, 1630 int offset, String html, int popDepth, 1631 int pushDepth, HTML.Tag addTag) { 1632 try { 1633 getHTMLEditorKit(editor).insertHTML(doc, offset, html, 1634 popDepth, pushDepth, 1635 addTag); 1636 } catch (IOException ioe) { 1637 throw new RuntimeException("Unable to insert: " + ioe); 1638 } catch (BadLocationException ble) { 1639 throw new RuntimeException("Unable to insert: " + ble); 1640 } 1641 } 1642 1643 /** 1644 * This is invoked when inserting at a boundary. It determines 1645 * the number of pops, and then the number of pushes that need 1646 * to be performed, and then invokes insertHTML. 1647 * @since 1.3 1648 */ 1649 protected void insertAtBoundary(JEditorPane editor, HTMLDocument doc, 1650 int offset, Element insertElement, 1651 String html, HTML.Tag parentTag, 1652 HTML.Tag addTag) { 1653 insertAtBoundry(editor, doc, offset, insertElement, html, 1654 parentTag, addTag); 1655 } 1656 1657 /** 1658 * This is invoked when inserting at a boundary. It determines 1659 * the number of pops, and then the number of pushes that need 1660 * to be performed, and then invokes insertHTML. 1661 * @deprecated As of Java 2 platform v1.3, use insertAtBoundary 1662 */ 1663 @Deprecated 1664 protected void insertAtBoundry(JEditorPane editor, HTMLDocument doc, 1665 int offset, Element insertElement, 1666 String html, HTML.Tag parentTag, 1667 HTML.Tag addTag) { 1668 // Find the common parent. 1669 Element e; 1670 Element commonParent; 1671 boolean isFirst = (offset == 0); 1672 1673 if (offset > 0 || insertElement == null) { 1674 e = doc.getDefaultRootElement(); 1675 while (e != null && e.getStartOffset() != offset && 1676 !e.isLeaf()) { 1677 e = e.getElement(e.getElementIndex(offset)); 1678 } 1679 commonParent = (e != null) ? e.getParentElement() : null; 1680 } 1681 else { 1682 // If inserting at the origin, the common parent is the 1683 // insertElement. 1684 commonParent = insertElement; 1685 } 1686 if (commonParent != null) { 1687 // Determine how many pops to do. 1688 int pops = 0; 1689 int pushes = 0; 1690 if (isFirst && insertElement != null) { 1691 e = commonParent; 1692 while (e != null && !e.isLeaf()) { 1693 e = e.getElement(e.getElementIndex(offset)); 1694 pops++; 1695 } 1696 } 1697 else { 1698 e = commonParent; 1699 offset--; 1700 while (e != null && !e.isLeaf()) { 1701 e = e.getElement(e.getElementIndex(offset)); 1702 pops++; 1703 } 1704 1705 // And how many pushes 1706 e = commonParent; 1707 offset++; 1708 while (e != null && e != insertElement) { 1709 e = e.getElement(e.getElementIndex(offset)); 1710 pushes++; 1711 } 1712 } 1713 pops = Math.max(0, pops - 1); 1714 1715 // And insert! 1716 insertHTML(editor, doc, offset, html, pops, pushes, addTag); 1717 } 1718 } 1719 1720 /** 1721 * If there is an Element with name <code>tag</code> at 1722 * <code>offset</code>, this will invoke either insertAtBoundary 1723 * or <code>insertHTML</code>. This returns true if there is 1724 * a match, and one of the inserts is invoked. 1725 */ 1726 /*protected*/ 1727 boolean insertIntoTag(JEditorPane editor, HTMLDocument doc, 1728 int offset, HTML.Tag tag, HTML.Tag addTag) { 1729 Element e = findElementMatchingTag(doc, offset, tag); 1730 if (e != null && e.getStartOffset() == offset) { 1731 insertAtBoundary(editor, doc, offset, e, html, 1732 tag, addTag); 1733 return true; 1734 } 1735 else if (offset > 0) { 1736 int depth = elementCountToTag(doc, offset - 1, tag); 1737 if (depth != -1) { 1738 insertHTML(editor, doc, offset, html, depth, 0, addTag); 1739 return true; 1740 } 1741 } 1742 return false; 1743 } 1744 1745 /** 1746 * Called after an insertion to adjust the selection. 1747 */ 1748 /* protected */ 1749 void adjustSelection(JEditorPane pane, HTMLDocument doc, 1750 int startOffset, int oldLength) { 1751 int newLength = doc.getLength(); 1752 if (newLength != oldLength && startOffset < newLength) { 1753 if (startOffset > 0) { 1754 String text; 1755 try { 1756 text = doc.getText(startOffset - 1, 1); 1757 } catch (BadLocationException ble) { 1758 text = null; 1759 } 1760 if (text != null && text.length() > 0 && 1761 text.charAt(0) == '\n') { 1762 pane.select(startOffset, startOffset); 1763 } 1764 else { 1765 pane.select(startOffset + 1, startOffset + 1); 1766 } 1767 } 1768 else { 1769 pane.select(1, 1); 1770 } 1771 } 1772 } 1773 1774 /** 1775 * Inserts the HTML into the document. 1776 * 1777 * @param ae the event 1778 */ 1779 public void actionPerformed(ActionEvent ae) { 1780 JEditorPane editor = getEditor(ae); 1781 if (editor != null) { 1782 HTMLDocument doc = getHTMLDocument(editor); 1783 int offset = editor.getSelectionStart(); 1784 int length = doc.getLength(); 1785 boolean inserted; 1786 // Try first choice 1787 if (!insertIntoTag(editor, doc, offset, parentTag, addTag) && 1788 alternateParentTag != null) { 1789 // Then alternate. 1790 inserted = insertIntoTag(editor, doc, offset, 1791 alternateParentTag, 1792 alternateAddTag); 1793 } 1794 else { 1795 inserted = true; 1796 } 1797 if (adjustSelection && inserted) { 1798 adjustSelection(editor, doc, offset, length); 1799 } 1800 } 1801 } 1802 1803 /** HTML to insert. */ 1804 protected String html; 1805 /** Tag to check for in the document. */ 1806 protected HTML.Tag parentTag; 1807 /** Tag in HTML to start adding tags from. */ 1808 protected HTML.Tag addTag; 1809 /** Alternate Tag to check for in the document if parentTag is 1810 * not found. */ 1811 protected HTML.Tag alternateParentTag; 1812 /** Alternate tag in HTML to start adding tags from if parentTag 1813 * is not found and alternateParentTag is found. */ 1814 protected HTML.Tag alternateAddTag; 1815 /** True indicates the selection should be adjusted after an insert. */ 1816 boolean adjustSelection; 1817 } 1818 1819 1820 /** 1821 * InsertHRAction is special, at actionPerformed time it will determine 1822 * the parent HTML.Tag based on the paragraph element at the selection 1823 * start. 1824 */ 1825 static class InsertHRAction extends InsertHTMLTextAction { 1826 InsertHRAction() { 1827 super("InsertHR", "<hr>", null, HTML.Tag.IMPLIED, null, null, 1828 false); 1829 } 1830 1831 /** 1832 * Inserts the HTML into the document. 1833 * 1834 * @param ae the event 1835 */ 1836 public void actionPerformed(ActionEvent ae) { 1837 JEditorPane editor = getEditor(ae); 1838 if (editor != null) { 1839 HTMLDocument doc = getHTMLDocument(editor); 1840 int offset = editor.getSelectionStart(); 1841 Element paragraph = doc.getParagraphElement(offset); 1842 if (paragraph.getParentElement() != null) { 1843 parentTag = (HTML.Tag)paragraph.getParentElement(). 1844 getAttributes().getAttribute 1845 (StyleConstants.NameAttribute); 1846 super.actionPerformed(ae); 1847 } 1848 } 1849 } 1850 1851 } 1852 1853 /* 1854 * Returns the object in an AttributeSet matching a key 1855 */ 1856 static private Object getAttrValue(AttributeSet attr, HTML.Attribute key) { 1857 Enumeration names = attr.getAttributeNames(); 1858 while (names.hasMoreElements()) { 1859 Object nextKey = names.nextElement(); 1860 Object nextVal = attr.getAttribute(nextKey); 1861 if (nextVal instanceof AttributeSet) { 1862 Object value = getAttrValue((AttributeSet)nextVal, key); 1863 if (value != null) { 1864 return value; 1865 } 1866 } else if (nextKey == key) { 1867 return nextVal; 1868 } 1869 } 1870 return null; 1871 } 1872 1873 /* 1874 * Action to move the focus on the next or previous hypertext link 1875 * or object. TODO: This method relies on support from the 1876 * javax.accessibility package. The text package should support 1877 * keyboard navigation of text elements directly. 1878 */ 1879 static class NavigateLinkAction extends TextAction implements CaretListener { 1880 1881 private static final FocusHighlightPainter focusPainter = 1882 new FocusHighlightPainter(null); 1883 private final boolean focusBack; 1884 1885 /* 1886 * Create this action with the appropriate identifier. 1887 */ 1888 public NavigateLinkAction(String actionName) { 1889 super(actionName); 1890 focusBack = "previous-link-action".equals(actionName); 1891 } 1892 1893 /** 1894 * Called when the caret position is updated. 1895 * 1896 * @param e the caret event 1897 */ 1898 public void caretUpdate(CaretEvent e) { 1899 Object src = e.getSource(); 1900 if (src instanceof JTextComponent) { 1901 JTextComponent comp = (JTextComponent) src; 1902 HTMLEditorKit kit = getHTMLEditorKit(comp); 1903 if (kit != null && kit.foundLink) { 1904 kit.foundLink = false; 1905 // TODO: The AccessibleContext for the editor should register 1906 // as a listener for CaretEvents and forward the events to 1907 // assistive technologies listening for such events. 1908 comp.getAccessibleContext().firePropertyChange( 1909 AccessibleContext.ACCESSIBLE_HYPERTEXT_OFFSET, 1910 Integer.valueOf(kit.prevHypertextOffset), 1911 Integer.valueOf(e.getDot())); 1912 } 1913 } 1914 } 1915 1916 /* 1917 * The operation to perform when this action is triggered. 1918 */ 1919 public void actionPerformed(ActionEvent e) { 1920 JTextComponent comp = getTextComponent(e); 1921 if (comp == null || comp.isEditable()) { 1922 return; 1923 } 1924 1925 Document doc = comp.getDocument(); 1926 HTMLEditorKit kit = getHTMLEditorKit(comp); 1927 if (doc == null || kit == null) { 1928 return; 1929 } 1930 1931 // TODO: Should start successive iterations from the 1932 // current caret position. 1933 ElementIterator ei = new ElementIterator(doc); 1934 int currentOffset = comp.getCaretPosition(); 1935 int prevStartOffset = -1; 1936 int prevEndOffset = -1; 1937 1938 // highlight the next link or object after the current caret position 1939 Element nextElement; 1940 while ((nextElement = ei.next()) != null) { 1941 String name = nextElement.getName(); 1942 AttributeSet attr = nextElement.getAttributes(); 1943 1944 Object href = getAttrValue(attr, HTML.Attribute.HREF); 1945 if (!(name.equals(HTML.Tag.OBJECT.toString())) && href == null) { 1946 continue; 1947 } 1948 1949 int elementOffset = nextElement.getStartOffset(); 1950 if (focusBack) { 1951 if (elementOffset >= currentOffset && 1952 prevStartOffset >= 0) { 1953 1954 kit.foundLink = true; 1955 comp.setCaretPosition(prevStartOffset); 1956 moveCaretPosition(comp, kit, prevStartOffset, 1957 prevEndOffset); 1958 kit.prevHypertextOffset = prevStartOffset; 1959 return; 1960 } 1961 } else { // focus forward 1962 if (elementOffset > currentOffset) { 1963 1964 kit.foundLink = true; 1965 comp.setCaretPosition(elementOffset); 1966 moveCaretPosition(comp, kit, elementOffset, 1967 nextElement.getEndOffset()); 1968 kit.prevHypertextOffset = elementOffset; 1969 return; 1970 } 1971 } 1972 prevStartOffset = nextElement.getStartOffset(); 1973 prevEndOffset = nextElement.getEndOffset(); 1974 } 1975 if (focusBack && prevStartOffset >= 0) { 1976 kit.foundLink = true; 1977 comp.setCaretPosition(prevStartOffset); 1978 moveCaretPosition(comp, kit, prevStartOffset, prevEndOffset); 1979 kit.prevHypertextOffset = prevStartOffset; 1980 } 1981 } 1982 1983 /* 1984 * Moves the caret from mark to dot 1985 */ 1986 private void moveCaretPosition(JTextComponent comp, HTMLEditorKit kit, 1987 int mark, int dot) { 1988 Highlighter h = comp.getHighlighter(); 1989 if (h != null) { 1990 int p0 = Math.min(dot, mark); 1991 int p1 = Math.max(dot, mark); 1992 try { 1993 if (kit.linkNavigationTag != null) { 1994 h.changeHighlight(kit.linkNavigationTag, p0, p1); 1995 } else { 1996 kit.linkNavigationTag = 1997 h.addHighlight(p0, p1, focusPainter); 1998 } 1999 } catch (BadLocationException e) { 2000 } 2001 } 2002 } 2003 2004 private HTMLEditorKit getHTMLEditorKit(JTextComponent comp) { 2005 if (comp instanceof JEditorPane) { 2006 EditorKit kit = ((JEditorPane) comp).getEditorKit(); 2007 if (kit instanceof HTMLEditorKit) { 2008 return (HTMLEditorKit) kit; 2009 } 2010 } 2011 return null; 2012 } 2013 2014 /** 2015 * A highlight painter that draws a one-pixel border around 2016 * the highlighted area. 2017 */ 2018 static class FocusHighlightPainter extends 2019 DefaultHighlighter.DefaultHighlightPainter { 2020 2021 FocusHighlightPainter(Color color) { 2022 super(color); 2023 } 2024 2025 /** 2026 * Paints a portion of a highlight. 2027 * 2028 * @param g the graphics context 2029 * @param offs0 the starting model offset ≥ 0 2030 * @param offs1 the ending model offset ≥ offs1 2031 * @param bounds the bounding box of the view, which is not 2032 * necessarily the region to paint. 2033 * @param c the editor 2034 * @param view View painting for 2035 * @return region in which drawing occurred 2036 */ 2037 public Shape paintLayer(Graphics g, int offs0, int offs1, 2038 Shape bounds, JTextComponent c, View view) { 2039 2040 Color color = getColor(); 2041 2042 if (color == null) { 2043 g.setColor(c.getSelectionColor()); 2044 } 2045 else { 2046 g.setColor(color); 2047 } 2048 if (offs0 == view.getStartOffset() && 2049 offs1 == view.getEndOffset()) { 2050 // Contained in view, can just use bounds. 2051 Rectangle alloc; 2052 if (bounds instanceof Rectangle) { 2053 alloc = (Rectangle)bounds; 2054 } 2055 else { 2056 alloc = bounds.getBounds(); 2057 } 2058 g.drawRect(alloc.x, alloc.y, alloc.width - 1, alloc.height); 2059 return alloc; 2060 } 2061 else { 2062 // Should only render part of View. 2063 try { 2064 // --- determine locations --- 2065 Shape shape = view.modelToView(offs0, Position.Bias.Forward, 2066 offs1,Position.Bias.Backward, 2067 bounds); 2068 Rectangle r = (shape instanceof Rectangle) ? 2069 (Rectangle)shape : shape.getBounds(); 2070 g.drawRect(r.x, r.y, r.width - 1, r.height); 2071 return r; 2072 } catch (BadLocationException e) { 2073 // can't render 2074 } 2075 } 2076 // Only if exception 2077 return null; 2078 } 2079 } 2080 } 2081 2082 /* 2083 * Action to activate the hypertext link that has focus. 2084 * TODO: This method relies on support from the 2085 * javax.accessibility package. The text package should support 2086 * keyboard navigation of text elements directly. 2087 */ 2088 static class ActivateLinkAction extends TextAction { 2089 2090 /** 2091 * Create this action with the appropriate identifier. 2092 */ 2093 public ActivateLinkAction(String actionName) { 2094 super(actionName); 2095 } 2096 2097 /* 2098 * activates the hyperlink at offset 2099 */ 2100 private void activateLink(String href, HTMLDocument doc, 2101 JEditorPane editor, int offset) { 2102 try { 2103 URL page = 2104 (URL)doc.getProperty(Document.StreamDescriptionProperty); 2105 URL url = new URL(page, href); 2106 HyperlinkEvent linkEvent = new HyperlinkEvent 2107 (editor, HyperlinkEvent.EventType. 2108 ACTIVATED, url, url.toExternalForm(), 2109 doc.getCharacterElement(offset)); 2110 editor.fireHyperlinkUpdate(linkEvent); 2111 } catch (MalformedURLException m) { 2112 } 2113 } 2114 2115 /* 2116 * Invokes default action on the object in an element 2117 */ 2118 private void doObjectAction(JEditorPane editor, Element elem) { 2119 View view = getView(editor, elem); 2120 if (view != null && view instanceof ObjectView) { 2121 Component comp = ((ObjectView)view).getComponent(); 2122 if (comp != null && comp instanceof Accessible) { 2123 AccessibleContext ac = comp.getAccessibleContext(); 2124 if (ac != null) { 2125 AccessibleAction aa = ac.getAccessibleAction(); 2126 if (aa != null) { 2127 aa.doAccessibleAction(0); 2128 } 2129 } 2130 } 2131 } 2132 } 2133 2134 /* 2135 * Returns the root view for a document 2136 */ 2137 private View getRootView(JEditorPane editor) { 2138 return editor.getUI().getRootView(editor); 2139 } 2140 2141 /* 2142 * Returns a view associated with an element 2143 */ 2144 private View getView(JEditorPane editor, Element elem) { 2145 Object lock = lock(editor); 2146 try { 2147 View rootView = getRootView(editor); 2148 int start = elem.getStartOffset(); 2149 if (rootView != null) { 2150 return getView(rootView, elem, start); 2151 } 2152 return null; 2153 } finally { 2154 unlock(lock); 2155 } 2156 } 2157 2158 private View getView(View parent, Element elem, int start) { 2159 if (parent.getElement() == elem) { 2160 return parent; 2161 } 2162 int index = parent.getViewIndex(start, Position.Bias.Forward); 2163 2164 if (index != -1 && index < parent.getViewCount()) { 2165 return getView(parent.getView(index), elem, start); 2166 } 2167 return null; 2168 } 2169 2170 /* 2171 * If possible acquires a lock on the Document. If a lock has been 2172 * obtained a key will be retured that should be passed to 2173 * <code>unlock</code>. 2174 */ 2175 private Object lock(JEditorPane editor) { 2176 Document document = editor.getDocument(); 2177 2178 if (document instanceof AbstractDocument) { 2179 ((AbstractDocument)document).readLock(); 2180 return document; 2181 } 2182 return null; 2183 } 2184 2185 /* 2186 * Releases a lock previously obtained via <code>lock</code>. 2187 */ 2188 private void unlock(Object key) { 2189 if (key != null) { 2190 ((AbstractDocument)key).readUnlock(); 2191 } 2192 } 2193 2194 /* 2195 * The operation to perform when this action is triggered. 2196 */ 2197 public void actionPerformed(ActionEvent e) { 2198 2199 JTextComponent c = getTextComponent(e); 2200 if (c.isEditable() || !(c instanceof JEditorPane)) { 2201 return; 2202 } 2203 JEditorPane editor = (JEditorPane)c; 2204 2205 Document d = editor.getDocument(); 2206 if (d == null || !(d instanceof HTMLDocument)) { 2207 return; 2208 } 2209 HTMLDocument doc = (HTMLDocument)d; 2210 2211 ElementIterator ei = new ElementIterator(doc); 2212 int currentOffset = editor.getCaretPosition(); 2213 2214 // invoke the next link or object action 2215 String urlString = null; 2216 String objString = null; 2217 Element currentElement; 2218 while ((currentElement = ei.next()) != null) { 2219 String name = currentElement.getName(); 2220 AttributeSet attr = currentElement.getAttributes(); 2221 2222 Object href = getAttrValue(attr, HTML.Attribute.HREF); 2223 if (href != null) { 2224 if (currentOffset >= currentElement.getStartOffset() && 2225 currentOffset <= currentElement.getEndOffset()) { 2226 2227 activateLink((String)href, doc, editor, currentOffset); 2228 return; 2229 } 2230 } else if (name.equals(HTML.Tag.OBJECT.toString())) { 2231 Object obj = getAttrValue(attr, HTML.Attribute.CLASSID); 2232 if (obj != null) { 2233 if (currentOffset >= currentElement.getStartOffset() && 2234 currentOffset <= currentElement.getEndOffset()) { 2235 2236 doObjectAction(editor, currentElement); 2237 return; 2238 } 2239 } 2240 } 2241 } 2242 } 2243 } 2244 2245 private static int getBodyElementStart(JTextComponent comp) { 2246 Element rootElement = comp.getDocument().getRootElements()[0]; 2247 for (int i = 0; i < rootElement.getElementCount(); i++) { 2248 Element currElement = rootElement.getElement(i); 2249 if("body".equals(currElement.getName())) { 2250 return currElement.getStartOffset(); 2251 } 2252 } 2253 return 0; 2254 } 2255 2256 /* 2257 * Move the caret to the beginning of the document. 2258 * @see DefaultEditorKit#beginAction 2259 * @see HTMLEditorKit#getActions 2260 */ 2261 2262 static class BeginAction extends TextAction { 2263 2264 /* Create this object with the appropriate identifier. */ 2265 BeginAction(String nm, boolean select) { 2266 super(nm); 2267 this.select = select; 2268 } 2269 2270 /** The operation to perform when this action is triggered. */ 2271 public void actionPerformed(ActionEvent e) { 2272 JTextComponent target = getTextComponent(e); 2273 int bodyStart = getBodyElementStart(target); 2274 2275 if (target != null) { 2276 if (select) { 2277 target.moveCaretPosition(bodyStart); 2278 } else { 2279 target.setCaretPosition(bodyStart); 2280 } 2281 } 2282 } 2283 2284 private boolean select; 2285 } 2286 } 2287