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