/*
* Copyright 2016 Laszlo Balazs-Csiki
*
* This file is part of Pixelitor. Pixelitor is free software: you
* can redistribute it and/or modify it under the terms of the GNU
* General Public License, version 3 as published by the Free
* Software Foundation.
*
* Pixelitor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Pixelitor. If not, see <http://www.gnu.org/licenses/>.
*/
package pixelitor.history;
import pixelitor.gui.PixelitorWindow;
import pixelitor.gui.utils.GUIUtils;
import pixelitor.utils.Messages;
import pixelitor.utils.VisibleForTesting;
import pixelitor.utils.debug.DebugNode;
import javax.swing.*;
import javax.swing.event.EventListenerList;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;
/**
* An undo manager that is also a list model for debugging history
*/
public class PixelitorUndoManager extends UndoManager implements ListModel<PixelitorEdit> {
private final HistoryListSelectionModel selectionModel;
private final EventListenerList listenerList = new EventListenerList();
private JDialog historyDialog;
private PixelitorEdit selectedEdit;
/**
* When we get a selection event and this variable is true,
* we can be sure that the change was initiated by the user
* through the history GUI, and not through addEdit, undo, redo calls
*/
private boolean manualSelectionChange = true;
public PixelitorUndoManager() {
selectionModel = new HistoryListSelectionModel();
selectionModel.setSelectionMode(DefaultListSelectionModel.SINGLE_SELECTION);
selectionModel.addListSelectionListener(e -> {
if (!manualSelectionChange) {
return;
}
// if the selection was changed by clicking on the JList
// in the history panel, then jump to the correct state
int selectedIndex = getSelectedIndex();
assert selectedIndex != -1;
PixelitorEdit newSelectedEdit = getElementAt(selectedIndex);
if (newSelectedEdit != selectedEdit) {
jumpTo(newSelectedEdit);
}
});
}
/**
* This method is necessary mostly because lastEdit() in CompoundEdit is protected
*/
public PixelitorEdit getLastEdit() {
UndoableEdit edit = super.lastEdit();
return (PixelitorEdit) edit;
}
@Override
public boolean addEdit(UndoableEdit edit) {
assert edit instanceof PixelitorEdit;
// 1. do the actual addEdit
boolean retVal = super.addEdit(edit);
// 2. update the selection model
manualSelectionChange = false;
int index = edits.size() - 1;
fireIntervalAdded(this, index, index);
selectionModel.setSelectionInterval(index, index);
manualSelectionChange = true;
selectedEdit = (PixelitorEdit) edit;
return retVal;
}
@Override
public void undo() throws CannotUndoException {
String editName = selectedEdit.getName();
// 1. do the actual undo
super.undo();
// 2. update the selection model
manualSelectionChange = false;
int index = getSelectedIndex();
if (index > 0) {
int prevIndex = index - 1;
selectionModel.setSelectionInterval(prevIndex, prevIndex);
selectedEdit = (PixelitorEdit) edits.get(prevIndex);
} else {
selectionModel.setAllowDeselect(true);
selectionModel.clearSelection();
selectionModel.setAllowDeselect(false);
selectedEdit = null;
}
manualSelectionChange = true;
// 3. show status message
Messages.showStatusMessage(editName + " undone.");
}
@Override
public void redo() throws CannotRedoException {
// 1. do the actual redo
super.redo();
// 2. update the selection model
manualSelectionChange = false;
if (selectionModel.isSelectionEmpty()) {
// the first gets selected
selectionModel.setSelectionInterval(0, 0);
selectedEdit = (PixelitorEdit) edits.get(0);
} else {
int index = getSelectedIndex();
int nextIndex = index + 1;
selectionModel.setSelectionInterval(nextIndex, nextIndex);
selectedEdit = (PixelitorEdit) edits.get(nextIndex);
}
manualSelectionChange = true;
// this will be true only after the redo is done!
String editName = selectedEdit.getName();
// 3. show status message
Messages.showStatusMessage(editName + " redone.");
}
public int getSelectedIndex() {
if (selectionModel.isSelectionEmpty()) {
return -1;
}
return selectionModel.getLeadSelectionIndex();
}
// ListModel methods
@Override
public int getSize() {
return edits.size();
}
@Override
public PixelitorEdit getElementAt(int index) {
return (PixelitorEdit) edits.get(index);
}
@Override
public void addListDataListener(ListDataListener l) {
listenerList.add(ListDataListener.class, l);
}
@Override
public void removeListDataListener(ListDataListener l) {
listenerList.remove(ListDataListener.class, l);
}
private void fireIntervalAdded(Object source, int index0, int index1) {
Object[] listeners = listenerList.getListenerList();
ListDataEvent e = null;
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ListDataListener.class) {
if (e == null) {
e = new ListDataEvent(source, ListDataEvent.INTERVAL_ADDED, index0, index1);
}
((ListDataListener) listeners[i + 1]).intervalAdded(e);
}
}
}
private void fireIntervalRemoved(Object source, int index0, int index1) {
Object[] listeners = listenerList.getListenerList();
ListDataEvent e = null;
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ListDataListener.class) {
if (e == null) {
e = new ListDataEvent(source, ListDataEvent.INTERVAL_REMOVED, index0, index1);
}
((ListDataListener) listeners[i + 1]).intervalRemoved(e);
}
}
}
/**
* Jumps in the history so that we have the state after the given edit
*/
private void jumpTo(PixelitorEdit targetEdit) {
assert targetEdit != selectedEdit;
int targetIndex = edits.indexOf(targetEdit);
int currentIndex = edits.indexOf(selectedEdit);
assert targetIndex != currentIndex;
if (targetIndex > currentIndex) {
// redo until necessary
while (currentIndex < targetIndex) {
super.redo();
currentIndex++;
}
} else {
// undo until necessary
while (currentIndex > targetIndex) {
super.undo();
currentIndex--;
}
}
selectedEdit = targetEdit;
}
public void showHistory() {
if (historyDialog == null) {
JList<PixelitorEdit> historyList = new JList<>(this);
historyList.setSelectionModel(selectionModel);
historyDialog = new JDialog(PixelitorWindow.getInstance(), "History", false);
JPanel p = new HistoryPanel(this, historyList);
historyDialog.getContentPane().add(p);
historyDialog.setSize(200, 300);
GUIUtils.centerOnScreen(historyDialog);
}
if (!historyDialog.isVisible()) {
historyDialog.setVisible(true);
}
}
public void dumpHistory() {
int numEdits = edits.size();
System.out.println("PixelitorUndoManager.dumpHistory:");
for (int i = 0; i < numEdits; i++) {
PixelitorEdit edit = (PixelitorEdit) edits.get(i);
System.out.println("edit [" + i + "] = " + edit.dump());
}
}
@VisibleForTesting
public ListSelectionModel getSelectionModel() {
return selectionModel;
}
// the super method is not public
public PixelitorEdit getEditToBeUndone() {
return (PixelitorEdit) super.editToBeUndone();
}
// the super method is not public
protected PixelitorEdit getEditToBeRedone() {
return (PixelitorEdit) super.editToBeRedone();
}
// this method is called whenever a not undoable edit was added
@Override
public synchronized void discardAllEdits() {
int numEdits = edits.size();
if (numEdits == 0) {
return;
}
// discard form the history
super.discardAllEdits();
// discard from the GUI
manualSelectionChange = false;
int maxIndex = numEdits - 1;
fireIntervalRemoved(this, 0, maxIndex);
manualSelectionChange = true;
}
public DebugNode getDebugNode() {
DebugNode node = new DebugNode("Edits", this);
for (int i = 0; i < getSize(); i++) {
PixelitorEdit edit = getElementAt(i);
node.add(edit.getDebugNode());
}
return node;
}
}