/** * 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); } } } }