/* * Created on Aug 18, 2006 */ package net.atlanticbb.tantlinger.ui.text; import java.lang.ref.WeakReference; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.HashMap; import java.util.List; import java.util.Vector; import java.util.Iterator; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JPopupMenu; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.UndoableEditEvent; import javax.swing.event.UndoableEditListener; import javax.swing.text.DefaultEditorKit; import javax.swing.text.JTextComponent; import javax.swing.text.TextAction; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; import net.atlanticbb.tantlinger.i18n.I18n; import net.atlanticbb.tantlinger.ui.UIUtils; /** * Manages an application-wide popup menu for JTextComponents. Any * JTextComponent registered with the manager will have a right-click invokable * popup menu, which provides options to undo, redo, cut, copy, paste, and * select-all. The popup manager is a singleton and must be retrieved with the * getInstance() method: * * <pre><code> * JTextField textField = new JTextField(20); * TextEditPopupManager.getInstance().registerJTextComponent(textField); * </code></pre> * * @author Bob Tantlinger * TODO Internationalize, add mnemonics, etc */ public class TextEditPopupManager { private static final I18n i18n = I18n.getInstance("net.atlanticbb.tantlinger.ui.text"); private static TextEditPopupManager singleton = null; public static final String CUT = "cut"; public static final String COPY = "copy"; public static final String PASTE = "paste"; public static final String SELECT_ALL = "selectAll"; public static final String UNDO = "undo"; public static final String REDO = "redo"; private HashMap actions = new HashMap(); // The actions we add to the popup menu private Action cut = new DefaultEditorKit.CutAction(); private Action copy = new DefaultEditorKit.CopyAction(); private Action paste = new DefaultEditorKit.PasteAction(); private Action selectAll = new NSelectAllAction(); private Action undo = new UndoAction(); private Action redo = new RedoAction(); // maintains a list of the currently registered JTextComponents private List textComps = new Vector(); private List undoers = new Vector(); private JTextComponent focusedComp;// the registered JTextComponent that is // focused private UndoManager undoer; // The undomanager for the focused // JTextComponent // Listeners for the JTextComponents private FocusListener focusHandler = new PopupFocusHandler(); private MouseListener popupHandler = new PopupHandler(); private UndoListener undoHandler = new UndoListener(); private CaretListener caretHandler = new CaretHandler(); private JPopupMenu popup = new JPopupMenu();// The one and only popup menu private TextEditPopupManager() { cut.putValue(Action.NAME, i18n.str("cut")); cut.putValue(Action.SMALL_ICON, UIUtils.getIcon(UIUtils.X16, "cut.png")); copy.putValue(Action.NAME, i18n.str("copy")); copy.putValue(Action.SMALL_ICON, UIUtils.getIcon(UIUtils.X16, "copy.png")); paste.putValue(Action.NAME, i18n.str("paste")); paste.putValue(Action.SMALL_ICON, UIUtils.getIcon(UIUtils.X16, "paste.png")); selectAll.putValue(Action.ACCELERATOR_KEY, null); popup.add(undo); popup.add(redo); popup.addSeparator(); popup.add(cut); popup.add(copy); popup.add(paste); popup.addSeparator(); popup.add(selectAll); actions.put(CUT, cut); actions.put(COPY, copy); actions.put(PASTE, paste); actions.put(SELECT_ALL, selectAll); actions.put(UNDO, undo); actions.put(REDO, redo); } /** * Gets the singleton instance of TextEditPopupManager * * @return The one and only TextEditPopupManager */ public static TextEditPopupManager getInstance() { if(singleton == null) singleton = new TextEditPopupManager(); return singleton; } public Action getAction(String name) { return (Action)actions.get(name); } /** * Registers a JTextComponent with the manager. Note that if you change the * document of the JTextComponent, you should unregister it with method * unregisterJTextComponent, and then re-register it with this method. * e.g... * * <pre><code> * TextEditPopupManager.getInstance().registerJTextComponent(comp); * ... * ... * TextEditPopupManager.getInstance().unregisterJTextComponent(comp); * comp.setDocument(new PlainDocument()); * TextEditPopupManager.getInstance().registerJTextComponent(comp); * </code></pre> * * @param tc The JTextComponent to register * @throws IllegalArgumentException If the component is null, or already * registered */ public void registerJTextComponent(JTextComponent tc) throws IllegalArgumentException { registerJTextComponent(tc, new UndoManager()); } /** * Registers a JTextComponent and UndoManager with the manager. This is * useful if you wish to supply a custom UndoManager * * @param tc The JTextComponent to register * @param um The UndoManger to register * @throws IllegalArgumentException If the component is null, or already * registered */ public void registerJTextComponent(JTextComponent tc, UndoManager um) throws IllegalArgumentException { if(tc == null || um == null) throw new IllegalArgumentException("null arguments aren't allowed"); if(getIndexOfJTextComponent(tc) != -1) throw new IllegalArgumentException("Component already registered"); tc.addFocusListener(focusHandler); tc.addCaretListener(caretHandler); tc.addMouseListener(popupHandler); tc.getDocument().addUndoableEditListener(undoHandler); textComps.add(new WeakReference(tc)); undoers.add(um); } /** * Unregisters a JTextComponent from the manager. * * @param tc The JTextComponent to unregister */ public void unregisterJTextComponent(JTextComponent tc) { int index = getIndexOfJTextComponent(tc); if(index != -1) { tc.removeFocusListener(focusHandler); tc.removeCaretListener(caretHandler); tc.removeMouseListener(popupHandler); tc.getDocument().removeUndoableEditListener(undoHandler); textComps.remove(index); undoers.remove(index); } } /** * Gets the index of a registered JTextComponent * * @param tc * @return */ protected int getIndexOfJTextComponent(JTextComponent tc) { for(int i = 0; i < textComps.size(); i++) { WeakReference wr = (WeakReference)textComps.get(i); if(wr.get() == tc) return i; } return -1; } /** * Clears any JTextComponent references from the manager that have been * garbage collected. */ private void clearEmptyReferences() { for(int i = 0; i < textComps.size(); i++) { WeakReference wr = (WeakReference)textComps.get(i); if(wr.get() == null) undoers.set(i, null); } for(Iterator it = textComps.iterator(); it.hasNext();) { WeakReference w = (WeakReference)it.next(); if(w.get() == null) it.remove(); } for(Iterator it = undoers.iterator(); it.hasNext();) { if(it.next() == null) it.remove(); } } /** * Updates the enabled state of the actions */ private void updateActions() { if(focusedComp != null && focusedComp.hasFocus()) { undo.setEnabled(undoer.canUndo()); redo.setEnabled(undoer.canRedo()); boolean hasSel = focusedComp.getSelectedText() != null; copy.setEnabled(hasSel); cut.setEnabled(hasSel); } } /* * Listens for undoable edits on the documents of registered JTextComponents */ private class UndoListener implements UndoableEditListener { public void undoableEditHappened(UndoableEditEvent e) { UndoableEdit edit = e.getEdit(); if(undoer != null) { undoer.addEdit(edit); updateActions(); } } } /* * Undo and redo actions */ private class RedoAction extends AbstractAction { private static final long serialVersionUID = 1L; public RedoAction() { super(i18n.str("redo"), UIUtils.getIcon(UIUtils.X16, "redo.png")); putValue(MNEMONIC_KEY, new Integer(i18n.mnem("redo"))); } public void actionPerformed(ActionEvent e) { try { if(undoer != null) { undoer.redo(); updateActions(); } } catch (Exception ex) { System.out.println("Cannot Redo"); } } } private class UndoAction extends AbstractAction { private static final long serialVersionUID = 1L; public UndoAction() { super(i18n.str("undo"), UIUtils.getIcon(UIUtils.X16, "undo.png")); putValue(MNEMONIC_KEY, new Integer(i18n.mnem("undo"))); } public void actionPerformed(ActionEvent e) { try { if(undoer != null) { undoer.undo(); updateActions(); } } catch (Exception ex) { System.out.println("Cannot Undo"); } } } /* * Select all action for the registered JTextComponents */ private class NSelectAllAction extends TextAction { private static final long serialVersionUID = 1L; public NSelectAllAction() { super(i18n.str("select_all")); putValue(MNEMONIC_KEY, new Integer(i18n.mnem("select_all"))); } public void actionPerformed(ActionEvent e) { getTextComponent(e).selectAll(); } } /* * Listens for focus changes on the registered components and updates the * UndoManager accordingly */ private class PopupFocusHandler implements FocusListener { public void focusGained(FocusEvent e) { if(!e.isTemporary()) { JTextComponent tc = (JTextComponent)e.getComponent(); int index = getIndexOfJTextComponent(tc); if(index != -1) { // set the current UndoManager for the currently focused // JTextComponent undoer = (UndoManager)undoers.get(index); focusedComp = tc; updateActions(); } // clean up any dead refs that have been garbage collected clearEmptyReferences(); } } public void focusLost(FocusEvent e) { } } /* * Listens for caret changes on the registered JTextComponents */ private class CaretHandler implements CaretListener { public void caretUpdate(CaretEvent e) { updateActions(); } } /* * Handles right clicks on the component to popup the menu */ private class PopupHandler extends MouseAdapter { public void mousePressed(MouseEvent e) { checkForPopupTrigger(e); } public void mouseReleased(MouseEvent e) { checkForPopupTrigger(e); } private void checkForPopupTrigger(MouseEvent e) { JTextComponent tc = (JTextComponent)e.getComponent(); if(e.isPopupTrigger() && tc.isEditable()) { if(!tc.isFocusOwner()) tc.requestFocusInWindow(); popup.show(tc, e.getX(), e.getY()); } } } }