/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.util.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.StringTokenizer;
import javax.accessibility.Accessible;
import javax.accessibility.AccessibleAction;
import javax.accessibility.AccessibleContext;
import javax.swing.Action;
import javax.swing.JEditorPane;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.HyperlinkEvent;
import javax.swing.plaf.TextUI;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.Element;
import javax.swing.text.ElementIterator;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.Position;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.TextAction;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTML.Tag;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLEditorKit.HTMLFactory;
import javax.swing.text.html.HTMLEditorKit.ParserCallback;
import javax.swing.text.html.HTMLFrameHyperlinkEvent;
import javax.swing.text.html.HTMLWriter;
import javax.swing.text.html.ImageView;
import javax.swing.text.html.InlineView;
import javax.swing.text.html.MinimalHTMLWriter;
import javax.swing.text.html.ObjectView;
import javax.swing.text.html.StyleSheet;
import com.servoy.j2db.IApplication;
import com.servoy.j2db.MediaURLStreamHandler;
import com.servoy.j2db.component.ISupportAsyncLoading;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.FixedStyleSheet;
import com.servoy.j2db.util.HtmlUtils;
import com.servoy.j2db.util.Utils;
/**
* @author jblok
*/
public class FixedHTMLEditorKit extends StyledEditorKit implements ISupportAsyncLoading
{
private final IApplication application;
static class BasicHTMLViewFactory extends HTMLEditorKit.HTMLFactory
{
private final MediaURLStreamHandler streamHandler;
public BasicHTMLViewFactory(MediaURLStreamHandler streamHandler)
{
this.streamHandler = streamHandler;
}
@Override
public View create(Element elem)
{
View view = null;
AttributeSet attributes = elem.getAttributes();
if (attributes != null)
{
AttributeSet anchor = (AttributeSet)attributes.getAttribute(HTML.Tag.A);
if (anchor != null)
{
String href = (String)anchor.getAttribute(HTML.Attribute.HREF);
Object o = elem.getAttributes().getAttribute(StyleConstants.NameAttribute);
if (o instanceof HTML.Tag)
{
HTML.Tag kind = (HTML.Tag)o;
if (kind == HTML.Tag.CONTENT) view = new HyperlinkInlineView(elem, href);
else if (kind == HTML.Tag.IMG) view = new HyperlinkImageView(elem, href, streamHandler);
else view = super.create(elem);
}
}
}
if (view == null)
{
Object o = elem.getAttributes().getAttribute(StyleConstants.NameAttribute);
if (o == HTML.Tag.IMG)
{
view = new UrlStreamHandlerImageView(elem, streamHandler);
}
else
{
view = super.create(elem);
}
}
if (view instanceof ImageView)
{
((ImageView)view).setLoadsSynchronously(true);
}
return view;
}
}
public static class BoldAction extends StyledTextAction
{
public BoldAction()
{
super("bold-font");
}
public void actionPerformed(ActionEvent e)
{
JEditorPane editor = getEditor(e);
if (editor != null)
{
StyledEditorKit kit = getStyledEditorKit(editor);
MutableAttributeSet attr = kit.getInputAttributes();
boolean bold = (StyleConstants.isBold(attr)) ? false : true;
SimpleAttributeSet sas = new SimpleAttributeSet();
StyleConstants.setBold(sas, bold);
setCharacterAttributes(editor, sas, false);
// readAsynchronously(editor);
}
}
}
public static class ItalicAction extends StyledTextAction
{
public ItalicAction()
{
super("italic-font");
}
public void actionPerformed(ActionEvent e)
{
JEditorPane editor = getEditor(e);
if (editor != null)
{
StyledEditorKit kit = getStyledEditorKit(editor);
MutableAttributeSet attr = kit.getInputAttributes();
boolean italic = (StyleConstants.isItalic(attr)) ? false : true;
SimpleAttributeSet sas = new SimpleAttributeSet();
StyleConstants.setItalic(sas, italic);
setCharacterAttributes(editor, sas, false);
// readAsynchronously(editor);
}
}
}
public static class UnderlineAction extends StyledTextAction
{
public UnderlineAction()
{
super("underline-font");
}
public void actionPerformed(ActionEvent e)
{
JEditorPane editor = getEditor(e);
if (editor != null)
{
StyledEditorKit kit = getStyledEditorKit(editor);
MutableAttributeSet attr = kit.getInputAttributes();
boolean underline = (StyleConstants.isUnderline(attr)) ? false : true;
SimpleAttributeSet sas = new SimpleAttributeSet();
StyleConstants.setUnderline(sas, underline);
setCharacterAttributes(editor, sas, false);
// readAsynchronously(editor);
}
}
}
static public class HyperlinkInlineView extends InlineView
{
private final String url;
public HyperlinkInlineView(Element elem, String url)
{
super(elem);
this.url = url;
}
@Override
public void paint(Graphics g, Shape a)
{
Object hkey = null;
Graphics2D g2d = null;
try
{
if (g instanceof Graphics2D)
{
g2d = (Graphics2D)g;
Iterator it = g2d.getRenderingHints().keySet().iterator();
while (it.hasNext())
{
Object key = it.next();
if ("HyperLinkKey".equals(key.toString()))
{
hkey = key;
}
}
if (hkey != null) g2d.setRenderingHint((RenderingHints.Key)hkey, url);
}
super.paint(g, a);
}
catch (Exception e)
{
Debug.error(e);
}
finally
{
if (g2d instanceof Graphics2D && hkey != null)
{
g2d.setRenderingHint((RenderingHints.Key)hkey, null);
}
}
}
}
static public class UrlStreamHandlerImageView extends ImageView
{
private final MediaURLStreamHandler streamHandler;
public UrlStreamHandlerImageView(Element elem, MediaURLStreamHandler streamHandler)
{
super(elem);
this.streamHandler = streamHandler;
}
@Override
public URL getImageURL()
{
String src = (String)getElement().getAttributes().getAttribute(HTML.Attribute.SRC);
if (src == null)
{
return null;
}
if (src.startsWith("media://"))
{
URL reference = ((HTMLDocument)getDocument()).getBase();
try
{
return new URL(reference, src, streamHandler);
}
catch (MalformedURLException e)
{
return null;
}
}
return super.getImageURL();
}
}
static public class HyperlinkImageView extends UrlStreamHandlerImageView
{
private final String url;
public HyperlinkImageView(Element elem, String url, MediaURLStreamHandler streamHandler)
{
super(elem, streamHandler);
this.url = url;
}
@Override
public void paint(Graphics g, Shape a)
{
Object hkey = null;
Graphics2D g2d = null;
try
{
if (g instanceof Graphics2D)
{
g2d = (Graphics2D)g;
Iterator it = g2d.getRenderingHints().keySet().iterator();
while (it.hasNext())
{
Object key = it.next();
if ("HyperLinkKey".equals(key.toString()))
{
hkey = key;
}
}
if (hkey != null) g2d.setRenderingHint((RenderingHints.Key)hkey, url);
}
super.paint(g, a);
}
catch (Exception e)
{
Debug.error(e);
}
finally
{
if (g2d instanceof Graphics2D && hkey != null)
{
g2d.setRenderingHint((RenderingHints.Key)hkey, null);
}
}
}
}
/**
* Constructs an HTMLEditorKit, creates a StyleContext, and loads the style sheet.
*/
public FixedHTMLEditorKit(IApplication application)
{
this.application = application;
}
private static NavigateLinkAction nextLinkAction = new NavigateLinkAction("next-link-action"); //$NON-NLS-1$
private static NavigateLinkAction previousLinkAction = new NavigateLinkAction("previous-link-action"); //$NON-NLS-1$
private static ActivateLinkAction activateLinkAction = new ActivateLinkAction("activate-link-action"); //$NON-NLS-1$
private LinkController linkHandler = new LinkController();
/** HTML used when inserting tables. */
private static final String INSERT_TABLE_HTML = "<table border=1><tr><td>#</td></tr></table>"; //$NON-NLS-1$
/** HTML used when inserting unordered lists. */
private static final String INSERT_UL_HTML = "<ul><li>#</li></ul>"; //$NON-NLS-1$
/** HTML used when inserting ordered lists. */
private static final String INSERT_OL_HTML = "<ol><li>#</li></ol>"; //$NON-NLS-1$
/** HTML used when inserting hr. */
private static final String INSERT_HR_HTML = "<hr>"; //$NON-NLS-1$
/** HTML used when inserting pre. */
private static final String INSERT_PRE_HTML = "<pre>#</pre>"; //$NON-NLS-1$
private static final String INSERT_SUB_HTML = "<sub>#</sub>"; //$NON-NLS-1$
private static final String INSERT_SUP_HTML = "<sup>#</sup>"; //$NON-NLS-1$
/** alignment attribute insertion */
private static final String LEFT_ALIGN_ATTRIBUTE = "style=\"text-align:left\""; //$NON-NLS-1$
private static final String CENTER_ALIGN_ATTRIBUTE = "style=\"text-align:center\""; //$NON-NLS-1$
private static final String RIGHT_ALIGN_ATTRIBUTE = "style=\"text-align:right\""; //$NON-NLS-1$
private static final Action[] defaultActions = { new InsertHTMLTextAction("InsertTable", INSERT_TABLE_HTML, HTML.Tag.BODY, HTML.Tag.TABLE), //$NON-NLS-1$
new InsertHTMLTextAction("InsertTableRow", INSERT_TABLE_HTML, HTML.Tag.TABLE, HTML.Tag.TR, HTML.Tag.BODY, HTML.Tag.TABLE), //$NON-NLS-1$
new InsertHTMLTextAction("InsertTableDataCell", INSERT_TABLE_HTML, HTML.Tag.TR, HTML.Tag.TD, HTML.Tag.BODY, HTML.Tag.TABLE), //$NON-NLS-1$
new InsertHTMLTextAction("InsertUnorderedList", INSERT_UL_HTML, HTML.Tag.BODY, HTML.Tag.UL), //$NON-NLS-1$
new InsertHTMLTextAction("InsertUnorderedListItem", INSERT_UL_HTML, HTML.Tag.UL, HTML.Tag.LI, HTML.Tag.BODY, HTML.Tag.UL), //$NON-NLS-1$
new InsertHTMLTextAction("InsertOrderedList", INSERT_OL_HTML, HTML.Tag.BODY, HTML.Tag.OL), //$NON-NLS-1$
new InsertHTMLTextAction("InsertOrderedListItem", INSERT_OL_HTML, HTML.Tag.OL, HTML.Tag.LI, HTML.Tag.BODY, HTML.Tag.OL), //$NON-NLS-1$
new InsertHTMLTextAction("InsertPre", INSERT_PRE_HTML, HTML.Tag.BODY, HTML.Tag.PRE), //$NON-NLS-1$
new InsertHTMLTextAction("InsertSub", INSERT_SUB_HTML, HTML.Tag.BODY, HTML.Tag.SUB), //$NON-NLS-1$
new InsertHTMLTextAction("InsertSup", INSERT_SUP_HTML, HTML.Tag.BODY, HTML.Tag.SUP), //$NON-NLS-1$
new InsertAttributeAction("center-justify", CENTER_ALIGN_ATTRIBUTE), //$NON-NLS-1$
new InsertAttributeAction("left-justify", LEFT_ALIGN_ATTRIBUTE), //$NON-NLS-1$
new InsertAttributeAction("right-justify", RIGHT_ALIGN_ATTRIBUTE), //$NON-NLS-1$
new StyledInsertBreakAction(), new InsertHRAction(), nextLinkAction, previousLinkAction, activateLinkAction, new BoldAction(), new ItalicAction(), new UnderlineAction()
// new ActivateLinkAction("activate-link-action")
};
@Override
public Action[] getActions()
{
return TextAction.augmentList(super.getActions(), FixedHTMLEditorKit.defaultActions);
}
/**
* needed after each operation into the document; after document is updated by different actions, this method must be called
*
* @param editor the editor that holds the (HTML) Document
*/
private static void readAsynchronously(JEditorPane editor)
{
if (editor == null) return;
String content = editor.getText();
int selectionStart = editor.getSelectionStart();
try
{
editor.setText(content);
ByteArrayInputStream bais = new ByteArrayInputStream(content.getBytes());
editor.read(bais, null);
if (selectionStart != -1 && editor.getDocument() != null && editor.getDocument().getLength() > selectionStart)
{
editor.setSelectionStart(selectionStart);
}
}
catch (IOException ioe)
{
throw new RuntimeException("Unable to insert: " + ioe);
}
catch (ClassCastException cce)
{
}
}
/**
* Class to watch the associated component and fire hyperlink events on it when appropriate.
*/
public static class LinkController extends MouseAdapter implements MouseMotionListener, Serializable
{
private Element curElem = null;
/**
* If true, the current element (curElem) represents an image.
*/
private boolean curElemImage = false;
private String href = null;
/**
* This is used by viewToModel to avoid allocing a new array each time.
*/
private final Position.Bias[] bias = new Position.Bias[1];
/**
* Current offset.
*/
private int curOffset;
private Point clickPoint;
// cannot use mouseClicked callback as we must also track mouse events that
// are repost from EditListUI, and that is ignoring reposting mouseClicked events
// so, we try to figure out if it is a click from mousePressed & mouseReleased
@Override
public void mousePressed(MouseEvent e)
{
clickPoint = e.getPoint();
}
@Override
public void mouseReleased(MouseEvent e)
{
// sometimes EditListUI will ignore reposting the mousePressed event, so also
// handle the case when we have no clickPoint
if (clickPoint == null || clickPoint.equals(e.getPoint())) // it is a mouse click
{
handleMouseClick(e);
}
clickPoint = null;
}
@Override
public void mouseExited(MouseEvent e)
{
clickPoint = null;
}
/**
* Called for a mouse click event. If the component is read-only (ie a browser) then the clicked event is used to drive an attempt to follow the
* reference specified by a link.
*
* @param e the mouse event
* @see MouseListener#mouseClicked
*/
private void handleMouseClick(MouseEvent e)
{
JEditorPane editor = (JEditorPane)e.getSource();
if (!editor.isEditable())
{
Point pt = new Point(e.getX(), e.getY());
int pos = editor.viewToModel(pt);
if (pos >= 0)
{
activateLink(pos, editor, e.getX(), e.getY());
}
}
}
// ignore the drags
@Override
public void mouseDragged(MouseEvent e)
{
}
// track the moving of the mouse.
@Override
public void mouseMoved(MouseEvent e)
{
JEditorPane editor = (JEditorPane)e.getSource();
FixedHTMLEditorKit kit = (FixedHTMLEditorKit)editor.getEditorKit();
boolean adjustCursor = true;
Cursor newCursor;
if (!editor.isEditable())
{
newCursor = kit.getDefaultCursor();
Point pt = new Point(e.getX(), e.getY());
int pos = editor.getUI().viewToModel(editor, pt, bias);
if (bias[0] == Position.Bias.Backward && pos > 0)
{
pos--;
}
if (pos >= 0 && (editor.getDocument() instanceof HTMLDocument))
{
HTMLDocument hdoc = (HTMLDocument)editor.getDocument();
Element elem = hdoc.getCharacterElement(pos);
if (!doesElementContainLocation(editor, elem, pos, e.getX(), e.getY()))
{
elem = null;
}
if (curElem != elem || curElemImage)
{
Element lastElem = curElem;
curElem = elem;
String href = null;
curElemImage = false;
if (elem != null)
{
AttributeSet a = elem.getAttributes();
AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A);
if (anchor == null)
{
curElemImage = (a.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.IMG);
if (curElemImage)
{
href = getMapHREF(editor, hdoc, elem, a, pos, e.getX(), e.getY());
}
}
else
{
href = (String)anchor.getAttribute(HTML.Attribute.HREF);
}
}
if (href != this.href)
{
// reference changed, fire event(s)
fireEvents(editor, hdoc, href, lastElem);
this.href = href;
if (href != null)
{
newCursor = kit.getLinkCursor();
}
}
else
{
adjustCursor = false;
}
}
else
{
adjustCursor = false;
}
curOffset = pos;
}
}
else newCursor = kit.getEditCursor();
if (adjustCursor && editor.getCursor() != newCursor)
{
editor.setCursor(newCursor);
}
}
/**
* Returns a string anchor if the passed in element has a USEMAP that contains the passed in location.
*/
private String getMapHREF(JEditorPane html, HTMLDocument hdoc, Element elem, AttributeSet attr, int offset, int x, int y)
{
// Object useMap = attr.getAttribute(HTML.Attribute.USEMAP);
// if (useMap != null && (useMap instanceof String)) {
// Map m = hdoc.getMap((String)useMap);
// if (m != null && offset < hdoc.getLength()) {
// Rectangle bounds;
// TextUI ui = html.getUI();
// try {
// Shape lBounds = ui.modelToView(html, offset,
// Position.Bias.Forward);
// Shape rBounds = ui.modelToView(html, offset + 1,
// Position.Bias.Backward);
// bounds = lBounds.getBounds();
// bounds.add((rBounds instanceof Rectangle) ?
// (Rectangle)rBounds : rBounds.getBounds());
// } catch (BadLocationException ble) {
// bounds = null;
// }
// if (bounds != null) {
// AttributeSet area = m.getArea(x - bounds.x,
// y - bounds.y,
// bounds.width,
// bounds.height);
// if (area != null) {
// return (String)area.getAttribute(HTML.Attribute.
// HREF);
// }
// }
// }
// }
return null;
}
/**
* Returns true if the View representing <code>e</code> contains the location <code>x</code>, <code>y</code>. <code>offset</code> gives the
* offset into the Document to check for.
*/
private boolean doesElementContainLocation(JEditorPane editor, Element e, int offset, int x, int y)
{
if (e != null && offset > 0 && e.getStartOffset() == offset)
{
try
{
TextUI ui = editor.getUI();
Shape s1 = ui.modelToView(editor, offset, Position.Bias.Forward);
Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle)s1 : s1.getBounds();
Shape s2 = ui.modelToView(editor, e.getEndOffset(), Position.Bias.Backward);
Rectangle r2 = (s2 instanceof Rectangle) ? (Rectangle)s2 : s2.getBounds();
r1.add(r2);
return r1.contains(x, y);
}
catch (BadLocationException ble)
{
}
}
return true;
}
/**
* Calls linkActivated on the associated JEditorPane if the given position represents a link.
* <p>
* This is implemented to forward to the method with the same name, but with the following args both == -1.
*
* @param pos the position
* @param html the editor pane
*/
protected void activateLink(int pos, JEditorPane editor)
{
activateLink(pos, editor, -1, -1);
}
/**
* Calls linkActivated on the associated JEditorPane if the given position represents a link. If this was the result of a mouse click, <code>x</code>
* and <code>y</code> will give the location of the mouse, otherwise they will be < 0.
*
* @param pos the position
* @param html the editor pane
*/
void activateLink(int pos, JEditorPane html, int x, int y)
{
Document doc = html.getDocument();
if (doc instanceof HTMLDocument)
{
HTMLDocument hdoc = (HTMLDocument)doc;
Element e = hdoc.getCharacterElement(pos);
AttributeSet a = e.getAttributes();
AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A);
HyperlinkEvent linkEvent = null;
if (anchor == null)
{
href = getMapHREF(html, hdoc, e, a, pos, x, y);
}
else
{
href = (String)anchor.getAttribute(HTML.Attribute.HREF);
}
if (href != null)
{
linkEvent = createHyperlinkEvent(html, hdoc, href, anchor, e);
}
if (linkEvent != null)
{
html.fireHyperlinkUpdate(linkEvent);
}
}
}
/**
* Creates and returns a new instance of HyperlinkEvent. If <code>hdoc</code> is a frame document a HTMLFrameHyperlinkEvent will be created.
*/
HyperlinkEvent createHyperlinkEvent(JEditorPane html, HTMLDocument hdoc, String href, AttributeSet anchor, Element element)
{
URL u;
try
{
URL base = hdoc.getBase();
u = new URL(base, href);
// Following is a workaround for 1.2, in which
// new URL("file://...", "#...") causes the filename to
// be lost.
if (href != null && "file".equals(u.getProtocol()) //$NON-NLS-1$
&& href.startsWith("#"))
{
String baseFile = base.getFile();
String newFile = u.getFile();
if (baseFile != null && newFile != null && !newFile.startsWith(baseFile))
{
u = new URL(base, baseFile + href);
}
}
}
catch (MalformedURLException m)
{
u = null;
}
HyperlinkEvent linkEvent = null;
String target = (anchor != null) ? (String)anchor.getAttribute(HTML.Attribute.TARGET) : null;
if (target == null)//!hdoc.isFrameDocument())
{
linkEvent = new HyperlinkEvent(html, HyperlinkEvent.EventType.ACTIVATED, u, href);//,
// element);
}
else
{
if ((target == null) || (target.equals("")))
{
target = "_self";
}
linkEvent = new HTMLFrameHyperlinkEvent(html, HyperlinkEvent.EventType.ACTIVATED, u, href, element, target);
}
return linkEvent;
}
void fireEvents(JEditorPane editor, HTMLDocument doc, String href, Element lastElem)
{
if (this.href != null)
{
// fire an exited event on the old link
URL u;
try
{
u = new URL(doc.getBase(), this.href);
}
catch (MalformedURLException m)
{
u = null;
}
HyperlinkEvent exit = new HyperlinkEvent(editor, HyperlinkEvent.EventType.EXITED, u);//, this.href,
// lastElem);
editor.fireHyperlinkUpdate(exit);
}
if (href != null)
{
// fire an entered event on the new link
URL u;
try
{
u = new URL(doc.getBase(), href);
}
catch (MalformedURLException m)
{
u = null;
}
HyperlinkEvent entered = new HyperlinkEvent(editor, HyperlinkEvent.EventType.ENTERED, u);//, href, curElem);
editor.fireHyperlinkUpdate(entered);
}
}
}
static class StyledInsertBreakAction extends HTMLTextAction //StyledEditorKit.StyledTextAction
{
StyledInsertBreakAction()
{
super(DefaultEditorKit.insertBreakAction);
}
public void actionPerformed(ActionEvent e)
{
JEditorPane target = getEditor(e);
if (target != null)
{
if ((!target.isEditable()) || (!target.isEnabled()))
{
// target.getToolkit().beep();
return;
}
InsertHTMLTextAction action = null;
int int4 = target.getSelectionStart();
if (e.getModifiers() != ActionEvent.CTRL_MASK)
{
Element[] elems = getElementsAt(getHTMLDocument(target), int4);
for (int i = elems.length - 1; i >= 0; i--)
{
Element elem = elems[i];
if ("ol".equalsIgnoreCase(elem.getName().toLowerCase()))
{
action = new InsertHTMLTextAction("InsertLine", "<li>#<li>", HTML.Tag.OL, HTML.Tag.LI);
break;
}
if ("ul".equalsIgnoreCase(elem.getName().toLowerCase()))
{
action = new InsertHTMLTextAction("InsertLine", "<li>#<li>", HTML.Tag.UL, HTML.Tag.LI);
break;
}
}
}
if (action == null)
{
target.replaceSelection("\n");
target.setSelectionStart(int4);
target.setSelectionEnd((int4 + 1));
SimpleAttributeSet simpleAttributeSet5 = new SimpleAttributeSet();
simpleAttributeSet5.addAttribute(StyleConstants.NameAttribute, HTML.Tag.BR);
setCharacterAttributes((target), (simpleAttributeSet5), false);
target.setSelectionStart((int4 + 1));
// readAsynchronously(target);
}
else
{
action.actionPerformed(e);
}
}
else
{
// See if we are in a JTextComponent.
JTextComponent text = getTextComponent(e);
if (text != null)
{
if ((!text.isEditable()) || (!text.isEnabled()))
{
text.getToolkit().beep();
return;
}
text.replaceSelection("<br>");
}
}
}
}
public static class InsertAttributeAction extends InsertHTMLTextAction
{
//the html attribute code
protected String html;
public InsertAttributeAction(String name, String html)
{
super(name);
this.html = html;
}
/**
* The selected text must be enclosed by an end of tag and a beginning of the next tag. More, the enclosing tags must be tags that support inline
* alignment style
*
* @param selectedText the initial selected text (as it is selected by the user in the htmlarea
* @param wholeText the whole text (including html tags - which aren't visible in the htmlarea)
* @return the new selected text (may contain other tags (like <b>selectedText</b> for example))
*/
protected String correctSelectedText(String selectedText, String wholeText, int startPosition)
{
//unfortunately editor.getSelectedText() doesn't include the formatting tags (like <b>, <i>, etc)
int startIndex = formattedIndexOf(selectedText, wholeText, startPosition);
if (startIndex < 0) return "";
String firstPart = wholeText.substring(0, startIndex);
int offset = startIndex + selectedText.length();
String secondPart = wholeText.substring(offset);
String leftTag = iterateThroughTags(firstPart);
String leftTagStripped = leftTag;
if (leftTag.indexOf(" ") > 0) leftTagStripped = leftTag.substring(0, leftTag.indexOf(" "));
String rightTag = !Utils.stringIsEmpty(leftTag) ? "</" + leftTagStripped.substring(1) : "";
if (!Utils.stringIsEmpty(leftTag) && !Utils.stringIsEmpty(rightTag) && wholeText.indexOf(rightTag) > 0)
{
int startOffset = firstPart.lastIndexOf(leftTag) + leftTag.length() + 1;
int endOffset = wholeText.indexOf(rightTag, startOffset);//offset + secondPart.indexOf(rightTag);
return wholeText.substring(startOffset, endOffset);
}
else return selectedText;
}
/**
* returns the classic indexOf(), but the checking is made on a String which has html tags, while the searched String is plain text this method of
* course works only in this case, where we know that a selected text's (editor.getSelectedText) tokens are included in the html document. Otherwise,
* this method gives only "the most probably" indexOf :)
*
* @param unformattedQuery the searched text
* @param target the String to search in
* @return the position of the unformatted text into the formatted one
*/
protected int formattedIndexOf(String unformattedQuery, String target, int startIndex)
{
if (Utils.stringIsEmpty(unformattedQuery)) return -1;
//simplest case: selected text is not formatted
int offset = target.indexOf(unformattedQuery);
if (offset != -1 && offset == target.lastIndexOf(unformattedQuery))
{
return offset;
}
offset = getFormattedOffset(unformattedQuery, target, offset, startIndex);
if (offset != -1 && offset > startIndex) return offset;
StringTokenizer tokens = new StringTokenizer(unformattedQuery, " ");
String currentToken = tokens.hasMoreTokens() ? tokens.nextToken() : "";
// I remember the first token, in case of an adjustment
String firstToken = currentToken;
int firstTokenPosition = target.indexOf(currentToken, startIndex);
// a limitation here: if you have "som<i>e text </i>" in the editor kit and the selected text is the whole text, then first token will be "some",
// which has the -1 index in the whole (formatted) text
if (firstTokenPosition == -1 || Utils.stringIsEmpty(currentToken)) return -1;
//all the tokens must have a position != -1 and greater than the position of the previous token
int currentPosition = firstTokenPosition;
int oldPosition = firstTokenPosition;
while (tokens.hasMoreTokens())
{
currentPosition = getFormattedOffset(currentToken, target, currentPosition, oldPosition);
if (currentToken.indexOf("") != -1 && (currentPosition == -1 || currentPosition < oldPosition)) return -1;
oldPosition = currentPosition + 1;
currentToken = tokens.nextToken();
}
if (currentPosition > firstTokenPosition)
{
String rawSelection = target.substring(firstTokenPosition, currentPosition);
int relativeIndex = rawSelection.lastIndexOf(firstToken);
if (relativeIndex > firstTokenPosition)
{
firstTokenPosition += relativeIndex;
}
}
return firstTokenPosition;
}
private int getFormattedOffset(String query, String target, int offset, int startIndex)
{
if (offset != -1)
{
int nextIndex = offset;
while (nextIndex < startIndex && nextIndex != -1 && offset < target.length())
{
nextIndex = target.indexOf(query, offset + 1);
if (nextIndex != -1) offset = nextIndex;
}
return offset;
}
return -1;
}
/**
* @param tag
*/
protected String iterateThroughTags(String text)
{
int startTagPosition = text.lastIndexOf("<");
int endTagPosition = text.lastIndexOf(">");
if (startTagPosition < 0 || endTagPosition < 0)
{
return "";
}
if (startTagPosition > endTagPosition)
{
text = text.substring(0, startTagPosition);
if (Utils.stringIsEmpty(text)) return "";
return iterateThroughTags(text);
}
//current tag should be something like <tag (without attributes)
String currentTag = text.substring(startTagPosition, endTagPosition);
String currentTagStripped = "";
if (currentTag.indexOf(" ") > 0) currentTagStripped = currentTag.substring(0, currentTag.indexOf(" "));
else currentTagStripped = currentTag.toLowerCase();
boolean valid = false;
for (int i = 0; i < HtmlUtils.tags.length && !valid; i++)
if (currentTagStripped.equalsIgnoreCase(HtmlUtils.tags[i])) valid = true;
for (int i = 0; i < HtmlUtils.alignNotSupportedTags.length && valid; i++)
if (currentTagStripped.equalsIgnoreCase(HtmlUtils.alignNotSupportedTags[i])) valid = false;
if (valid)
{
return currentTag;
}
else
{
text = startTagPosition > 1 ? text.substring(0, startTagPosition - 1) : "";
return iterateThroughTags(text);
}
}
/**
* Inserts the HTML attribute in the correct tag
*
* @param event the event
*/
@Override
public void actionPerformed(ActionEvent event)
{
JEditorPane editor = getEditor(event);
if (editor != null)
{
HTMLDocument htmlDocument = getHTMLDocument(editor);
String wholeText = editor.getText();
String selectedText = editor.getSelectedText();
selectedText = correctSelectedText(selectedText, wholeText, editor.getSelectionStart());
if (selectedText == null || selectedText.trim().length() == 0) return;
int offset = wholeText.indexOf(selectedText);
if (offset == -1) return;
String firstPart = wholeText.substring(0, offset);
String secondPart = wholeText.substring(offset + selectedText.length(), wholeText.length() - 1);
if (firstPart.lastIndexOf("<") > (firstPart.lastIndexOf(">") + 1)) return;
String wrapperElem = firstPart.substring(firstPart.lastIndexOf("<"), firstPart.lastIndexOf(">") + 1);
firstPart = firstPart.substring(0, firstPart.lastIndexOf(wrapperElem));
wrapperElem = transformWrapperElement(wrapperElem, html);
try
{
String modifiedContent = firstPart + wrapperElem + selectedText + secondPart;
htmlDocument.remove(0, htmlDocument.getLength());
// editor.setText(modifiedContent);
// put this instead of editor.setText(modifiedContent); the setText method calls the read from FixedHtmlEditorKit (but we use the fixedJeditorPane.read() instead
ByteArrayInputStream bais = new ByteArrayInputStream(modifiedContent.getBytes());
editor.read(bais, null);
}
catch (Exception e)
{
Debug.error(e);
}
}
}
/**
* looks at the current wrapper element and inspects the attributes; if the current set attribute overrides the old one, only its value is updated This
* is done in order to avoid duplication of attributes (especially style attributes). If you will want to add other style "sub-attributes", you'll have
* to extend this class and override this method
*
* @param wrapperElement is the element that wraps the selected text
* @param htmlAttribute is the attribute that must be set to the current wrapper element
* @return the updated wrapper element
*/
protected String transformWrapperElement(String wrapperElement, String htmlAttribute)
{
String wrapper = wrapperElement.replaceFirst("<", "");
if (wrapper.length() == 0) return "";
wrapper = wrapper.substring(0, wrapper.length() - 1);
StringTokenizer tokenizer = new StringTokenizer(wrapper, " ");
//for special case: editor automatically makes <td style="align:left"> into <td align="left">
boolean specialCase = false;
try
{
for (String element : HtmlUtils.specialCaseTags)
{
if (wrapper.equalsIgnoreCase(element) || wrapper.substring(0, wrapper.indexOf(" ")).equalsIgnoreCase(element))
{
specialCase = true;
break;
}
}
}
catch (NullPointerException e)
{
specialCase = false;
}
catch (StringIndexOutOfBoundsException e)
{
specialCase = false;
}
String attributeName = htmlAttribute.substring(0, html.indexOf('='));
String attributeValue;
if (specialCase && "style".equalsIgnoreCase(attributeName))
{
attributeName = "align";
attributeValue = htmlAttribute.substring(htmlAttribute.indexOf(":") + 1, htmlAttribute.lastIndexOf("\""));
attributeValue = "\"" + attributeValue + "\"";
}
else
{
attributeValue = htmlAttribute.substring(htmlAttribute.indexOf("\""), htmlAttribute.lastIndexOf("\""));
}
String oldAttributeValue = "";
while (tokenizer.hasMoreTokens())
{
String currentToken = tokenizer.nextToken();
if (currentToken.indexOf("=") > -1)
{
String name = currentToken.substring(0, currentToken.indexOf("="));
if (name.equalsIgnoreCase(attributeName)) oldAttributeValue = currentToken.substring(currentToken.indexOf("=") + 1, currentToken.length());
}
}
if ("".equals(oldAttributeValue.trim()))
{
// if attribute with the same name does not exist, put the new attribute at the "end" of the tag (before ">")
wrapperElement = wrapperElement.substring(0, wrapperElement.indexOf(">")) + " " + htmlAttribute + ">";
return wrapperElement;
}
else
{
//if same attribute exists, its value will be replaced
return wrapperElement.replaceAll(oldAttributeValue, attributeValue);
}
}
}
/**
* InsertHTMLTextAction can be used to insert an arbitrary string of HTML into an existing HTML document. At least two HTML.Tags need to be supplied. The
* first Tag, parentTag, identifies the parent in the document to add the elements to. The second tag, addTag, identifies the first tag that should be added
* to the document as seen in the HTML string. One important thing to remember, is that the parser is going to generate all the appropriate tags, even if
* they aren't in the HTML string passed in.
* <p>
* For example, lets say you wanted to create an action to insert a table into the body. The parentTag would be HTML.Tag.BODY, addTag would be
* HTML.Tag.TABLE, and the string could be something like <table><tr><td></td></tr></table>.
* <p>
* There is also an option to supply an alternate parentTag and addTag. These will be checked for if there is no parentTag at offset.
*/
public static class InsertHTMLTextAction extends HTMLTextAction
{
public InsertHTMLTextAction(String name)
{
super(name);
}
public InsertHTMLTextAction(String name, String html, HTML.Tag parentTag, HTML.Tag addTag)
{
this(name, html, parentTag, addTag, null, null);
}
public InsertHTMLTextAction(String name, String html, HTML.Tag parentTag, HTML.Tag addTag, HTML.Tag alternateParentTag, HTML.Tag alternateAddTag)
{
this(name, html, parentTag, addTag, alternateParentTag, alternateAddTag, true);
}
/* public */
InsertHTMLTextAction(String name, String html, HTML.Tag parentTag, HTML.Tag addTag, HTML.Tag alternateParentTag, HTML.Tag alternateAddTag,
boolean adjustSelection)
{
super(name);
this.html = html;
this.parentTag = parentTag;
this.addTag = addTag;
this.alternateParentTag = alternateParentTag;
this.alternateAddTag = alternateAddTag;
this.adjustSelection = adjustSelection;
}
/**
* A cover for HTMLEditorKit.insertHTML. If an exception it thrown it is wrapped in a RuntimeException and thrown.
*/
protected void insertHTML(JEditorPane editor, HTMLDocument doc, int offset, String html, int popDepth, int pushDepth, HTML.Tag addTag)
{
try
{
// int end = editor.getSelectionEnd();
// int pos = html.indexOf('#');
String sel = ""; //$NON-NLS-1$
if (addTag.isBlock() || addTag == HTML.Tag.SUP || addTag == HTML.Tag.SUB)
{
sel = editor.getSelectedText();
if (sel == null)
{
sel = "";
}
else
{
editor.replaceSelection("");
}
}
html = Utils.stringReplace(html, "#", sel);
getHTMLEditorKit(editor).insertHTML(doc, offset, html, popDepth, pushDepth, addTag);
// editor.setSelectionStart(end+pos+1);
}
catch (IOException ioe)
{
throw new RuntimeException("Unable to insert: " + ioe);
}
catch (BadLocationException ble)
{
throw new RuntimeException("Unable to insert: " + ble);
}
}
/**
* This is invoked when inserting at a boundary. It determines the number of pops, and then the number of pushes that need to be performed, and then
* invokes insertHTML.
*
* @since 1.3
*/
protected void insertAtBoundary(JEditorPane editor, HTMLDocument doc, int offset, Element insertElement, String html, HTML.Tag parentTag,
HTML.Tag addTag)
{
insertAtBoundry(editor, doc, offset, insertElement, html, parentTag, addTag);
}
/**
* This is invoked when inserting at a boundary. It determines the number of pops, and then the number of pushes that need to be performed, and then
* invokes insertHTML.
*
* @deprecated As of Java 2 platform v1.3, use insertAtBoundary
*/
@Deprecated
protected void insertAtBoundry(JEditorPane editor, HTMLDocument doc, int offset, Element insertElement, String html, HTML.Tag parentTag, HTML.Tag addTag)
{
// Find the common parent.
Element e;
Element commonParent;
boolean isFirst = (offset == 0);
if (offset > 0 || insertElement == null)
{
e = doc.getDefaultRootElement();
while (e != null && e.getStartOffset() != offset && !e.isLeaf())
{
e = e.getElement(e.getElementIndex(offset));
}
commonParent = (e != null) ? e.getParentElement() : null;
}
else
{
// If inserting at the origin, the common parent is the
// insertElement.
commonParent = insertElement;
}
if (commonParent != null)
{
// Determine how many pops to do.
int pops = 0;
int pushes = 0;
if (isFirst && insertElement != null)
{
e = commonParent;
while (e != null && !e.isLeaf())
{
e = e.getElement(e.getElementIndex(offset));
pops++;
}
}
else
{
e = commonParent;
offset--;
while (e != null && !e.isLeaf())
{
e = e.getElement(e.getElementIndex(offset));
pops++;
}
// And how many pushes
e = commonParent;
offset++;
while (e != null && e != insertElement)
{
e = e.getElement(e.getElementIndex(offset));
pushes++;
}
}
pops = Math.max(0, pops - 1);
// And insert!
insertHTML(editor, doc, offset, html, pops, pushes, addTag);
}
}
/**
* If there is an Element with name <code>tag</code> at <code>offset</code>, this will invoke either insertAtBoundary or <code>insertHTML</code>.
* This returns true if there is a match, and one of the inserts is invoked.
*/
/* protected */
boolean insertIntoTag(JEditorPane editor, HTMLDocument doc, int offset, HTML.Tag tag, HTML.Tag addTag)
{
Element e = findElementMatchingTag(doc, offset, tag);
if (e != null && e.getStartOffset() == offset)
{
insertAtBoundary(editor, doc, offset, e, html, tag, addTag);
return true;
}
else if (offset > 0)
{
int depth = elementCountToTag(doc, offset - 1, tag);
if (depth != -1)
{
insertHTML(editor, doc, offset, html, depth, 0, addTag);
return true;
}
}
return false;
}
/**
* Called after an insertion to adjust the selection.
*/
/* protected */
void adjustSelection(JEditorPane pane, HTMLDocument doc, int startOffset, int oldLength)
{
int newLength = doc.getLength();
if (newLength != oldLength && startOffset < newLength)
{
if (startOffset > 0)
{
String text;
try
{
text = doc.getText(startOffset - 1, 1);
}
catch (BadLocationException ble)
{
text = null;
}
if (text != null && text.length() > 0 && text.charAt(0) == '\n')
{
pane.select(startOffset, startOffset);
}
else
{
pane.select(startOffset + 1, startOffset + 1);
}
}
else
{
pane.select(1, 1);
}
}
}
/**
* Inserts the HTML into the document.
*
* @param ae the event
*/
public void actionPerformed(ActionEvent ae)
{
JEditorPane editor = getEditor(ae);
if (editor != null)
{
HTMLDocument doc = getHTMLDocument(editor);
int offset = editor.getSelectionStart();
int length = doc.getLength();
boolean inserted;
// Try first choice
if (!insertIntoTag(editor, doc, offset, parentTag, addTag) && alternateParentTag != null)
{
// Then alternate.
inserted = insertIntoTag(editor, doc, offset, alternateParentTag, alternateAddTag);
}
else
{
inserted = true;
}
if (adjustSelection && inserted)
{
// readAsynchronously(editor);
adjustSelection(editor, doc, offset, length);
}
}
}
/** HTML to insert. */
protected String html;
/** Tag to check for in the document. */
protected HTML.Tag parentTag;
/** Tag in HTML to start adding tags from. */
protected HTML.Tag addTag;
/**
* Alternate Tag to check for in the document if parentTag is not found.
*/
protected HTML.Tag alternateParentTag;
/**
* Alternate tag in HTML to start adding tags from if parentTag is not found and alternateParentTag is found.
*/
protected HTML.Tag alternateAddTag;
/** True indicates the selection should be adjusted after an insert. */
boolean adjustSelection;
}
/**
* Get the MIME type of the data that this kit represents support for. This kit supports the type <code>text/html</code>.
*
* @return the type
*/
@Override
public String getContentType()
{
return "text/html";
}
/**
* Fetch a factory that is suitable for producing views of any models that are produced by this kit.
*
* @return the factory
*/
@Override
public ViewFactory getViewFactory()
{
return currentFactory;
}
/**
* Create an uninitialized text storage model that is appropriate for this type of editor.
*
* @return the model
*/
@Override
public Document createDefaultDocument()
{
StyleSheet styles = getStyleSheet();
StyleSheet ss = createStyleSheet();
ss.addStyleSheet(styles);
// make <BR> tags add "\n" into the document and do not add white space "\n" to the document
HTMLDocument doc = new HTMLDocument(ss)
{
@Override
public ParserCallback getReader(int pos)
{
Object desc = getProperty(Document.StreamDescriptionProperty);
if (desc instanceof URL)
{
setBase((URL)desc);
}
HTMLReader reader = new HTMLReader(pos)
{
private boolean insideHead = false;
@Override
protected void addSpecialElement(Tag t, MutableAttributeSet a)
{
int l1 = parseBuffer.size();
super.addSpecialElement(t, a);
if (l1 < parseBuffer.size() && t == HTML.Tag.BR)
{
char[] one = new char[1];
one[0] = '\n';
ElementSpec es = parseBuffer.lastElement();
ElementSpec newEs = new ElementSpec(es.getAttributes().copyAttributes(), ElementSpec.ContentType, one, 0, 1);
parseBuffer.setElementAt(newEs, parseBuffer.size() - 1);
}
}
@Override
protected void blockClose(Tag t)
{
super.blockClose(t);
if (t == HTML.Tag.HEAD)
{
insideHead = false;
}
}
@Override
protected void blockOpen(Tag t, MutableAttributeSet attr)
{
super.blockOpen(t, attr);
if (t == HTML.Tag.HEAD)
{
insideHead = true;
}
}
@Override
protected void addContent(char[] data, int offs, int length, boolean generateImpliedPIfNecessary)
{
if (!insideHead)
{
super.addContent(data, offs, length, generateImpliedPIfNecessary);
}
}
};
return reader;
}
};
doc.setParser(getParser());
doc.setAsynchronousLoadPriority(4);
doc.setTokenThreshold(50);
return doc;
}
/**
* Inserts content from the given stream. If <code>doc</code> is an instance of HTMLDocument, this will read HTML 3.2 text. Inserting HTML into a
* non-empty document must be inside the body Element, if you do not insert into the body an exception will be thrown. When inserting into a non-empty
* document all tags outside of the body (head, title) will be dropped.
*
* @param in the stream to read from
* @param doc the destination for the insertion
* @param pos the location in the document to place the content
* @exception IOException on any I/O error
* @exception BadLocationException if pos represents an invalid location within the document
* @exception RuntimeException (will eventually be a BadLocationException) if pos is invalid
*/
@Override
public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException
{
if (doc instanceof HTMLDocument)
{
HTMLDocument hdoc = (HTMLDocument)doc;
HTMLEditorKit.Parser p = getParser();
if (p == null)
{
throw new IOException("Can't load parser");
}
if (pos > doc.getLength())
{
throw new BadLocationException("Invalid location", pos);
}
synchronized (this)
{
HTMLEditorKit.ParserCallback receiver = hdoc.getReader(pos);
Boolean ignoreCharset = (Boolean)doc.getProperty("IgnoreCharsetDirective"); //$NON-NLS-1$
p.parse(in, receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue());
receiver.flush();
}
}
else
{
super.read(in, doc, pos);
}
}
/**
* Inserts HTML into an existing document.
*
* @param doc the document to insert into
* @param offset the offset to insert HTML at
* @param popDepth the number of ElementSpec.EndTagTypes to generate before inserting
* @param pushDepth the number of ElementSpec.StartTagTypes with a direction of ElementSpec.JoinNextDirection that should be generated before inserting, but
* after the end tags have been generated
* @param insertTag the first tag to start inserting into document
* @exception RuntimeException (will eventually be a BadLocationException) if pos is invalid
*/
public void insertHTML(HTMLDocument doc, int offset, String html, int popDepth, int pushDepth, HTML.Tag insertTag) throws BadLocationException, IOException
{
HTMLEditorKit.Parser p = getParser();
if (p == null)
{
throw new IOException("Can't load parser");
}
if (offset > doc.getLength())
{
throw new BadLocationException("Invalid location", offset);
}
HTMLEditorKit.ParserCallback receiver = doc.getReader(offset, popDepth, pushDepth, insertTag);
Boolean ignoreCharset = (Boolean)doc.getProperty("IgnoreCharsetDirective"); //$NON-NLS-1$
p.parse(new StringReader(html), receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue());
receiver.flush();
}
/**
* Write content from a document to the given stream in a format appropriate for this kind of content handler.
*
* @param out the stream to write to
* @param doc the source for the write
* @param pos the location in the document to fetch the content
* @param len the amount to write out
* @exception IOException on any I/O error
* @exception BadLocationException if pos represents an invalid location within the document
*/
@Override
public void write(Writer out, Document doc, int pos, int len) throws IOException, BadLocationException
{
if (doc instanceof HTMLDocument)
{
HTMLWriter w = new HTMLWriter(out, (HTMLDocument)doc, pos, len);
w.write();
}
else if (doc instanceof StyledDocument)
{
MinimalHTMLWriter w = new MinimalHTMLWriter(out, (StyledDocument)doc, pos, len);
w.write();
}
else
{
super.write(out, doc, pos, len);
}
}
/**
* Called when the kit is being installed into the a JEditorPane.
*
* @param c the JEditorPane
*/
@Override
public void install(JEditorPane c)
{
c.addMouseListener(linkHandler);
c.addMouseMotionListener(linkHandler);
c.addCaretListener(nextLinkAction);
super.install(c);
}
/**
* Called when the kit is being removed from the JEditorPane. This is used to unregister any listeners that were attached.
*
* @param c the JEditorPane
*/
@Override
public void deinstall(JEditorPane c)
{
c.removeMouseListener(linkHandler);
c.removeMouseMotionListener(linkHandler);
super.deinstall(c);
}
/**
* Default Cascading Style Sheet file that sets up the tag views.
*/
public static final String DEFAULT_CSS = "default.css"; //$NON-NLS-1$
/**
* Set the set of styles to be used to render the various HTML elements. These styles are specified in terms of CSS specifications. Each document produced
* by the kit will have a copy of the sheet which it can add the document specific styles to. By default, the StyleSheet specified is shared by all
* HTMLEditorKit instances. This should be reimplemented to provide a finer granularity if desired.
*/
public void setStyleSheet(StyleSheet s)
{
defaultStyles = s;
}
/**
* Get the set of styles currently being used to render the HTML elements. By default the resource specified by DEFAULT_CSS gets loaded, and is shared by
* all HTMLEditorKit instances.
*/
public StyleSheet getStyleSheet()
{
if (defaultStyles == null)
{
defaultStyles = createStyleSheet();
try
{
InputStream is = HTMLEditorKit.class.getResourceAsStream(DEFAULT_CSS);
Reader r = new BufferedReader(new InputStreamReader(is));
defaultStyles.loadRules(r, null);
r.close();
}
catch (Throwable e)
{
// on error we simply have no styles... the html
// will look mighty wrong but still function.
}
}
return defaultStyles;
}
protected StyleSheet createStyleSheet()
{
return new FixedStyleSheet();
}
/**
* Fetch a resource relative to the HTMLEditorKit classfile. If this is called on 1.2 the loading will occur under the protection of a doPrivileged call to
* allow the HTMLEditorKit to function when used in an applet.
*
* @param name the name of the resource, relative to the HTMLEditorKit class
* @return a stream representing the resource
*/
static InputStream getResourceAsStream(String name)
{
return HTMLEditorKit.class.getResourceAsStream(name);
}
/**
* Copies the key/values in <code>element</code>s AttributeSet into <code>set</code>. This does not copy component, icon, or element names attributes.
* Subclasses may wish to refine what is and what isn't copied here. But be sure to first remove all the attributes that are in <code>set</code>.
* <p>
* This is called anytime the caret moves over a different location.
*
*/
@Override
protected void createInputAttributes(Element element, MutableAttributeSet set)
{
set.removeAttributes(set);
set.addAttributes(element.getAttributes());
set.removeAttribute(StyleConstants.ComposedTextAttribute);
Object o = set.getAttribute(StyleConstants.NameAttribute);
if (o instanceof HTML.Tag)
{
HTML.Tag tag = (HTML.Tag)o;
// PENDING: we need a better way to express what shouldn't be
// copied when editing...
if (tag == HTML.Tag.IMG)
{
// Remove the related image attributes, src, width, height
set.removeAttribute(HTML.Attribute.SRC);
set.removeAttribute(HTML.Attribute.HEIGHT);
set.removeAttribute(HTML.Attribute.WIDTH);
set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT);
}
else if (tag == HTML.Tag.HR || tag == HTML.Tag.BR)
{
// Don't copy HRs or BRs either.
set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT);
}
else if (tag == HTML.Tag.COMMENT)
{
// Don't copy COMMENTs either
set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT);
set.removeAttribute(HTML.Attribute.COMMENT);
}
else if (tag == HTML.Tag.INPUT)
{
// or INPUT either
set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT);
set.removeAttribute(HTML.Tag.INPUT);
}
else if (tag instanceof HTML.UnknownTag)
{
// Don't copy unknowns either:(
set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT);
set.removeAttribute(HTML.Attribute.ENDTAG);
}
}
}
/**
* Gets the input attributes used for the styled editing actions.
*
* @return the attribute set
*/
@Override
public MutableAttributeSet getInputAttributes()
{
if (input == null)
{
input = getStyleSheet().addStyle(null, null);
}
return input;
}
/**
* Sets the default cursor.
*
* @since 1.3
*/
public void setDefaultCursor(Cursor cursor)
{
defaultCursor = cursor;
}
/**
* Returns the default cursor.
*
* @since 1.3
*/
public Cursor getDefaultCursor()
{
return defaultCursor;
}
public void setEditCursor(Cursor cursor)
{
editCursor = cursor;
}
public Cursor getEditCursor()
{
return editCursor;
}
/**
* Sets the cursor to use over links.
*
* @since 1.3
*/
public void setLinkCursor(Cursor cursor)
{
linkCursor = cursor;
}
/**
* Returns the cursor to use over hyper links.
*/
public Cursor getLinkCursor()
{
return linkCursor;
}
/**
* Creates a copy of the editor kit.
*
* @return the copy
*/
@Override
public Object clone()
{
FixedHTMLEditorKit o = (FixedHTMLEditorKit)super.clone();
if (o != null)
{
o.input = null;
o.linkHandler = new LinkController();
}
return o;
}
/**
* Fetch the parser to use for reading HTML streams. This can be reimplemented to provide a different parser. The default implementation is loaded
* dynamically to avoid the overhead of loading the default parser if it's not used. The default parser is the HotJava parser using an HTML 3.2 DTD.
*/
protected HTMLEditorKit.Parser getParser()
{
if (defaultParser == null)
{
try
{
Class c = Class.forName("javax.swing.text.html.parser.ParserDelegator"); //$NON-NLS-1$
defaultParser = (HTMLEditorKit.Parser)c.newInstance();
}
catch (Throwable e)
{
}
}
return defaultParser;
}
// --- variables ------------------------------------------
private static final Cursor MoveCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
private static final Cursor DefaultCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
private static final Cursor EditCursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
/** Shared factory for creating HTML Views. */
private static final ViewFactory defaultFactory = new HTMLFactory();
private ViewFactory currentFactory = defaultFactory;
MutableAttributeSet input;
private static StyleSheet defaultStyles = null;
private static HTMLEditorKit.Parser defaultParser = null;
private Cursor defaultCursor = DefaultCursor;
private Cursor editCursor = EditCursor;
private Cursor linkCursor = MoveCursor;
// --- Action implementations ------------------------------
/**
* The bold action identifier
*/
public static final String BOLD_ACTION = "html-bold-action"; //$NON-NLS-1$
/**
* The italic action identifier
*/
public static final String ITALIC_ACTION = "html-italic-action"; //$NON-NLS-1$
/**
* The paragraph left indent action identifier
*/
public static final String PARA_INDENT_LEFT = "html-para-indent-left"; //$NON-NLS-1$
/**
* The paragraph right indent action identifier
*/
public static final String PARA_INDENT_RIGHT = "html-para-indent-right"; //$NON-NLS-1$
/**
* The font size increase to next value action identifier
*/
public static final String FONT_CHANGE_BIGGER = "html-font-bigger"; //$NON-NLS-1$
/**
* The font size decrease to next value action identifier
*/
public static final String FONT_CHANGE_SMALLER = "html-font-smaller"; //$NON-NLS-1$
/**
* The Color choice action identifier The color is passed as an argument
*/
public static final String COLOR_ACTION = "html-color-action"; //$NON-NLS-1$
/**
* The logical style choice action identifier The logical style is passed in as an argument
*/
public static final String LOGICAL_STYLE_ACTION = "html-logical-style-action"; //$NON-NLS-1$
/**
* Align images at the top.
*/
public static final String IMG_ALIGN_TOP = "html-image-align-top"; //$NON-NLS-1$
/**
* Align images in the middle.
*/
public static final String IMG_ALIGN_MIDDLE = "html-image-align-middle"; //$NON-NLS-1$
/**
* Align images at the bottom.
*/
public static final String IMG_ALIGN_BOTTOM = "html-image-align-bottom"; //$NON-NLS-1$
/**
* Align images at the border.
*/
public static final String IMG_BORDER = "html-image-border"; //$NON-NLS-1$
/**
* An abstract Action providing some convenience methods that may be useful in inserting HTML into an existing document.
* <p>
* NOTE: None of the convenience methods obtain a lock on the document. If you have another thread modifying the text these methods may have inconsistent
* behavior, or return the wrong thing.
*/
public static abstract class HTMLTextAction extends StyledTextAction
{
public HTMLTextAction(String name)
{
super(name);
}
/**
* @return HTMLDocument of <code>e</code>.
*/
protected HTMLDocument getHTMLDocument(JEditorPane e)
{
Document d = e.getDocument();
if (d instanceof HTMLDocument)
{
return (HTMLDocument)d;
}
throw new IllegalArgumentException("document must be HTMLDocument");
}
/**
* @return HTMLEditorKit for <code>e</code>.
*/
protected FixedHTMLEditorKit getHTMLEditorKit(JEditorPane e)
{
EditorKit k = e.getEditorKit();
if (k instanceof FixedHTMLEditorKit)
{
return (FixedHTMLEditorKit)k;
}
throw new IllegalArgumentException("EditorKit must be HTMLEditorKit");
}
/**
* Returns an array of the Elements that contain <code>offset</code>. The first elements corresponds to the root.
*/
protected Element[] getElementsAt(HTMLDocument doc, int offset)
{
return getElementsAt(doc.getDefaultRootElement(), offset, 0);
}
/**
* Recursive method used by getElementsAt.
*/
private Element[] getElementsAt(Element parent, int offset, int depth)
{
if (parent.isLeaf())
{
Element[] retValue = new Element[depth + 1];
retValue[depth] = parent;
return retValue;
}
Element[] retValue = getElementsAt(parent.getElement(parent.getElementIndex(offset)), offset, depth + 1);
retValue[depth] = parent;
return retValue;
}
/**
* Returns number of elements, starting at the deepest leaf, needed to get to an element representing <code>tag</code>. This will return -1 if no
* elements is found representing <code>tag</code>, or 0 if the parent of the leaf at <code>offset</code> represents <code>tag</code>.
*/
protected int elementCountToTag(HTMLDocument doc, int offset, HTML.Tag tag)
{
int depth = -1;
Element e = doc.getCharacterElement(offset);
while (e != null && e.getAttributes().getAttribute(StyleConstants.NameAttribute) != tag)
{
e = e.getParentElement();
depth++;
}
if (e == null)
{
return -1;
}
return depth;
}
/**
* Returns the deepest element at <code>offset</code> matching <code>tag</code>.
*/
protected Element findElementMatchingTag(HTMLDocument doc, int offset, HTML.Tag tag)
{
Element e = doc.getDefaultRootElement();
Element lastMatch = null;
while (e != null)
{
if (e.getAttributes().getAttribute(StyleConstants.NameAttribute) == tag)
{
lastMatch = e;
}
e = e.getElement(e.getElementIndex(offset));
}
return lastMatch;
}
}
/**
* InsertHRAction is special, at actionPerformed time it will determine the parent HTML.Tag based on the paragraph element at the selection start.
*/
static class InsertHRAction extends InsertHTMLTextAction
{
InsertHRAction()
{
super("InsertHR", "<hr>", null, HTML.Tag.IMPLIED, null, null, false);
}
/**
* Inserts the HTML into the document.
*
* @param ae the event
*/
@Override
public void actionPerformed(ActionEvent ae)
{
JEditorPane editor = getEditor(ae);
if (editor != null)
{
HTMLDocument doc = getHTMLDocument(editor);
int offset = editor.getSelectionStart();
Element paragraph = doc.getParagraphElement(offset);
if (paragraph.getParentElement() != null)
{
parentTag = (HTML.Tag)paragraph.getParentElement().getAttributes().getAttribute(StyleConstants.NameAttribute);
super.actionPerformed(ae);
}
}
}
}
/*
* Returns the object in an AttributeSet matching a key
*/
static private Object getAttrValue(AttributeSet attr, HTML.Attribute key)
{
Enumeration names = attr.getAttributeNames();
while (names.hasMoreElements())
{
Object nextKey = names.nextElement();
Object nextVal = attr.getAttribute(nextKey);
if (nextVal instanceof AttributeSet)
{
Object value = getAttrValue((AttributeSet)nextVal, key);
if (value != null)
{
return value;
}
}
else if (nextKey == key)
{
return nextVal;
}
}
return null;
}
/*
* Action to move the focus on the next or previous hypertext link or object. TODO: This method relies on support from the javax.accessibility package. The
* text package should support keyboard navigation of text elements directly.
*/
static class NavigateLinkAction extends TextAction implements CaretListener
{
private static int prevHypertextOffset = -1;
private static boolean foundLink = false;
private final FocusHighlightPainter focusPainter = new FocusHighlightPainter(null);
private Object selectionTag;
private boolean focusBack = false;
/*
* Create this action with the appropriate identifier.
*/
public NavigateLinkAction(String actionName)
{
super(actionName);
if ("previous-link-action".equals(actionName))
{
focusBack = true;
}
}
/**
* Called when the caret position is updated.
*
* @param e the caret event
*/
public void caretUpdate(CaretEvent e)
{
if (foundLink)
{
foundLink = false;
// TODO: The AccessibleContext for the editor should register
// as a listener for CaretEvents and forward the events to
// assistive technologies listening for such events.
Object src = e.getSource();
// if (src instanceof JTextComponent) {
// ((JTextComponent)src).getAccessibleContext().firePropertyChange(
// AccessibleContext.ACCESSIBLE_HYPERTEXT_OFFSET,
// new Integer(prevHypertextOffset),
// new Integer(e.getDot()));
// }
}
}
/*
* The operation to perform when this action is triggered.
*/
public void actionPerformed(ActionEvent e)
{
JTextComponent comp = getTextComponent(e);
if (comp == null || comp.isEditable())
{
return;
}
Document doc = comp.getDocument();
if (doc == null)
{
return;
}
// TODO: Should start successive iterations from the
// current caret position.
ElementIterator ei = new ElementIterator(doc);
int currentOffset = comp.getCaretPosition();
int prevStartOffset = -1;
int prevEndOffset = -1;
// highlight the next link or object after the current caret position
Element nextElement = null;
while ((nextElement = ei.next()) != null)
{
String name = nextElement.getName();
AttributeSet attr = nextElement.getAttributes();
Object href = getAttrValue(attr, HTML.Attribute.HREF);
if (!(name.equals(HTML.Tag.OBJECT.toString())) && href == null)
{
continue;
}
int elementOffset = nextElement.getStartOffset();
if (focusBack)
{
if (elementOffset >= currentOffset && prevStartOffset >= 0)
{
foundLink = true;
comp.setCaretPosition(prevStartOffset);
moveCaretPosition(comp, prevStartOffset, prevEndOffset);
prevHypertextOffset = prevStartOffset;
return;
}
}
else
{ // focus forward
if (elementOffset > currentOffset)
{
foundLink = true;
comp.setCaretPosition(elementOffset);
moveCaretPosition(comp, elementOffset, nextElement.getEndOffset());
prevHypertextOffset = elementOffset;
return;
}
}
prevStartOffset = nextElement.getStartOffset();
prevEndOffset = nextElement.getEndOffset();
}
}
/*
* Moves the caret from mark to dot
*/
private void moveCaretPosition(JTextComponent comp, int mark, int dot)
{
Highlighter h = comp.getHighlighter();
if (h != null)
{
int p0 = Math.min(dot, mark);
int p1 = Math.max(dot, mark);
try
{
if (selectionTag != null)
{
h.changeHighlight(selectionTag, p0, p1);
}
else
{
Highlighter.HighlightPainter p = focusPainter;
selectionTag = h.addHighlight(p0, p1, p);
}
}
catch (BadLocationException e)
{
}
}
}
/**
* A highlight painter that draws a one-pixel border around the highlighted area.
*/
class FocusHighlightPainter extends DefaultHighlighter.DefaultHighlightPainter
{
FocusHighlightPainter(Color color)
{
super(color);
}
/**
* Paints a portion of a highlight.
*
* @param g the graphics context
* @param offs0 the starting model offset >= 0
* @param offs1 the ending model offset >= offs1
* @param bounds the bounding box of the view, which is not necessarily the region to paint.
* @param c the editor
* @param view View painting for
* @return region in which drawing occurred
*/
@Override
public Shape paintLayer(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c, View view)
{
Color color = getColor();
if (color == null)
{
g.setColor(c.getSelectionColor());
}
else
{
g.setColor(color);
}
if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset())
{
// Contained in view, can just use bounds.
Rectangle alloc;
if (bounds instanceof Rectangle)
{
alloc = (Rectangle)bounds;
}
else
{
alloc = bounds.getBounds();
}
g.drawRect(alloc.x, alloc.y, alloc.width - 1, alloc.height);
return alloc;
}
else
{
// Should only render part of View.
try
{
// --- determine locations ---
Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1, Position.Bias.Backward, bounds);
Rectangle r = (shape instanceof Rectangle) ? (Rectangle)shape : shape.getBounds();
g.drawRect(r.x, r.y, r.width - 1, r.height);
return r;
}
catch (BadLocationException e)
{
// can't render
}
}
// Only if exception
return null;
}
}
}
/*
* Action to activate the hypertext link that has focus. TODO: This method relies on support from the javax.accessibility package. The text package should
* support keyboard navigation of text elements directly.
*/
static class ActivateLinkAction extends TextAction
{
/**
* Create this action with the appropriate identifier.
*/
public ActivateLinkAction(String actionName)
{
super(actionName);
}
/*
* activates the hyperlink at offset
*/
private void activateLink(String href, HTMLDocument doc, JEditorPane editor, int offset)
{
try
{
URL page = (URL)doc.getProperty(Document.StreamDescriptionProperty);
URL url = new URL(page, href);
HyperlinkEvent linkEvent = new HyperlinkEvent(editor, HyperlinkEvent.EventType.ACTIVATED, url);//, url.toExternalForm());//,
// doc.getCharacterElement(offset));
editor.fireHyperlinkUpdate(linkEvent);
}
catch (MalformedURLException m)
{
}
}
/*
* Invokes default action on the object in an element
*/
private void doObjectAction(JEditorPane editor, Element elem)
{
View view = getView(editor, elem);
if (view != null && view instanceof ObjectView)
{
Component comp = ((ObjectView)view).getComponent();
if (comp != null && comp instanceof Accessible)
{
AccessibleContext ac = ((Accessible)comp).getAccessibleContext();
if (ac != null)
{
AccessibleAction aa = ac.getAccessibleAction();
if (aa != null)
{
aa.doAccessibleAction(0);
}
}
}
}
}
/*
* Returns the root view for a document
*/
private View getRootView(JEditorPane editor)
{
return editor.getUI().getRootView(editor);
}
/*
* Returns a view associated with an element
*/
private View getView(JEditorPane editor, Element elem)
{
Object lock = lock(editor);
try
{
View rootView = getRootView(editor);
int start = elem.getStartOffset();
if (rootView != null)
{
return getView(rootView, elem, start);
}
return null;
}
finally
{
unlock(lock);
}
}
private View getView(View parent, Element elem, int start)
{
if (parent.getElement() == elem)
{
return parent;
}
int index = parent.getViewIndex(start, Position.Bias.Forward);
if (index != -1 && index < parent.getViewCount())
{
return getView(parent.getView(index), elem, start);
}
return null;
}
/*
* If possible acquires a lock on the Document. If a lock has been obtained a key will be retured that should be passed to <code>unlock</code>.
*/
private Object lock(JEditorPane editor)
{
Document document = editor.getDocument();
if (document instanceof AbstractDocument)
{
((AbstractDocument)document).readLock();
return document;
}
return null;
}
/*
* Releases a lock previously obtained via <code>lock</code>.
*/
private void unlock(Object key)
{
if (key != null)
{
((AbstractDocument)key).readUnlock();
}
}
/*
* The operation to perform when this action is triggered.
*/
public void actionPerformed(ActionEvent e)
{
JTextComponent c = getTextComponent(e);
if (c.isEditable() || !(c instanceof JEditorPane))
{
return;
}
JEditorPane editor = (JEditorPane)c;
Document d = editor.getDocument();
if (d == null || !(d instanceof HTMLDocument))
{
return;
}
HTMLDocument doc = (HTMLDocument)d;
ElementIterator ei = new ElementIterator(doc);
int currentOffset = editor.getCaretPosition();
// invoke the next link or object action
Element currentElement = null;
while ((currentElement = ei.next()) != null)
{
String name = currentElement.getName();
AttributeSet attr = currentElement.getAttributes();
Object href = getAttrValue(attr, HTML.Attribute.HREF);
if (href != null)
{
if (currentOffset >= currentElement.getStartOffset() && currentOffset <= currentElement.getEndOffset())
{
activateLink((String)href, doc, editor, currentOffset);
return;
}
}
else if (name.equals(HTML.Tag.OBJECT.toString()))
{
Object obj = getAttrValue(attr, HTML.Attribute.CLASSID);
if (obj != null)
{
if (currentOffset >= currentElement.getStartOffset() && currentOffset <= currentElement.getEndOffset())
{
doObjectAction(editor, currentElement);
return;
}
}
}
}
}
}
/**
* @see com.servoy.j2db.dataui.ISupportAsyncLoading#setAsyncLoadingEnabled(boolean)
*/
public void setAsyncLoadingEnabled(boolean asyncLoading)
{
if (asyncLoading)
{
currentFactory = defaultFactory;
}
else
{
currentFactory = new BasicHTMLViewFactory(new MediaURLStreamHandler(application));
}
}
}