/*
* 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;
import pixelitor.gui.HistogramsPanel;
import pixelitor.gui.ImageComponent;
import pixelitor.gui.ImageComponents;
import pixelitor.history.DeleteLayerEdit;
import pixelitor.history.DeselectEdit;
import pixelitor.history.History;
import pixelitor.history.ImageAndMaskEdit;
import pixelitor.history.ImageEdit;
import pixelitor.history.LayerOrderChangeEdit;
import pixelitor.history.LayerSelectionChangeEdit;
import pixelitor.history.LinkedEdit;
import pixelitor.history.NewLayerEdit;
import pixelitor.history.NotUndoableEdit;
import pixelitor.history.PixelitorEdit;
import pixelitor.history.SelectionChangeEdit;
import pixelitor.history.TranslationEdit;
import pixelitor.layers.ContentLayer;
import pixelitor.layers.Drawable;
import pixelitor.layers.ImageLayer;
import pixelitor.layers.Layer;
import pixelitor.layers.LayerButton;
import pixelitor.layers.LayerMask;
import pixelitor.layers.MaskViewMode;
import pixelitor.selection.IgnoreSelection;
import pixelitor.selection.Selection;
import pixelitor.selection.SelectionActions;
import pixelitor.selection.SelectionInteraction;
import pixelitor.utils.ImageUtils;
import pixelitor.utils.Messages;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE;
import static pixelitor.Composition.ImageChangeActions.FULL;
import static pixelitor.Composition.ImageChangeActions.INVALIDATE_CACHE;
import static pixelitor.io.FileExtensionUtils.stripExtension;
import static pixelitor.utils.Utils.createCopyName;
/**
* An image composition consisting of multiple layers
*/
public class Composition implements Serializable {
// serialization is used for save in the pxc format
private static final long serialVersionUID = 1L;
// a counter for the names of new layers
private int newLayerCount = 1;
private final List<Layer> layerList = new ArrayList<>();
private Layer activeLayer;
private String name; // the file name or something like "Untitled 1"
private Canvas canvas;
//
// the following variables are all transient, their state is not saved in PXC!
//
private transient File file;
private transient boolean dirty = false;
private transient boolean compositeImageUpToDate = false;
private transient BufferedImage cachedCompositeImage = null;
private transient ImageComponent ic;
private transient Selection selection;
private transient Selection builtSelection;
// A Composition can be created either with one of the following static
// factory methods or through deserialization (pxc)
/**
* Creates a single-layered composition from the given image
*/
public static Composition fromImage(BufferedImage img, File file, String name) {
assert img != null;
img = ImageUtils.toSysCompatibleImage(img);
Canvas canvas = new Canvas(img.getWidth(), img.getHeight());
Composition comp = new Composition(canvas);
comp.addBaseLayer(img);
if (file != null) {
comp.setFile(file); // also sets the name based on the file name
} else if (name != null) {
comp.setName(name);
} else {
// one of the file and name arguments must be given
throw new IllegalArgumentException("no name could be set");
}
return comp;
}
/**
* Creates an empty composition (no layers, no name)
*/
public static Composition createEmpty(int width, int height) {
Canvas canvas = new Canvas(width, height);
return new Composition(canvas);
}
public static Composition createCopy(Composition orig, boolean forUndo) {
Canvas canvasCopy = new Canvas(orig.getCanvas());
Composition compCopy = new Composition(canvasCopy);
// copy layers
for (Layer layer : orig.layerList) {
Layer layerCopy = layer.duplicate(true);
layerCopy.setComp(compCopy);
compCopy.layerList.add(layerCopy);
if (layer == orig.activeLayer) {
compCopy.activeLayer = layerCopy;
}
}
compCopy.newLayerCount = orig.newLayerCount;
if (orig.selection != null) {
compCopy.selection = new Selection(orig.selection, forUndo);
}
if (forUndo) {
compCopy.dirty = orig.dirty;
compCopy.file = orig.file;
compCopy.name = orig.name;
compCopy.ic = orig.ic;
compCopy.cachedCompositeImage = null;
compCopy.compositeImageUpToDate = false;
} else {
compCopy.dirty = true;
compCopy.file = null;
compCopy.name = createCopyName(stripExtension(orig.name));
compCopy.ic = null;
compCopy.cachedCompositeImage = orig.cachedCompositeImage;
compCopy.compositeImageUpToDate = orig.compositeImageUpToDate;
}
assert compCopy.checkInvariant();
return compCopy;
}
private Composition(Canvas canvas) {
this.canvas = canvas;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
//noinspection Convert2streamapi
for (Layer layer : layerList) {
if (layer instanceof ImageLayer) {
((ImageLayer) layer).updateIconImage();
}
if (layer.hasMask()) {
LayerMask mask = layer.getMask();
mask.getUI().addMaskIconLabel();
mask.updateIconImage();
}
}
}
/**
* Called when deserialized and when duplicated
*/
public void setIC(ImageComponent ic) {
this.ic = ic;
canvas.setIc(ic);
if (selection != null) { // can happen when duplicating
selection.setIC(ic);
}
}
public boolean isEmpty() {
return layerList.isEmpty();
}
public Canvas getCanvas() {
return canvas;
}
public void setCanvas(Canvas canvas) {
this.canvas = canvas;
}
public Rectangle getCanvasBounds() {
return canvas.getBounds();
}
public int getCanvasWidth() {
return canvas.getWidth();
}
public int getCanvasHeight() {
return canvas.getHeight();
}
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
public boolean isDirty() {
return dirty;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
if (ic != null) {
ic.updateTitle();
}
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
setName(file.getName());
}
private void addBaseLayer(BufferedImage baseLayerImage) {
ImageLayer newLayer = new ImageLayer(this, baseLayerImage, null, null);
addLayerNoGUI(newLayer);
activeLayer = newLayer;
}
/**
* Adds a layer to the top without updating any GUI
*/
public void addLayerNoGUI(Layer newLayer) {
layerList.add(newLayer); // adds it to top, ignoring the active layer position
setActiveLayer(newLayer, false);
// doesn't set the dirty flag because
// this method is used when adding the base layer
}
public ImageLayer addNewEmptyLayer(String name, boolean bellowActive) {
ImageLayer newLayer = new ImageLayer(this, name);
addLayer(newLayer, true, "New Empty Layer", false, bellowActive);
newLayer.updateIconImage();
return newLayer;
}
public void addLayer(Layer newLayer, boolean addToHistory, String historyName,
boolean updateHistogram, boolean bellowActive) {
int activeLayerIndex = layerList.indexOf(activeLayer);
int newLayerIndex;
if (activeLayerIndex == -1) { // no active layer yet
assert layerList.isEmpty();
newLayerIndex = 0;
} else {
if (bellowActive) {
newLayerIndex = activeLayerIndex;
} else {
newLayerIndex = activeLayerIndex + 1;
}
}
addLayer(newLayer, addToHistory, historyName, updateHistogram, newLayerIndex);
}
/**
* Adds the specified layer at the specified layer position
*/
public void addLayer(Layer newLayer, boolean addToHistory, String historyName, boolean updateHistogram, int newLayerIndex) {
Layer activeLayerBefore = activeLayer;
MaskViewMode oldViewMode = ic.getMaskViewMode();
layerList.add(newLayerIndex, newLayer);
setActiveLayer(newLayer, false);
ic.addLayerToGUI(newLayer, newLayerIndex);
History.addEdit(addToHistory, () -> new NewLayerEdit(this, newLayer, activeLayerBefore, historyName, oldViewMode));
if (updateHistogram) {
imageChanged(FULL); // if the histogram is updated, a repaint is also necessary
} else {
imageChanged(INVALIDATE_CACHE);
}
assert checkInvariant();
}
public void addLayersToGUI() {
assert checkInvariant();
// when adding layer buttons the last layer always gets active
// but here we don't want to change the selected layer
Layer previousActiveLayer = activeLayer;
layerList.forEach(this::addLayerToGUI);
setActiveLayer(previousActiveLayer, false);
}
private void addLayerToGUI(Layer layer) {
int layerIndex = layerList.indexOf(layer);
// setActiveLayer(layer, false);
ic.addLayerToGUI(layer, layerIndex);
}
public void duplicateActiveLayer() {
Layer duplicate = activeLayer.duplicate(false);
addLayer(duplicate, true, "Duplicate Layer", true, false);
assert checkInvariant();
}
public void mergeDown(boolean updateGUI) {
assert checkInvariant();
int activeIndex = layerList.indexOf(activeLayer);
if (activeIndex > 0 && activeLayer.isVisible()) {
Layer bellow = layerList.get(activeIndex - 1);
if (bellow instanceof ImageLayer) {
ImageLayer imageLayerBellow = (ImageLayer) bellow;
if (imageLayerBellow.isVisible()) {
// TODO for adjustment effects it is not necessary to copy
BufferedImage backupImage = ImageUtils.copyImage(imageLayerBellow.getImage());
activeLayer.mergeDownOn(imageLayerBellow);
imageLayerBellow.updateIconImage();
Layer mergedLayer = activeLayer;
deleteActiveLayer(updateGUI, false);
PixelitorEdit edit = new LinkedEdit(this, "Merge Down",
new ImageEdit(this, "", imageLayerBellow, backupImage, IgnoreSelection.YES, false),
new DeleteLayerEdit(this, mergedLayer, activeIndex)
);
History.addEdit(edit);
}
}
}
}
private void deleteLayer(int layerIndex) {
Layer layer = layerList.get(layerIndex);
deleteLayer(layer, true, true);
}
public void deleteActiveLayer(boolean updateGUI, boolean addToHistory) {
deleteLayer(activeLayer, addToHistory, updateGUI);
}
public void deleteLayer(Layer layerToBeDeleted, boolean addToHistory, boolean updateGUI) {
if (layerList.size() < 2) {
throw new IllegalStateException("there are " + layerList.size() + " layers");
}
int layerIndex = layerList.indexOf(layerToBeDeleted);
History.addEdit(addToHistory, () -> new DeleteLayerEdit(this, layerToBeDeleted, layerIndex));
layerList.remove(layerToBeDeleted);
if (layerToBeDeleted == activeLayer) {
if (layerIndex > 0) {
setActiveLayer(layerList.get(layerIndex - 1), false);
} else { // deleted the fist layer, set the new first layer as active
setActiveLayer(layerList.get(0), false);
}
}
if (updateGUI) {
LayerButton button = layerToBeDeleted.getUI().getLayerButton();
ic.deleteLayerButton(button);
if (isActiveComp()) {
AppLogic.activeCompLayerCountChanged(this, layerList.size());
}
imageChanged(FULL);
}
}
public void setActiveLayer(Layer newActiveLayer, boolean addToHistory) {
if (activeLayer != newActiveLayer) {
assert layerList.contains(newActiveLayer)
: String.format("new active layer '%s' (%s) not in the layer list of '%s'",
newActiveLayer.getName(), System.identityHashCode(newActiveLayer), getName());
Layer oldLayer = activeLayer;
activeLayer = newActiveLayer;
// notify UI
activeLayer.activateUI();
AppLogic.activeLayerChanged(newActiveLayer);
// notify history
History.addEdit(addToHistory, () -> new LayerSelectionChangeEdit(this, oldLayer, newActiveLayer));
assert checkInvariant();
}
}
public boolean isActiveLayer(Layer layer) {
return layer == activeLayer;
}
public Layer getActiveLayer() {
return activeLayer;
}
public int getActiveLayerIndex() {
return layerList.indexOf(activeLayer);
}
public int getLayerIndex(Layer layer) {
return layerList.indexOf(layer);
}
public Layer getLayer(int i) {
return layerList.get(i);
}
public int getNumLayers() {
return layerList.size();
}
public void forEachLayer(Consumer<Layer> action) {
layerList.forEach(action);
}
public void forEachContentLayer(Consumer<ContentLayer> action) {
for (Layer layer : layerList) {
if (layer instanceof ContentLayer) {
ContentLayer contentLayer = (ContentLayer) layer;
action.accept(contentLayer);
}
}
}
public void forEachDrawable(Consumer<Drawable> action) {
for (Layer layer : layerList) {
if (layer instanceof ImageLayer) {
action.accept((ImageLayer) layer);
}
if (layer.hasMask()) {
action.accept(layer.getMask());
}
}
}
public void updateAllIconImages() {
forEachDrawable(Drawable::updateIconImage);
}
public boolean activeIsDrawable() {
if (activeLayer instanceof ImageLayer) {
return true;
}
if (activeLayer.isMaskEditing()) {
return true;
}
return false;
}
private Layer getActiveMaskOrLayer() {
if (activeLayer.isMaskEditing()) {
return activeLayer.getMask();
}
return activeLayer;
}
public ContentLayer getAnyContentLayer() {
for (Layer layer : layerList) {
if (layer instanceof ContentLayer) {
return (ContentLayer) layer;
}
}
return null;
}
/**
* Returns the active mask or image layer or null
*/
public Drawable getActiveDrawableOrNull() {
assert checkInvariant();
if (activeLayer.isMaskEditing()) {
return activeLayer.getMask();
}
if (activeLayer instanceof ImageLayer) {
return (ImageLayer) activeLayer;
}
return null;
}
/**
* Returns the active mask or image layer
* This method assumes that the active layer is an image layer
*/
public Drawable getActiveDrawable() {
Drawable dr = getActiveDrawableOrNull();
if (dr == null) {
throw new IllegalStateException("The active layer is not an image layer or a mask, it is "
+ activeLayer.getClass().getSimpleName());
}
return dr;
}
public void startMovement(boolean duplicateLayer) {
if (duplicateLayer) {
duplicateActiveLayer();
}
Layer layer = getActiveMaskOrLayer();
layer.startMovement();
}
public void moveActiveContentRelative(double relativeX, double relativeY) {
Layer layer = getActiveMaskOrLayer();
layer.moveWhileDragging(relativeX, relativeY);
imageChanged(FULL);
}
public void endMovement() {
Layer layer = getActiveMaskOrLayer();
PixelitorEdit edit = layer.endMovement();
if (edit != null) {
// The layer, the mask, or both moved.
// We always should get here except if an adjustment
// layer without a mask was moved.
History.addEdit(edit);
imageChanged(FULL);
}
}
public void flattenImage(boolean updateGUI) {
if (updateGUI) {
assert isActiveComp();
}
if (layerList.size() < 2) {
return;
}
int numLayers = getNumLayers();
BufferedImage bi = getCompositeImage();
Layer flattenedLayer = new ImageLayer(this, bi, "flattened", null);
addLayer(flattenedLayer, false, null, false, numLayers); // add to the top
for (int i = numLayers - 1; i >= 0; i--) { // delete the rest
deleteLayer(i);
}
if (updateGUI) {
AppLogic.activeCompLayerCountChanged(this, 1);
// TODO should have a separate add to history argument?
History.addEdit(new NotUndoableEdit(this, "Flatten Image"));
}
}
public void moveActiveLayerUp() {
assert checkInvariant();
int oldIndex = layerList.indexOf(activeLayer);
swapLayers(oldIndex, oldIndex + 1, true);
}
public void moveActiveLayerDown() {
assert checkInvariant();
int oldIndex = layerList.indexOf(activeLayer);
swapLayers(oldIndex, oldIndex - 1, true);
}
public void moveActiveLayerToTop() {
assert checkInvariant();
int oldIndex = layerList.indexOf(activeLayer);
int newIndex = layerList.size() - 1;
swapLayers(oldIndex, newIndex, true);
}
public void moveActiveLayerToBottom() {
assert checkInvariant();
int oldIndex = layerList.indexOf(activeLayer);
swapLayers(oldIndex, 0, true);
}
public void swapLayers(int oldIndex, int newIndex, boolean addToHistory) {
if (newIndex < 0) {
return;
}
if (newIndex >= layerList.size()) {
return;
}
if (oldIndex == newIndex) {
return;
}
Layer layer = layerList.get(oldIndex);
layerList.remove(layer);
layerList.add(newIndex, layer);
ic.changeLayerOrderInTheGUI(oldIndex, newIndex);
imageChanged(FULL);
AppLogic.layerOrderChanged(this);
History.addEdit(addToHistory, () -> new LayerOrderChangeEdit(this, oldIndex, newIndex));
}
public void moveLayerSelectionUp() {
int oldIndex = layerList.indexOf(activeLayer);
int newIndex = oldIndex + 1;
if (newIndex >= layerList.size()) {
return;
}
setActiveLayer(layerList.get(newIndex), true);
assert ConsistencyChecks.fadeCheck(this);
}
public void moveLayerSelectionDown() {
int oldIndex = layerList.indexOf(activeLayer);
int newIndex = oldIndex - 1;
if (newIndex < 0) {
return;
}
setActiveLayer(layerList.get(newIndex), true);
assert ConsistencyChecks.fadeCheck(this);
}
public BufferedImage calculateCompositeImage() {
// TODO why is this not working
// if(getNrLayers() == 1) {
// ImageLayer layer = (ImageLayer) getLayer(0); // must be
// return layer.getImage();
// }
// BufferedImage imageSoFar = ImageUtils.createCompatibleImage(getCanvasWidth(), getCanvasHeight());
BufferedImage imageSoFar = new BufferedImage(
canvas.getWidth(), canvas.getHeight(), TYPE_INT_ARGB_PRE);
Graphics2D g = imageSoFar.createGraphics();
boolean firstVisibleLayer = true;
for (Layer layer : layerList) {
if (layer.isVisible()) {
BufferedImage result = layer.applyLayer(g, firstVisibleLayer, imageSoFar);
if (result != null) { // this was an adjustment layer
imageSoFar = result;
if (g != null) {
g.dispose();
}
g = imageSoFar.createGraphics();
}
firstVisibleLayer = false;
}
}
g.dispose();
return imageSoFar;
}
public String generateNewLayerName() {
String retVal = "layer " + newLayerCount;
newLayerCount++;
return retVal;
}
public void updateRegion(double startX, double startY, double endX, double endY, int thickness) {
compositeImageUpToDate = false;
ic.updateRegion(startX, startY, endX, endY, thickness);
ic.updateNavigator(false);
}
public void dispose() {
if (selection != null) {
// stop the timer thread
selection.die();
}
}
public void addNewLayerFromComposite(String newLayerName) {
ImageLayer newLayer = new ImageLayer(this, getCompositeImage(), newLayerName, null);
addLayer(newLayer, true, "New Layer from Composite", false, false);
}
public ImageComponent getIC() {
return ic;
}
public void paintSelection(Graphics2D g) {
boolean ruby = false;
if (ruby) {
Shape allSelection = null;
if (builtSelection != null) {
allSelection = builtSelection.getShape();
}
if (selection != null) {
if (allSelection == null) {
allSelection = selection.getShape();
} else {
allSelection = SelectionInteraction.ADD.combine(allSelection, selection.getShape());
}
}
if (allSelection != null) {
Shape inverted = canvas.invertShape(allSelection);
g.setComposite(AlphaComposite.SrcOver.derive(0.5f));
g.setColor(Color.RED);
g.fill(inverted);
}
} else {
if (builtSelection != null) {
builtSelection.paintMarchingAnts(g);
}
if (selection != null) {
selection.paintMarchingAnts(g);
}
}
}
public void deselect(boolean addToHistory) {
if (selection != null) {
if (addToHistory) {
DeselectEdit edit = createDeselectEdit();
if (edit != null) {
History.addEdit(edit);
}
}
boolean wasHidden = selection.isHidden();
selection.die();
selection = null;
if (isActiveComp()) {
if (wasHidden) {
SelectionActions.getShowHideSelectionAction().setHideName();
}
SelectionActions.setEnabled(false, this);
} else {
// we can get here from a DeselectEdit.redo on a non-active composition
}
}
}
public Shape clipShapeToCanvasSize(Shape shape) {
assert shape != null;
Area compBounds = new Area(new Rectangle(0, 0, canvas.getWidth(), canvas.getHeight()));
Area result = new Area(shape);
result.intersect(compBounds);
return result;
}
public DeselectEdit createDeselectEdit() {
DeselectEdit edit = null;
Shape shape = selection.getShape();
if (shape != null) { // for a simple click without a previous selection this is null
edit = new DeselectEdit(this, shape, "Composition.deselect");
}
return edit;
}
public void onSelection(Consumer<Selection> action) {
if (selection != null) {
action.accept(selection);
}
}
public Selection getSelection() {
return selection;
}
public Shape getSelectionShape() {
if (selection != null) {
return selection.getShape();
}
return null;
}
public Selection getBuiltSelection() {
return builtSelection;
}
public void setBuiltSelection(Selection selection) {
this.builtSelection = selection;
}
public void setSelection(Selection selection) {
this.selection = selection;
}
public void promoteSelection() {
assert selection == null || !selection.isAlive() : "selection = " + selection;
setNewSelection(builtSelection);
builtSelection = null;
}
public boolean hasSelection() {
return (selection != null);
}
public void applySelectionClipping(Graphics2D g2, AffineTransform at) {
if (selection != null) {
Shape shape;
if (at != null) {
Path2D.Float pathShape = new Path2D.Float(selection.getShape());
pathShape.transform(at);
shape = pathShape;
} else { // relative to the composition
shape = selection.getShape();
}
g2.setClip(shape);
}
}
public void invertSelection() {
if (selection != null) {
Shape backupShape = selection.getShape();
Shape inverted = canvas.invertShape(backupShape);
selection.setShape(inverted);
SelectionChangeEdit edit = new SelectionChangeEdit(this, backupShape, "Invert Selection");
History.addEdit(edit);
}
}
public void createSelectionFromShape(Shape selectionShape) {
if (selection != null) {
throw new IllegalStateException("createSelectionFromShape called while there was a selection: " + selection.toString());
}
setNewSelection(new Selection(selectionShape, ic));
}
public void setNewSelection(Selection selection) {
assert selection != null;
this.selection = selection;
if (isActiveComp()) {
SelectionActions.setEnabled(true, this);
}
}
/**
* Returns the composite image which jas the same dimensions as the canvas.
*/
public BufferedImage getCompositeImage() {
if (compositeImageUpToDate) {
return cachedCompositeImage; // this caching is useful for example when using the Color Picker Tool
}
cachedCompositeImage = calculateCompositeImage();
compositeImageUpToDate = true;
return cachedCompositeImage;
}
public void imageChanged(ImageChangeActions actions) {
imageChanged(actions, false);
}
/**
* The contents of this composition have been changed, the cache is invalidated,
* and additional actions might be necessary
*/
public void imageChanged(ImageChangeActions actions, boolean sizeChanged) {
compositeImageUpToDate = false;
if (actions.isRepaint()) {
if (ic != null) {
ic.repaint();
ic.updateNavigator(sizeChanged);
}
}
if (actions.isUpdateHistogram()) {
HistogramsPanel.INSTANCE.updateFromCompIfShown(this);
}
}
private boolean isActiveComp() {
if (ic.isMock()) {
// we are in a unit test
// TODO hack
return false;
}
return (ImageComponents.getActiveCompOrNull() == this);
}
public void activeLayerToCanvasSize() {
// TODO actually this should work with any layer
if (activeLayer instanceof ImageLayer) {
ImageLayer layer = (ImageLayer) this.activeLayer;
BufferedImage backupImage = layer.getImage();
TranslationEdit translationEdit = new TranslationEdit(this, layer, true);
boolean changed = layer.cropToCanvasSize();
if (changed) {
ImageEdit imageEdit;
String editName = "Layer to Canvas Size";
boolean maskChanged = false;
BufferedImage maskBackupImage = null;
if (layer.hasMask()) {
LayerMask mask = layer.getMask();
maskBackupImage = mask.getImage();
maskChanged = mask.cropToCanvasSize();
}
if (maskChanged) {
imageEdit = new ImageAndMaskEdit(this, editName, layer, backupImage, maskBackupImage, false);
} else {
// no mask or no mask change, a simple ImageEdit will do
imageEdit = new ImageEdit(this, editName, layer, backupImage, IgnoreSelection.YES, false);
imageEdit.setFadeable(false);
}
History.addEdit(new LinkedEdit(this, editName, translationEdit, imageEdit));
}
} else {
Messages.showNotImageLayerError();
}
}
/**
* The user reordered the layers by dragging
*/
public void dragFinished(Layer layer, int newIndex) {
layerList.remove(layer);
layerList.add(newIndex, layer);
imageChanged(FULL);
}
public void repaint() {
ic.repaint();
}
/**
* Applies the cropping to the selection
*/
public void cropSelection(Rectangle2D cropRect) {
if (selection != null) {
Shape currentShape = selection.getShape();
Shape intersection = SelectionInteraction.INTERSECT.combine(currentShape, cropRect);
if (intersection.getBounds().isEmpty()) {
selection.die();
selection = null;
} else {
// the intersection needs to be translated
// into the coordinate system of the new, cropped image
double txx = -cropRect.getX();
double txy = -cropRect.getY();
AffineTransform tx = AffineTransform.getTranslateInstance(txx, txy);
selection.setShape(tx.createTransformedShape(intersection));
}
}
}
// called from assertions and unit tests
@SuppressWarnings("SameReturnValue")
public boolean checkInvariant() {
if (layerList.isEmpty()) {
throw new IllegalStateException("no layer in " + getName());
}
if (activeLayer == null) {
throw new IllegalStateException("no active layer in " + getName());
}
if (!layerList.contains(activeLayer)) {
throw new IllegalStateException("active layer (" + activeLayer.getName() + ") not in list (" + layerList.toString() + ")");
}
return true;
}
public enum ImageChangeActions {
INVALIDATE_CACHE(false, false) {
}, REPAINT(true, false) {
}, HISTOGRAM(false, true) {
}, FULL(true, true) {
};
private final boolean repaint;
private final boolean updateHistogram;
ImageChangeActions(boolean repaint, boolean updateHistogram) {
this.repaint = repaint;
this.updateHistogram = updateHistogram;
}
private boolean isRepaint() {
return repaint;
}
private boolean isUpdateHistogram() {
return updateHistogram;
}
}
@Override
public String toString() {
return "Composition{name='" + name + '\''
+ ", activeLayer=" + (activeLayer == null ? "null" : activeLayer.getName())
+ ", layerList=" + layerList
+ ", canvas=" + canvas
+ ", selection=" + selection
+ ", dirty=" + dirty + '}';
}
/**
* Includes only the layer names and which layer is active
*/
public String toLayerNamesString() {
return layerList.stream()
.map((layer) -> layer == activeLayer ? "ACTIVE " + layer.getName() : layer.getName())
.collect(Collectors.joining(", ", "[", "]"));
}
}