/*
 * SimplyHTML, a word processor based on Java, HTML and CSS
 * Copyright (C) 2002 Ulrich Hilger
 * Copyright (C) 2006 Dimitri Polivaev
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package com.lightdev.app.shtm;

import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Enumeration;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.TransferHandler;
import javax.swing.event.CaretEvent;

import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Element;
import javax.swing.text.ElementIterator;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.NavigationFilter;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Position.Bias;
import javax.swing.text.html.CSS;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;

/**
 * An editor pane for application SimplyHTML.
 *
 * <p>This is extending <code>JEditorPane</code> by cut and paste
 * and drag and drop for HTML text.
 * <code>JEditorPane</code> inherits cut and paste from <code>
 * JTextComponent</code> where handling for plain text is implemented only.
 * <code>JEditorPane</code> has no additional functionality to add cut
 * and paste for the various content types it supports
 * (such as 'text/html').</p>
 *
 * <p>In stage 4 support for caret movement inside tables and
 * table manipulation methods are added.</p>
 *
 * <p>In stage 6 support for list manipulation was added.</p>
 *
 * @author Ulrich Hilger
 * @author Light Development
 * @author <a href="http://www.lightdev.com">http://www.lightdev.com</a>
 * @author <a href="mailto:info@lightdev.com">info@lightdev.com</a>
 * @author published under the terms and conditions of the
 *      GNU General Public License,
 *      for details see file gpl.txt in the distribution
 *      package of this software
 *
 * 
 *
 * @see com.lightdev.app.shtm.HTMLText
 * @see com.lightdev.app.shtm.HTMLTextSelection
 */

public class SHTMLEditorPane extends JEditorPane  implements
    DropTargetListener, DragSourceListener, DragGestureListener
{
    private static final boolean OLD_JAVA_VERSION = System
    .getProperty("java.version").compareTo("1.5.0") < 0;
	private JPopupMenu popup;
    private ListManager listManager = new ListManager();
    
    /**
     * construct a new <code>SHTMLEditorPane</code>
     */
    public SHTMLEditorPane() {
      super();
      setCaretColor(Color.black);
      setNavigationFilter(new MyNavigationFilter());
      /**
       * set the cursor by adding
       * a MouseListener that allows to display a text cursor when the
       * mouse pointer enters the editor. For some reason
       * (probably someone knows why and could let me know...) the method
       * setCursor does not have the same effect.
       */

      addMouseListener(
          new MouseAdapter() {
            public void mouseEntered(MouseEvent e) {
              Component gp = getRootPane().getGlassPane();
              gp.setCursor(textCursor);
              gp.setVisible(true);
            }
            public void mouseExited(MouseEvent e) {
              Component gp = getRootPane().getGlassPane();
              gp.setCursor(defaultCursor);
              gp.setVisible(false);
            }
            public void mousePressed(MouseEvent e) {
              maybeShowPopup(e);
            }

            public void mouseReleased(MouseEvent e) {
              maybeShowPopup(e);
            }

            public void mouseClicked(MouseEvent ev) {
              if ((ev.getModifiers() & MouseEvent.CTRL_MASK)!=0) { 
                String linkURL = getURLOfExistingLink();
                if (linkURL!=null) {              
                  SHTMLPanelImpl panel = SHTMLPanelImpl.getOwnerSHTMLPanel((Component)ev.getSource());
                  panel.openHyperlink(linkURL);
                }
              }
            }

            private void maybeShowPopup(MouseEvent e) {
              if (popup != null && e.isPopupTrigger()) {
                popup.show(e.getComponent(),
                    e.getX(), e.getY());
              }
            }

          }
      );

      /** implement customized caret movement */
      adjustKeyBindings();

      /** init drag and drop */
      initDnd();
    }

  /**
   * adjust the key bindings of the key map existing for this
   * editor pane to our needs (i.e. add actions to certain keys
   * such as tab/shift tab for caret movement inside tables, etc.)
   *
   * This method had to be redone for using InputMap / ActionMap
   * instead of Keymap.
   */
  private void adjustKeyBindings() {
    ActionMap myActionMap = new ActionMap();
    InputMap myInputMap = new InputMap();

    KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
    myActionMap.put(SHTMLPanelImpl.nextTableCellAction, new TabAction(SHTMLPanelImpl.nextTableCellAction));
    myInputMap.put(tab, SHTMLPanelImpl.nextTableCellAction);

    KeyStroke shiftTab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_MASK);
    myActionMap.put(SHTMLPanelImpl.prevTableCellAction, new ShiftTabAction(SHTMLPanelImpl.prevTableCellAction));
    myInputMap.put(shiftTab,SHTMLPanelImpl.prevTableCellAction);

    KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
    myActionMap.put(newListItemAction, new NewParagraphAction());
    myInputMap.put(enter, newListItemAction);

    KeyStroke lineBreak = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_MASK);
    myActionMap.put(insertLineBreakAction, new InsertLineBreakAction());
    myInputMap.put(lineBreak, insertLineBreakAction);

    KeyStroke backspace = KeyStroke.getKeyStroke('\b', 0);
    myActionMap.put(deletePrevCharAction, new DeletePrevCharAction());
    myInputMap.put(backspace, deletePrevCharAction);

    KeyStroke delete = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
    myActionMap.put(deleteNextCharAction, new DeleteNextCharAction());
    myInputMap.put(delete, deleteNextCharAction);
        
    myActionMap.put(moveUpAction, new MoveUpAction());
    myInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), moveUpAction);    
    
    myActionMap.put(moveDownAction, new MoveDownAction());
    myInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), moveDownAction);    

    myActionMap.put(homeAction, new HomeAction());
    myInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), homeAction);    
    
    myActionMap.put(endAction, new EndAction());
    myInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), endAction);        

    myActionMap.put(shiftHomeAction, new ShiftHomeAction());
    myInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, KeyEvent.SHIFT_MASK), shiftHomeAction); 

    myActionMap.put(shiftEndAction, new ShiftEndAction());
    myInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, KeyEvent.SHIFT_MASK), shiftEndAction);    
    
    //myActionMap.put("TTH", getDnew SHTMLEditorKitActions.ToggleTableHeaderCellAction(null));
    //myInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, KeyEvent.SHIFT_MASK), "TTH");    
    
    myActionMap.setParent(getActionMap());
    myInputMap.setParent(getInputMap());
    setActionMap(myActionMap);
    setInputMap(JComponent.WHEN_FOCUSED, myInputMap);

    /*
     implementation before 1.4.1
     -------------------------------------

    Keymap map = getKeymap();
    KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
    map.addActionForKeyStroke(tab, new NextTableCellAction(map.getAction(tab)));
    KeyStroke shiftTab = KeyStroke.getKeyStroke(
                            KeyEvent.VK_TAB, InputEvent.SHIFT_MASK);
    map.addActionForKeyStroke(shiftTab,
                    new PrevTableCellAction(map.getAction(shiftTab)));
    setKeymap(map);
    */
  }
  private static gnu.regexp.RE pattern1 = null;
  private static gnu.regexp.RE pattern2 = null;

  /* (non-Javadoc)
 * @see javax.swing.JComponent#processKeyBinding(javax.swing.KeyStroke, java.awt.event.KeyEvent, int, boolean)
 */
protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
    final int maximumEndSelection = ((SHTMLDocument)getDocument()).getLastDocumentPosition();
    if(getSelectionStart() >= maximumEndSelection  && 
            ! (ks.getKeyCode() == KeyEvent.VK_LEFT
               || ks.getKeyCode() == KeyEvent.VK_UP
               || ks.getKeyCode() == KeyEvent.VK_HOME)){
        return true;
    }
    if(getSelectionEnd() >= maximumEndSelection){
        setSelectionEnd(maximumEndSelection-1);
    }
    return super.processKeyBinding(ks, e, condition, pressed);
}

/**
   * Convenience method for setting the document text
   * contains hack around JDK bug 4799813
   * see http://developer.java.sun.com/developer/bugParade/bugs/4799813.html
   * regression in 1.4.x, to be fixed in 1.5
   * When setting the text to be "&amp; footext", it becomes "&amp;footext" (space disappears)
   * same ocurrs for "&lt;/a&gt; &amp;amp;", it becomes "&lt;/a&gt;&amp;amp;" (space disappears)
   * with the hack it now does not occur anymore.
   * @param sText the html-text of the document
   */
  public void setText(String sText) {
      try {
          if( System.getProperty("java.version").substring(0,3).equals("1.4") ) {
              if (pattern1 == null)
                  pattern1 = new gnu.regexp.RE("(&\\w+;|&#\\d+;)(\\s|&#160;|&nbsp;)(?=<|&\\w+;|&#\\d+;)");
              sText=pattern1.substituteAll(sText,"$1&#160;$3");
              if (pattern2 == null)
                  pattern2 = new gnu.regexp.RE("<(/[^>])>(\\s|&#160;|&nbsp;|\\n\\s+)(?!&#160;)(&\\w+;|&#\\d+;)");
              sText=pattern2.substituteAll(sText,"<$1>&#160;$3$4");
          }
      } catch (gnu.regexp.REException ree) {
          ree.printStackTrace();
      }
      SHTMLDocument doc = (SHTMLDocument) getDocument();
      doc.startCompoundEdit();
      if(sText == null || sText.equals("") ){
          sText = "<html><body><p></p></body></html>";
      }
      super.setText(sText);
      setCaretPosition(0);
      doc.endCompoundEdit();
      if(OLD_JAVA_VERSION){
          SHTMLPanelImpl.getOwnerSHTMLPanel(this).purgeUndos();
      }
  }

private class MyNavigationFilter extends NavigationFilter{

    /* (non-Javadoc)
     * @see javax.swing.text.NavigationFilter#moveDot(javax.swing.text.NavigationFilter.FilterBypass, int, javax.swing.text.Position.Bias)
     */
    public void moveDot(FilterBypass fb, int dot, Bias bias) {
        dot = getValidPosition(dot);
        super.moveDot(fb, dot, bias);
    }

    /* (non-Javadoc)
     * @see javax.swing.text.NavigationFilter#setDot(javax.swing.text.NavigationFilter.FilterBypass, int, javax.swing.text.Position.Bias)
     */
    public void setDot(FilterBypass fb, int dot, Bias bias) {
        dot = getValidPosition(dot);
        super.setDot(fb, dot, bias);
    }

}

private int getValidPosition(int position) {
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    final int lastValidPosition = doc.getLastDocumentPosition()-1;
    if(position > lastValidPosition ){
        position = lastValidPosition;
    }
      int startPos = 0;
      if(doc.getDefaultRootElement().getElementCount() > 1){
          startPos = doc.getDefaultRootElement().getElement(1).getStartOffset();
      }
      final int validPosition = Math.max(position,startPos);
    return validPosition;
}

private class DeletePrevCharAction extends AbstractAction{
  public void actionPerformed(ActionEvent actionEvent) {
    final int selectionStart = getSelectionStart();
    final int selectionEnd = getSelectionEnd();
    SHTMLDocument doc = (SHTMLDocument)getDocument();
    if(selectionEnd >= doc.getLastDocumentPosition())
      return;
    if(selectionStart == selectionEnd){

      boolean intervention = listManager.deletePrevChar(actionEvent);
      if (intervention)
        return;

      // Prevent deletion of table cell.
      Element tableCell = selectionStart==0?null:getTableCell(selectionStart - 1);
      if(tableCell != null && tableCell.getEndOffset() == selectionStart){
        performDefaultKeyStrokeAction(KeyEvent.VK_LEFT, 0, actionEvent);
        return;
      }

    }
    performDefaultKeyStrokeAction('\b', 0, actionEvent);
  }
}

private class DeleteNextCharAction extends AbstractAction{
  public void actionPerformed(ActionEvent actionEvent) {
    final int selectionStart = getSelectionStart();
    if(selectionStart == getSelectionEnd()){
      SHTMLDocument doc = (SHTMLDocument)getDocument();
      if(selectionStart >= doc.getLastDocumentPosition()-1)
        return;
 
      boolean intervention = treatTables(actionEvent);
      if (intervention)
        return;

      intervention = listManager.deleteNextChar(actionEvent);
      if (intervention)
        return;
              
    }
    performDefaultKeyStrokeAction(KeyEvent.VK_DELETE, 0, actionEvent);
  }
  
  /** Treats tables. Returns true if intervention was necessary. */
  private boolean treatTables(ActionEvent event) {
    final int selectionStart = getSelectionStart();
    final int nextPosition = selectionStart + 1;
    Element elem = null;
    SHTMLDocument doc = getSHTMLDocument();
    // Table cell element at the start of the selection
    elem = getCurrentTableCell();
    if(elem != null && elem.getEndOffset() == nextPosition){
      // Do nothing to avoid deletion of the whole cell.
      //return;
    }
    if(nextPosition < doc.getLength()) {
      // Table cell element at next position
      elem = getTableCell(nextPosition);
      if (elem != null && elem.getStartOffset() == nextPosition){
        // In most cases, do nothing to avoid deletion of parts of the following table.
        Element paragraphElement = getCurrentParagraphElement();
        boolean emptyParagraph = elementIsEmptyParagraph(paragraphElement);
        if (!caretWithinTableCell() && emptyParagraph ) 
          // Empty paragraph outside a table, before a table.
          return false;
        else if (caretWithinTableCell() && emptyParagraph) {
          // Remove empty paragraph at the end of the cell. 
          removeElement(paragraphElement);
          setCaretPosition(getCaretPosition() - 1);
          return true;
        }
        else 
          return true;
      }
    }    
    return false;
  }
}

private class MoveUpAction extends AbstractAction{
  public void actionPerformed(ActionEvent e) {
    if (caretWithinTableCell()) { 
      if (getCaretPosition()==0) {
        // The table is at the top of the document.
        // Insert new paragraph before the table.
        Element tableElement = getCurrentTableCell().getParentElement().getParentElement();
        try {
        getSHTMLDocument().insertBeforeStart(tableElement, "<p></p>");
        }
        catch (Exception ex) {          
        }
      }
      if (tryDefaultKeyStrokeActionWithinCell(KeyEvent.VK_UP, 0, e))     
        return;      
      
      Element cellElement = getCurrentTableCell();
      Element rowElement = cellElement.getParentElement();
      Element tableElement = rowElement.getParentElement();
      int cellIndexInRow = rowElement.getElementIndex(cellElement.getStartOffset());
      int rowIndexInTable = tableElement.getElementIndex(rowElement.getStartOffset());
      if (rowIndexInTable > 0) {
        Element previousRowElement = tableElement.getElement(rowIndexInTable-1);
        Element targetCellElement = previousRowElement.getElement(cellIndexInRow);
        setCaretPosition(targetCellElement.getEndOffset());        
      }
      else {
        setCaretPosition(tableElement.getStartOffset());
      }
      //}
    }    
    performDefaultKeyStrokeAction(KeyEvent.VK_UP, 0, e);
  }
}
private class MoveDownAction extends AbstractAction{
  public void actionPerformed(ActionEvent e) {
    if (caretWithinTableCell()) {
      if (tryDefaultKeyStrokeActionWithinCell(KeyEvent.VK_DOWN, 0, e))
        return;

      Element cellElement = getCurrentTableCell();
      Element rowElement = cellElement.getParentElement();
      Element tableElement = rowElement.getParentElement();
      int cellIndexInRow = rowElement.getElementIndex(cellElement.getStartOffset());
      int rowIndexInTable = tableElement.getElementIndex(rowElement.getStartOffset());
      if (rowIndexInTable < tableElement.getElementCount()-1) {
        Element nextRowElement = tableElement.getElement(rowIndexInTable+1);
        Element targetCellElement = nextRowElement.getElement(cellIndexInRow);
        //Element targetInnerElement = targetCellElement.getElement(0); // p or p-implied.
        setCaretPosition(targetCellElement.getStartOffset()-1);
      }
      else {
        setCaretPosition(tableElement.getEndOffset()-1);
      }
      //}
    }

    performDefaultKeyStrokeAction(KeyEvent.VK_DOWN, 0, e);
  }
}
private class HomeAction extends AbstractAction{
  public void actionPerformed(ActionEvent e) {
    if (caretWithinTableCell()) {
      if (tryDefaultKeyStrokeActionWithinCell(KeyEvent.VK_HOME, 0, e))
        return;
      setCaretPosition(getCurrentParagraphElement().getStartOffset());
    }
    else
      performDefaultKeyStrokeAction(KeyEvent.VK_HOME, 0, e);
  }
}
private class EndAction extends AbstractAction{
  public void actionPerformed(ActionEvent e) {
    if (caretWithinTableCell()) {
      if (tryDefaultKeyStrokeActionWithinCell(KeyEvent.VK_END, 0, e))
        return;          
      setCaretPosition(getCurrentParagraphElement().getEndOffset()-1);
    }
    else
      performDefaultKeyStrokeAction(KeyEvent.VK_END, 0, e);
  }
}
private class ShiftHomeAction extends AbstractAction{
  public void actionPerformed(ActionEvent e) {
    if (caretWithinTableCell()) {
      int originalCaretPosition = getCaretPosition();
      if (tryDefaultKeyStrokeActionWithinCell(KeyEvent.VK_HOME, KeyEvent.SHIFT_MASK, e))
        return;
      int newCaretPosition = getCurrentParagraphElement().getStartOffset();
      if (newCaretPosition > originalCaretPosition)
         select(originalCaretPosition, newCaretPosition);
      else
         select(newCaretPosition, originalCaretPosition);
    }
    else
      performDefaultKeyStrokeAction(KeyEvent.VK_HOME, KeyEvent.SHIFT_MASK, e);
  }
}

private class ShiftEndAction extends AbstractAction{
  public void actionPerformed(ActionEvent e) {
    if (caretWithinTableCell()) {
      int originalCaretPosition = getCaretPosition();
      if (tryDefaultKeyStrokeActionWithinCell(KeyEvent.VK_END, KeyEvent.SHIFT_MASK, e)) 
        return; 
      int newCaretPosition = getCurrentParagraphElement().getEndOffset()-1;
      if (newCaretPosition > originalCaretPosition)
         select(originalCaretPosition, newCaretPosition);
      else
         select(newCaretPosition, originalCaretPosition);
    }
    else
      performDefaultKeyStrokeAction(KeyEvent.VK_END, KeyEvent.SHIFT_MASK, e);
  }
}

/* ------- list manipulation start ------------------- */

  /**
   * apply a set of attributes to the list the caret is
   * currently in (if any)
   *
   * @param a  the set of attributes to apply
   */
  public void applyListAttributes(AttributeSet a) {
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    Element list = listManager.getListElement(getSelectionStart());
    if(list != null) {
      if(a.getAttributeCount() > 0) {
        doc.addAttributes(list, a);
        /**
         * for some reason above code does not show the changed attributes
         * of the table, although the element really has them (maybe somebody
         * could let me know why...). Therefore we update the editor pane
         * contents comparably rude (any other more elegant alternatives
         * welcome!)
         *
         * --> found out why: the swing package does not render short hand
         *                    properties such as MARGIN or PADDING. When
         *                    contained in a document inside an AttributeSet
         *                    they already have to be split into MARGIN-TOP,
         *                    MARGIN-LEFT, etc.
         *                    adjusted AttributeComponents accordingly so
         *                    we don't need refresh anymore
         */
        //refresh();
      }
    }
  }

  /**
   * <code>Action</code> to create a new paragraph element, which
   * may be a paragraph proper or a list item.
   */
  private class NewParagraphAction extends AbstractAction {
    /** construct a <code>NewParagraphAction</code> */
    public NewParagraphAction() {
    }

    /**
     * create a new list item, when the caret is inside a list
     *
     * <p>The new item is created after the item at the caret position</p>
     */
    public void actionPerformed(ActionEvent ae) {
      try {
        int caretPosition = getCaretPosition();

        // Turn paragraph starting with "* " into a bullet list.
        Element paragraphElement = getCurrentParagraphElement();
        String content = elementToHTML(paragraphElement);
        if (content.matches("(?ims)\\s*<p[^>]*>\\s*\\* .*</p>\\s*")) {
          final String newContent = "<ul><li>"+
          content.replaceAll("(?ims)\\s*<p[^>]*>\\s*\\* (.*)</p>\\s*","$1")+"</li></ul>";
          getSHTMLDocument().setOuterHTML(paragraphElement, newContent );
          setCaretPosition(paragraphElement.getEndOffset()-1);
          return; 
        }        
        
        // Turn paragraph starting with "1. " into a numbered list.
        paragraphElement = getCurrentParagraphElement();
        content = elementToHTML(paragraphElement);
        if (content.matches("(?ims)\\s*<p[^>]*>\\s*1\\. .*</p>\\s*")) {
          final String newContent = "<ol><li>"+
          content.replaceAll("(?ims)\\s*<p[^>]*>\\s*1\\. (.*)</p>\\s*","$1")+"</li></ol>";
          getSHTMLDocument().setOuterHTML(paragraphElement, newContent );
          setCaretPosition(paragraphElement.getEndOffset()-1);
          return; 
        }      
        
        // if we are in a list, create a new item
        Element listItemElement = listManager.getListItemElement(caretPosition);
        if (listItemElement != null) {
          listManager.newListItem();
          return;
        }
        // we are not in a list, call alternate action
        else {          
          performDefaultKeyStrokeAction(KeyEvent.VK_ENTER, 0, ae);
        }
      }
      catch(Exception e) {
        Util.errMsg(null, e.getMessage(), e);
      }
    }
  }

  /**
   * toggle list formatting on or off for the currently
   * selected text portion.
   *
   * <p>Switches list display on for the given type, if the selection
   * contains parts not formatted as list or parts formatted as list
   * of another type.</p>
   *
   * <p>Switches list formatting off, if the selection contains
   * only parts formatted as list of the given type.</p>
   *
   * @param listTag  the list tag type to toggle on or off (UL or OL)
   * @param attributeSet  the attributes to use for the list to toggle to
   * @param forceOff  indicator for toggle operation. If true, possibly
   * exisiting list formatting inside the selected parts always is switched
   * off. If false, the method decides, if list formatting for the parts
   * inside the selection needs to be switched on or off.
   */
  public void toggleList(final String listTag, final AttributeSet attributeSet, final boolean forceOff) {
      listManager.toggleList(listTag, attributeSet, forceOff);
  }


  /** range indicator for applying attributes to the current cell only */
  public static final int THIS_CELL = 0;

  /** range indicator for applying attributes to cells of the current column only */
  public static final int THIS_COLUMN = 1;

  /** range indicator for applying attributes to cells of the current row only */
  public static final int THIS_ROW = 2;

  /** range indicator for applying attributes to all cells */
  public static final int ALL_CELLS = 3;

  /** default table width */
  public static final String DEFAULT_TABLE_WIDTH = "80%";

  /** default vertical alignment */
  public static final String DEFAULT_VERTICAL_ALIGN = "top";

  /**
   * Insert a new table.
   *
   * @param colCount the number of columns the new table shall have
   */
  public void insertNewTable(int colCount) {
    final int selectionStart = getSelectionStart();
    int start = selectionStart;
    StringWriter sw = new StringWriter();
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    SHTMLWriter w = new SHTMLWriter(sw, doc);
    // some needed constants
    String table = HTML.Tag.TABLE.toString();
    String tr = HTML.Tag.TR.toString();
    String td = HTML.Tag.TD.toString();
    String th = HTML.Tag.TH.toString();        
    String p = HTML.Tag.P.toString();        

    boolean insertPlainTable = !Util.preferenceIsTrue("table.insertStyle","true");
    boolean insertTableHeader = Util.preferenceIsTrue("table.insertHeader");

    try {

      // the attribute set to use for applying attributes to tags
      SimpleAttributeSet tableAttributeSet = new SimpleAttributeSet();
      // build table attribute
      Util.styleSheet().addCSSAttribute(tableAttributeSet,  CSS.Attribute.WIDTH, DEFAULT_TABLE_WIDTH);
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_STYLE, "solid");
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_TOP_WIDTH, "0");
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_RIGHT_WIDTH, "0");
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_BOTTOM_WIDTH, "0");
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_LEFT_WIDTH, "0");
      tableAttributeSet.addAttribute(HTML.Attribute.BORDER, "0");

      w.writeStartTag(table, insertPlainTable?null:tableAttributeSet);

      // get width of each cell according to column count
      // build cell width attribute
      Util.styleSheet().addCSSAttribute(tableAttributeSet,
          CSS.Attribute.WIDTH,
          Integer.toString(100 / colCount) + Util.pct);
      tableAttributeSet.addAttribute(HTML.Attribute.VALIGN, DEFAULT_VERTICAL_ALIGN);
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_TOP_WIDTH, "1");
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_RIGHT_WIDTH, "1");
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_BOTTOM_WIDTH, "1");
      Util.styleSheet().addCSSAttribute(tableAttributeSet, CSS.Attribute.BORDER_LEFT_WIDTH, "1");

      SimpleAttributeSet pSet = new SimpleAttributeSet();
      Util.styleSheet().addCSSAttribute(pSet, CSS.Attribute.MARGIN_TOP, "1");
      Util.styleSheet().addCSSAttribute(pSet, CSS.Attribute.MARGIN_RIGHT, "1");
      Util.styleSheet().addCSSAttribute(pSet, CSS.Attribute.MARGIN_BOTTOM, "1");
      Util.styleSheet().addCSSAttribute(pSet, CSS.Attribute.MARGIN_LEFT, "1");
      tableAttributeSet.removeAttribute(HTML.Attribute.BORDER);

      if (insertTableHeader) {
        w.writeStartTag(tr, null);          
        for(int i=0; i<colCount; i++) {
          w.writeStartTag(th, insertPlainTable?null:tableAttributeSet);
          w.writeStartTag(p, insertPlainTable?null:pSet);
          w.writeEndTag(p);
          w.writeEndTag(th);
        }
        w.writeEndTag(tr);            
      }
      
      w.writeStartTag(tr, null);          
      for(int i=0; i<colCount; i++) {
        w.writeStartTag(td, insertPlainTable?null:tableAttributeSet);
        w.writeStartTag(p, insertPlainTable?null:pSet);
        w.writeEndTag(p);
        w.writeEndTag(td);
      }
      w.writeEndTag(tr);
      w.writeEndTag(table);

      // read table html into document
      Element para = doc.getParagraphElement(selectionStart);
      if(para == null) {
        throw new Exception("no text selected");
      }
      for(Element parent = para.getParentElement();
      ! parent.getName().equalsIgnoreCase(HTML.Tag.BODY.toString())
      && !parent.getName().equalsIgnoreCase(HTML.Tag.TD.toString());
      para = parent, parent = parent.getParentElement()
      );

      if(para != null) {
        try{
          doc.startCompoundEdit();
          doc.insertBeforeStart(para, sw.getBuffer().toString());
        }
        catch(Exception e) {
          Util.errMsg(null, e.getMessage(), e);
        }
        finally{
          doc.endCompoundEdit();
        }
//      w.write(para);
//doc.replaceHTML(para, 1, sw.getBuffer().toString());
      }
    }
    catch(Exception ex) {
      Util.errMsg(null, ex.getMessage(), ex);
    }
    select(start, start);
  }

  /**
   * apply a new anchor to the currently selected text
   *
   * <p>If nothing is selected, this method does nothing</p>
   *
   * @param anchorName  the name of the new anchor
   */
  public void insertAnchor(String anchorName) {
    if(getSelectionStart() != getSelectionEnd()) {
      SimpleAttributeSet aSet = new SimpleAttributeSet();
      aSet.addAttribute(HTML.Attribute.NAME, anchorName);
      SimpleAttributeSet set = new SimpleAttributeSet();
      set.addAttribute(HTML.Tag.A, aSet);
      applyAttributes(set, false);
    }
  }

  /**
   * insert a line break (i.e. a break for which paragraph
   * spacing is not applied)
   */
  public void insertBreak() {
    int caretPos = getCaretPosition();
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    try {
      ((SHTMLEditorKit) getEditorKit()).insertHTML(
          doc, caretPos, "<BR>", 0, 0, HTML.Tag.BR);
    }
    catch(Exception e) {}
    setCaretPosition(caretPos + 1);
  }

  /**
   * set a text link at the current selection replacing the selection
   * with a given text.
   *
   * <p>If nothing is selected, but the caret is inside a link, this will
   * replace the existing link. If nothing is selected and the caret
   * is not inside a link, this method does nothing.</p>
   *
   * @param linkText  the text that shall appear as link at the current selection
   * @param href  the target this link shall refer to
   * @param className  the style class to be used
   */
  public void setLink(String linkText, String href, String className) {
    setLink(linkText, href, className, null, null);
  }

  /**
   * Sets a hyperlink at the current selection, replacing the selection
   * with the given text or image.
   *
   * @param linkText  the text to show as link (or null, if an image shall appear instead)
   * @param href  the link reference
   * @param className  the style name to be used for the link
   * @param linkImage  the file name of the image be used for the link (or null, if a text link is to be set instead)
   * @param size  the size of the image or null
   */
  public void setLink(String linkText, String href, String className, String linkImage, Dimension size) {
    if(linkImage == null) 
      setTextLink(getCurrentLinkElement(), href, className, linkText, getSHTMLDocument());    
    else 
      setImageLink(getSHTMLDocument(), getCurrentLinkElement(), href, className, linkImage, size);
    
  }

  /**
   * set an image link replacing the current selection
   *
   * @param doc  the document to apply the link to
   * @param e  the link element found at the selection, or null if none was found
   * @param href  the link reference
   * @param className  the style name to be used for the link
   * @param linkImage  the file name of the image be used for the link
   * @param size  the size of the image
   */
  private void setImageLink(SHTMLDocument doc, Element e, String href, String className, String linkImage, Dimension size) {
    String a = HTML.Tag.A.toString();
    SimpleAttributeSet set = new SimpleAttributeSet();
    set.addAttribute(HTML.Attribute.HREF, href);
    set.addAttribute(HTML.Attribute.CLASS, className);
    StringWriter sw = new StringWriter();
    SHTMLWriter w = new SHTMLWriter(sw, doc);
    try {
      w.writeStartTag(a, set);
      set = new SimpleAttributeSet();
      set.addAttribute(HTML.Attribute.SRC,
                       Util.getRelativePath(new File(doc.getBase().getFile()), new File(linkImage)));
      set.addAttribute(HTML.Attribute.BORDER, "0");
      if(size != null) {
        set.addAttribute(HTML.Attribute.WIDTH, Integer.toString(new Double(size.getWidth()).intValue()));
        set.addAttribute(HTML.Attribute.HEIGHT, Integer.toString(new Double(size.getHeight()).intValue()));
      }
      w.writeStartTag(HTML.Tag.IMG.toString(), set);
      w.writeEndTag(a);
      if(e != null) {
        System.out.println("SHTMLEditorPane.setImageLink setOuterHTML html='" + sw.getBuffer() + "'");
        doc.setOuterHTML(e, sw.getBuffer().toString());
      }
      else {
        int start = getSelectionStart();
        if(start < getSelectionEnd()) {
          replaceSelection("");
          System.out.println("SHTMLEditorPane.setImageLink insertAfterEnd html='" + sw.getBuffer() + "'");
          doc.insertAfterEnd(doc.getCharacterElement(start), sw.getBuffer().toString());
        }
      }
    }
    catch(Exception ex) {
      Util.errMsg(this, ex.getMessage(), ex);
    }
  }

  /**
   * set a text link replacing the current selection
   *
   * @param e  the link element found at the selection, or null if none was found
   * @param href  the link reference
   * @param className  the style name to be used for the link
   * @param linkText  the text to show as link
   * @param doc  the document to apply the link to
   */
  private void setTextLink(Element e, String href, String className, String linkText, SHTMLDocument doc) {
    SimpleAttributeSet aSet = new SimpleAttributeSet();
    aSet.addAttribute(HTML.Attribute.HREF, href);
    String sStyleName = Util.getResourceString("standardStyleName");
    if(className != null && !className.equalsIgnoreCase(sStyleName)) {
      aSet.addAttribute(HTML.Attribute.CLASS, className);
    }
    SimpleAttributeSet set = new SimpleAttributeSet();
    if(e != null) {
      // replace existing link
      set.addAttributes(e.getAttributes());
      set.addAttribute(HTML.Tag.A, aSet);
      int start = e.getStartOffset();
      try {
        doc.replace(start, e.getEndOffset() - start, linkText, set);
      }
      catch(BadLocationException ex) {
        Util.errMsg(this, ex.getMessage(), ex);
      }
    }
    else {
      // create new link for text selection
      int start = getSelectionStart();
      if(start < getSelectionEnd()) {
        set.addAttribute(HTML.Tag.A, aSet);
        replaceSelection(linkText);
        doc.setCharacterAttributes(start, linkText.length(), set, false);
      }
    }
  }

  /**
   * remove an anchor with a given name
   *
   * @param anchorName  the name of the anchor to remove
   */
  public void removeAnchor(String anchorName) {
    //System.out.println("SHTMLEditorPane removeAnchor");
    AttributeSet attrs;
    Object nameAttr;
    Object link;
    ElementIterator eli = new ElementIterator(getDocument());
    Element elem = eli.first();
    while(elem != null) {
      attrs = elem.getAttributes();
      link = attrs.getAttribute(HTML.Tag.A);
      if(link != null /*&& link.toString().equalsIgnoreCase(HTML.Tag.A.toString())*/) {
        //System.out.println("found anchor attribute");
        nameAttr = ((AttributeSet) link).getAttribute(HTML.Attribute.NAME);
        if(nameAttr != null && nameAttr.toString().equalsIgnoreCase(anchorName)) {
          // remove anchor here
          //System.out.println("removing anchor name=" + nameAttr);
          SimpleAttributeSet newSet = new SimpleAttributeSet(attrs);
          newSet.removeAttribute(HTML.Tag.A);
          SHTMLDocument doc = (SHTMLDocument) getDocument();
          int start = elem.getStartOffset();
          doc.setCharacterAttributes(elem.getStartOffset(), elem.getEndOffset() - start, newSet, true);
        }
      }
      elem = eli.next();
    }
  }

  /**
   * insert a table column before the current column
   * (if any)
   */
  public void insertTableColumn() {
    Element cell = getCurrentTableCell();
    if(cell != null) {
      createTableColumn(cell, Util.getElementIndex(cell)/*getColNumber(cell)*/, true);
    }
  }

  /**
   * append a table column after the last column
   * (if any)
   */
  public void appendTableColumn() {
    Element cell = getCurrentTableCell();
    if(cell != null) {
      Element lastCell = getLastTableCell(cell);
      createTableColumn(lastCell, Util.getElementIndex(cell)/*getColNumber(lastCell)*/, false);
    }
  }

  /**
   * create a table column before or after a given column
   *
   * the width of the first cell in the column
   * (if there is a width attribute) is split into
   * half so that the new column and the column
   * inserted before are sharing the space originally
   * taken by the column inserted before.
   *
   * @param cell  the cell to copy from
   * @param cIndex  the number of the column 'cell' is in
   * @param before  true indicates insert before, false append after
   */
  private void createTableColumn(Element cell, int cIndex, boolean before) {

    // get the new width setting for this column and the new column
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    doc.startCompoundEdit();
    Element table = cell.getParentElement().getParentElement();
    Element srcCell = table.getElement(0).getElement(cIndex);
    SimpleAttributeSet set = new SimpleAttributeSet();
    Object attr = set.getAttribute(CSS.Attribute.WIDTH);
    if(attr != null) {
      //LengthValue lv = new LengthValue(attr);
      //String unit = lv.getUnit();
      //int width = (int) lv.getAttrValue(attr.toString(), unit);
      int width = (int) Util.getAbsoluteAttrVal(attr);  // Util.getAttrValue(attr);
      //System.out.println("SHTMLEditorPane.createTableColumn width=" + width);
      String unit = Util.getLastAttrUnit();
      //System.out.println("SHTMLEditorPane.createTableColumn unit=" + unit);
      String widthString = Integer.toString(width / 2) + unit;
      //System.out.println("SHTMLEditorPane.createTableColumn widthString=" + widthString);
      Util.styleSheet().addCSSAttribute(set, CSS.Attribute.WIDTH,
          widthString);
    }

    // adjust width and insert new column
    //SimpleAttributeSet a = new SimpleAttributeSet(set.copyAttributes());
    //int cellIndex = getCellIndex(srcCell);
    //boolean insertFirst = (before && (cellIndex == 0));
    for(int rIndex = 0; rIndex < table.getElementCount(); rIndex++) {
      srcCell = table.getElement(rIndex).getElement(cIndex);
      /*
      if(rIndex > 0) {
        adjustBorder(a, a, a, CombinedAttribute.ATTR_TOP);
      }
      adjustBorder(a, a, a, CombinedAttribute.ATTR_LEFT);
      */
      doc.addAttributes(srcCell, set);
      try {
        if(before) {
          doc.insertBeforeStart(srcCell, createTableCellHTML(srcCell));
        }
        else {
          doc.insertAfterEnd(srcCell, createTableCellHTML(srcCell));
        }
      }
      catch(IOException ioe) {
        Util.errMsg(null, ioe.getMessage(), ioe);
      }
      catch(BadLocationException ble) {
        Util.errMsg(null, ble.getMessage(), ble);
      }
    }
    //adjustColumnBorders(table.getElement(0).getElement(cIndex));
    //adjustColumnBorders(table.getElement(0).getElement(++cIndex));
    doc.endCompoundEdit();
  }

  /**
   * Appends a row to a table if the caret is inside a table.
   */
  public void appendTableRow() {
    Element cell = getCurrentTableCell();
    if(cell != null) {
      Element table = cell.getParentElement().getParentElement();
      Element lastRow = table.getElement(table.getElementCount()-1);
      createTableRow(lastRow, Util.getRowIndex(lastRow.getElement(0)), false, null);
    }
  }

  /**
   * Inserts a row to a table, assuming the caret is currently
   * inside a table.
   */
  public void insertTableRow(String forcedCellName) {
    Element cell = getCurrentTableCell();
    if(cell != null) {
      createTableRow(cell.getParentElement(), Util.getRowIndex(cell), true, forcedCellName);
    }
  }

  /**
   * Creates a new table row, inserting it before, or appending after the given
   * row, depending on a parameter.
   *
   * Is shared by appendRow and insertRow actions.
   *
   * @param srcRow  the row element to copy from
   * @param before  true indicates insert before, false append after
   * @param forcedCellName if non-null, that cell name will be used in the new table row. Values: "td", "th".
   */
  private void createTableRow(Element srcRow, int rowIndex, boolean before, String forcedCellName) {
    try {
      if (before) {
        getSHTMLDocument().insertBeforeStart(srcRow,
            createTableRowHTML(srcRow, forcedCellName));
        if(rowIndex == 0) {
          rowIndex++;
        }
      }
      else {
        getSHTMLDocument().insertAfterEnd(srcRow,
            createTableRowHTML(srcRow, forcedCellName));
        rowIndex++;
      }
    }
    catch(IOException ioe) {
      Util.errMsg(null, ioe.getMessage(), ioe);
    }
    catch(BadLocationException ble) {
      Util.errMsg(null, ble.getMessage(), ble);
    }
  }

  /**
   * Returns the HTML string of the given table row, without the cell contents, possibly
   * forcing the cell name on "td" or "th", if the parameter for forced cell name is non-null.
   *
   * For each table column found in srcRow, a start and end
   * tag TD is created with the same attributes as in the
   * column found in srcRow. The attributes of srcRow
   * are applied to the newly created row HTML string as well.
   *
   * @param modelRow  the table row Element to copy from
   * @param insert  indicates if a row is inserted before another row
   *
   * @return an HTML string representing the new table row
   * (without cell contents)
   */
  private String createTableRowHTML(Element modelRow, String forcedCellName) {
    StringWriter stringWriter = new StringWriter();
    SHTMLWriter shtmlWriter = new SHTMLWriter(stringWriter, (SHTMLDocument)getDocument());
    String tr = HTML.Tag.TR.toString();
    try {
    	shtmlWriter.writeStartTag(tr, modelRow.getAttributes());
    	for(int i = 0; i < modelRow.getElementCount(); i++) {
    		final Element modelCell = modelRow.getElement(i);
    		String cellName = forcedCellName!=null?forcedCellName:modelCell.getName();
    		createTableCellHTML(shtmlWriter, modelCell, cellName);
    	}
      shtmlWriter.writeEndTag(tr);
    }
    catch(IOException ex) {
      Util.errMsg(null, ex.getMessage(), ex);
    }
    return stringWriter.getBuffer().toString();
  }

  private void createTableCellHTML(SHTMLWriter w, final Element cell, String cellName) throws IOException {
	  {
		  w.writeStartTag(cellName, cell.getAttributes());
		  final Element paragraph = cell.getElement(0);
		  final String parName = "p"; //Was: paragraph.getName(); UL and OL are parNames not to be copied. --Dan
		  if(!parName.equalsIgnoreCase(HTML.Tag.IMPLIED.toString())){
			  w.writeStartTag(parName, paragraph.getAttributes());
			  w.writeEndTag(parName);
		  }
		  w.writeEndTag(cellName);
	  }
  }

  /**
   * build an HTML string copying from an existing table cell
   *
   * @param srcCell the cell to get the HTML for
   * @param a  set of attributes to copy if we are inserting first table column
   * @param insertFirst  indicates if we are inserting first table column
   * @param rNum  number of row a cell is to be inserted to
   *     (can be any value if insertFirst is false)
   *
   * @return the HTML string for the given cell (without cell contents)
   */
  private String createTableCellHTML(Element srcCell)
  {
    StringWriter sw = new StringWriter();
    SHTMLWriter w = new SHTMLWriter(sw, (SHTMLDocument)getDocument());
    try {
    	createTableCellHTML(w, srcCell, srcCell.getName());
    }
    catch(IOException e) {
    	Util.errMsg(null, e.getMessage(), e);
    }
    //System.out.println("getTableCellHTML buffer='" + sw.getBuffer().toString() + "'");
    return sw.getBuffer().toString();
  }  

  /**
   * delete the row of the table the caret is currently in (if any)
   */
  public void deleteTableRow() {
    Element cell = getCurrentTableCell();
    int finalCaretPosition = cell.getStartOffset();
    if(cell != null) {
      removeElement(cell.getParentElement());
      setCaretPosition(finalCaretPosition);
    }
  }

  /**
   * delete the column of the table the caret is currently in (if any)
   *
   * <p>width of adjacent column is adjusted, if there is more than one
   * column in the table. Width adjustment only works, if width
   * attributes of both the column to remove and its adjacent column
   * have the same unit (pt or %).</p>
   *
   * <p>If there is only one cell or if the caret is not in a table,
   * this method does nothing</p>
   *
   * <p>Smart border handling automatically sets the left border of a cell
   * to zero, if the cell on the left of that cell has a right border and
   * both cells have no margin. In that case removing the first column
   * will cause all cells of the new first column to have no left border.</p>
   */
  public void deleteTableCol() {
    Element cell = getCurrentTableCell();
    if(cell == null)
      return;

    Element row = cell.getParentElement();
    int lastColIndex = row.getElementCount() - 1;

    if(lastColIndex <= 0) 
      return;

    int cIndex = Util.getElementIndex(cell); //getColNumber(cell);
    int offset = -1; // adjacent cell is left of current cell
    if(cIndex == 0) { // if current cell is in first column...
      offset *= -1; // ...adjacent cell is right of current cell
    }

    Object attrC = cell.getAttributes().getAttribute(CSS.Attribute.WIDTH);
    Object attrA = row.getElement(cIndex + offset).getAttributes().
    getAttribute(CSS.Attribute.WIDTH);
    SimpleAttributeSet set = null;

    if(attrC != null && attrA != null) {
      //LengthValue lvC = new LengthValue(attrC);
      //LengthValue lvA = new LengthValue(attrA);
      int widthC = (int) Util.getAbsoluteAttrVal(attrC);  // Util.getAttrValue(attrC);
      String cUnit = Util.getLastAttrUnit();
      //String cUnit = lvC.getUnit();
      int widthA = (int)  Util.getAbsoluteAttrVal(attrA); // Util.getAttrValue(attrA);
      String aUnit = Util.getLastAttrUnit();
      if(aUnit.equalsIgnoreCase(cUnit)) {
        int width = 0;
        width += widthC;
        width += widthA;
        if(width > 0)
        {
          String widthString = Integer.toString(width) + cUnit;
          set = new SimpleAttributeSet(
              row.getElement(cIndex + offset).getAttributes());
          Util.styleSheet().addCSSAttribute(set, CSS.Attribute.WIDTH,
              widthString);
        }
      }
    }

    Element table = row.getParentElement();
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    doc.startCompoundEdit();

    if(cIndex < lastColIndex) {
      offset = 0;
    }

    for(int rIndex = table.getElementCount() - 1; rIndex >= 0; rIndex--) {
      row = table.getElement(rIndex);
      try {
        doc.removeElements(row, cIndex, 1);
        /*
            the following line does not work for the last column in a table
            so we use above code instead

            removeElement(row.getElement(cIndex));
         */
      }
      catch(BadLocationException ble) {
        Util.errMsg(null, ble.getMessage(), ble);
      }
      if(set != null) {
        doc.addAttributes(row.getElement(cIndex + offset), set);
      }
      //adjustColumnBorders(table.getElement(0).getElement(cIndex + offset));
    }
    
    doc.endCompoundEdit();

  }
  
  /** For each cell within the selection, turns a table data cell into a table header cell or vice
   * versa. */
  public void toggleTableHeaderCell () {
    int originalCaretPosition = getCaretPosition();
    
    int selectionStart = getSelectionStart();
    int selectionEnd = getSelectionEnd();
    
    Element tableCell = getTableCell(selectionStart);
    
    while (tableCell!=null && tableCell.getStartOffset() <= selectionEnd) {   
      String content = elementToHTML(tableCell);
      String newContent = content;
      if (content.matches("(?is)\\s*<td.*"))
        newContent = content.replaceFirst("(?is)^\\s*<td","<th").replaceFirst("(?is)</td>\\s*$","</th>");
      else if (content.matches("(?is)\\s*<th.*"))
        newContent = content.replaceFirst("(?is)^\\s*<th","<td").replaceFirst("(?is)</th>\\s*$","</td>");
      else {
        // Unexpected
      }
      Element row = tableCell.getParentElement();
      int tableCellIdx = row.getElementIndex(tableCell.getStartOffset()); 
      try {
        getSHTMLDocument().setOuterHTML(tableCell, newContent);
      } catch (Exception ex) {}
      tableCell = row.getElement(tableCellIdx); // Restore the cell reference after the operation.
      tableCell = getNextCell(tableCell);      
    }
    
    setCaretPosition(originalCaretPosition);
  }
  
  /** Moves the table row up. Does not treat multirow cells. */
  void moveTableRowUp() {
    Element tableCell = getCurrentTableCell();
    Element tableRow = tableCell.getParentElement();
    Element table = tableRow.getParentElement();
    int indexOfRowInTable = table.getElementIndex(getCaretPosition());
    if (indexOfRowInTable==0)
      return;
    try {
      getSHTMLDocument().startCompoundEdit();
      SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());
      writer.writeStartTag(table);
      for (int i=0;i<indexOfRowInTable-1;i++)
        writer.write(table.getElement(i));
      writer.write(tableRow);
      writer.write(table.getElement(indexOfRowInTable-1));
      for (int i=indexOfRowInTable+1;i<table.getElementCount();i++)
        writer.write(table.getElement(i));
      writer.writeEndTag(table);
      int offsetWithinCurrentRow = getCaretPosition() - tableRow.getStartOffset();
      int finalCaretPosition = table.getElement(indexOfRowInTable-1).getStartOffset() + offsetWithinCurrentRow;
      getSHTMLDocument().setOuterHTML(table, writer.getWrittenString());
      setCaretPosition(finalCaretPosition);
    } catch (Exception ex) {}
    finally {
      getSHTMLDocument().endCompoundEdit();
    }
  }
  
  /** Moves the table column left. Does not treat multicolumn cells. */
  void moveTableColumnLeft() {
    Element tableCell = getCurrentTableCell();
    Element tableRow = tableCell.getParentElement();
    Element table = tableRow.getParentElement();
    int indexOfCellInRow = tableRow.getElementIndex(getCaretPosition());
    if (indexOfCellInRow==0)
      return;
    try {
      getSHTMLDocument().startCompoundEdit();
      SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());
      writer.writeStartTag(table);
      for (int rowIdx=0;rowIdx<table.getElementCount();rowIdx++) {
        Element row = table.getElement(rowIdx);
        writer.writeStartTag(row);
        for (int i=0;i<indexOfCellInRow-1;i++)
          writer.write(row.getElement(i));
        writer.write(row.getElement(indexOfCellInRow));
        writer.write(row.getElement(indexOfCellInRow-1));
        for (int i=indexOfCellInRow+1;i<row.getElementCount();i++)
          writer.write(row.getElement(i));
        writer.writeEndTag(row);
      }
      writer.writeEndTag(table);    
      int offsetWithinCurrentCell = getCaretPosition() - tableCell.getStartOffset();
      int finalCaretPosition = tableRow.getElement(indexOfCellInRow-1).getStartOffset() + offsetWithinCurrentCell;
      getSHTMLDocument().setOuterHTML(table, writer.getWrittenString());
      setCaretPosition(finalCaretPosition);
    } catch (Exception ex) {}
    finally {
      getSHTMLDocument().endCompoundEdit();
    }
  }  
  
  /** Moves the table column right. Does not treat multicolumn cells. */
  void moveTableColumnRight() {
    Element tableCell = getCurrentTableCell();
    Element tableRow = tableCell.getParentElement();
    Element table = tableRow.getParentElement();
    int indexOfCellInRow = tableRow.getElementIndex(getCaretPosition());
    if (indexOfCellInRow==tableRow.getElementCount()-1)
      return;
    try {
      getSHTMLDocument().startCompoundEdit();
      SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());
      writer.writeStartTag(table);
      for (int rowIdx=0;rowIdx<table.getElementCount();rowIdx++) {
        Element row = table.getElement(rowIdx);
        writer.writeStartTag(row);
        for (int i=0;i<indexOfCellInRow;i++)
          writer.write(row.getElement(i));
        writer.write(row.getElement(indexOfCellInRow+1));
        writer.write(row.getElement(indexOfCellInRow));
        for (int i=indexOfCellInRow+2;i<row.getElementCount();i++)
          writer.write(row.getElement(i));
        writer.writeEndTag(row);
      }      
      Element cellToTheRight = tableRow.getElement(indexOfCellInRow+1);
      int finalCaretPosition = getCaretPosition() + cellToTheRight.getEndOffset()-cellToTheRight.getStartOffset();
      getSHTMLDocument().setOuterHTML(table, writer.getWrittenString());      
      setCaretPosition(finalCaretPosition);
    } catch (Exception ex) {}
    finally {
      getSHTMLDocument().endCompoundEdit();
    }
  }  
  
  /** Moves the table row down. Does not treat multirow cells. */
  void moveTableRowDown() {
    Element tableCell = getCurrentTableCell();
    if (tableCell==null)
      return;
    Element tableRow = tableCell.getParentElement();
    Element table = tableRow.getParentElement();
    int indexOfRowInTable = table.getElementIndex(getCaretPosition());
    if (indexOfRowInTable==table.getElementCount()-1)
      return;
    try {
      getSHTMLDocument().startCompoundEdit();
      SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());
      writer.writeStartTag(table);
      for (int i=0;i<indexOfRowInTable;i++)
        writer.write(table.getElement(i));
      writer.write(table.getElement(indexOfRowInTable+1));
      writer.write(tableRow);
      for (int i=indexOfRowInTable+2;i<table.getElementCount();i++)
        writer.write(table.getElement(i));
      writer.writeEndTag(table);
      Element rowBelow = table.getElement(indexOfRowInTable+1);
      int finalCaretPosition = getCaretPosition() + rowBelow.getEndOffset()-rowBelow.getStartOffset();      
      getSHTMLDocument().setOuterHTML(table, writer.getWrittenString());
      setCaretPosition(finalCaretPosition);
    } catch (Exception ex) {}
    finally {
      getSHTMLDocument().endCompoundEdit();
    }
  }  


  /**
   * Removes an element from the document of this editor. Removes it <i>manually</i> if the parent is not body.
   * Leaves it to the caller to set the caret position after the removal.
   */
  private void removeElement(Element element) {
    try {
      int start = element.getStartOffset();
      Element parent = element.getParentElement();
      if (parent.getName().equalsIgnoreCase("body")) 
        getSHTMLDocument().remove(start, element.getEndOffset() - start);
      else {
        // If the parent is not body, remove manually, to avoid quirks, e.g. in tables.
        int indexInParent = parent.getElementIndex(element.getStartOffset());
        SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());
        writer.writeStartTag(parent);
        for (int i=0; i< indexInParent; i++)
          writer.write(parent.getElement(i));
        for (int i=indexInParent+1; i< parent.getElementCount(); i++)
          writer.write(parent.getElement(i));
        writer.writeEndTag(parent);
        getSHTMLDocument().setOuterHTML(parent, writer.getWrittenString());
      }
    }
    catch(Exception ex) {
      Util.errMsg(null, ex.getMessage(), ex);
    }
  }
  

  /**
   * apply a set of attributes to the table the caret is
   * currently in (if any)
   *
   * @param a  the set of attributes to apply
   */
  public void applyTableAttributes(AttributeSet a) {
    Element cell = getCurrentTableCell();
    if(cell != null) {
      Element table = cell.getParentElement().getParentElement();
      if(a.getAttributeCount() > 0) {
        //System.out.println("applyTableAttributes count=" + a.getAttributeCount() + " a=" + a);
        ((SHTMLDocument) getDocument()).addAttributes(table, a);
        /**
         * for some reason above code does not show the changed attributes
         * of the table, although the element really has them (maybe somebody
         * could let me know why...). Therefore we update the editor pane
         * contents comparably rude (any other more elegant alternatives
         * welcome!)
         *
         * --> found out why: the swing package does not render short hand
         *                    properties such as MARGIN or PADDING. When
         *                    contained in a document inside an AttributeSet
         *                    they already have to be split into MARGIN-TOP,
         *                    MARGIN-LEFT, etc.
         *                    adjusted AttributeComponents accordingly so
         *                    we don't need refresh anymore
         */
        //refresh();
      }
    }
  }

  /**
   * refresh the whole contents of this editor pane with brute force
   */
  private void refresh() {
    int pos = getCaretPosition();
    String data = getText();
    setText("");
    setText(data);
    setCaretPosition(pos);
  }


  /**
   * apply a set of attributes to a given range of cells
   * of the table the caret is currently in (if any)
   *
   * @param a  the set of attributes to apply
   * @param range  the range of cells to apply attributes to
   */
  public void applyCellAttributes(AttributeSet a, int range) {
    //System.out.println("SHTMLEditorPane applyCellAttributes a=" + a);
    Element cell = getCurrentTableCell();
    int cIndex = 0;
    int rIndex = 0;
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    if(cell != null) {
      Element row = cell.getParentElement();
      Element table = row.getParentElement();
      Element aCell;
      switch(range) {
        case THIS_CELL:
          doc.addAttributes(cell, a);
          break;
        case THIS_ROW:
          for(cIndex = 0; cIndex < row.getElementCount(); cIndex++) {
            aCell = row.getElement(cIndex);
            doc.addAttributes(aCell, a);
          }
          break;
        case THIS_COLUMN:
          cIndex = Util.getElementIndex(cell); //getColNumber(cell);
          for(rIndex = 0; rIndex < table.getElementCount(); rIndex++) {
            aCell = table.getElement(rIndex).getElement(cIndex);
            doc.addAttributes(aCell, a);
          }
          break;
        case ALL_CELLS:
          while(rIndex < table.getElementCount()) {
            row = table.getElement(rIndex);
            cIndex = 0;
            while(cIndex < row.getElementCount()) {
              aCell = row.getElement(cIndex);
              //System.out.println("applyCellAttributes ALL_CELLS adjusted a=" + adjustCellBorders(aCell, a));
              doc.addAttributes(aCell, a);
              cIndex++;
            }
            rIndex++;
          }
          break;
      }
    }
  }

  public SHTMLDocument getSHTMLDocument() {
    return (SHTMLDocument)getDocument();
  }
  /**
   * Gets the number of the table column in which the given cell is located.
   *
   * @param cell  the cell to get the column number for
   * @return the column number of the given cell
   */
  private int getColNumber(Element cell) {
    int i = 0;
    Element thisRow = cell.getParentElement();
    int last = thisRow.getElementCount() - 1;
    Element aCell = thisRow.getElement(i);
    if(aCell != cell) {
      while((i < last) && (aCell != cell)) {
        aCell = thisRow.getElement(++i);
      }
    }
    return i;
  }

  /* ------- table manipulation end -------------------- */

  /* ------- table cell navigation start --------------- */

  /**
   * <code>Action</code> to move the caret from the current table cell
   * to the next table cell.
   */
  private class TabAction extends AbstractAction {

    /** action to use when not inside a table */
    /* removed for changes in J2SE 1.4.1
    private Action alternateAction;
     */

    /** construct a <code>NextTableCellAction</code> */
    public TabAction(String actionName) {
      super(actionName);
    }

    /**
     * construct a <code>NextTableCellAction</code>
     *
     * @param altAction  the action to use when the caret
     *      is not inside a table
     */
    /* removed for changes in J2SE 1.4.1
    public NextTableCellAction(Action altAction) {
      alternateAction = altAction;
    }
     */

    /**
     * move to the previous cell or invoke an alternate action if the
     * caret is not inside a table
     *
     * this will append a new table row when the caret
     * is inside the last table cell
     */
    public void actionPerformed(ActionEvent ae) {
      
      if (listManager.caretAtTheBeginningOfListItem()) {
        // Increase indent within list
        listManager.increaseIndent(true);
        return;
      }      
      
      Element cell = getCurrentTableCell();
      if(cell != null) {
        // Within a table cell.
        goNextCell(cell);
        return;
      }
      
      if (listManager.caretWithinListItem()) {
        // Increase indent within list
        listManager.increaseIndent(true);
        return;
      }      
      
      // Do nothing; above all, do not enter tab character.
      //performDefaultKeyStrokeAction(KeyEvent.VK_TAB, 0, ae);
      if (Util.preferenceIsTrue("table.insertNewOnTabKey", "false"))
        insertNewTable(3);

    }

  }

  /**
   * <code>Action</code> to move the caret from the current table cell
   * to the previous table cell.
   */
  private class ShiftTabAction extends AbstractAction {

    /** action to use when not inside a table */
    /* removed for changes in J2SE 1.4.1
    private Action alternateAction;
    */

    /** construct a <code>PrevTableCellAction</code> */
    public ShiftTabAction(String actionName) {
      super(actionName);
    }

    /**
     * construct a <code>PrevTableCellAction</code>
     *
     * @param altAction  the action to use when the caret
     *      is not inside a table
     */
    /* removed for changes in J2SE 1.4.1
    public PrevTableCellAction(Action altAction) {
      alternateAction = altAction;
    }
    */

    /**
     * Moves to the previous cell or invokes an alternate action if the
     * caret is not inside a table.
     */
    public void actionPerformed(ActionEvent ae) {
      
      // Decrease intent within list
      if (listManager.caretAtTheBeginningOfListItem()) {
        listManager.decreaseIndent(true);
        return;
      }            
      
      Element cell = getCurrentTableCell();
      if(cell != null) {
        goPrevCell(cell);
        return; 
      }

      // Decrease intent within list
      if (listManager.caretWithinListItem()) {
        listManager.decreaseIndent(true);
        return;
      }   
      
      performDefaultKeyStrokeAction(KeyEvent.VK_TAB, InputEvent.SHIFT_MASK, ae);
      
    }
  }

  /**
   * <code>Action</code> to create a new list item.
   */
  private class InsertLineBreakAction extends AbstractAction {

    /** construct a <code>NewListItemAction</code> */
    public InsertLineBreakAction() {
    }

    /**
     * create a new list item, when the caret is inside a list
     *
     * <p>The new item is created after the item at the caret position</p>
     */
    public void actionPerformed(ActionEvent ae) {
      try {
        SHTMLDocument doc = (SHTMLDocument) getDocument();
        int caretPosition = getCaretPosition();
        Element paragraphElement = doc.getParagraphElement(caretPosition);
        if(paragraphElement != null) {
          int so = paragraphElement.getStartOffset();
          int eo = paragraphElement.getEndOffset();
          if(so != eo) {
            StringWriter writer = new StringWriter();
            if(caretPosition > so){
              SHTMLWriter htmlStartWriter = new SHTMLWriter(writer, doc, so, caretPosition-so);
              htmlStartWriter.writeChildElements(paragraphElement);
            }
            // Workaround: <br> is written twice by Java.
            if(! doc.getCharacterElement(caretPosition).getName().equalsIgnoreCase(HTML.Tag.BR.toString())){
              writer.write("<br>");
            }	
            if(caretPosition < eo - 1){
              SHTMLWriter htmlEndWriter = new SHTMLWriter(writer, doc, caretPosition, eo - caretPosition);
              htmlEndWriter.writeChildElements(paragraphElement);
            }
            String text = writer.toString();
            try{
              doc.startCompoundEdit();
              doc.setInnerHTML(paragraphElement, text);
            }
            catch(Exception e) {
              Util.errMsg(null, e.getMessage(), e);
            }
            finally {
              doc.endCompoundEdit();
            }
            setCaretPosition(caretPosition + 1);
          }
        }
      }
      catch(Exception e) {
        Util.errMsg(null, e.getMessage(), e);
      }
    }
  }

  public void goNextCell(Element cell) {
    if (cell == getLastTableCell(cell)) {
      appendTableRow();
      setCaretPosition(getNextCell(cell).getStartOffset());
    }
    else
      setCaretPosition(getNextCell(cell).getStartOffset());
  }

  public void goPrevCell(Element cell) {
    int newPos;
    if(cell != getFirstTableCell(cell)) {
      cell = getPrevCell(cell);
      newPos = cell.getStartOffset();
      select(newPos, newPos);
    }
  }

  /**
   * Gets the table cell following the given table cell, continuing
   * on the next row if the cell is the last one in the row, returning
   * null if the cell is the last one in the table.
   *
   * @param cell  the cell whose following cell shall be found
   * @return the Element having the cell following the given cell or null
   *    if the given cell is the last cell in the table
   */
  private Element getNextCell(Element cell) {
    Element nextCell = null;
    Element row = cell.getParentElement();
    Element nextRow = null;
    Element table = row.getParentElement();
    final int lastCellIdx = row.getElementCount()-1;
    final Element lastCellInRow = row.getElement(lastCellIdx);
    if (lastCellInRow != cell) {
      // The cell is not the last one in the row.
      Element runningCell = lastCellInRow;
      int cellIdx = lastCellIdx;
      while ((cellIdx > 0) && (runningCell != cell)) {
        nextCell = runningCell;
        runningCell = row.getElement(--cellIdx);
      }
    }
    else {
      // The cell is the last one in the row.
      int rowIdx = table.getElementCount()-1;
      Element aRow = table.getElement(rowIdx);
      while((lastCellIdx > 0) && (aRow != row)) {
        nextRow = aRow;
        aRow = table.getElement(--rowIdx);
      }
      nextCell = nextRow==null ? null : nextRow.getElement(0);
    }
    return nextCell;
  }

  /**
   * get the table cell preceding a given table cell
   *
   * @param cell  the cell whose preceding cell shall be found
   * @return the Element having the cell preceding the given cell or null
   *    if the given cell is the first cell in the table
   */
  private Element getPrevCell(Element cell) {
    Element thisRow = cell.getParentElement();
    Element table = thisRow.getParentElement();
    Element prevCell = null;
    int i = 0;
    Element aCell = thisRow.getElement(i);
    if(aCell != cell) {
      while(aCell != cell) {
        prevCell = aCell;
        aCell = thisRow.getElement(i++);
      }
    }
    else {
      Element prevRow = null;
      Element aRow = table.getElement(i);
      while(aRow != thisRow) {
        prevRow = aRow;
        aRow = table.getElement(i++);
      }
      prevCell = prevRow.getElement(prevRow.getElementCount()-1);
    }
    return prevCell;
  }

  /**
   * get the last cell of the table a given table cell belongs to
   *
   * @param cell  a cell of the table to get the last cell of
   * @return the Element having the last table cell
   */
  private Element getLastTableCell(Element cell) {
    Element table = cell.getParentElement().getParentElement();
    Element lastRow = table.getElement(table.getElementCount()-1);
    Element lastCell = lastRow.getElement(lastRow.getElementCount()-1);
    return lastCell;
  }

  /**
   * get the first cell of the table a given table cell belongs to
   *
   * @param cell  a cell of the table to get the first cell of
   * @return the Element having the first table cell
   */
  private Element getFirstTableCell(Element cell) {
    Element table = cell.getParentElement().getParentElement();
    Element firstCell = table.getElement(0).getElement(0);
    return firstCell;
  }

  /**
   * Gets the table cell at the current caret position.
   *
   * @return the Element having the current table cell or null if
   *    the caret is not inside a table cell
   */
  public Element getCurrentTableCell() {
    return getTableCell(getCaretPosition());
  }
  /**
   * 
   */
  public Element getCurrentLinkElement() {
    Element element2 = null;
    Element element = getSHTMLDocument().getCharacterElement(getCaretPosition());
    Object linkAttribute = null; //elem.getAttributes().getAttribute(HTML.Tag.A);
    Object href = null;
    while (element != null && linkAttribute==null) {
      element2 = element;
      linkAttribute = element.getAttributes().getAttribute(HTML.Tag.A);
      if (linkAttribute != null)
        href = ((AttributeSet) linkAttribute).getAttribute(HTML.Attribute.HREF);
      element = element.getParentElement();
    }
    if(linkAttribute != null && href != null) 
      return element2;
    else 
      return null;
  }  
  
  /**
   * Gets the table cell element at the given position, or null if none.
   */  
  public Element getTableCell(int position) {
    final Element element = getSHTMLDocument().getCharacterElement(position);
    return Util.findElementUp("td", "th", element);
  }
  
  /** Gets the string URL of an existing link, or null if none. */
  String getURLOfExistingLink() {
    //setIgnoreActions(true);      
    Element linkElement = getCurrentLinkElement();
    boolean foundLink = (linkElement != null);
    if (!foundLink) return null;

    AttributeSet elemAttrs = linkElement.getAttributes();
    Object linkAttr = elemAttrs.getAttribute(HTML.Tag.A);
    Object href = ((AttributeSet) linkAttr).getAttribute(HTML.Attribute.HREF);
    return href != null?href.toString():null;        
  }      
  /** Gets the paragraph element in which the caret is located. */
  public Element getCurrentParagraphElement() {
    return getSHTMLDocument().getParagraphElement(getCaretPosition());    
  }

  /* ---------- table cell navigation end --------------*/

  
  /**
   * Replaces the currently selected content with new content
   * represented by the given <code>HTMLText</code>. If there is no selection
   * this amounts to an insert of the given text.  If there
   * is no replacement text this amounts to a removal of the
   * current selection.
   * This method overrides replaceSelection in <code>JEditorPane</code> for usage
   * of our own HTMLText object.
   *
   * @param replacementHTMLText  the content to replace the selection with
   */
  public void replaceSelection(HTMLText replacementHTMLText) {
    SHTMLDocument document = (SHTMLDocument) getDocument();
    Caret caret = getCaret();
    if (document != null) {
      try {
        int p0 = Math.min(caret.getDot(), caret.getMark());
        int p1 = Math.max(caret.getDot(), caret.getMark());
        if (p0 != p1) {
          document.remove(p0, p1 - p0);
        }
        if (replacementHTMLText != null) {
          pasteHTML(replacementHTMLText, p0);
        }
      }
      catch (Exception e) {
        getToolkit().beep();
      }
    }
  }
  
  /** */
  private void pasteHTML(final HTMLText pastedHTMLText, int position) throws Exception {

    SHTMLDocument sDocument = (SHTMLDocument) getDocument();

    if (!pastedHTMLText.usesStringRepresenation())  {
      pastedHTMLText.pasteHTML(sDocument, position);
      return;
    }

    // [ Uses string representation ]

    String pasteHtmlTextModified = pastedHTMLText.getHTMLText();
    Element characterElement = sDocument.getCharacterElement(position);
    Element paragraphElement = characterElement.getParentElement();

    if(position == paragraphElement.getStartOffset()) {

      if (caretWithinTableCell() && pastedHTMLText.isOneCellInOneRow()) {
        pasteHtmlTextModified = pasteHtmlTextModified.
        replaceAll("(?ims).*<td.*?>", "").
        replaceAll("(?ims)</td.*?>.*", "");      
      }

      // We are at the start of the paragraph to insert at.
      if (!HTMLText.containsParagraphTags(pasteHtmlTextModified)) { 
        sDocument.insertAfterStart(paragraphElement, pasteHtmlTextModified);
        // Remove whitespace before the end tag of paragraph element to avoid quircky behavior.       
        Element newParagraph = getCurrentParagraphElement();
        String fixedContent = elementToHTML(newParagraph).replaceAll("(?ims)\\s*</p>", "</p>");
        sDocument.setOuterHTML(newParagraph, fixedContent);
        //
        setCaretPosition(newParagraph.getEndOffset()-1);
        return;
      }
      // Contains paragraph tags.

      if (caretWithinTableCell() && pasteHtmlTextModified.matches("(?ims).*<table.*")) {
        // The condition above is simplistic.
        String strippedHTMLText = pasteHtmlTextModified.
        replaceAll("(?ims).*<table.*?>", "").
        replaceAll("(?ims)</table.*?>.*", "");
        Element cellElement = getCurrentTableCell();
        Element tableRowElement = cellElement.getParentElement();
        sDocument.insertBeforeStart(tableRowElement, strippedHTMLText);
        return;
      }
      // Contains paragraphs tags and
      // (a) is not within a table cell or
      // (b) the pasted content is not a table.
      sDocument.insertBeforeStart(paragraphElement, pasteHtmlTextModified);

      if (caretWithinTableCell()) {
        Element cellElement = getCurrentTableCell(); 
        Element lastElementInCell = cellElement.getElement(cellElement.getElementCount()-1);
        if (elementIsEmptyParagraph(lastElementInCell)) {
          // Remove empty paragraph at the end of the cell. A workaround.
          removeElement(lastElementInCell);
          setCaretPosition(cellElement.getEndOffset()-1);
        }
      }
      return;
    }

    if (paragraphElement.getEndOffset() == position + 1) {
      // We are at the end of the paragraph to insert at,
      if (HTMLText.containsParagraphTags(pasteHtmlTextModified))  
        sDocument.insertAfterEnd(paragraphElement, pasteHtmlTextModified);
      else
        sDocument.insertBeforeEnd(paragraphElement, pasteHtmlTextModified);
      return;
    }

    // We are somewhere else inside the paragraph to insert at.

    String newHtml = pastedHTMLText.splitPaste(sDocument, characterElement, paragraphElement,
        position, pasteHtmlTextModified, HTMLText.containsParagraphTags(pasteHtmlTextModified));
    int paragraphOldEndOffset = paragraphElement.getEndOffset();
    int oldCaretPosition = getCaretPosition();
    sDocument.setOuterHTML(paragraphElement, newHtml);
    Element newParagraphElement = getSHTMLDocument().getParagraphElement(oldCaretPosition);
    // Place the caret after the pasted text
    setCaretPosition(oldCaretPosition + newParagraphElement.getEndOffset() - paragraphOldEndOffset);
    
  }
  /* ------ start of drag and drop implementation -------------------------
              (see also constructor of this class) */

  /** enables this component to be a Drop Target */
  DropTarget dropTarget = null;

  /** enables this component to be a Drag Source */
  DragSource dragSource = null;

  /** the last selection start */
  private int lastSelStart = 0;

  /** the last selection end */
  private int lastSelEnd = 0;

  /** the location of the last event in the text component */
  private int dndEventLocation = 0;

  /**
   * <p>This flag is set by this objects dragGestureRecognizer to indicate that
   * a drag operation has been started from this object. It is cleared once
   * dragDropEnd is captured by this object.</p>
   *
   * <p>If a drop occurs in this object and this object started the drag
   * operation, then the element to be dropped comes from this object and thus
   * has to be removed somewhere else in this object.</p>
   *
   * <p>To the contrary if a drop occurs in this object and the drag operation
   * was not started in this object, then the element to be dropped does not
   * come from this object and has not to be removed here.</p>
   */
  private boolean dragStartedHere = false;

  /**
   * Initialize the drag and drop implementation for this component.
   *
   * <p>DropTarget, DragSource and DragGestureRecognizer are instantiated
   * and a MouseListener is established to track the selection in drag
   * operations</p>
   *
   * <p>this ideally is called in the constructor of a class which
   * would like to implement drag and drop</p>
   */
  public void initDnd() {
    dropTarget = new DropTarget (this, this);
    dragSource = new DragSource();
    dragSource.createDefaultDragGestureRecognizer(
                          this, DnDConstants.ACTION_MOVE, this);
    this.addMouseListener(new MouseAdapter() {
      public void mouseReleased(MouseEvent e) {
        this_mouseReleased(e);
      }
      public void mouseClicked(MouseEvent e) {
        this_mouseClicked(e);
      }
    });
  }

  /** a drag gesture has been initiated */
  public void dragGestureRecognized(DragGestureEvent event) {
    int selStart = getSelectionStart();
    try {
      if( (lastSelEnd > lastSelStart) &&
          (selStart >= lastSelStart) &&
          (selStart < lastSelEnd) )
      {
        dragStartedHere = true;
        select(lastSelStart, lastSelEnd);
        HTMLText text = new HTMLText();
        int start = getSelectionStart();
        text.copyHTML(this, start, getSelectionEnd() - start);
        HTMLTextSelection trans = new HTMLTextSelection(text);
        dragSource.startDrag(event, DragSource.DefaultMoveDrop, trans, this);
      }
    }
    catch(Exception e) {
      //getToolkit().beep();
    }
  }

  /**
   * this message goes to DragSourceListener, informing it that the dragging
   * has ended
   */
  public void dragDropEnd (DragSourceDropEvent event) {
    dragStartedHere = false;
  }

  /** is invoked when a drag operation is going on */
  public void dragOver (DropTargetDragEvent event) {
    dndEventLocation = viewToModel(event.getLocation());
    try {
      setCaretPosition(dndEventLocation);
    }
    catch(Exception e) {
      //getToolkit().beep();
    }
  }

  /**
   * a drop has occurred. If the dragged element has a suitable
   * <code>DataFlavor</code>, do the drop.
   *
   * @param event - the event specifiying the drop operation
   * @see java.awt.datatransfer.DataFlavor
   */
  public void drop(DropTargetDropEvent event) {
    dndEventLocation = viewToModel(event.getLocation());
    if( (dndEventLocation >= lastSelStart) &&
        (dndEventLocation <= lastSelEnd) )
    {
      event.rejectDrop();
      select(lastSelStart, lastSelEnd);
    }
    else {
        final SHTMLDocument doc = (SHTMLDocument)getDocument(); 
        doc.startCompoundEdit();
      try {
        Transferable transferable = event.getTransferable();
        if(transferable.isDataFlavorSupported(htmlTextDataFlavor)){
          HTMLText s = (HTMLText) transferable.getTransferData(htmlTextDataFlavor);
          doDrop(event, s);
        }
        else if(transferable.isDataFlavorSupported(DataFlavor.stringFlavor)){
          String s = (String)transferable.getTransferData(DataFlavor.stringFlavor);
          doDrop(event, s);
        }
        else {
          event.rejectDrop();
        }
      }
      catch (Exception exception) {
        exception.printStackTrace();
        event.rejectDrop();
      }
      finally{
          doc.endCompoundEdit();
      }
    }
  }

  /**
   * do the drop operation consisting of adding the dragged element and
   * necessarily removing the dragged element from the original position
   */
  private void doDrop(DropTargetDropEvent event, Object s) {
    int removeOffset = 0;
    int moveOffset = 0;
    int newSelStart;
    int newSelEnd;
    event.acceptDrop(DnDConstants.ACTION_MOVE);
    setCaretPosition(dndEventLocation);
    if(s instanceof HTMLText) {
      replaceSelection((HTMLText) s);
    }
    else if(s instanceof String) {
      replaceSelection((String) s);
    }
    if(dndEventLocation < lastSelStart) {
      removeOffset = s.toString().length();
    }
    else {
      moveOffset = s.toString().length();
    }
    newSelEnd = dndEventLocation + (lastSelEnd - lastSelStart) - moveOffset;
    newSelStart = dndEventLocation - moveOffset;
    if(dragStartedHere) {
      lastSelStart += removeOffset;
      lastSelEnd += removeOffset;
      select(lastSelStart, lastSelEnd);
      replaceSelection("");
    }
    lastSelEnd = newSelEnd;
    lastSelStart = newSelStart;
    select(lastSelStart, lastSelEnd);
    event.getDropTargetContext().dropComplete(true);
  }

  /** A groupping of list operations, mostly related to toggling of lists. */
  private class ListManager{
    //private Element parentElement;
    class SwitchListException extends Exception {
    }
    private ListManager(){
    }

    private Element getParagraphElement(int pos) {
      Element paragraphElement = getSHTMLDocument().getParagraphElement(pos);
      if(paragraphElement.getName().equalsIgnoreCase("p-implied")){
        paragraphElement = paragraphElement.getParentElement();
      }
      return paragraphElement;
    }

    /** */
    private Element getListParent(Element elem) {
      Element listParent = elem.getParentElement();
      if(elem.getName().equalsIgnoreCase(HTML.Tag.LI.toString()))
        listParent = listParent.getParentElement();      
      return listParent;
    }
    
    /** Gets the list item element at the caret position, or null if the position
     * is not within a list. */
    private Element getListItemElement(int caretPosition) {
      Element paragraphElement = getParagraphElement(caretPosition);
      return Util.findElementUp(HTML.Tag.LI.toString(), paragraphElement);

    }
    
    /** Gets the innermost list element at the caret position, or null if the position is not
     * within a list.
     */
    private Element getListElement(int caretPosition) {
      Element paragraphElement = getParagraphElement(caretPosition);
      Element listElement = Util.findElementUp("ul","ol", paragraphElement);
      return listElement;
    }

    private boolean isValidParentElement(Element e){
      final String name = e.getName();
      return name.equalsIgnoreCase(HTML.Tag.BODY.toString())
      || name.equalsIgnoreCase(HTML.Tag.TD.toString())
      || name.equalsIgnoreCase(HTML.Tag.LI.toString());
    }

    private boolean isValidElement(Element e){
      final String name = e.getName();
      return name.equalsIgnoreCase(HTML.Tag.P.toString())
      || name.equalsIgnoreCase(HTML.Tag.UL.toString())
      || name.equalsIgnoreCase(HTML.Tag.OL.toString())
      || name.equalsIgnoreCase(HTML.Tag.H1.toString())
      || name.equalsIgnoreCase(HTML.Tag.H2.toString())
      || name.equalsIgnoreCase(HTML.Tag.H3.toString())
      || name.equalsIgnoreCase(HTML.Tag.H4.toString())
      || name.equalsIgnoreCase(HTML.Tag.H5.toString())
      || name.equalsIgnoreCase(HTML.Tag.H6.toString());
    }

    private boolean isListRootElement(Element e){
      final String name = e.getName();
      return name.equalsIgnoreCase(HTML.Tag.UL.toString())
      || name.equalsIgnoreCase(HTML.Tag.OL.toString());
    }
    /**
     * Switches OFF list formatting for a given block of elements.
     *
     * <p>Switches off all list formatting inside the block for the
     * given tag.</p>
     *
     * <p>Splits lists if the selection covers only part of a list.</p>
     * @throws BadLocationException 
     * @throws IOException 
     */
    private void listOff() throws IOException, BadLocationException
    {            
      final int selectionStart = getSelectionStart();
      final int selectionEnd = getSelectionEnd();     
      final Element firstParagraphElement = getParagraphElement(selectionStart);      
      final int fistParagraphElementStart = firstParagraphElement.getStartOffset();
      final int lastParagraphElementEnd = getParagraphElement(selectionEnd).getEndOffset();
      final Element parentOfTheListElement = getListParent(firstParagraphElement);             
      
      if (isListElement(parentOfTheListElement))
        // Currently, no support for nested lists.
        return;
      
      SHTMLWriter sHTMLwriter = new SHTMLWriter(getSHTMLDocument());
      int elementIdx ;
      Element nextElement = null;
      for (elementIdx = 0; elementIdx < parentOfTheListElement.getElementCount(); elementIdx++){
        nextElement = parentOfTheListElement.getElement(elementIdx);
        if(nextElement.getEndOffset() > fistParagraphElementStart)
          break;
      }
      final Element startElementToBeReplaced = nextElement;
      
      int nrElementsToBeReplaced = 1;
      int listItemIdx = 0;
      Element listItemElement = null;
      if(nextElement.getStartOffset() < fistParagraphElementStart){
        sHTMLwriter.writeStartTag(nextElement);
        elementIdx++;
        // Write list item elements before the selection. 
        for(;;listItemIdx++){
          listItemElement = nextElement.getElement(listItemIdx);
          if(listItemElement.getStartOffset() == fistParagraphElementStart)
            break;          
          sHTMLwriter.write(listItemElement);
        }
        sHTMLwriter.writeEndTag(nextElement);
        // Turn the list item elements in the selection into paragraph element.
        for(;listItemIdx < nextElement.getElementCount();listItemIdx++){
          listItemElement = nextElement.getElement(listItemIdx);
          if(listItemElement.getStartOffset() >= lastParagraphElementEnd)
            break;          
          sHTMLwriter.writeStartTag("p", null);
          sHTMLwriter.writeChildElements(listItemElement);
          sHTMLwriter.writeEndTag("p");
        }
      }
      
      if(nextElement.getEndOffset() <= lastParagraphElementEnd){
        for (; elementIdx < parentOfTheListElement.getElementCount(); elementIdx++){
          nextElement = parentOfTheListElement.getElement(elementIdx);
          if(nextElement.getEndOffset() > lastParagraphElementEnd){
            break;
          }
          if(nextElement != startElementToBeReplaced && nextElement.getStartOffset() < lastParagraphElementEnd){
            nrElementsToBeReplaced++;
          }
          if(isListRootElement(nextElement)){
            for(listItemIdx = 0; listItemIdx < nextElement.getElementCount(); listItemIdx++){
              sHTMLwriter.writeStartTag("p", null);
              sHTMLwriter.writeChildElements(nextElement.getElement(listItemIdx));                          
              sHTMLwriter.writeEndTag("p");
            }
          }
          else{
            sHTMLwriter.writeStartTag("p", null);
            sHTMLwriter.writeChildElements(nextElement);
            sHTMLwriter.writeEndTag("p");
          }
        }
      }

      // nextElement is final at this point.
      
      if (elementIdx <= parentOfTheListElement.getElementCount() &&
          nextElement.getStartOffset() < lastParagraphElementEnd){ 
        if(nextElement != startElementToBeReplaced){
          nrElementsToBeReplaced++;
        }
        for(; listItemIdx < nextElement.getElementCount();listItemIdx++){
          listItemElement = nextElement.getElement(listItemIdx);
          if(listItemElement.getStartOffset() >= lastParagraphElementEnd)
            break;
          sHTMLwriter.writeStartTag("p", null);
          sHTMLwriter.writeChildElements(listItemElement);
          sHTMLwriter.writeEndTag("p");
        }
        if(listItemIdx < nextElement.getElementCount()){
          sHTMLwriter.writeStartTag(nextElement);
          for(;listItemIdx < nextElement.getElementCount();listItemIdx++){
            listItemElement = nextElement.getElement(listItemIdx);
            sHTMLwriter.write(listItemElement);
          }
          sHTMLwriter.writeEndTag(nextElement);
        }
      }
      
      getSHTMLDocument().replaceHTML(startElementToBeReplaced, nrElementsToBeReplaced,
          sHTMLwriter.getWrittenString()); 
    }

    /**
     * switch ON list formatting for a given block of elements.
     *
     * <p>Takes care of merging existing lists before, after and inside
     * respective element block.</p>
     * @throws BadLocationException 
     * @throws IOException 
     *
     */
    private void listOn(final String listTag, final AttributeSet attributeSet, final boolean forceOff)
        throws IOException, BadLocationException
    {
      
      final int selectionStart = getSelectionStart();
      final int selectionEnd = getSelectionEnd();
      final Element firstParagraphElement = getParagraphElement(selectionStart);
      
      int fistParagraphElementStart = firstParagraphElement.getStartOffset();
      int lastParagraphElementEnd = getParagraphElement(selectionEnd).getEndOffset();
      Element parentElement = getListParent(firstParagraphElement);             
      
      Element startElementToBeRemoved = null;
      int removeCount = 0;      
      SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());
      if(fistParagraphElementStart > 0){
        Element before = getParagraphElement(fistParagraphElementStart - 1);
        if(before.getName().equalsIgnoreCase(HTML.Tag.LI.toString())){
          final Element listRoot = before.getParentElement();
          if(listRoot.getParentElement()== parentElement && listRoot.getName().equalsIgnoreCase(listTag)){
            fistParagraphElementStart = listRoot.getStartOffset();
          }
        }
      }
      if(lastParagraphElementEnd < getSHTMLDocument().getLength() - 1){
        Element after = getParagraphElement(lastParagraphElementEnd);
        if(after.getName().equalsIgnoreCase(HTML.Tag.LI.toString())){
          final Element listRoot = after.getParentElement();
          if(listRoot.getParentElement()== parentElement && listRoot.getName().equalsIgnoreCase(listTag)){
            lastParagraphElementEnd = listRoot.getEndOffset();
          }
        }
      }
      int i ;
      Element next = null;
      for (i = 0; i < parentElement.getElementCount(); i++){
        next = parentElement.getElement(i);
        if(next.getEndOffset() > fistParagraphElementStart){
          break;
        }
      }
      startElementToBeRemoved = next;
      removeCount = 1;
      int j = 0;
      Element li = null;
      if(next.getStartOffset() < fistParagraphElementStart){
        i++;
        writer.writeStartTag(next);
        for(;;j++){
          li = next.getElement(j);
          if(li.getStartOffset() == fistParagraphElementStart){
            break;
          }
          writer.write(li);
        }
        writer.writeEndTag(next);
        writer.writeStartTag(listTag,attributeSet);
        for(;j < next.getElementCount();j++){
          li = next.getElement(j);
          if(li.getStartOffset() >= lastParagraphElementEnd){
            break;
          }
          writer.write(li);
        }
      }
      else{
        writer.writeStartTag(listTag,attributeSet);
      }

      if(next.getEndOffset() <= lastParagraphElementEnd){
        for (; i < parentElement.getElementCount(); i++){
          next = parentElement.getElement(i);
          if(next.getEndOffset() > lastParagraphElementEnd){
            break;
          }
          if(startElementToBeRemoved != next&& next.getStartOffset() < lastParagraphElementEnd){
            removeCount++;
          }
          if(isListRootElement(next)){
            writer.writeChildElements(next);
          }
          else{
            writer.writeStartTag("li", null);
            writer.writeChildElements(next);
            writer.writeEndTag("li");
          }
        }
      }
      if(i < parentElement.getElementCount() && next.getStartOffset() < lastParagraphElementEnd){
        if(startElementToBeRemoved != next){
          removeCount++;
        }
        for(; j < next.getElementCount();j++){
          li = next.getElement(j);
          if(li.getStartOffset() >= lastParagraphElementEnd){
            break;
          }
          writer.write(li);
        }
        writer.writeEndTag(listTag);
        if(j < next.getElementCount()){
          writer.writeStartTag(next);
          for(;j < next.getElementCount();j++){
            li = next.getElement(j);
            writer.write(li);
          }
          writer.writeEndTag(next);
        }
      }
      else{
        writer.writeEndTag(listTag);
      }

      getSHTMLDocument().replaceHTML(startElementToBeRemoved, removeCount,
          writer.getWrittenString());      
      
    }

    /**
     * decide to switch on or off list formatting
     * @return  true, if list formatting is to be switched on, false if not
     * @throws SwitchListException 
     */
    private boolean switchOn(final String listTag,
        final AttributeSet attributeSet,
        final boolean forceOff,
        int start,
        int end,
        Element parentElement) throws SwitchListException
    {
      boolean listOn = false;
      int count = parentElement.getElementCount();
      for(int i = 0; i < count && !listOn; i++) {
        Element elem = parentElement.getElement(i);
        if (elem.getStartOffset() >= start
            && elem.getEndOffset() <= end
            && !isValidElement(elem)) {
          throw new SwitchListException();
        }
        int eStart = elem.getStartOffset();
        int eEnd = elem.getEndOffset();
        if(!elem.getName().equalsIgnoreCase(listTag)) {
          if(((eStart > start) && (eStart < end)) ||
              ((eEnd > start) && (eEnd < end)) ||
              ((start >= eStart) && (end <= eEnd)))
          {
            listOn = true;
          }
        }
      }
      return listOn;
    }

    /** */
    private void toggleList(final String listTag, final AttributeSet attributeSet, final boolean forceOff) 
    {
      try {
        final int selectionStart = getSelectionStart();
        final int selectionEnd = getSelectionEnd();

        Element firstParagraphElement = getParagraphElement(selectionStart);
        int fistParagraphElementStart = firstParagraphElement.getStartOffset();
        int lastParagraphElementEnd = getParagraphElement(selectionEnd).getEndOffset();
        Element parentElement = getListParent(firstParagraphElement);       

        getSHTMLDocument().startCompoundEdit();
        
        // Why is OL not a valid parent element? How could a parent element be invalid? --Dan
        //if (!isValidParentElement(parentElement))
        //  throw new SwitchListException();

        if(selectionStart != selectionEnd){
          Element last = getParagraphElement(lastParagraphElementEnd-1);
          if(parentElement != getListParent(last)){
            throw new SwitchListException();
          }
        }
        //
        if (!switchOn(listTag, attributeSet, forceOff,
            fistParagraphElementStart, lastParagraphElementEnd, parentElement)
            || forceOff) 
          listOff();
        else 
          listOn(listTag, attributeSet, forceOff);
        
        if(selectionStart == selectionEnd) 
          setCaretPosition(selectionStart);          
        else 
          select(selectionStart, selectionEnd);          
        requestFocus();
      }
      catch(SwitchListException e) {
      }
      catch(Exception e) {
        Util.errMsg(null, e.getMessage(), e);
      }
      finally{
        getSHTMLDocument().endCompoundEdit();
      }
    }
    
    /** */
    private boolean isListElement(Element element) {
      return "ul".equalsIgnoreCase(element.getName()) ||
          "ol".equalsIgnoreCase(element.getName());
    }
    /** */
    private boolean isListItemElement(Element element) {
      return "li".equalsIgnoreCase(element.getName());
    }
    
    /** Determines whether the caret is currently at the beginning of a list item. */
    private boolean caretAtTheBeginningOfListItem() {
      Element parent = getCurrentParagraphElement().getParentElement();
      return ("li".equalsIgnoreCase(parent.getName()) &&
          getCaretPosition() == parent.getStartOffset());
    }
    
    /** Determines whether the caret is currently at the beginning of a list item. */
    private boolean caretWithinListItem() {
      Element parent = getCurrentParagraphElement().getParentElement();
      return ("li".equalsIgnoreCase(parent.getName()));
    }
    
 
    
    /** Increases the intent of selected list items. ("Including subitems" should default to "true".) */
    private void increaseIndent(boolean includingSubitems) {
      Element paragraphElement = getCurrentParagraphElement();
      Element listItemElement = paragraphElement.getParentElement();

      if (!isListItemElement(listItemElement))
        return;

      // Increase indent
      int selectionStart = getSelectionStart();
      int selectionEnd = getSelectionEnd();
      if (selectionStart!=selectionEnd &&
          getListElement(selectionStart) == getListElement(selectionEnd)) {
        // Of block
      }
      else {
        // Of single list item
        selectionStart = getCaretPosition();
        selectionEnd = getCaretPosition();
      }
            
      // 
      
      Element list = getListElement(selectionStart);
      int indexOfSelectionStart = list.getElementIndex(selectionStart);
      int indexOfSelectionEnd = list.getElementIndex(selectionEnd);
      try {
        getSHTMLDocument().startCompoundEdit();        
        SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());
        writer.writeStartTag(list);
        for (int i=0; i < indexOfSelectionStart-1; i++)
          writer.write(list.getElement(i));
        Element tagModel = null;
        if (indexOfSelectionStart > 0 &&
            isListElement(list.getElement(indexOfSelectionStart-1)))
          tagModel = list.getElement(indexOfSelectionStart-1);
        else if (indexOfSelectionEnd+1 < list.getElementCount() &&
            isListElement(list.getElement(indexOfSelectionEnd+1)))
          tagModel = list.getElement(indexOfSelectionEnd+1);
        else
          tagModel = list;
        // The list item before the selection start should be the new parent.
        if (indexOfSelectionStart==0)
          // Cannot increase indent of the first item in a list, unlike in MSO and OOo.
          return; 
        Element newParentListItem = list.getElement(indexOfSelectionStart-1);
        writer.writeStartTag(newParentListItem);
        writer.writeChildElements(newParentListItem);
        //
        writer.writeStartTag(tagModel);
        for (int i=indexOfSelectionStart; i <= indexOfSelectionEnd; i++)
          if (includingSubitems)
            writer.write(list.getElement(i));
          else {
            Element listItem = list.getElement(i);
            writer.writeStartTag(listItem);
            for (int j=0; j < listItem.getElementCount(); j++)
              if (!isListElement(listItem.getElement(j)))
                writer.write(listItem.getElement(j));
            writer.writeEndTag(listItem);
            for (int j=0; j < listItem.getElementCount(); j++)
              if (isListElement(listItem.getElement(j)))
                writer.writeChildElements(listItem.getElement(j));
          }          
        writer.writeEndTag(tagModel);
        writer.writeEndTag(newParentListItem);
        //
        for (int i=indexOfSelectionEnd+1; i < list.getElementCount(); i++)
          writer.write(list.getElement(i));
        writer.writeEndTag(list);                
        String newContent = writer.getWrittenString().
        replaceAll("(?ims)</ul>\\s*<ul[^>]*>","").
        replaceAll("(?ims)</ol>\\s*<ol[^>]*>","");
        getSHTMLDocument().setOuterHTML(list, newContent);
        select(selectionStart, selectionEnd);
      }
      catch (Exception ex) {  
      }
      finally {
        getSHTMLDocument().endCompoundEdit();
      }        
    }
    
    /** Decreases the indent of selected list items. ("Including subitems" should default to "true". */
    private void decreaseIndent(boolean includingSubitems) {

      // The parameter "includingSubitems should always be set to "true". The value of "false" currently causes
      // problems, in that it leads to the creation of incorrect HTML such as 
      // <ul><ul><li>c</li></ul></ul>; the consecutive ULs are incorrect.
      
      // Example:
      // * a                   <-- Outer list item, beginning the outer list.
      //   * b                 <-- Inner list. Cursor here, before "b".
      //     * c
      // * d

      // paragraphElement: P or P-implied.
      // parent: LI (b)
      // list: UL (containing LI (b))
      // outerListItem: LI (a)
      // outerList: UL (containing LI (a) and the rest.)

      // Result, with "including subitems" set to "true":
      // * a
      // * b
      //   * c
      // * d

      Element paragraphElement = getCurrentParagraphElement();
      Element parent = paragraphElement.getParentElement();

      // Guard 1
      if (!isListItemElement(parent))
        return;

      // Guard 2
      Element list = parent.getParentElement();
      Element outerListItem = list.getParentElement();
      if (!isListItemElement(outerListItem))
        return; // Already the outermost item.

      Element outerList = outerListItem.getParentElement();

      // Decrease indent
      int selectionStart = getSelectionStart();
      int selectionEnd = getSelectionEnd();
      if (selectionStart!=selectionEnd &&
          getListElement(selectionStart) == getListElement(selectionEnd)) {
        // Of block
      }
      else {
        // Of single list item
        selectionStart = getCaretPosition();
        selectionEnd = getCaretPosition();
      }

      int indexOfSelectionStart = list.getElementIndex(selectionStart);
      int indexOfSelectionEnd = list.getElementIndex(selectionEnd);
      int indexOfSelectionInOuterItem = outerListItem.getElementIndex(selectionStart);
      int indexOfSelectionInOuterList = outerList.getElementIndex(selectionStart);
      try {
        getSHTMLDocument().startCompoundEdit();        
        SHTMLWriter writer = new SHTMLWriter(getSHTMLDocument());

        // Write the beginning of the outer list
        writer.writeStartTag(outerList);
        for (int i=0; i < indexOfSelectionInOuterList; i++)
          writer.write(outerList.getElement(i));

        // Write the beginning of the outer list item
        writer.writeStartTag(outerListItem);        
        for (int i=0; i < indexOfSelectionInOuterItem; i++)
          writer.write(outerListItem.getElement(i));

        // Write the beginning of the inner list
        if (indexOfSelectionStart > 0) {
          writer.writeStartTag(list);
          for (int i=0; i < indexOfSelectionStart; i++)
            writer.write(list.getElement(i));
          writer.writeEndTag(list);
        }

        // Close the outer list item
        writer.writeEndTag(outerListItem);

        // Write the promoted (moved to the left) items except for the last one
        for (int i=indexOfSelectionStart; i <= indexOfSelectionEnd-1; i++) {
          if (includingSubitems)
            writer.write(list.getElement(i));
          else {
            writeListItemForDecreaseIndent(writer,list.getElement(i), includingSubitems, false);
          }
        }

        // Write the end of the inner list into the last promoted item
        writeListItemForDecreaseIndent(writer, list.getElement(indexOfSelectionEnd),
            includingSubitems, /*withoutEndTag=*/true);

        if ((indexOfSelectionEnd+1)<list.getElementCount()) {
          writer.writeStartTag(list);
          for (int i=indexOfSelectionEnd+1; i < list.getElementCount(); i++)
            writer.write(list.getElement(i));
          writer.writeEndTag(list);                
        }
        writer.writeEndTag(list.getElement(indexOfSelectionEnd));

        // Write the end of the outer list item, as another list item
        if (indexOfSelectionInOuterItem+1 < outerListItem.getElementCount()) {
          writer.writeStartTag(outerListItem);    
          for (int i=indexOfSelectionInOuterItem+1; i < outerListItem.getElementCount(); i++)
            writer.write(outerListItem.getElement(i));
          writer.writeEndTag(outerListItem);
        }

        // Write the end of the outer list
        for (int i=indexOfSelectionInOuterList+1; i < outerList.getElementCount(); i++)
          writer.write(outerList.getElement(i));        
        writer.writeEndTag(outerList);        

        String newContent = writer.getWrittenString().
        replaceAll("(?ims)</ul>\\s*<ul>","").
        replaceAll("(?ims)</ol>\\s*<ol>","");
        getSHTMLDocument().setOuterHTML(outerList, newContent);
        select(selectionStart, selectionEnd);
      }
      catch (Exception ex) {  
      }
      finally {
        getSHTMLDocument().endCompoundEdit();
      }        
    }
    
    /** Writes the list item for the "descrease indent" action. */
    private void writeListItemForDecreaseIndent(SHTMLWriter writer, Element listItem, 
        boolean includingSubitems,
        boolean withoutEndTag) {
      try {
        writer.writeStartTag(listItem);
        if (includingSubitems)
          writer.writeChildElements(listItem);
        else {
          boolean childListItemsPresent = false;
          // Write all child elements that are not list items
          for (int j=0; j < listItem.getElementCount(); j++)
            if (isListElement(listItem.getElement(j)))
              childListItemsPresent = true;              
            else
              writer.write(listItem.getElement(j));
          //
          if (childListItemsPresent) {
            writer.writeStartTag(listItem.getParentElement());
            for (int j=0; j < listItem.getElementCount(); j++)
              if (isListElement(listItem.getElement(j)))
                writer.write(listItem.getElement(j));
            writer.writeEndTag(listItem.getParentElement());
          }          
        }
        if (!withoutEndTag)
          writer.writeEndTag(listItem);
      } catch (Exception ex) {}
    }
    
    /** Performs the action appropriate on pressing of the key delete, as far as lists
     *  are concerned, treating those cases that Java handles poorly. Returns whether
     *  intervention was necessary. */
    private boolean deleteNextChar(ActionEvent actionEvent) {

      final int nextPosition = getCaretPosition() + 1;
      SHTMLDocument doc = getSHTMLDocument();

      Element listAtNextPosition = getListElement(nextPosition);
      if(listAtNextPosition != null && listAtNextPosition.getStartOffset() == nextPosition) {
        // List element starting at the next position.
        if (!caretWithinListItem()){
          if (elementIsEmptyParagraph(getCurrentParagraphElement()))
            return false;
          mergeSecondElementIntoFirst(
              getParagraphElement(getCaretPosition()),
              getListItemElement(nextPosition));
          return true;
        }
        else {
          // Caret within list item.
          Element listAtCaret = getListElement(getCaretPosition());
          boolean isSurroundingList= listAtCaret.getStartOffset() <= listAtNextPosition.getStartOffset() &&
          listAtCaret.getEndOffset() >= listAtNextPosition.getEndOffset();
          if (isSurroundingList) {
            mergeNestedListItemIntoParent(
                getListItemElement(getCaretPosition()), 
                getListItemElement(nextPosition));
            return true;
          }
          else {
            // Two adjacent lists. To be merged.
            return false;
          }
        }
      }

      // Empty paragraph within a list item.
      if (caretWithinListItem()) {
        Element paragraphElement = getCurrentParagraphElement(); 
        boolean emptyParagraph = elementIsEmptyParagraph(paragraphElement);
        if (emptyParagraph) {
          // Remove the empty paragraph manually, to avoid quircks.          
          removeElement(paragraphElement);
          //setCaretPosition(getCaretPosition() - 1);
          return true;
        }
      }      

      // List item element starting at the next position
      Element listItem = getListItemElement(nextPosition);
      if(listItem != null && listItem.getStartOffset() == nextPosition){
        mergeSecondElementIntoFirst(getListItemElement(getCaretPosition()), listItem);
        return true;
      }      

      Element listElementAtCurrentPosition = getListElement(getCaretPosition());
      Element parentElementOfNextPosition = doc.getParagraphElement(nextPosition).getParentElement();
      if(listElementAtCurrentPosition != null && !isListItemElement(parentElementOfNextPosition)) {
        // A non-list after a list.
        // Avoid currently buggy behavior. A workaround.
        if ("body".equalsIgnoreCase(parentElementOfNextPosition.getName()) ||
            getTableCell(getCaretPosition()) == getTableCell(nextPosition))
          mergeSecondElementIntoFirst(getListItemElement(getCaretPosition()),
              getParagraphElement(nextPosition));        
        else
          performDefaultKeyStrokeAction(KeyEvent.VK_RIGHT, 0, actionEvent);
        return true;
      }
      return false;
    }
    
    /** Performs the action appropriate on pressing of the key backspace, as far as lists
     *  are concerned, treating those cases that Java's Swing handles poorly. Returns whether
     *  intervention was necessary. */
    private boolean deletePrevChar(ActionEvent actionEvent) {
      int selectionStart = getSelectionStart();

      Element list = getListElement(selectionStart);
      if(list != null && list.getStartOffset() == selectionStart){
        // A list starts at the caret position.
        Element listAtPrevPosition = selectionStart==0?null:getListElement(selectionStart-1);
        boolean isSurroundingList= listAtPrevPosition.getStartOffset() <= list.getStartOffset() &&
        listAtPrevPosition.getEndOffset() >= list.getEndOffset();
        if (listAtPrevPosition==null) {
          performToggleListAction(actionEvent, list.getName());
          return true;
        }
        else if (isSurroundingList) {
          mergeNestedListItemIntoParent(
              getListItemElement(selectionStart-1), 
              getListItemElement(selectionStart));
          return true;
        }
        else {
          // Two adjacent lists. To be merged.
          return false;
        }
      }

      // Empty paragraph within a list item.
      if (caretWithinListItem()) {
        Element paragraphElement = getCurrentParagraphElement(); 
        boolean emptyParagraph = elementIsEmptyParagraph(paragraphElement);
        if (emptyParagraph) {
          // Remove the empty paragraph manually, to avoid quirks.          
          removeElement(paragraphElement);
          setCaretPosition(getCaretPosition() - 1);
          return true;
        }
      }            

      Element listItem = getListItemElement(selectionStart);
      if(listItem != null && listItem.getStartOffset() == selectionStart){
        // List item starts at the current position.
        int previousPosition = listItem.getStartOffset() - 1;
        mergeSecondElementIntoFirst(getListItemElement(previousPosition), listItem);
        setCaretPosition(previousPosition);
        return true;
      }
      if (selectionStart > 0) {
        int previousPosition = selectionStart - 1;
        Element listElementAtPreviousPosition = getListElement(previousPosition);
        Element parentElement = getCurrentParagraphElement().getParentElement();
        if(listElementAtPreviousPosition != null && !"li".equalsIgnoreCase(parentElement.getName())) {
          // A non-list after a list.
          // Avoid currently buggy behavior. A workaround.
          if ("body".equalsIgnoreCase(parentElement.getName()) ||
              getTableCell(previousPosition) == getTableCell(selectionStart))
            mergeSecondElementIntoFirst(getListItemElement(previousPosition),
                getParagraphElement(selectionStart));
          else
            performDefaultKeyStrokeAction(KeyEvent.VK_LEFT, 0, actionEvent);
          return true;
        }
      }
      return false;      
    }  
    
    /** Determines whether a list item element contains nested list items. */
    private boolean containsNestedListItems(Element listItem) {
      for (int i=0; i<listItem.getElementCount(); i++)
        if (isListElement(listItem.getElement(i)))
          return true;
      return false;
    }
    

    
    /** Merges the second paragraph element into the first one,
     * setting the caret at the point where the two elements are newly joined. */
    private void mergeSecondElementIntoFirst(Element first, Element second) {
      final SHTMLDocument doc = (SHTMLDocument)getDocument();
      final SHTMLWriter writer = new SHTMLWriter(doc);
      doc.startCompoundEdit();
      try {
        int finalCaretPosition = first.getEndOffset()-1;
        writer.writeStartTag(first);
        writer.writeChildElements(first);
        writer.removeLastWrittenNewline();
        writer.writeChildElements(second);
        writer.writeEndTag(first);
        getSHTMLDocument().setOuterHTML(first, writer.getWrittenString());
        removeElement(second);
        setCaretPosition(finalCaretPosition);
      } catch (IOException e) {
        e.printStackTrace();
      } catch (BadLocationException e) {
        e.printStackTrace();
      }
      finally {
        doc.endCompoundEdit();
      }
    }
    
    /** Merges a nested list item into a list item in the parent list. (Inaccurate description.)
     * See also {@link #mergeSecondElementIntoFirst(Element first, Element second)}. 
     * @param parentListItem a list item
     * @param childListItem a list item 
     */
    private void mergeNestedListItemIntoParent(Element parentListItem, Element childListItem) {
      final SHTMLDocument doc = (SHTMLDocument)getDocument();
      final SHTMLWriter writer = new SHTMLWriter(doc);
      doc.startCompoundEdit();
      try {
        if(!isListItemElement(parentListItem))
          return;
        int finalCaretPosition = parentListItem.getElement(0).getEndOffset()-1;
        writer.writeStartTag(parentListItem);
        for (int i=0; i < parentListItem.getElementCount(); i++)
          if (!isListElement(parentListItem.getElement(i))) 
            writer.write(parentListItem.getElement(i));        
        writer.removeLastWrittenNewline();
        writer.writeChildElements(childListItem);
        for (int i=0; i < parentListItem.getElementCount(); i++)
          if (isListElement(parentListItem.getElement(i))) {
            // Write the nested list except for the merged child.
            Element list = parentListItem.getElement(i);
            if (list.getElementCount() >= 2) {
              // Write the nested list only if it had at least two elements.
              writer.writeStartTag(list);
              for (int j=0; j <list.getElementCount(); j++)
                if(list.getElement(j)!=childListItem)
                  writer.write(list.getElement(j));
              writer.writeEndTag(list);
            }
          }
        writer.writeEndTag(parentListItem);
        getSHTMLDocument().setOuterHTML(parentListItem, writer.getWrittenString());
        setCaretPosition(finalCaretPosition);
      } catch (IOException e) {
        e.printStackTrace();
      } catch (BadLocationException e) {
        e.printStackTrace();
      }
      finally {
        doc.endCompoundEdit();
      }
    }    

    /** Inserts a new list item after the current list item, breaking the text in the list
     * item into two parts if the caret is not at the end of the current list item. */
    private void newListItem() {
      SHTMLDocument doc = getSHTMLDocument();
      int caretPosition = getCaretPosition();
      Element listItemElement = listManager.getListItemElement(caretPosition);
      if (listItemElement==null)
        return;

      try {
        String listItemContent = elementToHTML(listItemElement);
        if (listItemContent.matches("(?ims)\\s*<li[^>]*>\\s*</li>\\s*")) {
          // Empty list item, so switch the list off.
          toggleList("", null, true);
          return;   
        }
        
        int so = listItemElement.getStartOffset();
        int eo = listItemElement.getEndOffset();      
        if(so != eo) {
          StringWriter stringWriter = new StringWriter();
          SHTMLWriter writer = new SHTMLWriter(stringWriter, doc);
          writer.writeStartTag(listItemElement);
          if(caretPosition > so){
            SHTMLWriter htmlStartWriter = new SHTMLWriter(stringWriter, doc, so, caretPosition-so);
            htmlStartWriter.writeChildElements(listItemElement);
          }
          writer.writeEndTag(listItemElement);
          writer.writeStartTag(listItemElement);
          
          if (containsNestedListItems(listItemElement) &&
              (listItemElement.getElement(0).getEndOffset()-1)==caretPosition ) {
            // Contains nested list and the caret is at the end of the list items's paragraph.
            // Let it be. Workaround for the user: press undo.
            // ---Old:---
            // Block the action. User can enter new item by pressing enter when the caret
            // is not at the end of the list item.
            //return;
            //stringWriter.write("<p></p>");
            // A workaround. Otherwise, the caret cannot reach the position in the new 
            // list item. Ideally, Java's editor kit would create implied paragraph, which
            // it does not.
          }
          
          if(caretPosition < eo - 1){
            SHTMLWriter htmlEndWriter = new SHTMLWriter(stringWriter, doc, caretPosition, eo - caretPosition);
            htmlEndWriter.writeChildElements(listItemElement);
          }
          writer.writeEndTag(listItemElement);

          String text = stringWriter.toString();
          try{
            doc.startCompoundEdit();
            doc.setOuterHTML(listItemElement, text);
          }
          catch(Exception e) {
            Util.errMsg(null, e.getMessage(), e);
          }
          finally {
            doc.endCompoundEdit();
          }
          setCaretPosition(caretPosition + 1);
        }    
      }
      catch (Exception ex) {}
    }
  }

  /** remember current selection when mouse button is released */
  void this_mouseReleased(MouseEvent e) {
    lastSelStart = getSelectionStart();
    lastSelEnd = getSelectionEnd();
  }

  /** remember current selection when mouse button is double clicked */
  void this_mouseClicked(MouseEvent e) {
    if(e.getClickCount() > 1) {
      lastSelStart = getSelectionStart();
      lastSelEnd = getSelectionEnd();
    }
  }

  /** is invoked if the user modifies the current drop gesture */
  public void dropActionChanged ( DropTargetDragEvent event ) {
  }

  /** is invoked when the user changes the dropAction */
  public void dropActionChanged ( DragSourceDragEvent event) {
  }

  /** is invoked when you are dragging over the DropSite */
  public void dragEnter (DropTargetDragEvent event) {
  }

  /** is invoked when you are exit the DropSite without dropping */
  public void dragExit (DropTargetEvent event) {
  }

  /**
   * this message goes to DragSourceListener, informing it that the dragging
   * has entered the DropSite
   */
  public void dragEnter (DragSourceDragEvent event) {
  }

  /**
   * this message goes to DragSourceListener, informing it that the dragging
   * has exited the DropSite
   */
  public void dragExit (DragSourceEvent event) {
  }

  /**
   * this message goes to DragSourceListener, informing it that
   * the dragging is currently ocurring over the DropSite
   */
  public void dragOver (DragSourceDragEvent event) {
  }

  // ------ end of drag and drop implementation ----------------------------

  /* ------ start of cut, copy and paste implementation ------------------- */

  public TransferHandler getTransferHandler() {
      final TransferHandler defaultTransferHandler = super.getTransferHandler();
      if(defaultTransferHandler == null) return null;
      class LocalTransferHandler extends TransferHandler{
        /* (non-Javadoc)
         * @see javax.swing.TransferHandler#canImport(javax.swing.JComponent, java.awt.datatransfer.DataFlavor[])
         */
        public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
            return defaultTransferHandler.canImport(comp, transferFlavors);
        }

        /* (non-Javadoc)
         * @see javax.swing.TransferHandler#exportAsDrag(javax.swing.JComponent, java.awt.event.InputEvent, int)
         */
        public void exportAsDrag(JComponent comp, InputEvent e, int action) {
            defaultTransferHandler.exportAsDrag(comp, e, action);
        }

        /* (non-Javadoc)
         * @see javax.swing.TransferHandler#exportToClipboard(javax.swing.JComponent, java.awt.datatransfer.Clipboard, int)
         */
        public void exportToClipboard(JComponent comp, Clipboard clip, int action) {            
            SHTMLDocument document = (SHTMLDocument)getDocument();
            if(document.getParagraphElement(getSelectionStart()) != document.getParagraphElement(getSelectionEnd())){
                defaultTransferHandler.exportToClipboard(comp, clip, action);
                return;
            }
            try
            {
                HTMLText htmlText = new HTMLText();
                int start = getSelectionStart();
                htmlText.copyHTML(SHTMLEditorPane.this, start, getSelectionEnd() - start);
                final Transferable additionalContents = new HTMLTextSelection(htmlText);
                Clipboard temp = new Clipboard("");
                defaultTransferHandler.exportToClipboard(comp, temp, action);
                final Transferable defaultContents = temp.getContents(this);
                clip.setContents(new Transferable(){
                    public DataFlavor[] getTransferDataFlavors() {
                        DataFlavor[] defaultFlavors = defaultContents.getTransferDataFlavors();
                        DataFlavor[] additionalFlavors = additionalContents.getTransferDataFlavors();
                        DataFlavor[] resultFlavor = new DataFlavor[defaultFlavors.length + additionalFlavors.length];
                        System.arraycopy(defaultFlavors, 0, resultFlavor, 0, defaultFlavors.length);
                        System.arraycopy(additionalFlavors, 0, resultFlavor, defaultFlavors.length, additionalFlavors.length);
                        return resultFlavor;
                    }

                    public boolean isDataFlavorSupported(DataFlavor flavor) {
                        return additionalContents.isDataFlavorSupported(flavor) 
                        || defaultContents.isDataFlavorSupported(flavor);
                    }

                    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
                        if(additionalContents.isDataFlavorSupported(flavor))
                            return additionalContents.getTransferData(flavor);
                        return defaultContents.getTransferData(flavor);
                    }
                }, null);
            }
            catch(Exception e) {
                getToolkit().beep();
            }
        }

        /* (non-Javadoc)
         * @see javax.swing.TransferHandler#getSourceActions(javax.swing.JComponent)
         */
        public int getSourceActions(JComponent c) {
            return defaultTransferHandler.getSourceActions(c);
        }

        /* (non-Javadoc)
         * @see javax.swing.TransferHandler#getVisualRepresentation(java.awt.datatransfer.Transferable)
         */
        public Icon getVisualRepresentation(Transferable t) {
            return defaultTransferHandler.getVisualRepresentation(t);
        }

        /* (non-Javadoc)
         * @see javax.swing.TransferHandler#importData(javax.swing.JComponent, java.awt.datatransfer.Transferable)
         */
        public boolean importData(JComponent comp, Transferable transferable) {
          SHTMLDocument doc = (SHTMLDocument)getDocument();
          doc.startCompoundEdit();
          boolean result = false;
          try {
            if (transferable.isDataFlavorSupported(htmlTextDataFlavor)) {
              // This path is taken if:
              // (a) the copy and paste is internal, from SimplyHTML to SimplyHTML, and
              // (b) the copied part is not multi paragraph.
              HTMLText htmlText = (HTMLText)transferable.getTransferData(htmlTextDataFlavor);
              replaceSelection(htmlText);
              result = true;
            }
            else{
              final DataFlavor htmlFlavor = new DataFlavor("text/html; class=java.lang.String");
              String stringContent = null;
              String htmlContent = null;
              if (transferable.isDataFlavorSupported(htmlFlavor) &&
                  transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                htmlContent = (String)transferable.getTransferData(htmlFlavor);
                if(htmlContent.charAt(0) ==  65533){
                	return importDataWithoutHtmlFlavor(comp, transferable);
                }
                stringContent = (String)transferable.getTransferData(DataFlavor.stringFlavor);
                htmlContent = htmlContent.replaceAll("(?ims).*<body.*?>\\s*", "").
                replaceAll("(?ims)\\s*</body.*","").
                replaceAll("<!--StartFragment-->","").
                replaceAll("<!--EndFragment-->","");
                HTMLText htmlText = new HTMLText(htmlContent, stringContent);
                replaceSelection(htmlText);
                result = true;
              }
              else 
                //result = defaultImportData(comp, transferable);
                result = importExternalData(comp, transferable);
            }
          }
          catch (Exception e) {
            getToolkit().beep();
          }
          doc.endCompoundEdit();
          return result;
        }

		private boolean importExternalData(JComponent comp, final Transferable t)
				throws ClassNotFoundException, UnsupportedFlavorException,
				IOException {
			// workaround for java decoding bug 
			// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6740877
			final DataFlavor htmlFlavor = new DataFlavor("text/html; class=java.lang.String");
			if(t.isDataFlavorSupported(htmlFlavor)){
				final String s = (String)t.getTransferData(htmlFlavor);
				if(s.charAt(0) ==  65533){
					return importDataWithoutHtmlFlavor(comp, t);
				}
			}
			return defaultImportData(comp, t);
		}

		private boolean importDataWithoutHtmlFlavor(JComponent comp,
				final Transferable t) {
			return  defaultImportData(comp, new Transferable(){

				public Object getTransferData(DataFlavor flavor)
						throws UnsupportedFlavorException, IOException {
					if(isValid(flavor)) return  t.getTransferData(flavor);
					throw new UnsupportedFlavorException(flavor);
				}

				public DataFlavor[] getTransferDataFlavors() {
					final DataFlavor[] transferDataFlavors = t.getTransferDataFlavors();
					int counter = 0;
					for(int i = 0; i < transferDataFlavors.length; i++){
						if(isValid(transferDataFlavors[i])){
							counter++;
						}
					}
					final DataFlavor[] validDataFlavors = new DataFlavor[counter];
					int j = 0;
					for(int i = 0; i < transferDataFlavors.length; i++){
						final DataFlavor flavor = transferDataFlavors[i];
						if(isValid(flavor)){
							validDataFlavors[j++] = flavor;
						}
					}
					return validDataFlavors;
				}

				public boolean isDataFlavorSupported(DataFlavor flavor) {
					return isValid(flavor) && t.isDataFlavorSupported(flavor);
				}

				private boolean isValid(DataFlavor flavor) {
					return !flavor.isMimeTypeEqual("text/html");
				}
				
			});
		}

        private boolean defaultImportData(JComponent comp, Transferable t) {
            return defaultTransferHandler.importData(comp, t);
        }
      }
      return new LocalTransferHandler();
  }

  /* ------ end of cut, copy and paste implementation --------------- */

  /* ------ start of font/paragraph manipulation --------------- */

  public void removeCharacterAttributes() {
      int p0 = getSelectionStart();
      int p1 = getSelectionEnd();
      if (p0 != p1) {
          SHTMLDocument doc = (SHTMLDocument)getDocument();
          doc.startCompoundEdit();
          // clear all character attributes in selection
			SimpleAttributeSet sasText = null;
			for (int i = p0; i < p1; i++) {
				final Element characterElement = doc.getCharacterElement(i);
				sasText = new SimpleAttributeSet(characterElement.getAttributes().copyAttributes());
				final int endOffset = characterElement.getEndOffset();
				Enumeration attribEntries1 = sasText.getAttributeNames();
				while (attribEntries1.hasMoreElements()) {
					Object entryKey = attribEntries1.nextElement();
					if (!entryKey.toString().equals(HTML.Attribute.NAME.toString())) {
						sasText.removeAttribute(entryKey);
					}
				}
				final int last = p1 < endOffset ? p1 : endOffset;
				try {
					doc.setCharacterAttributes(i, last - i, sasText, true);
				}
				catch (Exception e) {
				}
				i = last;
			}
			doc.endCompoundEdit();
		}
	}

	public void removeParagraphAttributes() {
      int p0 = getSelectionStart();
      int p1 = getSelectionEnd();
      SHTMLDocument doc = (SHTMLDocument)getDocument();
      doc.removeParagraphAttributes(p0, p1-p0 + 1);
      select(p0, p1);
  }
  
  public void applyAttributes(AttributeSet attributeSet, boolean applyToCompleteParagraph) {
    applyAttributes(attributeSet, applyToCompleteParagraph, false);
  }

  /**
   * Sets the attributes for a given part of this editor. If a range of
   * text is selected, the attributes are applied to the selection.
   * If nothing is selected, the input attributes of the given
   * editor are set thus applying the given attributes to future
   * inputs.
   *
   * @param attributeSet  the set of attributes to apply
   * @param applyToCompleteParagraph  true, if the attributes shall be applied to the whole
   *     paragraph, false, if only the selected range of characters shall have them
   * @param replace  true, if existing attribtes are to be replaced, false if not
   */
  public void applyAttributes(AttributeSet attributeSet,
      boolean applyToCompleteParagraph,
      boolean replace) {
    SHTMLDocument doc = getSHTMLDocument();
    requestFocus();
    int selectionStart = getSelectionStart();
    int selectionEnd = getSelectionEnd();
    if(applyToCompleteParagraph) {
      doc.setParagraphAttributes(selectionStart, selectionEnd - selectionStart, attributeSet, replace);
    }
    else {
      if(selectionEnd != selectionStart) {
        doc.setCharacterAttributes(selectionStart, selectionEnd - selectionStart, attributeSet, replace);
      }
      else {
        MutableAttributeSet inputAttributes =
          ((SHTMLEditorKit) getEditorKit()).getInputAttributes();
        inputAttributes.addAttributes(attributeSet);
      }
    }
  }

  /** (Unfinished.)*/
  public void applyCharacterTag(String tag) {
    int selectionStart = getSelectionStart();
    int selectionEnd = getSelectionEnd();
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    doc.startCompoundEdit();
    doc.endCompoundEdit();
  }
  /**
   * Switches the elements in the current selection to the given tag. If allowedTags
   * is non-null, applies the tag only if it is contained in allowedTags.
   * 
   * TODO: The new parameter does not work. So the method only works for paragraph tags,
   * like H1, H2 etc. --Dan
   *
   * @param tag  the tag name to switche elements to
   * @param overwritableTags  Tags that may be overwritten by the new tag.
   */
  public void applyParagraphTag(String tag, Vector overwritableTags) {
    int selectionStart = getSelectionStart();
    int selectionEnd = getSelectionEnd();

    StringWriter stringWriter = new StringWriter();
    SHTMLDocument doc = (SHTMLDocument) getDocument();
    try {
      doc.startCompoundEdit();
      SHTMLWriter writer = new SHTMLWriter(stringWriter, doc);
      Element paragraphElement = doc.getParagraphElement(selectionStart);
      
      int regionStart = paragraphElement.getStartOffset();
      int regionEnd = Math.max(paragraphElement.getEndOffset(), selectionEnd);
      
      //System.out.println("SHTMLEditorPane applyTag start=" + start + ", end=" + end);
      Element parentOfparagraphElement = paragraphElement.getParentElement();
      int replaceStart = -1;
      int replaceEnd = -1;
      int index = -1;
      int elementsToRemoveCount = 0;
      int elementCount = parentOfparagraphElement.getElementCount();
      //System.out.println("SHTMLEditorPane applyTag parent elem=" + elem.getName() + ", eCount=" + eCount);
      for(int i = 0; i < elementCount; i++) {
        Element child = parentOfparagraphElement.getElement(i);
        //System.out.println("SHTMLEditorPane applyTag child elem=" + child.getName() + ", eCount=" + eCount);
        int elementStart = child.getStartOffset();
        int elementEnd = child.getEndOffset();
        //System.out.println("SHTMLEditorPane applyTag eStart=" + eStart + ", eEnd=" + eEnd);
        if((elementStart >= regionStart && elementStart < regionEnd) ||
            (elementEnd > regionEnd && elementEnd <= regionEnd)) {
          elementsToRemoveCount++;
          if(overwritableTags.contains(child.getName())) {
            //System.out.println("SHTMLEditorPane applyTag element is in selection");
            writer.writeStartTag(tag.toString(), child.getAttributes());
            writer.writeChildElements(child);
            writer.writeEndTag(tag.toString());
            if(index < 0) {
              index = i;
            }
            if(replaceStart < 0 || replaceStart > elementStart) {
              replaceStart = elementStart;
            }
            if(replaceEnd < 0 || replaceEnd < elementEnd) {
              replaceEnd = elementEnd;
            }
          }
          else {
            writer.write(child);
          }
        }
      }
      //System.out.println("SHTMLEditorPane applyTag remove index=" + index + ", removeCount=" + removeCount);
      if(index>-1){
        doc.insertAfterEnd(parentOfparagraphElement.getElement(index), stringWriter.getBuffer().toString());
        doc.removeElements(parentOfparagraphElement, index, elementsToRemoveCount);
      }
      //SHTMLEditorKit kit = (SHTMLEditorKit) getEditorKit();
      //System.out.println("SHTMLEditorPane applyTag new HTML=\r\n" + sw.getBuffer().toString() );
      //kit.read(new StringReader(sw.getBuffer().toString()), doc, getCaretPosition());
    }
    catch(Exception e) {
      Util.errMsg(this, e.getMessage(), e);
    }
    finally{
      doc.endCompoundEdit();
    }
  }

  /* ------ end of font/paragraph manipulation --------------- */

  /* ---------- class fields start -------------- */

  static Action toggleBulletListAction = null;
  static Action toggleNumberListAction = null;

  private void performToggleListAction(ActionEvent e, String elemName){
      if(elemName.equalsIgnoreCase(HTML.Tag.UL.toString())){
          if(toggleBulletListAction == null){
              Component c = (Component)e.getSource();
              SHTMLPanelImpl panel = SHTMLPanelImpl.getOwnerSHTMLPanel(c);
              toggleBulletListAction = panel.dynRes.getAction(SHTMLPanelImpl.toggleBulletsAction);
          }
          toggleBulletListAction.actionPerformed(e);
      }
      else if(elemName.equalsIgnoreCase(HTML.Tag.OL.toString())){
          if(toggleNumberListAction == null){
              Component c = (Component)e.getSource();
              SHTMLPanelImpl panel = SHTMLPanelImpl.getOwnerSHTMLPanel(c);
              toggleNumberListAction = panel.dynRes.getAction(SHTMLPanelImpl.toggleNumbersAction);
          }
          toggleNumberListAction.actionPerformed(e);
      }
  }

  public static final String newListItemAction = "newListItem";
  public static final String insertLineBreakAction = "insertLineBreak";

  public static final String deletePrevCharAction = "deletePrevChar";
  public static final String deleteNextCharAction = "deleteNextChar";
  public static final String moveUpAction = "moveUp";
  public static final String homeAction = "home";
  public static final String shiftHomeAction = "shiftHome";
  public static final String shiftEndAction = "shiftEnd";
  public static final String endAction = "end";
  public static final String moveDownAction = "moveDown";

  /** a data flavor for transferables processed by this component */
  private DataFlavor htmlTextDataFlavor =
        new DataFlavor(com.lightdev.app.shtm.HTMLText.class, "HTMLText");

  /* Cursors for mouseovers in the editor */
  private Cursor textCursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
  private Cursor defaultCursor =
                  Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);

  void updateInputAttributes() {
      ((SHTMLEditorKit)getEditorKit()).updateInputAttributes(this);
      fireCaretUpdate(new CaretEvent(this){

          public int getDot() {
              return getSelectionStart();
          }

          public int getMark() {
              return getSelectionEnd();
          }
          
      });
  }

public JPopupMenu getPopup() {
	return popup;
}

public void setPopup(JPopupMenu popup) {
	this.popup = popup;
}

/** Determines whether the caret is currently within a table cell. */
private boolean caretWithinTableCell() {
  Element tableCell = getCurrentTableCell();
  return tableCell != null;  
}



/** Returns the string HTML representation of the element. */
public String elementToHTML(Element element) {

  HTMLDocument document = (HTMLDocument)getDocument();
  StringWriter stringWriter = new StringWriter();
  SHTMLWriter shtmlWriter = new SHTMLWriter(stringWriter, document);
  try {
    shtmlWriter.write(element);
  }
  catch (Exception ex) {      
  }
  return stringWriter.getBuffer().toString();    

}

/** Determines whether an element is an empty paragraph. */
private boolean elementIsEmptyParagraph(Element element) {
  String elementContent = elementToHTML(element);
  return elementContent.matches("(?ims)\\s*<p[^>]*>\\s*</p>\\s*");
}

private void performDefaultKeyStrokeAction (int keyCode, int modifiers, ActionEvent event) {
  KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers);
  Object key = getInputMap().getParent().get(keyStroke);
  if(key != null) 
    getActionMap().getParent().get(key).actionPerformed(event);
  
}

/** Performs the default key stroke action, assuming that the caret is within
 * a table cell and that the action is a cursor move; if the cursor leaves
 * the current table cell, undoes the action.
 * Returns true if the cursor stayed within the table cell. */
public boolean tryDefaultKeyStrokeActionWithinCell(int keyCode, int modifiers, ActionEvent event) {
  int originalCaretPosition = getCaretPosition();      
  Element cellElement = getCurrentTableCell();
  performDefaultKeyStrokeAction(keyCode, modifiers, event);
  Element cellElementAfter = getCurrentTableCell();
  if (cellElement==cellElementAfter)
    return true;
  setCaretPosition(originalCaretPosition);
  return false;
}



/* (non-Javadoc)
 * @see javax.swing.JComponent#getTransferHandler()
 */

  /* ---------- class fields end -------------- */

}
