/* * 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.layers; import com.bric.util.JVM; import org.jdesktop.swingx.painter.CheckerboardPainter; import pixelitor.gui.ImageComponent; import pixelitor.gui.PixelitorWindow; import pixelitor.utils.IconUtils; import pixelitor.utils.ImageUtils; import pixelitor.utils.VisibleForTesting; import javax.swing.*; import javax.swing.border.Border; import java.awt.Color; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; /** * A GUI element representing a layer in an image */ public class LayerButton extends JToggleButton { private static final Icon OPEN_EYE_ICON = IconUtils.loadIcon("eye_open.png"); private static final Icon CLOSED_EYE_ICON = IconUtils.loadIcon("eye_closed.png"); private static final CheckerboardPainter checkerBoardPainter = ImageUtils.createCheckerboardPainter(); private static final String uiClassID = "LayerButtonUI"; public static final Color UNSELECTED_COLOR = new Color(214, 217, 223); public static final Color SELECTED_COLOR = new Color(48, 76, 111); public static final int BORDER_WIDTH = 2; private DragReorderHandler dragReorderHandler; /** * In pxc files the mask might be added before the drag handler * and in unit tests the drag handler is not added at all. */ private boolean maskAddedBeforeDragHandler; private enum SelectionState { UNSELECTED { @Override public void activate(JLabel layer, JLabel mask) { layer.setBorder(unSelectedIconOnUnselectedLayerBorder); if (mask != null) { mask.setBorder(unSelectedIconOnUnselectedLayerBorder); } } }, SELECT_LAYER { @Override public void activate(JLabel layer, JLabel mask) { layer.setBorder(selectedBorder); if (mask != null) { mask.setBorder(unSelectedIconOnSelectedLayerBorder); } } }, SELECT_MASK { @Override public void activate(JLabel layer, JLabel mask) { layer.setBorder(unSelectedIconOnSelectedLayerBorder); if (mask != null) { mask.setBorder(selectedBorder); } } }; private static final Border lightBorder; static { if (JVM.isMac) { // seems to be a Mac-specific problem: with LineBorder, // a one pixel wide line disappears lightBorder = BorderFactory.createMatteBorder(1, 1, 1, 1, UNSELECTED_COLOR); } else { lightBorder = BorderFactory.createLineBorder(UNSELECTED_COLOR, 1); } } private static final Border darkBorder = BorderFactory.createLineBorder(SELECTED_COLOR, 1); private static final Border selectedBorder = BorderFactory.createCompoundBorder(lightBorder, darkBorder); private static final Border unSelectedIconOnSelectedLayerBorder = BorderFactory.createLineBorder(SELECTED_COLOR, BORDER_WIDTH); private static final Border unSelectedIconOnUnselectedLayerBorder = BorderFactory.createLineBorder(UNSELECTED_COLOR, BORDER_WIDTH); public abstract void activate(JLabel layer, JLabel mask); } private SelectionState selectionState = SelectionState.UNSELECTED; private final Layer layer; private boolean userInteraction = true; private JCheckBox visibilityCB; private LayerNameEditor nameEditor; private final JLabel layerIconLabel; private JLabel maskIconLabel; /** * The Y coordinate in the parent when it is not dragging */ private int staticY; public LayerButton(Layer layer) { this.layer = layer; setLayout(new LayerButtonLayout(layer)); initVisibilityControl(layer); initLayerNameEditor(layer); if (layer instanceof TextLayer) { Icon textLayerIcon = IconUtils.getTextLayerIcon(); layerIconLabel = new JLabel(textLayerIcon); layerIconLabel.setToolTipText("Double-click to edit the text layer."); } else if (layer instanceof AdjustmentLayer) { Icon adjLayerIcon = IconUtils.getAdjLayerIcon(); layerIconLabel = new JLabel(adjLayerIcon); } else { layerIconLabel = new JLabel("", null, CENTER); } layerIconLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { int clickCount = e.getClickCount(); if (clickCount == 1) { MaskViewMode.NORMAL.activate(layer); } else { if (layer instanceof TextLayer) { ((TextLayer) layer).edit(PixelitorWindow.getInstance()); } else if (layer instanceof AdjustmentLayer) { ((AdjustmentLayer) layer).configure(); } } } @Override public void mousePressed(MouseEvent e) { // by putting it into mouse pressed, it is consistent // with the mask clicks if (SwingUtilities.isLeftMouseButton(e)) { selectLayerIfIconClicked(e); } } }); configureLayerIcon(layerIconLabel, "layerIcon"); configureBorders(layer.isMaskEditing()); add(layerIconLabel, LayerButtonLayout.LAYER); wireSelectionWithLayerActivation(layer); } private static void configureLayerIcon(JLabel layerIcon, String name) { // layerIcon.putClientProperty("JComponent.sizeVariant", "mini"); layerIcon.setName(name); } public static void selectLayerIfIconClicked(MouseEvent e) { // By adding a mouse listener to the JLabel, it loses the // ability to automatically transmit the mouse events to its // parent, and therefore the layer cannot be selected anymore // by left-clicking on this label. This is the workaround. JLabel source = (JLabel) e.getSource(); LayerButton layerButton = (LayerButton) source.getParent(); layerButton.setSelected(true); } private void initVisibilityControl(Layer layer) { visibilityCB = new JCheckBox(CLOSED_EYE_ICON); visibilityCB.setRolloverIcon(CLOSED_EYE_ICON); visibilityCB.setSelected(true); visibilityCB.setToolTipText("Click to hide/show this layer."); visibilityCB.setSelectedIcon(OPEN_EYE_ICON); add(visibilityCB, LayerButtonLayout.CHECKBOX); visibilityCB.addItemListener(e -> layer.setVisible(visibilityCB.isSelected(), true)); } private void initLayerNameEditor(Layer layer) { nameEditor = new LayerNameEditor(this, layer); add(nameEditor, LayerButtonLayout.NAME_EDITOR); addPropertyChangeListener("name", evt -> nameEditor.setText(getName())); } private void wireSelectionWithLayerActivation(Layer layer) { addItemListener(e -> { if (isSelected()) { layer.makeActive(userInteraction ? true : false); } else { nameEditor.disableEditing(); // Invoke later because we can get here in the middle // of a new layer activation, when isSelected still // returns false, but the layer will be selected during // the same event processing. SwingUtilities.invokeLater(() -> configureBorders(layer.isMaskEditing())); } }); } public void setOpenEye(boolean newVisibility) { visibilityCB.setSelected(newVisibility); } @VisibleForTesting public boolean hasOpenEye() { return visibilityCB.isSelected(); } public void setUserInteraction(boolean userInteraction) { this.userInteraction = userInteraction; } public void addDragReorderHandler(DragReorderHandler handler) { dragReorderHandler = handler; handler.attachToComponent(this); handler.attachToComponent(nameEditor); handler.attachToComponent(layerIconLabel); if (maskAddedBeforeDragHandler) { assert maskIconLabel != null; handler.attachToComponent(maskIconLabel); } } public void removeDragReorderHandler(DragReorderHandler handler) { handler.detachFromComponent(this); handler.detachFromComponent(nameEditor); handler.detachFromComponent(layerIconLabel); if (maskIconLabel != null) { handler.detachFromComponent(maskIconLabel); } } public int getStaticY() { return staticY; } public void setStaticY(int staticY) { this.staticY = staticY; } public void dragFinished(int newLayerIndex) { layer.dragFinished(newLayerIndex); } public Layer getLayer() { return layer; } public String getLayerName() { return layer.getName(); } public boolean isNameEditing() { return nameEditor.isEditable(); } public boolean isVisibilityChecked() { return visibilityCB.isSelected(); } public void changeNameProgrammatically(String newName) { nameEditor.setText(newName); } public void updateLayerIconImage(ImageLayer layer) { boolean isMask = layer instanceof LayerMask; BufferedImage img = layer.getCanvasSizedSubImage(); Runnable notEDT = () -> { CheckerboardPainter painter = null; if(!isMask) { painter = checkerBoardPainter; } BufferedImage thumb = ImageUtils.createThumbnail(img, LayerButtonLayout.thumbSize, painter); Runnable edt = () -> { if (isMask) { if (maskIconLabel == null) { return; } boolean disabledMask = !layer.getParent().isMaskEnabled(); if (disabledMask) { ImageUtils.paintRedXOnThumb(thumb); } maskIconLabel.setIcon(new ImageIcon(thumb)); } else { layerIconLabel.setIcon(new ImageIcon(thumb)); } repaint(); }; SwingUtilities.invokeLater(edt); }; new Thread(notEDT).start(); } public void addMaskIconLabel() { maskIconLabel = new JLabel("", null, CENTER); maskIconLabel.setToolTipText("<html>Shift-click to disable/enable,<br>Alt-click to show mask/layer,<br>Right-click for more options"); LayerMaskActions.addPopupMenu(maskIconLabel, layer); configureLayerIcon(maskIconLabel, "maskIcon"); configureBorders(layer.isMaskEditing()); add(maskIconLabel, LayerButtonLayout.MASK); // there is another mouse listener for the right-click popups maskIconLabel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { boolean altClick = e.isAltDown(); boolean shiftClick = e.isShiftDown(); if (altClick && shiftClick) { // shift-alt-click switches to RUBYLITH except when // it is already RUBYLITH ImageComponent ic = layer.getComp().getIC(); if (ic.getMaskViewMode() == MaskViewMode.RUBYLITH) { MaskViewMode.EDIT_MASK.activate(ic, layer); } else { MaskViewMode.RUBYLITH.activate(ic, layer); } } else if (altClick) { // alt-click switches to SHOW_MASK except when it // already is in SHOW_MASK ImageComponent ic = layer.getComp().getIC(); if (ic.getMaskViewMode() == MaskViewMode.SHOW_MASK) { MaskViewMode.EDIT_MASK.activate(ic, layer); } else { MaskViewMode.SHOW_MASK.activate(ic, layer); } } else if (shiftClick) { // shift-click disables except when it is already disabled layer.setMaskEnabled(!layer.isMaskEnabled(), true); } else { ImageComponent ic = layer.getComp().getIC(); // don't change SHOW_MASK into EDIT_MASK if (ic.getMaskViewMode() == MaskViewMode.NORMAL) { MaskViewMode.EDIT_MASK.activate(layer); } } } }); if (dragReorderHandler != null) { dragReorderHandler.attachToComponent(maskIconLabel); this.maskAddedBeforeDragHandler = false; } else { this.maskAddedBeforeDragHandler = true; } revalidate(); } public void deleteMaskIconLabel() { // TODO remove the two mouse listeners (left-click, right-click)? // at least remove the drag reorder handler if (dragReorderHandler != null) { // null in unit tests dragReorderHandler.detachFromComponent(maskIconLabel); } remove(maskIconLabel); revalidate(); repaint(); maskIconLabel = null; } public void configureBorders(boolean maskEditing) { SelectionState newSelectionState; if (!isSelected()) { newSelectionState = SelectionState.UNSELECTED; } else { if (maskEditing) { newSelectionState = SelectionState.SELECT_MASK; } else { newSelectionState = SelectionState.SELECT_LAYER; } } if (newSelectionState != selectionState) { selectionState = newSelectionState; selectionState.activate(layerIconLabel, maskIconLabel); } } @Override public String getUIClassID() { return uiClassID; } @Override public void updateUI() { setUI(new LayerButtonUI()); } @Override public String toString() { return "LayerButton{" + "name='" + getLayerName() + '\'' + "is maskIconLabel null: " + (maskIconLabel == null ? "YES" : "NO") + '}'; } }