/*
* 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.selection;
import pixelitor.Composition;
import pixelitor.gui.ImageComponent;
import pixelitor.history.History;
import pixelitor.history.SelectionChangeEdit;
import pixelitor.menus.view.ShowHideAction;
import javax.swing.*;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import static java.awt.BasicStroke.CAP_BUTT;
import static java.awt.BasicStroke.JOIN_ROUND;
import static java.awt.Color.BLACK;
import static java.awt.Color.WHITE;
/**
* Represents a selection on an image.
*/
public class Selection {
private float dashPhase;
private ImageComponent ic;
private Timer marchingAntsTimer;
// the shape that is currently drawn
private Shape shape;
private static final double DASH_WIDTH = 1.0;
private static final float DASH_LENGTH = 4.0f;
private static final float[] MARCHING_ANTS_DASH = {DASH_LENGTH, DASH_LENGTH};
private boolean hidden = false;
private boolean dead = false;
private boolean frozen = false;
public Selection(Shape shape, ImageComponent ic) {
assert ic != null;
this.shape = shape;
this.ic = ic;
startMarching();
}
// copy constructor
public Selection(Selection orig, boolean shareIC) {
if (shareIC) {
this.ic = orig.ic;
}
// the shapes can be shared
this.shape = orig.shape;
// the Timer is not copied! - setIC starts it
}
public void startMarching() {
if (frozen) {
return;
}
assert !dead : "dead selection";
assert ic != null : "no ic in selection";
marchingAntsTimer = new Timer(100, null);
marchingAntsTimer.addActionListener(evt -> {
if(!hidden) {
dashPhase += 1 / ic.getViewScale();
repaint();
}
});
marchingAntsTimer.start();
}
public void stopMarching() {
if (marchingAntsTimer != null) {
marchingAntsTimer.stop();
marchingAntsTimer = null;
}
}
public boolean isMarching() {
return marchingAntsTimer != null;
}
public void paintMarchingAnts(Graphics2D g2) {
assert !dead : "dead selection";
if (shape == null || hidden) {
return;
}
paintAnts(g2, shape, dashPhase);
}
private void paintAnts(Graphics2D g2, Shape shape, float phase) {
double viewScale = ic.getViewScale();
float lineWidth = (float) (DASH_WIDTH / viewScale);
g2.setPaint(WHITE);
float[] dash;
if (viewScale == 1.0) { // the most common case
dash = MARCHING_ANTS_DASH;
} else {
float scaledDashLength = (float) (DASH_LENGTH / viewScale);
dash = new float[]{scaledDashLength, scaledDashLength};
}
Stroke stroke = new BasicStroke(lineWidth, CAP_BUTT,
JOIN_ROUND, 0.0f, dash,
phase);
g2.setStroke(stroke);
g2.draw(shape);
g2.setPaint(BLACK);
Stroke stroke2 = new BasicStroke(lineWidth, CAP_BUTT,
JOIN_ROUND, 0.0f, dash,
(float) (phase + DASH_LENGTH / viewScale));
g2.setStroke(stroke2);
g2.draw(shape);
}
/**
* Inverts the selection shape.
*/
public void invert(Rectangle fullImage) {
if (shape != null) {
Area area = new Area(shape);
Area fullArea = new Area(fullImage);
fullArea.subtract(area);
shape = fullArea;
}
}
public void die() {
stopMarching();
repaint();
ic = null;
dead = true;
}
public void repaint() {
// Rectangle selBounds = shape.getBounds();
//
// if(lastShape != null) {
// Rectangle r = lastShape.getBounds();
// selBounds = selBounds.union(r);
// }
//
// component.repaint(selBounds.x, selBounds.y, selBounds.width + 1, selBounds.height + 1);
// TODO the above optimization is not enough, the previous positions should be also considered for the
// case when the selection is shrinking while dragging...
ic.repaint();
}
public void setShape(Shape currentShape) {
this.shape = currentShape;
}
/**
* Intersects the selection shape with the composition bounds
*
* @return true if something is still selected
*/
public boolean clipToCompSize(Composition comp) {
if (shape != null) {
shape = comp.clipShapeToCanvasSize(shape);
repaint();
return !shape.getBounds().isEmpty();
}
return false;
}
public Shape getShape() {
return shape;
}
/**
* Returns the shape bounds of the selection
* Like everything else in this class, this is in image coordinates
* (but relative to the composition, not to the image)
*/
public Rectangle getShapeBounds() {
return shape.getBounds();
}
public void modify(SelectionModifyType type, float amount) {
BasicStroke outlineStroke = new BasicStroke(amount);
Shape outlineShape = outlineStroke.createStrokedShape(shape);
Area oldArea = new Area(shape);
Area outlineArea = new Area(outlineShape);
Shape backupShape = shape;
shape = type.createModifiedShape(oldArea, outlineArea);
SelectionChangeEdit edit = new SelectionChangeEdit(ic.getComp(), backupShape, "Modify Selection");
History.addEdit(edit);
}
public Shape transform(AffineTransform at) {
Shape backupShape = shape;
shape = at.createTransformedShape(shape);
return backupShape;
}
public void nudge(AffineTransform at) {
Shape backupShape = transform(at);
History.addEdit(new SelectionChangeEdit(ic.getComp(), backupShape, "Nudge Selection"));
}
public boolean isHidden() {
return hidden;
}
public void setHidden(boolean hide, boolean fromMenu) {
assert !dead : "dead selection";
boolean change = this.hidden != hide;
if (!change) {
return;
}
this.hidden = hide;
if(hide) {
stopMarching();
} else {
if(marchingAntsTimer == null) {
startMarching();
}
}
repaint();
// if not called from the menu, update the menu name
if(!fromMenu) {
ShowHideAction action = SelectionActions.getShowHideSelectionAction();
if(hide) {
action.setShowName();
} else {
action.setHideName();
}
}
}
public boolean isFrozen() {
return frozen;
}
public void setFrozen(boolean b) {
this.frozen = b;
if (b) {
stopMarching();
} else if (!hidden) {
startMarching();
}
}
// called when the composition is duplicated
public void setIC(ImageComponent ic) {
assert ic != null;
this.ic = ic;
startMarching();
}
public boolean isAlive() {
return !dead;
}
@Override
public String toString() {
return "Selection{" +
"composition=" + ic.getComp().getName() +
", shape-class=" + (shape == null ? "null" : shape.getClass().getName()) +
", shapeBounds=" + (shape == null ? "null" : shape.getBounds()) +
'}';
}
}