/* * 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.gui.GlobalKeyboardWatch; import pixelitor.gui.ImageComponent; import pixelitor.gui.ImageComponents; import pixelitor.history.History; import pixelitor.history.ImageEdit; import pixelitor.history.PartialImageEdit; import pixelitor.layers.Drawable; import pixelitor.selection.IgnoreSelection; import pixelitor.tools.toolhandlers.ColorPickerToolHandler; import pixelitor.tools.toolhandlers.CurrentToolHandler; import pixelitor.tools.toolhandlers.HandToolHandler; import pixelitor.tools.toolhandlers.ImageLayerCheckHandler; import pixelitor.tools.toolhandlers.ToolHandler; import pixelitor.utils.Utils; import pixelitor.utils.debug.DebugNode; import javax.swing.*; import java.awt.Component; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; /** * An abstract superclass for all tools */ public abstract class Tool implements KeyboardObserver { private boolean mouseDown = false; private boolean altDown = false; private ToolButton toolButton; private final String name; private final String iconFileName; private final String toolMessage; private final Cursor cursor; private final boolean constrainIfShiftDown; private final ClipStrategy clipStrategy; private boolean endPointInitialized = false; protected boolean spaceDragBehavior = false; protected UserDrag userDrag; private final char activationKeyChar; private ToolHandler handlerChainStart; private HandToolHandler handToolHandler; protected ToolSettingsPanel settingsPanel; protected boolean ended = false; // a dialog with more settings that will be closed automatically // when the user switches to another tool protected JDialog toolDialog; protected Tool(char activationKeyChar, String name, String iconFileName, String toolMessage, Cursor cursor, boolean allowOnlyImageLayers, boolean handToolForwarding, boolean constrainIfShiftDown, ClipStrategy clipStrategy) { this.activationKeyChar = activationKeyChar; this.name = name; this.iconFileName = iconFileName; this.toolMessage = toolMessage; this.cursor = cursor; this.constrainIfShiftDown = constrainIfShiftDown; this.clipStrategy = clipStrategy; initHandlerChain(cursor, allowOnlyImageLayers, handToolForwarding); } private void initHandlerChain(Cursor cursor, boolean allowOnlyImageLayers, boolean handToolForwarding) { ToolHandler lastHandler = null; if (handToolForwarding) { // most tools behave like the hand tool if the space is pressed handToolHandler = new HandToolHandler(cursor); lastHandler = addHandlerToChain(handToolHandler, lastHandler); } if (allowOnlyImageLayers) { lastHandler = addHandlerToChain( new ImageLayerCheckHandler(this), lastHandler); } if(doColorPickerForwarding()) { // brush tools behave like the color picker if Alt is pressed ColorPickerToolHandler colorPickerHandler = new ColorPickerToolHandler(); lastHandler = addHandlerToChain(colorPickerHandler, lastHandler); } // if there was no special case, the current tool should handle the events addHandlerToChain(new CurrentToolHandler(this), lastHandler); } protected boolean doColorPickerForwarding() { return false; } /** * Adds the new handler to the end of the chain and returns the new end of the chain */ private ToolHandler addHandlerToChain(ToolHandler newHandler, ToolHandler lastOne) { if (lastOne == null) { handlerChainStart = newHandler; return handlerChainStart; } else { lastOne.setSuccessor(newHandler); return newHandler; } } public String getToolMessage() { return toolMessage; } public boolean dispatchMouseClicked(MouseEvent e, ImageComponent ic) { // empty for the convenience of subclasses return false; } public void dispatchMousePressed(MouseEvent e, ImageComponent ic) { if (mouseDown) { // can happen if the tool is changed while drawing, and then changed back MouseEvent fake = new MouseEvent((Component) e.getSource(), e.getID(), e.getWhen(), e.getModifiers(), (int)userDrag.getEndX(), (int)userDrag.getEndY(), 1, false); dispatchMouseReleased(fake, ic); // try to clean-up } mouseDown = true; userDrag = new UserDrag(); userDrag.setStartFromMouseEvent(e, ic); handlerChainStart.handleMousePressed(e, ic); endPointInitialized = false; } public void dispatchMouseReleased(MouseEvent e, ImageComponent ic) { if (!mouseDown) { // can happen if the tool is changed while drawing dispatchMousePressed(e, ic); // try to initialize } mouseDown = false; userDrag.setEndFromMouseEvent(e, ic); handlerChainStart.handleMouseReleased(e, ic); endPointInitialized = false; } public void dispatchMouseDragged(MouseEvent e, ImageComponent ic) { if (!mouseDown) { // can happen if the tool is changed while drawing dispatchMousePressed(e, ic); // try to initialize } mouseDown = true; if (spaceDragBehavior) { userDrag.saveEndValues(); } if (constrainIfShiftDown) { userDrag.setConstrainPoints(e.isShiftDown()); } userDrag.setEndFromMouseEvent(e, ic); if (spaceDragBehavior) { if (endPointInitialized && GlobalKeyboardWatch.isSpaceDown()) { userDrag.adjustStartForSpaceDownMove(); } endPointInitialized = true; } handlerChainStart.handleMouseDragged(e, ic); } void setButton(ToolButton toolButton) { this.toolButton = toolButton; } public ToolButton getButton() { return toolButton; } public abstract void initSettingsPanel(); public String getName() { return name; } protected String getIconFileName() { return iconFileName; } public char getActivationKeyChar() { return activationKeyChar; } /** * Saves the full image or the selected area only if there is a selection */ void saveFullImageForUndo(Composition comp) { BufferedImage copy = comp.getActiveDrawable() .getImageOrSubImageIfSelected(true, true); ImageEdit edit = new ImageEdit(comp, getName(), comp.getActiveDrawable(), copy, IgnoreSelection.NO, false); History.addEdit(edit); } /** * This saving method is used by the brush tools, by the shapes and by the paint bucket. * It saves the intersection of the selection (if there is one) with the maximal affected area. */ // TODO currently it does not take the selection into account protected void saveSubImageForUndo(BufferedImage originalImage, ToolAffectedArea affectedArea) { assert (originalImage != null); Rectangle rectangleAffectedByTool = affectedArea.getRectangle(); if (rectangleAffectedByTool.isEmpty()) { return; } Drawable dr = affectedArea.getDrawable(); Composition comp = dr.getComp(); // Rectangle fullImageBounds = new Rectangle(0, 0, originalImage.getWidth(), originalImage.getHeight()); // Rectangle saveRectangle = rectangleAffectedByTool.intersection(fullImageBounds); Rectangle saveRectangle = SwingUtilities.computeIntersection( 0, 0, originalImage.getWidth(), originalImage.getHeight(), // full image bounds rectangleAffectedByTool ); if (!saveRectangle.isEmpty()) { PartialImageEdit edit = new PartialImageEdit(getName(), comp, dr, originalImage, saveRectangle, false); History.addEdit(edit); } } protected void toolStarted() { ended = false; GlobalKeyboardWatch.setObserver(this); ImageComponents.setCursorForAll(cursor); } protected void toolEnded() { ended = true; closeToolDialog(); } protected void closeToolDialog() { if (toolDialog != null && toolDialog.isVisible()) { toolDialog.setVisible(false); toolDialog.dispose(); } } public Cursor getCursor() { return cursor; } public void paintOverLayer(Graphics2D g, Composition comp) { // empty for the convenience of subclasses } /** * A possibility to paint temporarily something (like marching ants) on the ImageComponent * after all the layers have been painted. */ public void paintOverImage(Graphics2D g2, Canvas canvas, ImageComponent callingIC, AffineTransform unscaledTransform) { // empty for the convenience of subclasses } public void dispatchMouseMoved(MouseEvent e, ImageComponent ic) { // empty for the convenience of subclasses } public abstract void mousePressed(MouseEvent e, ImageComponent ic); public abstract void mouseDragged(MouseEvent e, ImageComponent ic); public abstract void mouseReleased(MouseEvent e, ImageComponent ic); public void setSettingsPanel(ToolSettingsPanel settingsPanel) { this.settingsPanel = settingsPanel; } public void randomize() { Utils.randomizeGUIWidgetsOn(settingsPanel); } public UserDrag getUserDrag() { return userDrag; } public void setClip(Graphics2D g, ImageComponent ic) { clipStrategy.setClip(g, ic); } @Override public void spacePressed() { if (handToolHandler != null) { // there is hand tool forwarding handToolHandler.spacePressed(); } } @Override public void spaceReleased() { if (handToolHandler != null) { // there is hand tool forwarding handToolHandler.spaceReleased(); } } @Override public boolean arrowKeyPressed(ArrowKey key) { // empty for the convenience of subclasses return false; // not consumed } @Override public void escPressed() { // empty by default } @Override public void altPressed() { if (!altDown && doColorPickerForwarding()) { ImageComponents.forAllImages(ic -> ic.setCursor(Tools.COLOR_PICKER.getCursor())); } altDown = true; } @Override public void altReleased() { if(doColorPickerForwarding()) { ImageComponents.forAllImages(ic -> ic.setCursor(cursor)); } altDown = false; } @Override public String toString() { return name; // so that they can be easily selected from a JComboBox } // used for debugging public String getStateInfo() { return null; } public DebugNode getDebugNode() { DebugNode toolNode = new DebugNode("Active Tool", this); toolNode.addStringChild("Name", getName()); return toolNode; } }