/*
* 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.shapestool;
import org.jdesktop.swingx.combobox.EnumComboBoxModel;
import org.jdesktop.swingx.painter.effects.AreaEffect;
import pixelitor.Composition;
import pixelitor.filters.gui.StrokeParam;
import pixelitor.filters.painters.AreaEffects;
import pixelitor.filters.painters.EffectsPanel;
import pixelitor.gui.ImageComponent;
import pixelitor.gui.utils.GUIUtils;
import pixelitor.gui.utils.OKCancelDialog;
import pixelitor.history.History;
import pixelitor.history.NewSelectionEdit;
import pixelitor.history.PixelitorEdit;
import pixelitor.history.SelectionChangeEdit;
import pixelitor.layers.Drawable;
import pixelitor.selection.Selection;
import pixelitor.tools.ClipStrategy;
import pixelitor.tools.ShapeType;
import pixelitor.tools.ShapesAction;
import pixelitor.tools.StrokeType;
import pixelitor.tools.Tool;
import pixelitor.tools.ToolAffectedArea;
import pixelitor.tools.UserDrag;
import pixelitor.utils.debug.DebugNode;
import javax.swing.*;
import java.awt.BasicStroke;
import java.awt.Cursor;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import static java.awt.RenderingHints.KEY_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
import static pixelitor.Composition.ImageChangeActions.FULL;
import static pixelitor.Composition.ImageChangeActions.REPAINT;
/**
* The Shapes Tool
*/
public class ShapesTool extends Tool {
private final EnumComboBoxModel<ShapesAction> actionModel = new EnumComboBoxModel<>(ShapesAction.class);
private final EnumComboBoxModel<ShapeType> typeModel = new EnumComboBoxModel<>(ShapeType.class);
private final EnumComboBoxModel<TwoPointBasedPaint> fillModel = new EnumComboBoxModel<>(TwoPointBasedPaint.class);
private final EnumComboBoxModel<TwoPointBasedPaint> strokeFillModel = new EnumComboBoxModel<>(TwoPointBasedPaint.class);
private final StrokeParam strokeParam = new StrokeParam("");
private JButton strokeSettingsButton;
private BasicStroke basicStrokeForOpenShapes;
private final JComboBox<TwoPointBasedPaint> strokeFillCombo;
private final JComboBox<TwoPointBasedPaint> fillCombo = new JComboBox<>(fillModel);
private JButton effectsButton;
private OKCancelDialog effectsDialog;
private EffectsPanel effectsPanel;
private Shape backupSelectionShape = null;
private boolean drawing = false;
private Stroke stroke;
public ShapesTool() {
super('u', "Shapes", "shapes_tool_icon.png",
"Click and drag to draw a shape. Hold SPACE down while drawing to move the shape. ",
Cursor.getDefaultCursor(), true, true, false, ClipStrategy.IMAGE_ONLY);
strokeFillModel.setSelectedItem(TwoPointBasedPaint.BACKGROUND);
strokeFillCombo = new JComboBox<>(strokeFillModel);
spaceDragBehavior = true;
}
@Override
public void initSettingsPanel() {
JComboBox<ShapeType> shapeTypeCB = new JComboBox<>(typeModel);
settingsPanel.addWithLabel("Shape:", shapeTypeCB, "shapeTypeCB");
// make sure all values are visible without a scrollbar
shapeTypeCB.setMaximumRowCount(ShapeType.values().length);
JComboBox<ShapesAction> actionCB = new JComboBox<>(actionModel);
settingsPanel.addWithLabel("Action:", actionCB, "actionCB");
actionCB.addActionListener(e -> updateWhichSettingsAreEnabled());
settingsPanel.addWithLabel("Fill:", fillCombo);
settingsPanel.addWithLabel("Stroke:", strokeFillCombo);
strokeSettingsButton = settingsPanel.addButton("Stroke Settings...",
e -> initAndShowStrokeSettingsDialog());
effectsButton = settingsPanel.addButton("Effects...",
e -> showEffectsDialog());
updateWhichSettingsAreEnabled();
}
private void showEffectsDialog() {
if (effectsPanel == null) {
effectsPanel = new EffectsPanel(null, null);
}
effectsDialog = new OKCancelDialog(effectsPanel, "Effects") {
@Override
protected void dialogAccepted() {
effectsDialog.close();
effectsPanel.updateEffectsFromGUI();
}
@Override
protected void dialogCanceled() {
super.dialogCanceled();
effectsDialog.close();
}
};
effectsDialog.setVisible(true);
}
@Override
public void mousePressed(MouseEvent e, ImageComponent ic) {
Composition comp = ic.getComp();
backupSelectionShape = comp.getSelectionShape();
}
@Override
public void mouseDragged(MouseEvent e, ImageComponent ic) {
// hack to prevent AssertionError when dragging started
// from negative coordinates bug
// TODO investigate
ShapesAction action = actionModel.getSelectedItem();
if(action.drawEffects() && effectsPanel != null) {
AreaEffects effects = effectsPanel.getEffects();
if(effects.hasAny()) {
if (userDrag.getStartX() < 0) {
return;
}
if (userDrag.getStartY() < 0) {
return;
}
}
}
// end hack
drawing = true;
userDrag.setStartFromCenter(e.isAltDown());
Composition comp = ic.getComp();
// this will trigger paintOverLayer, therefore the continuous drawing of the shape
comp.imageChanged(REPAINT); // TODO optimize, the whole image should not be repainted
}
@Override
public void mouseReleased(MouseEvent e, ImageComponent ic) {
userDrag.setStartFromCenter(e.isAltDown());
Composition comp = ic.getComp();
Drawable dr = comp.getActiveDrawable();
ShapesAction action = actionModel.getSelectedItem();
boolean selectionMode = action.createSelection();
if (!selectionMode) {
// saveImageForUndo(comp);
int thickness = 0;
int extraStrokeThickness = 0;
if (action.enableStrokePaintSelection()) {
thickness = strokeParam.getStrokeWidth();
StrokeType strokeType = strokeParam.getStrokeType();
extraStrokeThickness = strokeType.getExtraWidth(thickness);
thickness += extraStrokeThickness;
}
int effectThickness = 0;
if (effectsPanel != null) {
effectThickness = effectsPanel.getMaxEffectThickness();
// the extra stroke thickness must be added to this because the effect can be on the stroke
effectThickness += extraStrokeThickness;
}
if (effectThickness > thickness) {
thickness = effectThickness;
}
ShapeType shapeType = typeModel.getSelectedItem();
Shape currentShape = shapeType.getShape(userDrag);
Rectangle shapeBounds = currentShape.getBounds();
shapeBounds.grow(thickness, thickness);
if (!shapeBounds.isEmpty()) {
ToolAffectedArea affectedArea = new ToolAffectedArea(dr, shapeBounds, false);
saveSubImageForUndo(dr.getImage(), affectedArea);
}
paintShapeOnIC(dr, userDrag);
comp.imageChanged(FULL);
dr.updateIconImage();
} else { // selection mode
comp.onSelection(selection -> {
selection.clipToCompSize(comp); // the selection can be too big
PixelitorEdit edit;
if (backupSelectionShape != null) {
edit = new SelectionChangeEdit(comp, backupSelectionShape, "Selection Change");
} else {
edit = new NewSelectionEdit(comp, selection.getShape());
}
History.addEdit(edit);
});
}
drawing = false;
stroke = null;
}
private void updateWhichSettingsAreEnabled() {
ShapesAction action = actionModel.getSelectedItem();
enableEffectSettings(action.drawEffects());
enableStrokeSettings(action.enableStrokeSettings());
enableFillPaintSelection(action.enableFillPaintSelection());
enableStrokePaintSelection(action.enableStrokePaintSelection());
}
private void initAndShowStrokeSettingsDialog() {
if (toolDialog == null) {
toolDialog = strokeParam.createSettingsDialogForShapesTool();
}
GUIUtils.centerOnScreen(toolDialog);
toolDialog.setVisible(true);
}
private void closeEffectsDialog() {
if (effectsDialog != null) {
effectsDialog.setVisible(false);
effectsDialog.dispose();
}
}
@Override
protected void toolEnded() {
super.toolEnded();
closeEffectsDialog();
}
@Override
public void paintOverLayer(Graphics2D g, Composition comp) {
if (drawing) {
// updates continuously the shape while drawing
paintShape(g, userDrag, comp);
}
}
/**
* Paint a shape on the given Drawable. Can be used programmatically.
* The start and end point points are given relative to the Composition (not Layer)
*/
public void paintShapeOnIC(Drawable dr, UserDrag userDrag) {
int tx = -dr.getTX();
int ty = -dr.getTY();
BufferedImage bi = dr.getImage();
Graphics2D g2 = bi.createGraphics();
g2.translate(tx, ty);
Composition comp = dr.getComp();
comp.applySelectionClipping(g2, null);
paintShape(g2, userDrag, comp);
g2.dispose();
}
/**
* Paints the selected shape on the given Graphics2D within the bounds of the given UserDrag
* Called by paintOnImage while dragging, and by paintShapeOnIC on mouse release
*/
private void paintShape(Graphics2D g, UserDrag userDrag, Composition comp) {
if (userDrag.isClick()) {
return;
}
if (basicStrokeForOpenShapes == null) {
basicStrokeForOpenShapes = new BasicStroke(1);
}
ShapeType shapeType = typeModel.getSelectedItem();
Shape currentShape = shapeType.getShape(userDrag);
ShapesAction action = actionModel.getSelectedItem();
g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
if (action.hasFill()) {
TwoPointBasedPaint fillType = fillModel.getSelectedItem();
if (shapeType.isClosed()) {
//g.setPaint(fillType.getPaint(userDrag));
fillType.setupPaint(g, userDrag);
g.fill(currentShape);
fillType.restorePaint(g);
} else if (!action.hasStroke()) {
// special case: a shape that is not closed can be only stroked, even if stroke is disabled
// stroke it with the basic stroke
g.setStroke(basicStrokeForOpenShapes);
// g.setPaint(fillType.getPaint(userDrag));
fillType.setupPaint(g, userDrag);
g.draw(currentShape);
fillType.restorePaint(g);
}
}
if (action.hasStroke()) {
TwoPointBasedPaint strokeFill = strokeFillModel.getSelectedItem();
if (stroke == null) {
// During a single mouse drag, only one stroke should be created
// This is particularly important for "random shape"
stroke = strokeParam.createStroke();
}
g.setStroke(stroke);
// g.setPaint(strokeFill.getPaint(userDrag));
strokeFill.setupPaint(g, userDrag);
g.draw(currentShape);
strokeFill.restorePaint(g);
}
if (action.drawEffects()) {
if (effectsPanel != null) {
AreaEffect[] areaEffects = effectsPanel.getEffects().asArray();
for (AreaEffect effect : areaEffects) {
if (action.hasFill()) {
effect.apply(g, currentShape, 0, 0);
} else if (action.hasStroke()) { // special case if there is only stroke
if (stroke == null) {
stroke = strokeParam.createStroke();
}
effect.apply(g, stroke.createStrokedShape(currentShape), 0, 0);
} else { // "effects only"
effect.apply(g, currentShape, 0, 0);
}
}
}
}
if (action.createSelection()) {
Shape selectionShape;
if (action.enableStrokeSettings()) {
if (stroke == null) {
stroke = strokeParam.createStroke();
}
selectionShape = stroke.createStrokedShape(currentShape);
} else if (!shapeType.isClosed()) {
if (basicStrokeForOpenShapes == null) {
throw new IllegalStateException("action = " + action + ", shapeType = " + shapeType);
}
selectionShape = basicStrokeForOpenShapes.createStrokedShape(currentShape);
} else {
selectionShape = currentShape;
}
Selection selection = comp.getSelection();
if (selection != null) {
selection.setShape(selectionShape);
} else {
comp.createSelectionFromShape(selectionShape);
}
}
}
public boolean isDrawing() {
return drawing;
}
private void enableStrokeSettings(boolean b) {
strokeSettingsButton.setEnabled(b);
if (!b) {
closeToolDialog();
}
}
private void enableEffectSettings(boolean b) {
effectsButton.setEnabled(b);
if (!b) {
closeEffectsDialog();
}
}
private void enableStrokePaintSelection(boolean b) {
strokeFillCombo.setEnabled(b);
}
private void enableFillPaintSelection(boolean b) {
fillCombo.setEnabled(b);
}
/**
* Used for testing
*/
public void setShapeType(ShapeType newType) {
typeModel.setSelectedItem(newType);
}
/**
* Can be used for debugging
*/
public void setAction(ShapesAction action) {
actionModel.setSelectedItem(action);
}
@Override
public DebugNode getDebugNode() {
DebugNode node = super.getDebugNode();
node.addStringChild("Type", typeModel.getSelectedItem().toString());
node.addStringChild("Action", actionModel.getSelectedItem().toString());
node.addStringChild("Fill", fillModel.getSelectedItem().toString());
node.addStringChild("Stroke", strokeFillModel.getSelectedItem().toString());
strokeParam.addDebugNodeInfo(node);
return node;
}
}