/* * 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 com.bric.util.JVM; import pixelitor.Composition; import pixelitor.filters.gui.EnumParam; import pixelitor.filters.gui.RangeParam; import pixelitor.gui.ImageComponent; import pixelitor.gui.PixelitorWindow; import pixelitor.gui.utils.GridBagHelper; import pixelitor.gui.utils.OKDialog; import pixelitor.layers.Drawable; import pixelitor.tools.brushes.BrushAffectedArea; import pixelitor.tools.brushes.CloneBrush; import pixelitor.tools.brushes.CopyBrushType; import pixelitor.utils.Messages; import pixelitor.utils.ScalingMirror; import pixelitor.utils.VisibleForTesting; import pixelitor.utils.debug.DebugNode; import pixelitor.utils.test.RandomGUITest; import javax.swing.*; import java.awt.Cursor; import java.awt.GridBagLayout; import java.awt.Point; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.Random; import static pixelitor.gui.utils.SliderSpinner.TextPosition.NONE; import static pixelitor.tools.CloneTool.State.CLONING; import static pixelitor.tools.CloneTool.State.NO_SOURCE; import static pixelitor.tools.CloneTool.State.SOURCE_DEFINED_FIRST_STROKE; /** * The Clone Stamp tool */ public class CloneTool extends BlendingModeBrushTool { enum State { NO_SOURCE, SOURCE_DEFINED_FIRST_STROKE, CLONING } private State state = NO_SOURCE; private boolean sampleAllLayers = false; private CloneBrush cloneBrush; private final RangeParam scaleParam = new RangeParam("", 10, 100, 400, true, NONE); private final RangeParam rotationParam = new RangeParam("", -180, 0, 180, true, NONE); private final EnumParam<ScalingMirror> mirrorParam = new EnumParam<>("", ScalingMirror.class); protected CloneTool() { super('s', "Clone Stamp", "clone_tool_icon.png", "Alt-click (or right-click) to select the source, then paint with the copied pixels", Cursor.getDefaultCursor()); } @Override public void initSettingsPanel() { settingsPanel.addCopyBrushTypeSelector( CopyBrushType.SOFT, cloneBrush::typeChanged); addSizeSelector(); addBlendingModePanel(); settingsPanel.addSeparator(); settingsPanel.addCheckBox("Aligned", true, "alignedCB", cloneBrush::setAligned); settingsPanel.addSeparator(); settingsPanel.addCheckBox("Sample All Layers", false, "sampleAllLayersCB", selected -> sampleAllLayers = selected); settingsPanel.addSeparator(); settingsPanel.addButton("Transform", e -> { if (RandomGUITest.isRunning()) { return; } JPanel p = new JPanel(new GridBagLayout()); GridBagHelper gbh = new GridBagHelper(p); gbh.addLabelWithControl("Scale (%):", scaleParam.createGUI()); gbh.addLabelWithControl("Rotate (Degrees):", rotationParam.createGUI()); gbh.addLabelWithControl("Mirror:", mirrorParam.createGUI()); toolDialog = new OKDialog(PixelitorWindow.getInstance(), p, "Clone Transform", "Close"); }); } @Override protected void initBrushVariables() { cloneBrush = new CloneBrush(getRadius(), CopyBrushType.SOFT); brush = new BrushAffectedArea(cloneBrush); brushAffectedArea = (BrushAffectedArea) brush; } @Override public void mousePressed(MouseEvent e, ImageComponent ic) { double x = userDrag.getStartX(); double y = userDrag.getStartY(); if (e.isAltDown() || SwingUtilities.isRightMouseButton(e)) { setCloningSource(ic, x, y); } else { boolean notWithLine = !withLine(e); if (state == NO_SOURCE) { handleUndefinedSource(ic, x, y); return; } startNewCloningStroke(x, y, notWithLine); super.mousePressed(e, ic); } } private void startNewCloningStroke(double x, double y, boolean notWithLine) { state = CLONING; // must be a new stroke after the source setting float scaleAbs = scaleParam.getValueAsPercentage(); ScalingMirror mirror = mirrorParam.getSelected(); cloneBrush.setScale( mirror.getScaleX(scaleAbs), mirror.getScaleY(scaleAbs)); cloneBrush.setRotate(rotationParam.getValueInRadians()); if (notWithLine) { // when drawing with line, the destination should not change for mouse press cloneBrush.setCloningDestPoint(x, y); } } @Override public void mouseDragged(MouseEvent e, ImageComponent ic) { if (state == CLONING) { // make sure that the first source-setting stroke does not clone super.mouseDragged(e, ic); } } private void handleUndefinedSource(ImageComponent ic, double x, double y) { if (RandomGUITest.isRunning()) { // special case: do not show dialogs for RandomGUITest, // just act as if this was an alt-click setCloningSource(ic, x, y); } else { String msg = "Define a source point first with Alt-Click (or with right-click)."; if (JVM.isLinux) { msg += "\n(For Alt-Click you might need to disable Alt-Click for window dragging in the window manager)"; } Messages.showError("No source", msg); } } private void setCloningSource(ImageComponent ic, double x, double y) { BufferedImage sourceImage; int dx = 0; int dy = 0; if (sampleAllLayers) { sourceImage = ic.getComp().getCompositeImage(); } else { Drawable dr = ic.getComp().getActiveDrawable(); sourceImage = dr.getImage(); dx = -dr.getTX(); dy = -dr.getTY(); } cloneBrush.setSource(sourceImage, x + dx, y + dy); state = SOURCE_DEFINED_FIRST_STROKE; } @Override protected boolean doColorPickerForwarding() { return false; // this tool uses Alt-click for source selection } @Override protected Symmetry getSymmetry() { throw new UnsupportedOperationException("no symmetry"); } @VisibleForTesting protected void setState(State state) { this.state = state; } @Override protected void prepareProgrammaticBrushStroke(Drawable dr, Point start) { super.prepareProgrammaticBrushStroke(dr, start); setupRandomSource(dr, start); } private void setupRandomSource(Drawable dr, Point start) { Composition comp = dr.getComp(); int canvasWidth = comp.getCanvasWidth(); int canvasHeight = comp.getCanvasHeight(); Random rand = new Random(); int sourceX = rand.nextInt(canvasWidth); int sourceY = rand.nextInt(canvasHeight); setCloningSource(comp.getIC(), sourceX, sourceY); startNewCloningStroke(start.x, start.y, true); } @Override public DebugNode getDebugNode() { DebugNode node = super.getDebugNode(); node.addStringChild("Brush", cloneBrush.getType().toString()); node.addStringChild("State", state.toString()); node.addBooleanChild("Sample All Layers", sampleAllLayers); node.addBooleanChild("Aligned", cloneBrush.isAligned()); node.addFloatChild("Scale", scaleParam.getValueAsPercentage()); node.addIntChild("Rotation", rotationParam.getValue()); node.addStringChild("Mirror", mirrorParam.getSelected().toString()); return node; } }