/*
* 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.tools;
import pixelitor.Canvas;
import pixelitor.Composition;
import pixelitor.filters.gui.RangeParam;
import pixelitor.gui.ImageComponent;
import pixelitor.gui.ImageComponents;
import pixelitor.gui.utils.SliderSpinner;
import pixelitor.transform.TransformSupport;
import pixelitor.utils.ImageSwitchListener;
import pixelitor.utils.Messages;
import pixelitor.utils.debug.DebugNode;
import javax.swing.*;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Cursor;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import static java.awt.AlphaComposite.SRC_OVER;
import static java.awt.Color.BLACK;
import static pixelitor.Composition.ImageChangeActions.FULL;
import static pixelitor.gui.utils.SliderSpinner.TextPosition.WEST;
import static pixelitor.tools.CropToolState.INITIAL;
import static pixelitor.tools.CropToolState.TRANSFORM;
import static pixelitor.tools.CropToolState.USER_DRAG;
/**
* The crop tool
*/
public class CropTool extends Tool implements ImageSwitchListener {
private CropToolState state = INITIAL;
private TransformSupport transformSupport;
private final RangeParam maskOpacity = new RangeParam("Mask Opacity (%)", 0, 75, 100);
private Composite hideComposite = AlphaComposite.getInstance(SRC_OVER, maskOpacity.getValueAsPercentage());
private final JButton cancelButton = new JButton("Cancel");
private JButton cropButton;
// The crop rectangle in image space.
// This variable is used only while the image component is resized
private Rectangle2D lastCropRect;
private JCheckBox allowGrowingCB;
CropTool() {
super('c', "Crop", "crop_tool_icon.png",
"Click and drag to define the crop area. Hold SPACE down to move the entire region.",
Cursor.getDefaultCursor(), false, true, true, ClipStrategy.IMAGE_ONLY);
spaceDragBehavior = true;
maskOpacity.addChangeListener(e -> {
float alpha = maskOpacity.getValueAsPercentage();
// because of a swing bug, the slider can get out of range
if (alpha < 0.0f) {
alpha = 0.0f;
maskOpacity.setValue(0);
} else if (alpha > 1.0f) {
alpha = 1.0f;
maskOpacity.setValue(100);
}
hideComposite = AlphaComposite.getInstance(SRC_OVER, alpha);
ImageComponents.repaintActive();
});
ImageComponents.addImageSwitchListener(this);
}
@Override
public void initSettingsPanel() {
SliderSpinner maskOpacitySpinner = new SliderSpinner(maskOpacity, WEST, false);
settingsPanel.add(maskOpacitySpinner);
allowGrowingCB = new JCheckBox("Allow Growing", false);
allowGrowingCB.setToolTipText("Enables the enlargement of the canvas");
settingsPanel.add(allowGrowingCB);
cropButton = settingsPanel.addButton("Crop",
e -> {
ImageComponents.toolCropActiveImage(allowGrowingCB.isSelected());
ImageComponents.repaintActive();
resetStateToInitial();
});
cropButton.setEnabled(false);
cancelButton.addActionListener(e -> state.cancel(this));
cancelButton.setEnabled(false);
settingsPanel.add(cancelButton);
}
@Override
public void mousePressed(MouseEvent e, ImageComponent ic) {
// in case of crop/image change the ended is set to true even if the tool is not ended
// if a new drag is started, then reset it
ended = false;
state = state.getNextAfterMousePressed();
if (state == TRANSFORM) {
assert transformSupport != null;
transformSupport.mousePressed(e);
cropButton.setEnabled(true);
cancelButton.setEnabled(true);
} else if (state == USER_DRAG) {
cropButton.setEnabled(true);
cancelButton.setEnabled(true);
}
}
@Override
public void mouseDragged(MouseEvent e, ImageComponent ic) {
if (state == TRANSFORM) {
transformSupport.mouseDragged(e, ic);
}
ic.repaint();
}
// TODO: this is done directly with the dispatch mechanism
@Override
public void dispatchMouseMoved(MouseEvent e, ImageComponent ic) {
super.dispatchMouseMoved(e, ic);
if (state == TRANSFORM) {
transformSupport.mouseMoved(e, ic);
}
}
@Override
public void mouseReleased(MouseEvent e, ImageComponent ic) {
Composition comp = ic.getComp();
comp.imageChanged(FULL);
switch (state) {
case INITIAL:
break;
case USER_DRAG:
if (transformSupport != null) {
throw new IllegalStateException();
}
Rectangle2D imageSpaceRect = userDrag.createPositiveRect();
Rectangle compSpaceRect = ic.fromImageToComponentSpace(imageSpaceRect);
transformSupport = new TransformSupport(compSpaceRect, imageSpaceRect);
state = TRANSFORM;
break;
case TRANSFORM:
if (transformSupport == null) {
throw new IllegalStateException();
}
transformSupport.mouseReleased();
break;
}
}
@Override
public void paintOverImage(Graphics2D g2, Canvas canvas, ImageComponent callingIC, AffineTransform unscaledTransform) {
if (ended) {
return;
}
if (callingIC != ImageComponents.getActiveIC()) {
return;
}
Rectangle2D cropRect = getCropRect(callingIC);
if (cropRect == null) {
return;
}
// here we have the cropping rectangle in image space, therefore
// this is a good opportunity to update the status bar message
// even if it has nothing to do with painting
int width = (int) cropRect.getWidth();
int height = (int) cropRect.getHeight();
Messages.showStatusMessage("Cropping area is " + width + " x " + height + " pixels.");
// paint the semi-transparent dark area outside the crop rectangle
Shape previousClip = g2.getClip(); // save for later use
Rectangle canvasBounds = canvas.getBounds();
// Similar to ClipStrategy.INTERNAL_FRAME, but we need intermediary some variables
Rectangle componentSpaceViewRect = callingIC.getViewRect();
// ...but first get this to image space...
Rectangle2D imageSpaceViewRect = callingIC.fromComponentToImageSpace(componentSpaceViewRect);
// ... and now we can intersect
Rectangle2D canvasImgIntersection = canvasBounds.createIntersection(imageSpaceViewRect);
Path2D darkAreaClip = new Path2D.Double(Path2D.WIND_EVEN_ODD);
darkAreaClip.append(canvasImgIntersection, false);
darkAreaClip.append(cropRect, false);
g2.setClip(darkAreaClip);
Color previousColor = g2.getColor();
g2.setColor(BLACK);
Composite previousComposite = g2.getComposite();
g2.setComposite(hideComposite);
g2.fill(canvasImgIntersection);
g2.setColor(previousColor);
g2.setComposite(previousComposite);
if (state == TRANSFORM) {
// Paint the handles.
// The zooming is temporarily reset because the transformSupport works in component space
AffineTransform scaledTransform = g2.getTransform();
g2.setTransform(unscaledTransform);
// prevents drawing outside the InternalImageFrame/ImageComponent
// it is important to call this AFTER setting the unscaled transform
g2.setClip(componentSpaceViewRect);
transformSupport.paintHandles(g2);
g2.setTransform(scaledTransform);
}
g2.setClip(previousClip);
}
/**
* Returns the crop rectangle in image space
*/
public Rectangle2D getCropRect(ImageComponent ic) {
switch (state) {
case INITIAL:
lastCropRect = null;
break;
case USER_DRAG:
lastCropRect = userDrag.createPositiveRect();
break;
case TRANSFORM:
lastCropRect = transformSupport.getImageSpaceRect(ic);
break;
}
return lastCropRect;
}
@Override
protected void toolEnded() {
super.toolEnded();
resetStateToInitial();
}
@Override
public void noOpenImageAnymore() {
resetStateToInitial();
}
@Override
public void newImageOpened(Composition comp) {
resetStateToInitial();
}
@Override
public void activeImageHasChanged(ImageComponent oldIC, ImageComponent newIC) {
oldIC.repaint();
resetStateToInitial();
}
public void resetStateToInitial() {
ended = true;
transformSupport = null;
state = INITIAL;
cancelButton.setEnabled(false);
cropButton.setEnabled(false);
ImageComponents.repaintActive();
}
public void icResized(ImageComponent ic) {
if (transformSupport != null && lastCropRect != null && state == TRANSFORM) {
transformSupport.setComponentSpaceRect(ic.fromImageToComponentSpace(lastCropRect));
}
}
@Override
public boolean arrowKeyPressed(ArrowKey key) {
if (state == TRANSFORM) {
ImageComponent ic = ImageComponents.getActiveIC();
if (ic != null) {
transformSupport.arrowKeyPressed(key, ic);
return true;
}
}
return false;
}
@Override
public void escPressed() {
state.cancel(this);
}
@Override
public DebugNode getDebugNode() {
DebugNode node = super.getDebugNode();
node.addFloatChild("Mask Opacity", maskOpacity.getValueAsPercentage());
node.addBooleanChild("Allow Growing", allowGrowingCB.isSelected());
node.addStringChild("State", state.toString());
return node;
}
}