/* * Copyright 2001-2013 Stephen Colebourne * * 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.joda.beans.ui.swing.component; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.KeyStroke; import javax.swing.event.UndoableEditListener; import javax.swing.text.AbstractDocument; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.CompoundEdit; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; /** * An advanced {@code UndoManager} for text fields. * <p> * This undo manager adds time-based grouping to undo management. * Any edits made where the duration between two edits is less than 500ms are merged. * The duration is configurable. */ public final class TextUndoManager extends UndoManager { /** * Serialization version. */ private static final long serialVersionUID = 1L; /** * Ctrl+Z or similar. */ private static final KeyStroke CTRL_Z = KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); /** * Ctrl+Y or similar. */ private static final KeyStroke CTRL_Y = KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); /** * Undo key. */ private static final String UNDO = "Undo"; /** * Redo key. */ private static final String REDO = "Redo"; /** * The delay milliseconds to use. */ private int delayMillis = 500; /** * The current edit. */ private CompoundEdit currentEdit; /** * The instant of the last edit, millis. */ private long lastEditInstantMillis; //------------------------------------------------------------------------- /** * Applies the undo manager to a text component. * <p> * Stores 100 edits. * * @param component the text field to add to, not null */ public static void applyTo(JTextComponent component) { applyTo(component, 100); } /** * Applies the undo manager to a text component. * * @param component the text field to add to, not null * @param limit the maximum history of edits */ public static void applyTo(JTextComponent component, int limit) { final TextUndoManager undo = new TextUndoManager(); undo.setLimit(limit); component.getDocument().addUndoableEditListener(undo); InputMap im = component.getInputMap(); im.put(CTRL_Z, UNDO); im.put(CTRL_Y, REDO); ActionMap am = component.getActionMap(); am.put(UNDO, new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent event) { if (undo.canUndo()) { undo.undo(); } } }); am.put(REDO, new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent event) { if (undo.canRedo()) { undo.redo(); } } }); } /** * Resets the undo manager. * * @param textComponent the text field to change, not null */ public static void reset(JTextComponent textComponent) { Document document = textComponent.getDocument(); if (document instanceof AbstractDocument) { for (UndoableEditListener undo : ((AbstractDocument) document).getUndoableEditListeners()) { if (undo instanceof TextUndoManager) { ((TextUndoManager) undo).discardAllEdits(); } } } } //------------------------------------------------------------------------- /** * Creates an undo manager. * <p> * This will not be attached to a text field. * Use {@link #applyTo(JTextComponent)} for full integration. */ public TextUndoManager() { } //------------------------------------------------------------------------- /** * Gets the delay in milliseconds before declaring edits complete. * * @return the delay in milliseconds, not negative */ public synchronized int getDelayMillis() { return delayMillis; } /** * Sets the delay in milliseconds before declaring edits complete. * * @param delayMillis the delay in milliseconds, not negative */ public synchronized void setDelayMillis(int delayMillis) { if (delayMillis < 0) { throw new IllegalArgumentException("Delay must be zero or greater"); } this.delayMillis = delayMillis; } //------------------------------------------------------------------------- @Override public synchronized boolean addEdit(UndoableEdit edit) { long now = System.currentTimeMillis(); boolean inProgress; if (currentEdit == null) { currentEdit = new CompoundEdit(); inProgress = currentEdit.addEdit(edit); } else { if (now - lastEditInstantMillis > delayMillis) { finishEdit(); currentEdit = new CompoundEdit(); } inProgress = currentEdit.addEdit(edit); } lastEditInstantMillis = now; return inProgress; } @Override public synchronized void discardAllEdits() { currentEdit = null; super.discardAllEdits(); } @Override public void die() { currentEdit = null; super.die(); } @Override public synchronized void end() { finishEdit(); super.end(); } @Override public synchronized boolean canUndo() { finishEdit(); return super.canUndo(); } @Override public synchronized void undo() throws CannotUndoException { finishEdit(); super.undo(); } @Override public synchronized boolean canRedo() { finishEdit(); return super.canRedo(); } @Override public synchronized void redo() throws CannotRedoException { finishEdit(); super.redo(); } private void finishEdit() { if (currentEdit != null) { currentEdit.end(); super.addEdit(currentEdit); currentEdit = null; } } @Override public synchronized boolean canUndoOrRedo() { finishEdit(); return super.canUndoOrRedo(); } @Override public synchronized void undoOrRedo() throws CannotRedoException, CannotUndoException { finishEdit(); super.undoOrRedo(); } }