/*
* Copyright 2017 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.Build;
import pixelitor.Composition;
import pixelitor.ConsistencyChecks;
import pixelitor.gui.ImageComponents;
import pixelitor.layers.Drawable;
import pixelitor.menus.MenuAction;
import pixelitor.menus.MenuAction.AllowedLayerType;
import pixelitor.utils.AppPreferences;
import pixelitor.utils.IconUtils;
import pixelitor.utils.Messages;
import pixelitor.utils.VisibleForTesting;
import pixelitor.utils.debug.DebugNode;
import pixelitor.utils.test.Events;
import javax.swing.*;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEditSupport;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Static methods for managing history and undo/redo for all open images
*/
public class History {
private static final UndoableEditSupport undoableEditSupport = new UndoableEditSupport();
private static final PixelitorUndoManager undoManager = new PixelitorUndoManager();
private static int numUndoneEdits = 0;
private static boolean ignoreEdits = false;
static {
setUndoLevels(AppPreferences.loadUndoLevels());
}
public static final Action UNDO_ACTION = new MenuAction("Undo",
IconUtils.getUndoIcon(), AllowedLayerType.ANY) {
@Override
public void onClick() {
History.undo();
}
};
public static final Action REDO_ACTION = new MenuAction("Redo",
IconUtils.getRedoIcon(), AllowedLayerType.ANY) {
@Override
public void onClick() {
History.redo();
}
};
private History() {
}
/**
* This is used to notify the menu items
*/
public static void notifyMenus(PixelitorEdit edit) {
undoableEditSupport.postEdit(edit);
}
public static void addEdit(boolean addToHistory, Supplier<PixelitorEdit> supplier) {
if (addToHistory) {
addEdit(supplier.get());
}
}
public static void addEdit(PixelitorEdit edit) {
assert edit != null;
if (ignoreEdits) {
return;
}
edit.getComp().setDirty(true);
if (edit.canUndo()) {
undoManager.addEdit(edit);
} else {
undoManager.discardAllEdits();
}
numUndoneEdits = 0; // reset BEFORE posting, so that the fade menu item can become enabled
undoableEditSupport.postEdit(edit);
if (Build.CURRENT != Build.FINAL) {
Events.postAddToHistoryEvent(edit);
ConsistencyChecks.checkAll(edit.getComp(), false);
}
}
public static String getUndoPresentationName() {
return undoManager.getUndoPresentationName();
}
public static String getRedoPresentationName() {
return undoManager.getRedoPresentationName();
}
public static void undo() {
if (Build.CURRENT != Build.FINAL) {
Events.postUndoEvent(undoManager.getEditToBeUndone());
}
try {
numUndoneEdits++; // increase it before calling undoManager.undo() so that the result of undo is not fadeable
undoManager.undo();
} catch (CannotUndoException e) {
Messages.showInfo("No undo available", "No undo available, probably because the undo image was discarded in order to save memory");
}
}
public static void redo() {
if (Build.CURRENT != Build.FINAL) {
Events.postRedoEvent(undoManager.getEditToBeRedone());
}
try {
numUndoneEdits--; // after redo we should be fadeable again
undoManager.redo();
} catch (CannotRedoException e) {
// TODO is a "No redo available" scenario possible?
Messages.showException(e);
}
}
public static boolean canUndo() {
return undoManager.canUndo();
}
public static boolean canRedo() {
return undoManager.canRedo();
}
public static void addUndoableEditListener(UndoableEditListener listener) {
undoableEditSupport.addUndoableEditListener(listener);
}
public static void setUndoLevels(int undoLevels) {
undoManager.setLimit(undoLevels);
}
public static int getUndoLevels() {
return undoManager.getLimit();
}
public static boolean canRepeatOperation() {
if (numUndoneEdits > 0) {
return false;
}
PixelitorEdit lastEdit = undoManager.getLastEdit();
if (lastEdit != null) {
return lastEdit.canRepeat();
}
return false;
}
/**
* Used for the name of the fade/repeat menu items
*/
public static String getLastEditName() {
PixelitorEdit lastEdit = undoManager.getLastEdit();
if (lastEdit != null) {
return lastEdit.getPresentationName();
}
return "";
}
/**
* If the last edit in the history is a FadeableEdit for the given
* image layer, return it, otherwise return empty Optional
*/
public static Optional<FadeableEdit> getPreviousEditForFade(Drawable dr) {
if (numUndoneEdits > 0 || dr == null) {
return Optional.empty();
}
PixelitorEdit lastEdit = undoManager.getLastEdit();
if (lastEdit != null) {
if (lastEdit instanceof FadeableEdit) {
FadeableEdit fadeableEdit = (FadeableEdit) lastEdit;
if (!fadeableEdit.isFadeable()) {
return Optional.empty();
}
Drawable lastLayer = fadeableEdit.getFadingLayer();
if (dr != lastLayer) {
// this happens if the active image layer has changed
// since the last edit, for example by going to mask edit
return Optional.empty();
}
return Optional.of(fadeableEdit);
}
}
return Optional.empty();
}
public static boolean canFade() {
Composition comp = ImageComponents.getActiveCompOrNull();
if (comp == null) {
return false;
}
Drawable dr = comp.getActiveDrawableOrNull();
if (dr == null) {
return false;
}
return canFade(dr);
}
public static boolean canFade(Drawable dr) {
return getPreviousEditForFade(dr).isPresent();
}
public static void onAllImagesClosed() {
numUndoneEdits = 0;
undoManager.discardAllEdits();
undoableEditSupport.postEdit(null);
}
public static void showHistory() {
undoManager.showHistory();
}
// for debugging only
public static PixelitorEdit getLastEdit() {
return undoManager.getLastEdit();
}
@VisibleForTesting
public static void clear() {
undoManager.discardAllEdits();
assertNumEditsIs(0);
}
@VisibleForTesting
public static void assertNumEditsIs(int expectedEdits) {
int numEdits = undoManager.getSize();
if (numEdits != expectedEdits) {
throw new AssertionError(String.format(
"Expected %d edits, but found %d",
expectedEdits, numEdits));
}
}
@VisibleForTesting
public static void assertLastEditNameIs(String expectedName) {
String lastEditName = undoManager.getLastEdit().getName();
if (!lastEditName.equals(expectedName)) {
throw new AssertionError(String.format(
"Expected '%s' as the last edit name, but found '%s'",
expectedName, lastEditName));
}
}
public static void setIgnoreEdits(boolean ignoreEdits) {
History.ignoreEdits = ignoreEdits;
}
public static DebugNode getDebugNode() {
DebugNode node = new DebugNode("History", undoManager);
node.addIntChild("Num undone edits", numUndoneEdits);
node.addBooleanChild("Ignore edits", ignoreEdits);
node.add(undoManager.getDebugNode());
return node;
}
}