/**
* Copyright 1999-2009 The Pegadi Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pegadi.artis;
// UI imports
import com.kitfox.svg.app.beans.SVGIcon;
import org.pegadi.artis.text.LocalEditorKit;
import org.pegadi.artis.text.PersonInText;
import org.pegadi.artis.text.PersonInfo;
import org.pegadi.artis.text.SourceDBPersonResolver;
import org.pegadi.model.ArticleLock;
import org.pegadi.model.LoginContext;
import org.pegadi.util.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.swing.*;
import javax.swing.event.CaretEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.*;
import javax.swing.undo.*;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.io.IOException;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.*;
/**
* An editor for editing the body of an {@link org.pegadi.model.Article Article}.
* This is usually the main part of the article.
*
* @author HÃ¥vard Wigtil <havardw at pvv.org>
*/
public abstract class AbstractTextEditor extends Editor implements ClipboardOwner, TextDocumentProvider {
/**
* Editor for the text.
*/
protected LocalJTextPane text;
/** The document {@link #text} is editing. */
//replaced with the TextDocumentProvider interface
//protected TextDocument textDoc;
/**
* Toolbar for text actions.
*/
protected JToolBar toolBar;
/**
* Selector for paragraph styles.
*/
protected JComboBox styleCombo;
/**
* Toggle button for emphasis.
*/
protected JToggleButton emButton;
/**
* Toggle button for bold.
*/
protected JToggleButton boldButton;
/**
* Translatable resources.
*/
protected ResourceBundle textStr;
/**
* Icon for this editor.
*/
protected ImageIcon icon;
/**
* Tracks the start of the current selection. Updates with each {@link javax.swing.event.CaretEvent Caret event}.
*/
protected int dot;
/**
* Tracks the end of the current selection. Updates with each {@link javax.swing.event.CaretEvent Caret event}.
*/
protected int mark;
/**
* Flag used to prevent that combo box changes as a result of cursor
* movement is seen as combo box selections. When this is <code>false</code>,
* all {@link #styleCombo} events will be ignored. FIXME: This is an evil hack!
*/
protected boolean styleChange = true;
/**
* Flag used to prevent updating of cursor information when we are doing
* something to the cursor.
*/
protected boolean cursorChange = false;
/**
* This vector holds all the style buttons. The buttons are expected to
* be toggle buttons.
*/
protected Vector charStyleButtons;
/**
* Action for text search.
*/
protected AbstractAction searchAction;
/**
* Action for text search.
*/
protected AbstractAction searchAgainAction;
/**
* Actions for character buttons*
*/
protected AbstractAction bulletAction, dashAction, lqAction, rqAction, lsqAction, rsqAction, personAction;
/**
* actions for italic and bold
*/
protected AbstractAction emAction, boldAction;
/**
* Actions for word deleting
*/
protected AbstractAction deleteWordForwardAction, deleteWordBackwardsAction;
/**
* Action for selecting auto correct on/off
*/
protected AbstractAction autoCorrectAction;
/**
* Vector holding AbstractActions for text styles
*/
protected Vector styleVector;
/**
* Character buttons
*/
JButton lqButton;
JButton rqButton;
JButton dashButton;
JButton bulletButton;
JButton lsqbracketButton;
JButton rsqbracketButton;
JButton personButton;
/**
* Actions for text replace.
*/
protected AbstractAction replaceAction, replaceAllAction;
/**
* Window for replace dialog
*/
protected JDialog replaceDialog;
/**
* Window for yesOrCancel dialog
*/
protected JDialog yesOrCancelDialog;
/**
* Global answer from yesOrCancel window
*/
protected boolean yesOrCancelAnswer;
/**
* Last search string
*/
String lastSearchString;
/**
* Last replace string
*/
String lastReplaceString;
/**
* Toggle replace all
*/
protected boolean replaceAll = false;
/**
* The undo manager
*/
CompoundUndoManager undo;
public UndoAction undoAction;
public RedoAction redoAction;
/**
* Local clipboard
*/
static Clipboard clipboard = new Clipboard("Clipboard");
static boolean lostOwnership = true;
public static DataFlavor paraFlavor = new DataFlavor(CopiedParagraph[].class, "Element");
private Properties prefs;
private String PREF_DOMAIN = "Artis";
private String PREF_KEY = "autoCorrect";
Element schema;
private boolean autoCorrectionState;
public AbstractTextEditor(Element xml) {
this(xml, null);
}
public AbstractTextEditor(Element xml, Artis artis) {
super(xml, artis);
addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent e) {
recieveFocus(e);
}
});
PersonInText.resolver = new SourceDBPersonResolver();
ResourceBundle strings = ResourceBundle.getBundle("org.pegadi.artis.EditorStrings");
SVGIcon undoIcon = initSvgIcon(getClass().getResource(strings.getString("icon_undo")));
SVGIcon redoIcon = initSvgIcon(getClass().getResource(strings.getString("icon_redo")));
undoAction = new UndoAction(strings.getString("action_undo"), undoIcon);
redoAction = new RedoAction(strings.getString("action_redo"), redoIcon);
undoAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke((java.awt.event.KeyEvent.VK_Z), java.awt.event.KeyEvent.CTRL_MASK, false));
redoAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_Z, java.awt.event.KeyEvent.CTRL_MASK + java.awt.event.KeyEvent.SHIFT_MASK, false));
createCharacterActions();
createDeleteWordActions();
createStyleActions();
loadPrefs();
setupUndoManager();
}
private void loadPrefs() {
try {
prefs = LoginContext.server.getPreferences(PREF_DOMAIN, LoginContext.sessionKey);
} catch (Exception e) {
log.error("Exception getting preferences for domain " + PREF_DOMAIN, e);
prefs = new Properties();
}
String auto = prefs.getProperty(PREF_KEY);
if (auto == null) {
auto = "false";
prefs.put(PREF_KEY, auto);
}
if (auto.equals("false")) {
setAutoCorrectState(false);
} else if (auto.equals("true")) {
setAutoCorrectState(true);
}
}
public void setupUndoManager() {
if (getTextDoc() == null)
log.debug("textDoc ER NULL");
undo = new CompoundUndoManager();
getTextDoc().addUndoableEditListener(undo);
}
private class ElementSelection implements Transferable, ClipboardOwner {
private DataFlavor[] supportedFlavors = {paraFlavor};
CopiedParagraph[] paras;
int firstOffset, lastOffset;
public ElementSelection(CopiedParagraph[] paras) {
this.paras = paras;
}
public synchronized DataFlavor[] getTransferDataFlavors() {
return (supportedFlavors);
}
public boolean isDataFlavorSupported(DataFlavor parFlavor) {
return (parFlavor.equals(paraFlavor));
}
public synchronized Object getTransferData(DataFlavor parFlavor)
throws UnsupportedFlavorException {
if (parFlavor.equals(paraFlavor))
return (paras);
else
throw new UnsupportedFlavorException(paraFlavor);
}
public void lostOwnership(Clipboard parClipboard, Transferable parTransferable) {
}
}
/**
* This method creates a 'yes or cancel' window.
*/
protected boolean requestYesOrCancel() {
if (yesOrCancelDialog != null) // Only one dialog open.
return (false);
yesOrCancelDialog = new JDialog(new JFrame(), textStr.getString("replace_text"), true);
yesOrCancelAnswer = false;
JLabel questionLabel = new JLabel(textStr.getString("replace_query"));// "Text to replace:");
JButton cancel = new JButton(textStr.getString("cancel"));
cancel.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
yesOrCancelAnswer = false;
yesOrCancelDialog.dispose();
yesOrCancelDialog = null;
}
});
JButton ok = new JButton(textStr.getString("ok"));
ok.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
yesOrCancelAnswer = true;
yesOrCancelDialog.dispose();
yesOrCancelDialog = null;
}
});
Container container = yesOrCancelDialog.getContentPane();
container.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 2, 2, 2);
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.anchor = GridBagConstraints.NORTH;
c.gridwidth = 2;
c.gridx = 1;
c.gridy = 1;
container.add(questionLabel, c);
c.gridy = 2;
c.gridwidth = 1;
container.add(ok, c);
c.gridx = 2;
container.add(cancel, c);
yesOrCancelDialog.pack();
yesOrCancelDialog.setLocation(600, 300);
Thread t = new Thread() {
public void run() {
try {
Thread.sleep(500);
} catch (Exception e) {
log.error("Sleeperror in requestYesOrCancel\n");
}
text.requestFocus();
}
};
t.start();
yesOrCancelDialog.setVisible(true);
return (yesOrCancelAnswer);
}
/**
* This method is called when 'OK' is pressed in the search/replace dialog.
*
* @param searchText The text user is searching for
* @param replaceText Replace text.
*/
protected void replaceOkPressed(String searchText, String replaceText) {
if ((searchText.length() == 0) || (replaceText.length() == 0))
return;
int cursorPosition = text.getCaret().getDot();
int startPosition = cursorPosition;
int cursor_paragraphNumber = getParagraphNumber(cursorPosition); // Needed for the search to work properly past queryString.length() paragraphs
String document = text.getText().substring(cursorPosition + cursor_paragraphNumber, text.getText().length());
int result_relativePosition = document.indexOf(searchText); //result position, relative to cursorPosition.
boolean wrapped = false;
while (result_relativePosition != -1 || !wrapped) {
if (result_relativePosition == -1) {
wrapped = true;
text.getCaret().setDot(0);
cursorPosition = 0;
document = text.getText();
result_relativePosition = document.indexOf(searchText);
continue;
}
int correct = getParagraphNumber(cursorPosition + result_relativePosition) - cursor_paragraphNumber;
result_relativePosition -= correct;
text.getCaret().setDot(result_relativePosition + cursorPosition); // Mark found words
text.getCaret().moveDot(result_relativePosition + cursorPosition + searchText.length());
text.requestFocus();
if (replaceAll || requestYesOrCancel()) {
text.replaceSelection(replaceText);
lastSearchString = searchText;
lastReplaceString = replaceText;
}
cursorPosition = text.getCaret().getDot();
cursor_paragraphNumber = getParagraphNumber(cursorPosition);
document = text.getText().substring(cursorPosition + cursor_paragraphNumber, text.getText().length());
result_relativePosition = document.indexOf(searchText);
}
JOptionPane.showMessageDialog(this, textStr.getString("search_failed")); // Report failure
text.getCaret().setDot(startPosition);
replaceAll = false;
}
/**
* This method is triggered by the replace action.
*
* @param e The event for the action.
* @see #getEditActions
* @see #searchAction
*/
protected void replacePerformed(ActionEvent e) {
if (replaceDialog != null) // Only one dialog open.
return;
replaceDialog = new JDialog(new JFrame(), textStr.getString("replace_text"));
JLabel searchLabel = new JLabel(textStr.getString("replace_texttoreplace"));
JLabel replaceLabel = new JLabel(textStr.getString("replace_replacewith"));
final JTextField searchText = new JTextField(lastSearchString);
final JTextField replaceText = new JTextField(lastReplaceString);
JButton cancel = new JButton(textStr.getString("cancel"));
cancel.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
replaceDialog.dispose();
replaceDialog = null;
}
});
JButton ok = new JButton(textStr.getString("ok"));
ok.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
replaceDialog.dispose();
replaceDialog = null;
replaceOkPressed(searchText.getText(), replaceText.getText());
}
});
Container container = replaceDialog.getContentPane();
container.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 2, 2, 2);
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.anchor = GridBagConstraints.NORTH;
c.gridwidth = 2;
c.gridx = 1;
c.gridy = 1;
container.add(searchLabel, c);
c.gridy = 2;
container.add(searchText, c);
c.gridy = 3;
container.add(replaceLabel, c);
c.gridy = 4;
container.add(replaceText, c);
c.gridy = 5;
c.gridwidth = 1;
container.add(ok, c);
c.gridx = 2;
container.add(cancel, c);
replaceDialog.pack();
replaceDialog.setLocation(300, 300);
replaceDialog.show();
}
/**
* This method is triggered by the replace all action.
* It uses the replacePerformed-method, but doesn't ask for each word to replace.
*
* @param e The event for the action
* @see #replacePerformed
*/
protected void replaceAllPerformed(ActionEvent e) {
replaceAll = true;
replacePerformed(e);
}
/**
* This method is triggered by the search action.
*
* @param e The event for the action.
* @see #getEditActions
* @see #searchAction
*/
protected void searchAgainPerformed(ActionEvent e) {
int cursorPosition = text.getCaret().getDot();
String queryString = lastSearchString;
if (queryString != null) { // Is this a valid search?
// A simple test. If cursor is placed at the end of the text, start from the beginning.
if (cursorPosition >= text.getText().length()) {
JOptionPane.showMessageDialog(this, textStr.getString("search_endtext"));
cursorPosition = 0;
}
int cursor_paragraphNumber = getParagraphNumber(cursorPosition); // Needed for the search to work properly past queryString.length() paragraphs
String document = text.getText().toLowerCase().substring(cursorPosition + cursor_paragraphNumber, text.getText().length());
int result_relativePosition = document.indexOf(queryString.toLowerCase()); //result position, relative to cursorPosition.
// Try again if search failed, but this time from the top.
if ((result_relativePosition == -1) && (cursorPosition != 0)) {
JOptionPane.showMessageDialog(this, textStr.getString("search_endtext"));
cursorPosition = 0;
document = text.getText().toLowerCase().substring(cursorPosition, text.getText().length());
result_relativePosition = document.indexOf(queryString.toLowerCase());
cursor_paragraphNumber = getParagraphNumber(cursorPosition);
}
if (result_relativePosition == -1) {
JOptionPane.showMessageDialog(this, textStr.getString("search_failed")); // Rapport failure
} else {
int correct = getParagraphNumber(cursorPosition + result_relativePosition) - cursor_paragraphNumber;
result_relativePosition -= correct;
text.getCaret().setDot(result_relativePosition + cursorPosition); // Mark found words
text.getCaret().moveDot(result_relativePosition + cursorPosition + queryString.length());
text.requestFocus();
}
}
}
/**
* Finds and returns the number of the paragraph in which one finds the given position. Used to find and correct
* the search functions' error offset (which is the number of the result's paragraph, as the paragraph is saved
* as a character(?) that is counted by the String.indexOf() function).
*
* @param result_absPosition The position of the found word
* @return paraCounter The number of the found word's paragraph
*/
private int getParagraphNumber(int result_absPosition){
javax.swing.text.Element thisPara = getTextDoc().getParagraphElement(result_absPosition);
javax.swing.text.Element fetchedPara = getTextDoc().getParagraphElement(0);
javax.swing.text.Element checkPara = null;
int paraCounter = 0;
int checkCounter = 0;
boolean finishedCounting = false;
do{
try {
checkPara = getTextDoc().getParagraphElement(checkCounter);
} catch (IndexOutOfBoundsException the_game){
finishedCounting = true;
}
if (fetchedPara != checkPara){ // if (entered new paragraph)
paraCounter++;
fetchedPara = checkPara;
if (fetchedPara == thisPara){
finishedCounting = true;
}
}
if (checkCounter == 0 && fetchedPara == thisPara){
finishedCounting = true;
}
checkCounter++;
} while (!finishedCounting);
return paraCounter;
}
/**
* This method deletes one word after the caret
*/
private void deleteWordForward() {
// FIXME: This should be done using internal methods in Swing
// instead of searching for spaces and newlines (handegar)
int cursorPosition = text.getCaret().getDot();
if (cursorPosition <= text.getText().length()) {
String doc = text.getText().substring(cursorPosition, text.getText().length());
int skipCounter = 0;
while (doc.indexOf(" ", skipCounter) == skipCounter)
++skipCounter;
int firstSpace = doc.indexOf(" ", skipCounter);
int firstNL = doc.indexOf("\n");
if ((firstSpace < 0) || ((firstNL < firstSpace) && (firstNL > 0)))
firstSpace = firstNL;
if (firstSpace < 0)
return;
text.getCaret().moveDot(cursorPosition + firstSpace);
text.replaceSelection("");
}
}
/**
* This method deletes one word before the caret
*/
private void deleteWordBackwards() {
// FIXME: This should be done using internal methods in Swing
// instead of searching for spaces and newlines (handegar)
int cursorPosition = text.getCaret().getDot();
if (cursorPosition > 0) {
String doc = text.getText().substring(0, cursorPosition);
int lastSpace = doc.lastIndexOf(" ") + 1;
while (doc.lastIndexOf(" ", lastSpace) == lastSpace)
--lastSpace;
int lastNL = doc.lastIndexOf("\n") + 1;
if (lastNL > lastSpace)
lastSpace = lastNL;
text.getCaret().setDot(lastSpace);
text.getCaret().moveDot(cursorPosition);
text.replaceSelection("");
}
}
private void createDeleteWordActions() {
ResourceBundle strings = ResourceBundle.getBundle("org.pegadi.artis.EditorStrings");
deleteWordForwardAction = new AbstractAction(textStr.getString("action_deletenextword"),
new ImageIcon(getClass().getResource(strings.getString("icon_cut")))) {
public void actionPerformed(ActionEvent e) {
deleteWordForward();
}
};
deleteWordBackwardsAction = new AbstractAction(textStr.getString("action_deleteprevword"),
new ImageIcon(getClass().getResource(strings.getString("icon_cut")))) {
public void actionPerformed(ActionEvent e) {
deleteWordBackwards();
}
};
deleteWordBackwardsAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(8, java.awt.event.KeyEvent.CTRL_MASK, false));
deleteWordForwardAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(68, java.awt.event.KeyEvent.CTRL_MASK, false));
}
private void createStyleActions() {
styleVector = new Vector();
StyleContext styles = StyleContext.getDefaultStyleContext();
Enumeration names = styles.getStyleNames();
int counter = 0;
while (names.hasMoreElements()) {
String styleNameOrig = names.nextElement().toString();
Style s = styles.getStyle(styleNameOrig);
Object display = s.getAttribute("display");
if (!(display != null && display.equals("block")))
continue;
// Raise the first letter in the style name. Look better in the menu.
String styleName = (styleNameOrig.substring(0, 1).toUpperCase()) + styleNameOrig.substring(1);
AbstractAction a = new AbstractAction(styleName) {
public void actionPerformed(ActionEvent e) {
// FIXME: A very ugly way to fetch shortkey
// for this action. Maybe 'KeyStroke' class is
// the solution, but my attempts failed. It
// now extracts keycode form the
// AbstractAction class string. This limits
// shortkeys to numbers (1-9) but that seems
// ok. (handegar).
int key = Integer.parseInt(this.getValue(AbstractAction.ACCELERATOR_KEY).toString().substring(12, 13));
styleCombo.setSelectedIndex(key - 1);
}
};
a.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke((java.awt.event.KeyEvent.VK_1 + counter), java.awt.event.KeyEvent.CTRL_MASK, false));
styleVector.addElement(a);
++counter;
}
}
private void createCharacterActions() {
lqAction = new AbstractAction(textStr.getString("action_lq")) {
public void actionPerformed(ActionEvent e) {
lqButton.doClick();
}
};
lqAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(81, java.awt.event.KeyEvent.CTRL_MASK + java.awt.event.KeyEvent.SHIFT_MASK, false)); //86
rqAction = new AbstractAction(textStr.getString("action_rq")) {
public void actionPerformed(ActionEvent e) {
rqButton.doClick();
}
};
rqAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(87, java.awt.event.KeyEvent.CTRL_MASK + java.awt.event.KeyEvent.SHIFT_MASK, false)); //66
dashAction = new AbstractAction(textStr.getString("action_dash")) {
public void actionPerformed(ActionEvent e) {
dashButton.doClick();
}
};
dashAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(69, java.awt.event.KeyEvent.CTRL_MASK + java.awt.event.KeyEvent.SHIFT_MASK, false)); //76
bulletAction = new AbstractAction(textStr.getString("action_bullet")) {
public void actionPerformed(ActionEvent e) {
bulletButton.doClick();
}
};
bulletAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(82, java.awt.event.KeyEvent.CTRL_MASK + java.awt.event.KeyEvent.SHIFT_MASK, false)); //79
lsqAction = new AbstractAction(textStr.getString("action_lsq")) {
public void actionPerformed(ActionEvent e) {
lsqbracketButton.doClick();
}
};
lsqAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(84, java.awt.event.KeyEvent.CTRL_MASK + java.awt.event.KeyEvent.SHIFT_MASK, false)); //89
rsqAction = new AbstractAction(textStr.getString("action_rsq")) {
public void actionPerformed(ActionEvent e) {
rsqbracketButton.doClick();
}
};
rsqAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(89, java.awt.event.KeyEvent.CTRL_MASK + java.awt.event.KeyEvent.SHIFT_MASK, false)); // 85
personAction = new AbstractAction(textStr.getString("action_person")) {
public void actionPerformed(ActionEvent e) {
personButton.doClick();
}
};
boldAction = new AbstractAction(textStr.getString("action_bold"), new ImageIcon(getClass().getResource(textStr.getString("icon_bold")))) {
public void actionPerformed(ActionEvent e) {
boldButton.doClick();
}
};
boldAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(66, java.awt.event.KeyEvent.CTRL_MASK, false));
boldAction.setEnabled(false);
emAction = new AbstractAction(textStr.getString("action_em"), new ImageIcon(getClass().getResource(textStr.getString("icon_em")))) {
public void actionPerformed(ActionEvent e) {
emButton.doClick();
}
};
emAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(73, java.awt.event.KeyEvent.CTRL_MASK, false));
emAction.setEnabled(false);
}
/**
* This method is triggered by the search action.
*
* @param e The event for the action.
* @see #getEditActions
* @see #searchAction
*/
protected void searchPerformed(ActionEvent e) {
int cursorPosition = text.getCaret().getDot();
String queryString = (String) JOptionPane.showInputDialog(this, textStr.getString("search_text"),
textStr.getString("search_text"), // Maybe this should be different...
JOptionPane.PLAIN_MESSAGE,
null, null, lastSearchString);
if (queryString != null) { // Is this a valid search?
// A simple test. If cursor is placed at the end of the text, start from the beginning.
if (cursorPosition >= text.getText().length()) {
JOptionPane.showMessageDialog(this, textStr.getString("search_endtext"));
cursorPosition = 0;
}
int cursor_paragraphNumber = getParagraphNumber(cursorPosition); // Needed for the search to work properly past queryString.length() paragraphs
String document = text.getText().toLowerCase().substring(cursorPosition + cursor_paragraphNumber, text.getText().length());
int result_relativePosition = document.indexOf(queryString.toLowerCase()); //result position, relative to cursorPosition.
// Try again if search failed, but this time from the top.
if ((result_relativePosition == -1) && (cursorPosition != 0)) {
JOptionPane.showMessageDialog(this, textStr.getString("search_endtext"));
cursorPosition = 0;
document = text.getText().toLowerCase().substring(cursorPosition, text.getText().length());
result_relativePosition = document.indexOf(queryString.toLowerCase());
cursor_paragraphNumber = getParagraphNumber(cursorPosition);
}
if (result_relativePosition == -1) {
JOptionPane.showMessageDialog(this, textStr.getString("search_failed")); // Rapport failure
} else {
int correct = getParagraphNumber(cursorPosition + result_relativePosition) - cursor_paragraphNumber;
result_relativePosition -= correct;
text.getCaret().setDot( result_relativePosition + cursorPosition); // Mark found words
text.getCaret().moveDot(result_relativePosition + cursorPosition + queryString.length());
text.requestFocus();
}
lastSearchString = queryString; // Save search-query for later.
}
}
/**
* Returns editing actions for use in the edit menu and the toolbar.
* The actions returned for this implementation is cut, copy and paste.
* An editor should always return these actions, and rather disable the
* actions if they are not applicable.
*
* @return Actions for editing.
*/
public AbstractAction[] getEditActions() {
return new AbstractAction[]{undoAction, redoAction,
cutAction, copyAction, pasteAction};
}
public AbstractAction[] getEditActions2() {
return new AbstractAction[]{searchAction, searchAgainAction, replaceAction, replaceAllAction,
deleteWordForwardAction, deleteWordBackwardsAction};
}
public AbstractAction getAutoCorrectAction() {
return autoCorrectAction;
}
public AbstractAction[] getFormatActions() {
return new AbstractAction[]{emAction, boldAction};
}
public AbstractAction[] getStyleActions() {
AbstractAction[] a = new AbstractAction[styleVector.size()];
for (int i = 0; i < styleVector.size(); ++i)
a[i] = (AbstractAction) styleVector.elementAt(i);
return a;
}
public AbstractAction[] getCharacterActions() {
//AbstractAction[] ca = { bulletAction, dashAction, lqAction, rqAction, lsqAction, rsqAction };
return new AbstractAction[]{lqAction, rqAction, dashAction, bulletAction, lsqAction, rsqAction};
}
/**
* Sets the element to edit.
*
* @param xml The new element for the editor to use.
*/
public abstract void setXML(Element xml);
/**
* Loads styles for the editor. The styles are defined in
* pegadi.artis.TextStyles.properties, and the parsing is done in
* {@link #parseStyle}.<br>
* Only the styles which corresponds to a non-null entry in <code>elements</code>
* are added.
*
* @return The styles defined in the file. Note that if the loading fails
* (likely source is an IOException) this function will still return
* a non-null <code>StyleContext</code>, but this will be the context
* resulting from a call to <code>getDefaultStyleContext()</code>.
* @see #parseStyle
*/
protected StyleContext loadStyles(Hashtable elements) {
StyleContext styles = StyleContext.getDefaultStyleContext();
Properties props = new Properties();
try {
URL url = getClass().getResource("/org/pegadi/artis/TextStyles.properties");
props.load(url.openStream());
} catch (IOException e) {
log.error("Exception loading style properties", e);
return styles;
} catch (NullPointerException npe) {
log.error("Unable to get URL for TextStyles.properties", npe);
}
Style def = styles.getStyle(StyleContext.DEFAULT_STYLE);
Enumeration names = props.propertyNames();
while (names.hasMoreElements()) {
String n = names.nextElement().toString();
Style s;
if (n.equals("default")) {
s = def;
parseStyle(s, props.getProperty(n));
} else {
if (elements.containsKey(n)) {
s = styles.addStyle(n, def);
parseStyle(s, props.getProperty(n));
} else {
log.warn("Name '" + n + "' is not in element list, skipping.");
}
}
}
return styles;
}
/**
* This function parses a style description and updates the style. This
* is a helper function for {@link #loadStyles}.<p>
* <p/>
* The style is given as a comma separated string, and keys that are
* recognized are:
* <ul>
* <li>font-size <int>
* <li>font-face <int>
* <li>bold
* <li>italic
* <li>space-above <float>
* <li>space-belov <float>
* <li>first-indent <int>
* <li>display (block|inline)
* <li>color
* </ul>
*
* @param s The style to modify
* @param attrib The attributes for the style
*/
protected void parseStyle(Style s, String attrib) {
// comma is the only valid delimeter
StringTokenizer st = new StringTokenizer(attrib, ",");
// variables for each token
String token;
int delim;
String key;
String value;
while (st.hasMoreTokens()) {
token = st.nextToken().trim();
// space is the delimeter within a attribute
delim = token.indexOf(' ');
// Remember, not all keys (e.g. bold) have values.
if (delim == -1) {
key = token;
value = "";
} else {
key = token.substring(0, delim);
value = token.substring(delim + 1).trim();
}
// find a match for key
if (key.equals("font-face")) {
StyleConstants.setFontFamily(s, value);
} else if (key.equals("font-size")) {
try {
StyleConstants.setFontSize(s, Integer.parseInt(value));
} catch (NumberFormatException nfe) {
log.error("Value is not a number for font-size", nfe);
}
} else if (key.equals("bold")) {
StyleConstants.setBold(s, true);
} else if (key.equals("italic")) {
StyleConstants.setItalic(s, true);
} else if (key.equals("space-above")) {
try {
StyleConstants.setSpaceAbove(s, new Float(value));
} catch (NumberFormatException nfe) {
log.error("Value is not a number for space-above", nfe);
}
} else if (key.equals("space-below")) {
try {
StyleConstants.setSpaceBelow(s, new Float(value));
} catch (NumberFormatException nfe) {
log.error("Value is not a number for space-below", nfe);
}
} else if (key.equals("first-indent")) {
try {
StyleConstants.setFirstLineIndent(s, Integer.parseInt(value));
} catch (NumberFormatException nfe) {
log.error("Value is not a number for first-indent", nfe);
}
} else if (key.equals("display")) {
s.addAttribute(key, value);
} else if (key.equals("background-color")) {
StyleConstants.setBackground(s, java.awt.Color.decode(value));
} else if (key.equals("color")) {
StyleConstants.setForeground(s, java.awt.Color.decode(value));
} else {
// No match found
log.error("No match for key '" + key + "'!");
}
}
}
/**
* Create a list of all allowed elements with regards to the schema.
*
* @param root The root node for the element which this editor is editing.
* @return A list of elements, with the element name as key.
*/
protected Hashtable createElementList(Element root) {
//log.debug("Creating element list for root " + root);
Hashtable list = new Hashtable(10);
Stack elements = new Stack();
elements.push(root);
while (!elements.empty()) {
Object o = elements.pop();
if (o instanceof Node) {
NodeList nl = ((Node) o).getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node in = nl.item(i);
if (in instanceof Element &&
((Element) in).getTagName().equals("xsd:element")) {
String name = ((Element) in).getAttribute("name");
if (name == null || name.equals("")) {
name = ((Element) in).getAttribute("ref");
Element refElement = XMLUtil.getSchemaElement(name, schema);
elements.push(refElement);
}
if (name != null && !name.equals("")) {
if (!list.containsKey(name)) {
list.put(name, in);
}
}
} else {
elements.push(in);
}
}
} else {
if (o != null)
log.warn("Object on stack is not a Node: " + o);
}
// Check if this node has a type reference
if (o instanceof Element && ((Element) o).getTagName().equals("xsd:element")) {
// If this is a type, add the (element) children of the type
String type = ((Element) o).getAttribute("type");
// FIXME: This will also add native types, e.g. 'string' and 'integer'
if (type != null) {
Element typeElem = XMLUtil.getSchemaElement(type, schema);
elements.push(typeElem);
} // if the element has a type reference
}
} // While the stack has elements
return list;
}
/**
* This method will update the main document with this editor's
* content.
*/
public void updateXML() {
javax.swing.text.Element root = text.getDocument().getDefaultRootElement();
// Remove all old nodes from root XML element.
while (mElement.getChildNodes().getLength() > 0) {
mElement.removeChild(mElement.getFirstChild());
}
for (int i = 0; i < root.getElementCount(); i++) {
addParagraph(root.getElement(i), mElement);
}
}
/**
* Builds an XML element from a swing <code>Document</code> element. The new element
* will be appended to the element given for the <code>parent</code> parameter.<p>
* <p/>
* The <code>Document</code> has only a three level structure, with <code>section</code>,
* <code>paragraph</code> and <code>content</code>. The element given to this method
* should be on the <code>paragraph</code> level.
*
* @param paragraph The <code>Document</code> element.
* @param parent The parent XML element
* @see javax.swing.text.Element
* @see javax.swing.text.Document
*/
protected void addParagraph(javax.swing.text.Element paragraph, Element parent) {
if (paragraph.getElementCount() == 0) {
return;
}
String baseName = paragraph.getAttributes().getAttribute(StyleConstants.NameAttribute).toString();
// Force non-styled text to default style.
if (baseName == null) {
// This is pretty ugly. Need a more flexible solution, but this works for now...
baseName = textStr.getString("default_textstyle");
}
Element base = parent.getOwnerDocument().createElement(baseName);
parent.appendChild(base);
int count = paragraph.getElementCount();
javax.swing.text.Element elem;
Element cont;
String contName;
Node text;
Document doc = parent.getOwnerDocument();
for (int i = 0; i < count; i++) {
elem = paragraph.getElement(i);
contName = null;
Enumeration e = elem.getAttributes().getAttributeNames();
while (e.hasMoreElements()) {
Object name = e.nextElement();
if (name.toString().equals("name")) {
contName = elem.getAttributes().getAttribute(name).toString();
}
}
if (contName == null) {
text = doc.createTextNode(getElementText(elem));
base.appendChild(text);
} else if (contName.equals("person")) {
PersonInText box = (PersonInText) StyleConstants.getComponent(elem.getAttributes());
if (box != null) {
PersonInfo person = box.getPerson();
String personName;
cont = parent.getOwnerDocument().createElement(contName);
if (person != null) {
cont.setAttribute("idRef", person.getId());
cont.setAttribute("sourceRef", person.getSource());
personName = person.getName();
} else {
personName = box.getPersonName();
}
base.appendChild(cont);
text = doc.createTextNode(personName);
cont.appendChild(text);
} else {
text = doc.createTextNode(getElementText(elem));
base.appendChild(text);
}
} else {
cont = parent.getOwnerDocument().createElement(contName);
base.appendChild(cont);
text = doc.createTextNode(getElementText(elem));
cont.appendChild(text);
}
}
}
/**
* Helper method for <code>addParagraph</code>.
*
* @param elem The element to extract text from.
*/
private String getElementText(javax.swing.text.Element elem) {
int start = elem.getStartOffset();
int end = elem.getEndOffset();
try {
String text = elem.getDocument().getText(start, end - start);
// Check for trailing newline, and remove it if found
if (text.endsWith("\n")) {
return text.substring(0, text.length() - 1);
} else {
return text;
}
} catch (BadLocationException ble) {
log.warn("Bad location, returning empty string.", ble);
return "";
}
}
class LocalJTextPane extends JTextPane {
public void copy() {
copyPerformed(null);
}
public void paste() {
pastePerformed(null);
}
public void cut() {
cutPerformed(null);
}
public void systemPaste() {
super.paste();
}
}
/**
* Creates all user visible resources.
*
* @param loc The locale to use for the resources.
*/
protected void createUI(Locale loc) {
super.createUI(loc);
textStr = ResourceBundle.getBundle("org.pegadi.artis.TextStrings", loc);
text = new LocalJTextPane();
text.setEditorKit(new LocalEditorKit());
text.setDragEnabled(true);
text.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e))
showPopup(e);
}
});
toolBar = new JToolBar();
styleCombo = new JComboBox();
styleCombo.setEditable(false);
Dimension d = styleCombo.getPreferredSize();
d.width = 150;
styleCombo.setPreferredSize(d);
styleCombo.setToolTipText(textStr.getString("parastyle_tooltip"));
styleCombo.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
styleComboAction(e);
}
});
toolBar.add(styleCombo);
toolBar.addSeparator();
ActionListener styleButtonListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
styleButtonAction(e);
}
};
charStyleButtons = new Vector();
String emIconName = textStr.getString("icon_em");
URL emIconURL = getClass().getResource(emIconName);
emButton = new JToggleButton(new ImageIcon(emIconURL));
emButton.setActionCommand("i");
emButton.addActionListener(styleButtonListener);
emButton.setEnabled(false);
emButton.setToolTipText(textStr.getString("emstyle_tooltip"));
charStyleButtons.addElement(emButton);
boldButton = new JToggleButton(new ImageIcon(getClass().getResource(textStr.getString("icon_bold"))));
boldButton.setActionCommand("b");
boldButton.addActionListener(styleButtonListener);
boldButton.setEnabled(false);
boldButton.setToolTipText(textStr.getString("boldstyle_tooltip"));
charStyleButtons.addElement(boldButton);
toolBar.add(emButton);
toolBar.add(boldButton);
toolBar.addSeparator();
// Character buttons
ActionListener charButtonListener;
charButtonListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
charButtonAction(e);
}
};
lqButton = new JButton("\u00AB");
lqButton.setFont(new Font("SansSerif", Font.PLAIN, 18));
lqButton.addActionListener(charButtonListener);
lqButton.setToolTipText(textStr.getString("leftquote_tooltip"));
toolBar.add(lqButton);
rqButton = new JButton("\u00BB");
rqButton.setFont(new Font("SansSerif", Font.PLAIN, 18));
rqButton.addActionListener(charButtonListener);
rqButton.setToolTipText(textStr.getString("rightquote_tooltip"));
toolBar.add(rqButton);
dashButton = new JButton("\u2013");
dashButton.setFont(new Font("SansSerif", Font.PLAIN, 18));
dashButton.addActionListener(charButtonListener);
dashButton.setToolTipText(textStr.getString("dash_tooltip"));
dashButton.setActionCommand("\u2013 ");
toolBar.add(dashButton);
bulletButton = new JButton("\u2022");
bulletButton.setFont(new Font("SansSerif", Font.PLAIN, 18));
bulletButton.addActionListener(charButtonListener);
bulletButton.setToolTipText(textStr.getString("bullet_tooltip"));
bulletButton.setActionCommand("\u2022 ");
toolBar.add(bulletButton);
lsqbracketButton = new JButton("\u005B");
lsqbracketButton.setFont(new Font("SansSerif", Font.PLAIN, 18));
lsqbracketButton.addActionListener(charButtonListener);
lsqbracketButton.setToolTipText(textStr.getString("lsqbracket_tooltip"));
lsqbracketButton.setActionCommand("\u005B");
toolBar.add(lsqbracketButton);
rsqbracketButton = new JButton("\u005D");
personButton = new JButton("Person");
rsqbracketButton.setFont(new Font("SansSerif", Font.PLAIN, 18));
rsqbracketButton.addActionListener(charButtonListener);
rsqbracketButton.setToolTipText(textStr.getString("rsqbracket_tooltip"));
rsqbracketButton.setActionCommand("\u005D");
toolBar.add(rsqbracketButton);
ImageIcon personIcon = new ImageIcon(getClass().getResource("/images/texteditor/personintext.png"));
personButton = new JButton(new AbstractAction("", personIcon) {
public void actionPerformed(ActionEvent e) {
personButton_actionPerformed(e);
}
});
personButton.setToolTipText(textStr.getString("person_tooltip"));
//FIXME: Uncomment to enable PersonInText. This should be looked more carefully at.
//toolBar.add(personButton);
/*toolBar.add(new JButton(new AbstractAction("Dump") {
public void actionPerformed(ActionEvent e) {
getTextDoc().dump(System.out);
}
}));*/
setLayout(new BorderLayout());
add(toolBar, BorderLayout.NORTH);
JScrollPane scroll = new JScrollPane(text,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
add(scroll, BorderLayout.CENTER);
icon = new ImageIcon(getClass().getResource(textStr.getString("icon_editor")));
addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent e) {
recieveFocus(e);
}
});
searchAction = new AbstractAction(textStr.getString("action_search"),
new ImageIcon(getClass().getResource(textStr.getString("icon_search")))) {
public void actionPerformed(ActionEvent e) {
searchPerformed(e);
}
};
searchAgainAction = new AbstractAction(textStr.getString("action_searchagain"),
new ImageIcon(getClass().getResource(textStr.getString("icon_searchagain")))) {
public void actionPerformed(ActionEvent e) {
searchAgainPerformed(e);
}
};
replaceAction = new AbstractAction(textStr.getString("action_replace"),
new ImageIcon(getClass().getResource(textStr.getString("icon_replace")))) {
public void actionPerformed(ActionEvent e) {
replacePerformed(e);
}
};
replaceAllAction = new AbstractAction(textStr.getString("action_replaceall"),
new ImageIcon(getClass().getResource(textStr.getString("icon_replaceall")))) {
public void actionPerformed(ActionEvent e) {
replaceAllPerformed(e);
}
};
autoCorrectAction = new AbstractAction(textStr.getString("action_autoCorrect")) {
public void actionPerformed(ActionEvent e) {
autoCorrectPerformed(e);
}
};
searchAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(70, java.awt.event.KeyEvent.CTRL_MASK, false));
searchAgainAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(82, java.awt.event.KeyEvent.CTRL_MASK, false));
replaceAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(69, java.awt.event.KeyEvent.CTRL_MASK, false));
replaceAllAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(75, java.awt.event.KeyEvent.CTRL_MASK, false));
autoCorrectAction.putValue(AbstractAction.ACCELERATOR_KEY, javax.swing.KeyStroke.getKeyStroke(84, java.awt.event.KeyEvent.CTRL_MASK, false));
}
private void personButton_actionPerformed(ActionEvent event) {
int index = this.text.getCaretPosition();
SimpleAttributeSet attr = new SimpleAttributeSet();
PersonInText box = new PersonInText(new PersonInfo("0", "", "Default"));
StyleConstants.setComponent(attr, box);
try {
this.getTextDoc().insertString(index, " ", attr);
this.getTextDoc().insertString(index + 1, " ", null);
} catch (BadLocationException ex) {
log.error("Error: Requesting bad location in document", ex);
}
box.setEditable(true);
}
public void showPopup(MouseEvent e) {
AbstractAction[] actions = getEditActions();
JPopupMenu popup = new JPopupMenu();
for (AbstractAction action : actions) popup.add(action);
popup.show(e.getComponent(), e.getX(), e.getY());
}
/**
* Get the length of this editor. The length is the text in characters.
*
* @return The lenght
*/
public int getLength() {
return text.getDocument().getLength();
}
/**
* The name to display for this editor, according to the current locale.
*
* @return The Name.
*/
public String getDisplayName() {
return textStr.getString("display_name");
}
/**
* An icon to display as a symbol for this editor. May return <code>null</code>.
*
* @return Icon representing the editor.
*/
public ImageIcon getDisplayIcon() {
return icon;
}
/**
* Gets the <code>Action</code> that will insert a new item of this type.
* This method may return <code>null</code> if it is not possible to add
* items of this type.
*
* @return Action for adding a new item.
*/
public Action getInsertAction() {
return null;
}
/**
* Gets the menu for this editor. May return <code>null</code>.
*
* @return The menu for this editor.
*/
public JMenu getMenu() {
return null;
}
/**
* This method is triggered by the cut action.
*
* @param e The event for the action.
* @see #getEditActions
* @see #cutAction
*/
protected void cutPerformed(ActionEvent e) {
copyPerformed(e);
int start = Math.min(dot, mark), end = Math.max(dot, mark);
if (start != end) {
try {
getTextDoc().remove(start, end - start);
}
catch (BadLocationException ex) {
log.error("Error: Requesting bad location in document", ex);
}
}
}
public void lostOwnership(Clipboard clipboard, Transferable contents) {
lostOwnership = true;
}
/**
* This method is triggered by the copy action.
*
* @param e The event for the action.
* @see #getEditActions
* @see #copyAction
*/
protected void copyPerformed(ActionEvent e) {
Clipboard system = Toolkit.getDefaultToolkit().getSystemClipboard();
int start = Math.min(dot, mark);
int end = Math.max(dot, mark);
if (start == end) {
return;
}
String plain = "";
try {
plain = getTextDoc().getText(start, end - start);
}
catch (Exception ex) {
log.error("Error retrieving document text (plain)", ex);
}
Transferable ss = new StringSelection(plain);
system.setContents(ss, this);
lostOwnership = false;
javax.swing.text.Element para;
Vector paraV = new Vector();
for (int here = start; here < end; here = para.getEndOffset()) {
para = getTextDoc().getParagraphElement(here);
CopiedParagraph p = new CopiedParagraph(para, start, end);
paraV.addElement(p);
}
CopiedParagraph[] paras = new CopiedParagraph[paraV.size()];
for (int i = 0; i < paras.length; i++)
paras[i] = (CopiedParagraph) paraV.elementAt(i);
ElementSelection es = new ElementSelection(paras);
clipboard.setContents(es, es);
}
protected class CopiedParagraph {
CopiedElement[] elements;
AttributeSet as;
public CopiedParagraph(javax.swing.text.Element paragraph, int after, int before) {
Vector v = new Vector();
for (int i = 0; i < paragraph.getElementCount(); i++) {
if (!(paragraph.getElement(i).getEndOffset() < after
|| paragraph.getElement(i).getStartOffset() > before)) {
v.addElement(new CopiedElement(paragraph.getElement(i), after, before));
}
}
elements = new CopiedElement[v.size()];
for (int i = 0; i < elements.length; i++) {
elements[i] = (CopiedElement) v.elementAt(i);
}
as = paragraph.getAttributes();
}
public CopiedElement[] getElements() {
return elements;
}
public AttributeSet getAttributes() {
return as;
}
}
protected class CopiedElement {
private AttributeSet attributeSet;
private String text;
public CopiedElement(javax.swing.text.Element e, int after, int before) {
attributeSet = e.getAttributes();
int start = Math.max(after, e.getStartOffset());
int end = Math.min(before, e.getEndOffset());
try {
text = e.getDocument().getText(start, end - start);
} catch (BadLocationException ex) {
log.error("Error: Requesting bad location in document", ex);
}
}
public AttributeSet getAttributes() {
return attributeSet;
}
public String getText() {
return text;
}
}
/**
* This method is triggered by the paste action.
*
* @param e The event for the action.
* @see #getEditActions
* @see #pasteAction
*/
protected void pastePerformed(ActionEvent e) {
if (lostOwnership) {
text.systemPaste();
return;
}
Transferable clipboardContent = clipboard.getContents(this);
if ((clipboardContent != null) && (clipboardContent.isDataFlavorSupported(paraFlavor))) {
try {
// Do something
CopiedParagraph[] paras = (CopiedParagraph[]) clipboardContent.getTransferData(paraFlavor);
if (dot != mark)
text.replaceSelection("");
javax.swing.text.Element whereInserted = getTextDoc().getParagraphElement(dot);
String wName = (String) whereInserted.getAttributes().getAttribute(AttributeSet.NameAttribute);
if (paras.length == 1) {
for (int el = 0; el < paras[0].getElements().length; el++) {
getTextDoc().insertString(dot, paras[0].getElements()[el].getText(), paras[0].getElements()[el].getAttributes());
}
return;
}
for (int p = 0; p < paras.length; p++) {
if (!wName.equals(paras[p].getAttributes().getAttribute(AttributeSet.NameAttribute))) {
if (p == 0 && whereInserted.getStartOffset() != whereInserted.getEndOffset() - 1) { // First line and not inserting into empty line
getTextDoc().insertString(dot, "\n", whereInserted.getAttributes());
}
}
for (int el = 0; el < paras[p].getElements().length; el++) {
getTextDoc().insertString(dot, paras[p].getElements()[el].getText(), paras[p].getElements()[el].getAttributes());
}
int tdot = dot;
if (!wName.equals(paras[p].getAttributes().getAttribute(AttributeSet.NameAttribute))) {
if (p == paras.length - 1) { // last line
getTextDoc().insertString(dot, "\n", whereInserted.getAttributes());
tdot -= 1;
}
}
if (p != paras.length - 1)
tdot = dot - 1;
javax.swing.text.Element newPara = getTextDoc().getParagraphElement(tdot);
getTextDoc().setParagraphAttributes(newPara.getStartOffset(),
0,
paras[p].getAttributes(), true);
}
}
catch (Exception ex) {
log.error("Error pasting in document", ex);
}
}
}
/**
* This method is triggered by the autoCorrect action
*/
protected void autoCorrectPerformed(ActionEvent e) {
getTextDoc().toggleAutoCorrection();
autoCorrectionState = getTextDoc().getAutoCorrectionState();
try {
LoginContext.server.savePreference(PREF_DOMAIN, PREF_KEY, "" + autoCorrectionState, LoginContext.sessionKey);
}
catch (Exception ex) {
log.error("Can't save preference" + PREF_KEY, ex);
}
}
protected boolean getAutoCorrectionState() {
return autoCorrectionState;
}
private void setAutoCorrectState(boolean state) {
getTextDoc().setAutoCorrectState(state);
autoCorrectionState = getTextDoc().getAutoCorrectionState();
}
/**
* Returns the text of the article being edited
*/
public String getText() {
try {
return getTextDoc().getText(0, getTextDoc().getLength());
}
catch (BadLocationException ble) {
return "";
}
}
/**
* This method is called when the paragraph style combo box
* is changed.
*
* @param e The <code>ActionEvent</code> from the selection.
*/
protected void styleComboAction(ActionEvent e) {
if (styleChange) {
javax.swing.text.Element para;
Style s = getTextDoc().getStyle(styleCombo.getSelectedItem().toString());
if (s != null) {
int start = Math.min(dot, mark), end = Math.max(dot, mark);
for (int here = end; here >= start; here = para.getStartOffset() - 1) {
para = getTextDoc().getParagraphElement(here);
getTextDoc().setParagraphAttributes(para.getStartOffset(),
para.getEndOffset() - para.getStartOffset(),
s, true);
}
fireTextChanged();
}
}
text.requestFocus();
}
/**
* This method is called when one of the inline style buttons
* are pressed.
*
* @param e The <code>ActionEvent</code> from the selection.
*/
protected void styleButtonAction(ActionEvent e) {
int start = Math.min(dot, mark);
int end = Math.max(dot, mark);
String style = e.getActionCommand();
//if the style already is set, we don't want to insert another space
if(!style.equals(getCharacterStyle(start-1))){
//nothing is selected, and we want to start typing bold letters
if (start==end){
try{
//if there already is a space: make it boldstyle
if(getTextDoc().getText(end-1,1).equals(" ")){
toggleCharacterStyle(start-1, start, style);
text.getCaret().moveDot(start-1);
text.getCaret().moveDot(start);
}
//there is no space and we have to insert one
else{
this.getTextDoc().insertString(start," ",null);
toggleCharacterStyle(start, start+1, style);
text.getCaret().moveDot(start);
text.getCaret().moveDot(start+1);
}
}
catch (BadLocationException ex){
log.error("Error: Requesting bad location in document", ex);
}
}
//we have selected some letters and want to convert them to bold letters
else {
toggleCharacterStyle(start, end, style);
}
}
//the style is already set and we want to change it back
else {
try{
//if there already is a space: make it normal style
AttributeSet as = StyleContext.getDefaultStyleContext().getEmptySet();
if(getTextDoc().getText(end-1,1).equals(" ")){
getTextDoc().setCharacterAttributes(start-1,1,as,true);
}
//there is no space and we have to insert one
else{
this.getTextDoc().insertString(start," ",null);
getTextDoc().setCharacterAttributes(start,1,as,true);
}
}
catch (BadLocationException ex){
log.error("Error: Requesting bad location in document", ex);
}
}
updateCharacterButtons();
if (!text.hasFocus()) {
text.requestFocus();
}
}
/**
* Called when one of the buttons for special characters are pressed.
*
* @param e The <code>ActionEvent</code> for the press.
*/
protected void charButtonAction(ActionEvent e) {
String ch = e.getActionCommand();
text.replaceSelection(ch);
text.requestFocus();
text.setCaretPosition(dot);
text.moveCaretPosition(dot);
}
/**
* Called when the caret is moved or the selection in the document is changed.<p>
* Remember that the dot is always the position of the cursor, while mark is
* the other end of the selection. Dragging the cursor from 2 to 5 will
* give dot=5 and mark=2.
*
* @param e The event that triggered this method.
*/
protected abstract void caretMoved(CaretEvent e);
/**
* Updates the character buttons according to the current selection.
*/
protected void updateCharacterButtons() {
// Trivial case: No selection
if (dot == mark) {
// Subtract 1 because typing at the start of a bold
// element gives normal text, typing at the
// end gives bold.
// In other words, the selection is offset by 1 to reflect
// the actual result.
setButtonsForStyle(getCharacterStyle(dot - 1), true);
} else {
// we have a selection
int start = Math.min(dot, mark);
int end = Math.max(dot, mark);
//Log.m(this, "updateCharacterButtons",
// "start=" + start + ", end=" + end);
javax.swing.text.Element first = getTextDoc().getCharacterElement(start);
javax.swing.text.Element last = getTextDoc().getCharacterElement(end - 1);
//Log.m(this, "updateCharacterButtons",
// "First element=" + first);
//Log.m(this, "updateCharacterButtons",
// "Last element=" + last);
if (first == last) {
// Simple, this is within the same element
setButtonsForStyle(getCharacterStyle(start), true);
} else {
//setButtonsForStyle("", false);
if (isCoherentSelection(start, end)) {
setButtonsForStyle(getCharacterStyle(start), true);
} else {
// If the selection spans multiple paragraphs, disable all actions
javax.swing.text.Element firstPara = getTextDoc().getParagraphElement(start);
javax.swing.text.Element lastPara = getTextDoc().getParagraphElement(end - 1);
if (firstPara != lastPara) {
setButtonsForStyle("", false);
} else {
setButtonsForStyle("", true);
}
}
}
}
}
/**
* Returns true if all elements between start inclusive and end exclusive
* is of the same type.
*
* @param start Start of the selection.
* @param end End of the selection.
* @return <code>true</code> if the selection is coherent.
*/
protected boolean isCoherentSelection(int start, int end) {
javax.swing.text.Element current = getTextDoc().getCharacterElement(start);
Object style = current.getAttributes().getAttribute(AttributeSet.NameAttribute);
int pos = current.getEndOffset();
while (pos < end) {
current = getTextDoc().getCharacterElement(pos + 1);
Object s = current.getAttributes().getAttribute(AttributeSet.NameAttribute);
if (!style.equals(s)) {
return false;
}
pos = current.getEndOffset();
}
return true;
}
/**
* Toggle the style of the selection from start inclusive to end exclusive.
* If the style is set on all elements within the selection the style will
* be turned off, if only some (or none) of the elements is set it will
* be turned off.<p>
* This implementation only works when the selection is within the same
* paragraph, as with {@link #isCoherentSelection isCoherentSelection}.
*
* @param start The start of the selection.
* @param end The end of the selection.
* @param style The style to toggle.
*/
protected void toggleCharacterStyle(int start, int end, String style) {
AttributeSet as;
if (isCoherentSelection(start, end)) {
// Toggle style, check the first element
String firstStyle = getCharacterStyle(start);
if (style.equals(firstStyle)) {
// Set style to no style
as = StyleContext.getDefaultStyleContext().getEmptySet();
} else {
as = getTextDoc().getStyle(style);
}
} else {
as = getTextDoc().getStyle(style);
}
getTextDoc().setCharacterAttributes(start, (end - start), as, true);
}
/**
* Update all character buttons for the specified style.
*
* @param style The style.
* @param selection The selection status.
*/
protected void setButtonsForStyle(String style, boolean selection) {
for (int i = 0; i < charStyleButtons.size(); i++) {
JToggleButton b = (JToggleButton) charStyleButtons.elementAt(i);
b.setEnabled(selection);
emAction.setEnabled(selection);
boldAction.setEnabled(selection);
if (style.equals(b.getActionCommand())) {
b.setSelected(true);
} else {
b.setSelected(false);
}
}
}
/**
* Updates the enabled status of the edit actions.
*/
protected void updateEditActions() {
// No selection
if (dot == mark) {
if (cutAction.isEnabled()) {
cutAction.setEnabled(false);
}
if (copyAction.isEnabled()) {
copyAction.setEnabled(false);
}
// Editor has selection
} else {
if (!cutAction.isEnabled()) {
cutAction.setEnabled(true);
}
if (!copyAction.isEnabled()) {
copyAction.setEnabled(true);
}
}
// FIXME: Check if there is something on the clipboard
if (!pasteAction.isEnabled() && artis.canEditArticle()) {
pasteAction.setEnabled(true);
}
if (!artis.canEditArticle()){
cutAction.setEnabled(false);
}
}
/**
* Called when the component is focused. Used to set the selection
* and update the interface.
*
* @param e The focus event.
*/
protected void recieveFocus(FocusEvent e) {
updateEditActions();
text.requestFocus();
}
/**
* Loads all the paragraph styles from the current document into
* the <code>JComboBox</code>.
* <br>A paragraph style is a style that
* has a certain value for the <code>display</code> property. At
* the moment the only valid value for this propery is <code>block</code>.
*
* @param list The <code>JComboBox</code> to fill.
* @param styles The source of styles.
*/
protected void loadParagraphStyles(JComboBox list, StyleContext styles) {
Enumeration e = styles.getStyleNames();
Style s;
Object display;
while (e.hasMoreElements()) {
s = styles.getStyle(e.nextElement().toString());
display = s.getAttribute("display");
if (display != null && display.equals("block")) {
// We have to make sure that styles are not added more
// than once to ComboBox.
int items = list.getItemCount();
boolean flag = false;
for (int i = 0; i < items; ++i) {
if (list.getItemAt(i).equals(s.getName())) {
flag = true;
break;
}
}
// FIXME: Here we should include shortkey description
// in combobox in same style as the pulldown
// menus. Description in plain text is horribly
// ugly... (handegar)
if (!flag)
list.addItem(s.getName());
}
}
}
/**
* Gets the name of the paragraph style for the given location.
*
* @param pos The position to get the style from.
* @return The name for the style.
*/
protected String getParagraphStyle(int pos) {
javax.swing.text.Element para = getTextDoc().getParagraphElement(pos);
AttributeSet attSet = para.getAttributes();
Object name = attSet.getAttribute("name");
if (name == null) {
Enumeration e = attSet.getAttributeNames();
Object key, value;
while (e.hasMoreElements()) {
key = e.nextElement();
value = attSet.getAttribute(key);
if (key.toString().equals("name")) {
name = value;
}
}
}
if (name == null) {
return null;
} else {
return name.toString();
}
}
/**
* Gets the name of the element style for the given location. Note that
* this will return the style to the right of the cursor.
*
* @param pos The position to get the style from.
* @return The name for the style.
*/
protected String getCharacterStyle(int pos) {
javax.swing.text.Element elem = getTextDoc().getCharacterElement(pos);
if (elem == null) {
log.error("Unable to find character element!");
return null;
}
AttributeSet attSet = elem.getAttributes();
Object name = attSet.getAttribute(AttributeSet.NameAttribute);
if (name != null) {
return name.toString();
} else {
return null;
}
}
protected class CompoundUndoManager extends UndoManager implements
UndoableEditListener {
public CompoundEdit compoundEdit;
private int lastOffset;
private int lastLength;
public CompoundUndoManager() {
}
/*
* * Whenever an UndoableEdit happens the edit will either be absorbed* by
* the current compound edit or a new compound edit will be started
*/
public void undoableEditHappened(UndoableEditEvent e) {
// Start a new compound edit
// Check for an attribute change
AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent) e
.getEdit();
if (compoundEdit == null) {
compoundEdit = startCompoundEdit(event, e.getEdit());
lastLength = event.getDocument().getLength();
return;
}
if (event.getType().equals(DocumentEvent.EventType.CHANGE)) {
compoundEdit.addEdit(e.getEdit());
return;
}
int offsetChange = event.getOffset() - lastOffset;
int lengthChange = event.getDocument().getLength() - lastLength;
try {
if (Math.abs(offsetChange) == 1 && Math.abs(lengthChange) == 1) { // Moved
// one
// letter
// Line breaks and spaces separate the edits that are undone
if ((event.getDocument().getLength() > lastOffset)
&& (event.getDocument().getText(lastOffset, 1).equals(
" ") || event.getDocument().getText(
lastOffset, 1).equals("\n"))) {
compoundEdit.end();
compoundEdit = startCompoundEdit(event, e.getEdit());
return;
} else { // If there is not a space, add it to an edit (word)
compoundEdit.addEdit(e.getEdit());
lastOffset = event.getOffset();
lastLength = event.getDocument().getLength();
return;
}
}
} catch (BadLocationException e1) {
// e1.printStackTrace();
}
// Just in case of error make sure we capture the edit.
compoundEdit.end();
compoundEdit = startCompoundEdit(event, e.getEdit());
undoAction.updateUndoState();
redoAction.updateRedoState();
}
/*
* * Each CompoundEdit will store a group of related incremental edits* (ie.
* each character typed or backspaced is an incremental edit)
*/
private CompoundEdit startCompoundEdit(AbstractDocument.DefaultDocumentEvent event, UndoableEdit anEdit) {
// Track Caret and Document information of this compound edit
lastOffset = event.getOffset();
lastLength = event.getDocument().getLength();
// The compound edit is used to store incremental edits
compoundEdit = new MyCompoundEdit();
compoundEdit.addEdit(anEdit);
// The compound edit is added to the UndoManager. All incremental
// edits stored in the compound edit will be undone/redone at once
addEdit(compoundEdit);
return compoundEdit;
}
class MyCompoundEdit extends CompoundEdit {
public boolean isInProgress() {
// in order for the canUndo() and canRedo() methods to work
// assume that the compound edit is never in progress
return false;
}
public void undo() throws CannotUndoException {
// End the edit so future edits don't get absorbed by this edit
if (compoundEdit != null)
compoundEdit.end();
super.undo();
// Always start a new compound edit after an undo
compoundEdit = null;
}
}
}
class UndoAction extends AbstractAction {
String origName;
public UndoAction(String name, Icon icon) {
super(name, icon);
setEnabled(false);
this.origName = name;
}
public void actionPerformed(ActionEvent e) {
try {
undo.undo();
} catch (CannotUndoException ex) {
log.error("Unable to undo: " + ex, ex);
}
updateUndoState();
redoAction.updateRedoState();
}
protected void updateUndoState() {
if (undo.canUndo()) {
setEnabled(true);
} else {
setEnabled(false);
}
}
}
class RedoAction extends AbstractAction {
String origName;
public RedoAction(String name, Icon icon) {
super(name, icon);
setEnabled(false);
origName = name;
}
public void actionPerformed(ActionEvent e) {
try {
undo.redo();
} catch (CannotRedoException ex) {
log.error("Unable to redo: " + ex, ex);
}
updateRedoState();
undoAction.updateUndoState();
}
protected void updateRedoState() {
if (undo.canRedo()) {
setEnabled(true);
} else {
setEnabled(false);
}
}
}
}