blob: bf3754be1d334c8fbad6e2b32548e4fcc689e111 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.draw9patch.ui;
import com.android.draw9patch.graphics.GraphicsUtilities;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.BorderFactory;
import javax.swing.JSlider;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.JCheckBox;
import javax.swing.Box;
import javax.swing.JFileChooser;
import javax.swing.JSplitPane;
import javax.swing.JButton;
import javax.swing.border.EmptyBorder;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.Graphics2D;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.TexturePaint;
import java.awt.Shape;
import java.awt.BasicStroke;
import java.awt.RenderingHints;
import java.awt.Rectangle;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.AWTEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.AWTEventListener;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Line2D;
import java.awt.geom.Area;
import java.awt.geom.RoundRectangle2D;
import java.io.IOException;
import java.io.File;
import java.net.URL;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
class ImageEditorPanel extends JPanel {
private static final String EXTENSION_9PATCH = ".9.png";
private static final int DEFAULT_ZOOM = 8;
private static final float DEFAULT_SCALE = 2.0f;
// For stretch regions and padding
private static final int BLACK_TICK = 0xFF000000;
// For Layout Bounds
private static final int RED_TICK = 0xFFFF0000;
private String name;
private BufferedImage image;
private boolean is9Patch;
private ImageViewer viewer;
private StretchesViewer stretchesViewer;
private JLabel xLabel;
private JLabel yLabel;
private TexturePaint texture;
private List<Rectangle> patches;
private List<Rectangle> horizontalPatches;
private List<Rectangle> verticalPatches;
private List<Rectangle> fixed;
private boolean verticalStartWithPatch;
private boolean horizontalStartWithPatch;
private Pair<Integer> horizontalPadding;
private Pair<Integer> verticalPadding;
ImageEditorPanel(MainFrame mainFrame, BufferedImage image, String name) {
this.image = image;
this.name = name;
setTransferHandler(new ImageTransferHandler(mainFrame));
checkImage();
setOpaque(false);
setLayout(new BorderLayout());
loadSupport();
buildImageViewer();
buildStatusPanel();
}
private void loadSupport() {
try {
URL resource = getClass().getResource("/images/checker.png");
BufferedImage checker = GraphicsUtilities.loadCompatibleImage(resource);
texture = new TexturePaint(checker, new Rectangle2D.Double(0, 0,
checker.getWidth(), checker.getHeight()));
} catch (IOException e) {
e.printStackTrace();
}
}
private void buildImageViewer() {
viewer = new ImageViewer();
JSplitPane splitter = new JSplitPane();
splitter.setContinuousLayout(true);
splitter.setResizeWeight(0.8);
splitter.setBorder(null);
JScrollPane scroller = new JScrollPane(viewer);
scroller.setOpaque(false);
scroller.setBorder(null);
scroller.getViewport().setBorder(null);
scroller.getViewport().setOpaque(false);
splitter.setLeftComponent(scroller);
splitter.setRightComponent(buildStretchesViewer());
add(splitter);
}
private JComponent buildStretchesViewer() {
stretchesViewer = new StretchesViewer();
JScrollPane scroller = new JScrollPane(stretchesViewer);
scroller.setBorder(null);
scroller.getViewport().setBorder(null);
scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
scroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
return scroller;
}
private void buildStatusPanel() {
JPanel status = new JPanel(new GridBagLayout());
status.setOpaque(false);
JLabel label = new JLabel();
label.setForeground(Color.WHITE);
label.setText("Zoom: ");
label.putClientProperty("JComponent.sizeVariant", "small");
status.add(label, new GridBagConstraints(0, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets(0, 6, 0, 0), 0, 0));
label = new JLabel();
label.setForeground(Color.WHITE);
label.setText("100%");
label.putClientProperty("JComponent.sizeVariant", "small");
status.add(label, new GridBagConstraints(1, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
JSlider zoomSlider = new JSlider(1, 16, DEFAULT_ZOOM);
zoomSlider.setSnapToTicks(true);
zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
zoomSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent evt) {
viewer.setZoom(((JSlider) evt.getSource()).getValue());
}
});
status.add(zoomSlider, new GridBagConstraints(2, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
JLabel maxZoomLabel = new JLabel();
maxZoomLabel.setForeground(Color.WHITE);
maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
maxZoomLabel.setText("800%");
status.add(maxZoomLabel, new GridBagConstraints(3, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
label = new JLabel();
label.setForeground(Color.WHITE);
label.setText("Patch scale: ");
label.putClientProperty("JComponent.sizeVariant", "small");
status.add(label, new GridBagConstraints(0, 1, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 6, 0, 0), 0, 0));
label = new JLabel();
label.setForeground(Color.WHITE);
label.setText("2x");
label.putClientProperty("JComponent.sizeVariant", "small");
status.add(label, new GridBagConstraints(1, 1, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
zoomSlider = new JSlider(200, 600, (int) (DEFAULT_SCALE * 100.0f));
zoomSlider.setSnapToTicks(true);
zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
zoomSlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent evt) {
stretchesViewer.setScale(((JSlider) evt.getSource()).getValue() / 100.0f);
}
});
status.add(zoomSlider, new GridBagConstraints(2, 1, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
maxZoomLabel = new JLabel();
maxZoomLabel.setForeground(Color.WHITE);
maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
maxZoomLabel.setText("6x");
status.add(maxZoomLabel, new GridBagConstraints(3, 1, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
JCheckBox showLock = new JCheckBox("Show lock");
showLock.setOpaque(false);
showLock.setForeground(Color.WHITE);
showLock.setSelected(true);
showLock.putClientProperty("JComponent.sizeVariant", "small");
showLock.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
viewer.setLockVisible(((JCheckBox) event.getSource()).isSelected());
}
});
status.add(showLock, new GridBagConstraints(4, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 12, 0, 0), 0, 0));
JCheckBox showPatches = new JCheckBox("Show patches");
showPatches.setOpaque(false);
showPatches.setForeground(Color.WHITE);
showPatches.putClientProperty("JComponent.sizeVariant", "small");
showPatches.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
viewer.setPatchesVisible(((JCheckBox) event.getSource()).isSelected());
}
});
status.add(showPatches, new GridBagConstraints(4, 1, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 12, 0, 0), 0, 0));
JCheckBox showPadding = new JCheckBox("Show content");
showPadding.setOpaque(false);
showPadding.setForeground(Color.WHITE);
showPadding.putClientProperty("JComponent.sizeVariant", "small");
showPadding.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
stretchesViewer.setPaddingVisible(((JCheckBox) event.getSource()).isSelected());
}
});
status.add(showPadding, new GridBagConstraints(5, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets(0, 12, 0, 0), 0, 0));
status.add(Box.createHorizontalGlue(), new GridBagConstraints(6, 0, 1, 1, 1.0f, 1.0f,
GridBagConstraints.LINE_START, GridBagConstraints.BOTH,
new Insets(0, 0, 0, 0), 0, 0));
label = new JLabel("X: ");
label.setForeground(Color.WHITE);
label.putClientProperty("JComponent.sizeVariant", "small");
status.add(label, new GridBagConstraints(7, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
xLabel = new JLabel("0px");
xLabel.setForeground(Color.WHITE);
xLabel.putClientProperty("JComponent.sizeVariant", "small");
status.add(xLabel, new GridBagConstraints(8, 0, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets(0, 0, 0, 6), 0, 0));
label = new JLabel("Y: ");
label.setForeground(Color.WHITE);
label.putClientProperty("JComponent.sizeVariant", "small");
status.add(label, new GridBagConstraints(7, 1, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets(0, 0, 0, 0), 0, 0));
yLabel = new JLabel("0px");
yLabel.setForeground(Color.WHITE);
yLabel.putClientProperty("JComponent.sizeVariant", "small");
status.add(yLabel, new GridBagConstraints(8, 1, 1, 1, 0.0f, 0.0f,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets(0, 0, 0, 6), 0, 0));
add(status, BorderLayout.SOUTH);
}
private void checkImage() {
is9Patch = name.endsWith(EXTENSION_9PATCH);
if (!is9Patch) {
convertTo9Patch();
} else {
ensure9Patch();
}
}
private void ensure9Patch() {
int width = image.getWidth();
int height = image.getHeight();
for (int i = 0; i < width; i++) {
int pixel = image.getRGB(i, 0);
if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
image.setRGB(i, 0, 0);
}
pixel = image.getRGB(i, height - 1);
if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
image.setRGB(i, height - 1, 0);
}
}
for (int i = 0; i < height; i++) {
int pixel = image.getRGB(0, i);
if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
image.setRGB(0, i, 0);
}
pixel = image.getRGB(width - 1, i);
if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
image.setRGB(width - 1, i, 0);
}
}
}
private void convertTo9Patch() {
BufferedImage buffer = GraphicsUtilities.createTranslucentCompatibleImage(
image.getWidth() + 2, image.getHeight() + 2);
Graphics2D g2 = buffer.createGraphics();
g2.drawImage(image, 1, 1, null);
g2.dispose();
image = buffer;
name = name.substring(0, name.lastIndexOf('.')) + ".9.png";
}
File chooseSaveFile() {
if (is9Patch) {
return new File(name);
} else {
JFileChooser chooser = new JFileChooser(
name.substring(0, name.lastIndexOf(File.separatorChar)));
chooser.setFileFilter(new PngFileFilter());
int choice = chooser.showSaveDialog(this);
if (choice == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
if (!file.getAbsolutePath().endsWith(EXTENSION_9PATCH)) {
String path = file.getAbsolutePath();
if (path.endsWith(".png")) {
path = path.substring(0, path.lastIndexOf(".png")) + EXTENSION_9PATCH;
} else {
path = path + EXTENSION_9PATCH;
}
name = path;
is9Patch = true;
return new File(path);
}
is9Patch = true;
return file;
}
}
return null;
}
RenderedImage getImage() {
return image;
}
private class StretchesViewer extends JPanel {
private static final int MARGIN = 24;
private StretchView horizontal;
private StretchView vertical;
private StretchView both;
private Dimension size;
private float horizontalPatchesSum;
private float verticalPatchesSum;
private boolean showPadding;
StretchesViewer() {
setOpaque(false);
setLayout(new GridBagLayout());
setBorder(BorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, MARGIN));
horizontal = new StretchView();
vertical = new StretchView();
both = new StretchView();
setScale(DEFAULT_SCALE);
add(vertical, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
add(horizontal, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
add(both, new GridBagConstraints(0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setPaint(texture);
g2.fillRect(0, 0, getWidth(), getHeight());
g2.dispose();
}
void setScale(float scale) {
int patchWidth = image.getWidth() - 2;
int patchHeight = image.getHeight() - 2;
int scaledWidth = (int) (patchWidth * scale);
int scaledHeight = (int) (patchHeight * scale);
horizontal.scaledWidth = scaledWidth;
vertical.scaledHeight = scaledHeight;
both.scaledWidth = scaledWidth;
both.scaledHeight = scaledHeight;
size = new Dimension(scaledWidth, scaledHeight);
computePatches();
}
void computePatches() {
boolean measuredWidth = false;
boolean endRow = true;
int remainderHorizontal = 0;
int remainderVertical = 0;
if (fixed.size() > 0) {
int start = fixed.get(0).y;
for (Rectangle rect : fixed) {
if (rect.y > start) {
endRow = true;
measuredWidth = true;
}
if (!measuredWidth) {
remainderHorizontal += rect.width;
}
if (endRow) {
remainderVertical += rect.height;
endRow = false;
start = rect.y;
}
}
}
horizontal.remainderHorizontal = horizontal.scaledWidth - remainderHorizontal;
vertical.remainderHorizontal = vertical.scaledWidth - remainderHorizontal;
both.remainderHorizontal = both.scaledWidth - remainderHorizontal;
horizontal.remainderVertical = horizontal.scaledHeight - remainderVertical;
vertical.remainderVertical = vertical.scaledHeight - remainderVertical;
both.remainderVertical = both.scaledHeight - remainderVertical;
horizontalPatchesSum = 0;
if (horizontalPatches.size() > 0) {
int start = -1;
for (Rectangle rect : horizontalPatches) {
if (rect.x > start) {
horizontalPatchesSum += rect.width;
start = rect.x;
}
}
} else {
int start = -1;
for (Rectangle rect : patches) {
if (rect.x > start) {
horizontalPatchesSum += rect.width;
start = rect.x;
}
}
}
verticalPatchesSum = 0;
if (verticalPatches.size() > 0) {
int start = -1;
for (Rectangle rect : verticalPatches) {
if (rect.y > start) {
verticalPatchesSum += rect.height;
start = rect.y;
}
}
} else {
int start = -1;
for (Rectangle rect : patches) {
if (rect.y > start) {
verticalPatchesSum += rect.height;
start = rect.y;
}
}
}
setSize(size);
ImageEditorPanel.this.validate();
repaint();
}
void setPaddingVisible(boolean visible) {
showPadding = visible;
repaint();
}
private class StretchView extends JComponent {
private final Color PADDING_COLOR = new Color(0.37f, 0.37f, 1.0f, 0.5f);
int scaledWidth;
int scaledHeight;
int remainderHorizontal;
int remainderVertical;
StretchView() {
scaledWidth = image.getWidth();
scaledHeight = image.getHeight();
}
@Override
protected void paintComponent(Graphics g) {
int x = (getWidth() - scaledWidth) / 2;
int y = (getHeight() - scaledHeight) / 2;
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.translate(x, y);
x = 0;
y = 0;
if (patches.size() == 0) {
g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
g2.dispose();
return;
}
int fixedIndex = 0;
int horizontalIndex = 0;
int verticalIndex = 0;
int patchIndex = 0;
boolean hStretch;
boolean vStretch;
float vWeightSum = 1.0f;
float vRemainder = remainderVertical;
vStretch = verticalStartWithPatch;
while (y < scaledHeight - 1) {
hStretch = horizontalStartWithPatch;
int height = 0;
float vExtra = 0.0f;
float hWeightSum = 1.0f;
float hRemainder = remainderHorizontal;
while (x < scaledWidth - 1) {
Rectangle r;
if (!vStretch) {
if (hStretch) {
r = horizontalPatches.get(horizontalIndex++);
float extra = r.width / horizontalPatchesSum;
int width = (int) (extra * hRemainder / hWeightSum);
hWeightSum -= extra;
hRemainder -= width;
g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y,
r.x + r.width, r.y + r.height, null);
x += width;
} else {
r = fixed.get(fixedIndex++);
g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y,
r.x + r.width, r.y + r.height, null);
x += r.width;
}
height = r.height;
} else {
if (hStretch) {
r = patches.get(patchIndex++);
vExtra = r.height / verticalPatchesSum;
height = (int) (vExtra * vRemainder / vWeightSum);
float extra = r.width / horizontalPatchesSum;
int width = (int) (extra * hRemainder / hWeightSum);
hWeightSum -= extra;
hRemainder -= width;
g.drawImage(image, x, y, x + width, y + height, r.x, r.y,
r.x + r.width, r.y + r.height, null);
x += width;
} else {
r = verticalPatches.get(verticalIndex++);
vExtra = r.height / verticalPatchesSum;
height = (int) (vExtra * vRemainder / vWeightSum);
g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y,
r.x + r.width, r.y + r.height, null);
x += r.width;
}
}
hStretch = !hStretch;
}
x = 0;
y += height;
if (vStretch) {
vWeightSum -= vExtra;
vRemainder -= height;
}
vStretch = !vStretch;
}
if (showPadding) {
g.setColor(PADDING_COLOR);
g.fillRect(horizontalPadding.first, verticalPadding.first,
scaledWidth - horizontalPadding.first - horizontalPadding.second,
scaledHeight - verticalPadding.first - verticalPadding.second);
}
g2.dispose();
}
@Override
public Dimension getPreferredSize() {
return size;
}
}
}
private class ImageViewer extends JComponent {
private final Color CORRUPTED_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.7f);
private final Color LOCK_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.7f);
private final Color STRIPES_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.5f);
private final Color BACK_COLOR = new Color(0xc0c0c0);
private final Color HELP_COLOR = new Color(0xffffe1);
private final Color PATCH_COLOR = new Color(1.0f, 0.37f, 0.99f, 0.5f);
private final Color PATCH_ONEWAY_COLOR = new Color(0.37f, 1.0f, 0.37f, 0.5f);
private static final float STRIPES_WIDTH = 4.0f;
private static final double STRIPES_SPACING = 6.0;
private static final int STRIPES_ANGLE = 45;
private int zoom = DEFAULT_ZOOM;
private boolean showPatches;
private boolean showLock = true;
private final Dimension size;
private boolean locked;
private int[] row;
private int[] column;
private int lastPositionX;
private int lastPositionY;
private int currentButton;
private boolean showCursor;
private JLabel helpLabel;
private boolean eraseMode;
private JButton checkButton;
private List<Rectangle> corruptedPatches;
private boolean showBadPatches;
private JPanel helpPanel;
ImageViewer() {
setLayout(new GridBagLayout());
helpPanel = new JPanel(new BorderLayout());
helpPanel.setBorder(new EmptyBorder(0, 6, 0, 6));
helpPanel.setBackground(HELP_COLOR);
helpLabel = new JLabel("Press Shift to erase pixels."
+ " Press Control to draw layout bounds");
helpLabel.putClientProperty("JComponent.sizeVariant", "small");
helpPanel.add(helpLabel, BorderLayout.WEST);
checkButton = new JButton("Show bad patches");
checkButton.putClientProperty("JComponent.sizeVariant", "small");
checkButton.putClientProperty("JButton.buttonType", "roundRect");
helpPanel.add(checkButton, BorderLayout.EAST);
add(helpPanel, new GridBagConstraints(0, 0, 1, 1,
1.0f, 1.0f, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL,
new Insets(0, 0, 0, 0), 0, 0));
setOpaque(true);
// Exact size will be set by setZoom() in AncestorListener#ancestorMoved.
size = new Dimension(0, 0);
addAncestorListener(new AncestorListener() {
@Override
public void ancestorRemoved(AncestorEvent event) {
}
@Override
public void ancestorMoved(AncestorEvent event) {
// Set exactly size.
viewer.setZoom(DEFAULT_ZOOM);
viewer.removeAncestorListener(this);
}
@Override
public void ancestorAdded(AncestorEvent event) {
}
});
findPatches();
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent event) {
// Store the button here instead of retrieving it again in MouseDragged
// below, because on linux, calling MouseEvent.getButton() for the drag
// event returns 0, which appears to be technically correct (no button
// changed state).
currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton();
currentButton = event.isControlDown() ? MouseEvent.BUTTON2 : currentButton;
paint(event.getX(), event.getY(), currentButton);
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent event) {
if (!checkLockedRegion(event.getX(), event.getY())) {
// use the stored button, see note above
paint(event.getX(), event.getY(), currentButton);
}
}
@Override
public void mouseMoved(MouseEvent event) {
checkLockedRegion(event.getX(), event.getY());
}
});
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
public void eventDispatched(AWTEvent event) {
enableEraseMode((KeyEvent) event);
}
}, AWTEvent.KEY_EVENT_MASK);
checkButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
if (!showBadPatches) {
findBadPatches();
checkButton.setText("Hide bad patches");
} else {
checkButton.setText("Show bad patches");
corruptedPatches = null;
}
repaint();
showBadPatches = !showBadPatches;
}
});
}
private void findBadPatches() {
corruptedPatches = new ArrayList<Rectangle>();
for (Rectangle patch : patches) {
if (corruptPatch(patch)) {
corruptedPatches.add(patch);
}
}
for (Rectangle patch : horizontalPatches) {
if (corruptHorizontalPatch(patch)) {
corruptedPatches.add(patch);
}
}
for (Rectangle patch : verticalPatches) {
if (corruptVerticalPatch(patch)) {
corruptedPatches.add(patch);
}
}
}
private boolean corruptPatch(Rectangle patch) {
int[] pixels = GraphicsUtilities.getPixels(image, patch.x, patch.y,
patch.width, patch.height, null);
if (pixels.length > 0) {
int reference = pixels[0];
for (int pixel : pixels) {
if (pixel != reference) {
return true;
}
}
}
return false;
}
private boolean corruptHorizontalPatch(Rectangle patch) {
int[] reference = new int[patch.height];
int[] column = new int[patch.height];
reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
1, patch.height, reference);
for (int i = 1; i < patch.width; i++) {
column = GraphicsUtilities.getPixels(image, patch.x + i, patch.y,
1, patch.height, column);
if (!Arrays.equals(reference, column)) {
return true;
}
}
return false;
}
private boolean corruptVerticalPatch(Rectangle patch) {
int[] reference = new int[patch.width];
int[] row = new int[patch.width];
reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
patch.width, 1, reference);
for (int i = 1; i < patch.height; i++) {
row = GraphicsUtilities.getPixels(image, patch.x, patch.y + i, patch.width, 1, row);
if (!Arrays.equals(reference, row)) {
return true;
}
}
return false;
}
private void enableEraseMode(KeyEvent event) {
boolean oldEraseMode = eraseMode;
eraseMode = event.isShiftDown();
if (eraseMode != oldEraseMode) {
if (eraseMode) {
helpLabel.setText("Release Shift to draw pixels");
} else {
helpLabel.setText("Press Shift to erase pixels."
+ " Press Control to draw layout bounds");
}
}
}
private void paint(int x, int y, int button) {
int color;
switch (button) {
case MouseEvent.BUTTON1:
color = BLACK_TICK;
break;
case MouseEvent.BUTTON2:
color = RED_TICK;
break;
case MouseEvent.BUTTON3:
color = 0;
break;
default:
return;
}
int left = (getWidth() - size.width) / 2;
int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
x = (x - left) / zoom;
y = (y - top) / zoom;
int width = image.getWidth();
int height = image.getHeight();
if (((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
((x > 0 && x < width - 1) && (y == 0 || y == height - 1))) {
image.setRGB(x, y, color);
findPatches();
stretchesViewer.computePatches();
if (showBadPatches) {
findBadPatches();
}
repaint();
}
}
private boolean checkLockedRegion(int x, int y) {
int oldX = lastPositionX;
int oldY = lastPositionY;
lastPositionX = x;
lastPositionY = y;
int left = (getWidth() - size.width) / 2;
int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
x = (x - left) / zoom;
y = (y - top) / zoom;
int width = image.getWidth();
int height = image.getHeight();
xLabel.setText(Math.max(0, Math.min(x, width - 1)) + " px");
yLabel.setText(Math.max(0, Math.min(y, height - 1)) + " px");
boolean previousLock = locked;
locked = x > 0 && x < width - 1 && y > 0 && y < height - 1;
boolean previousCursor = showCursor;
showCursor = ((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
((x > 0 && x < width - 1) && (y == 0 || y == height - 1));
if (locked != previousLock) {
repaint();
} else if (showCursor || (showCursor != previousCursor)) {
Rectangle clip = new Rectangle(lastPositionX - 1 - zoom / 2,
lastPositionY - 1 - zoom / 2, zoom + 2, zoom + 2);
clip = clip.union(new Rectangle(oldX - 1 - zoom / 2,
oldY - 1 - zoom / 2, zoom + 2, zoom + 2));
repaint(clip);
}
return locked;
}
@Override
protected void paintComponent(Graphics g) {
int x = (getWidth() - size.width) / 2;
int y = helpPanel.getHeight() + (getHeight() - size.height) / 2;
Graphics2D g2 = (Graphics2D) g.create();
g2.setColor(BACK_COLOR);
g2.fillRect(0, 0, getWidth(), getHeight());
g2.translate(x, y);
g2.setPaint(texture);
g2.fillRect(0, 0, size.width, size.height);
g2.scale(zoom, zoom);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g2.drawImage(image, 0, 0, null);
if (showPatches) {
g2.setColor(PATCH_COLOR);
for (Rectangle patch : patches) {
g2.fillRect(patch.x, patch.y, patch.width, patch.height);
}
g2.setColor(PATCH_ONEWAY_COLOR);
for (Rectangle patch : horizontalPatches) {
g2.fillRect(patch.x, patch.y, patch.width, patch.height);
}
for (Rectangle patch : verticalPatches) {
g2.fillRect(patch.x, patch.y, patch.width, patch.height);
}
}
if (corruptedPatches != null) {
g2.setColor(CORRUPTED_COLOR);
g2.setStroke(new BasicStroke(3.0f / zoom));
for (Rectangle patch : corruptedPatches) {
g2.draw(new RoundRectangle2D.Float(patch.x - 2.0f / zoom, patch.y - 2.0f / zoom,
patch.width + 2.0f / zoom, patch.height + 2.0f / zoom,
6.0f / zoom, 6.0f / zoom));
}
}
if (showLock && locked) {
int width = image.getWidth();
int height = image.getHeight();
g2.setColor(LOCK_COLOR);
g2.fillRect(1, 1, width - 2, height - 2);
g2.setColor(STRIPES_COLOR);
g2.translate(1, 1);
paintStripes(g2, width - 2, height - 2);
g2.translate(-1, -1);
}
g2.dispose();
if (showCursor) {
Graphics cursor = g.create();
cursor.setXORMode(Color.WHITE);
cursor.setColor(Color.BLACK);
cursor.drawRect(lastPositionX - zoom / 2, lastPositionY - zoom / 2, zoom, zoom);
cursor.dispose();
}
}
private void paintStripes(Graphics2D g, int width, int height) {
//draws pinstripes at the angle specified in this class
//and at the given distance apart
Shape oldClip = g.getClip();
Area area = new Area(new Rectangle(0, 0, width, height));
if(oldClip != null) {
area = new Area(oldClip);
}
area.intersect(new Area(new Rectangle(0,0,width,height)));
g.setClip(area);
g.setStroke(new BasicStroke(STRIPES_WIDTH));
double hypLength = Math.sqrt((width * width) +
(height * height));
double radians = Math.toRadians(STRIPES_ANGLE);
g.rotate(radians);
double spacing = STRIPES_SPACING;
spacing += STRIPES_WIDTH;
int numLines = (int)(hypLength / spacing);
for (int i=0; i<numLines; i++) {
double x = i * spacing;
Line2D line = new Line2D.Double(x, -hypLength, x, hypLength);
g.draw(line);
}
g.setClip(oldClip);
}
@Override
public Dimension getPreferredSize() {
return size;
}
void setZoom(int value) {
int width = image.getWidth();
int height = image.getHeight();
zoom = value;
if (size.height == 0 || (getHeight() - size.height) == 0) {
size.setSize(width * zoom, height * zoom + helpPanel.getHeight());
} else {
size.setSize(width * zoom, height * zoom);
}
if (!size.equals(getSize())) {
setSize(size);
ImageEditorPanel.this.validate();
repaint();
}
}
void setPatchesVisible(boolean visible) {
showPatches = visible;
findPatches();
repaint();
}
private void findPatches() {
int width = image.getWidth();
int height = image.getHeight();
row = GraphicsUtilities.getPixels(image, 0, 0, width, 1, row);
column = GraphicsUtilities.getPixels(image, 0, 0, 1, height, column);
boolean[] result = new boolean[1];
Pair<List<Pair<Integer>>> left = getPatches(column, result);
verticalStartWithPatch = result[0];
result = new boolean[1];
Pair<List<Pair<Integer>>> top = getPatches(row, result);
horizontalStartWithPatch = result[0];
fixed = getRectangles(left.first, top.first);
patches = getRectangles(left.second, top.second);
if (fixed.size() > 0) {
horizontalPatches = getRectangles(left.first, top.second);
verticalPatches = getRectangles(left.second, top.first);
} else {
if (top.first.size() > 0) {
horizontalPatches = new ArrayList<Rectangle>(0);
verticalPatches = getVerticalRectangles(top.first);
} else if (left.first.size() > 0) {
horizontalPatches = getHorizontalRectangles(left.first);
verticalPatches = new ArrayList<Rectangle>(0);
} else {
horizontalPatches = verticalPatches = new ArrayList<Rectangle>(0);
}
}
row = GraphicsUtilities.getPixels(image, 0, height - 1, width, 1, row);
column = GraphicsUtilities.getPixels(image, width - 1, 0, 1, height, column);
top = getPatches(row, result);
horizontalPadding = getPadding(top.first);
left = getPatches(column, result);
verticalPadding = getPadding(left.first);
}
private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) {
List<Rectangle> rectangles = new ArrayList<Rectangle>();
for (Pair<Integer> top : topPairs) {
int x = top.first;
int width = top.second - top.first;
rectangles.add(new Rectangle(x, 1, width, image.getHeight() - 2));
}
return rectangles;
}
private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) {
List<Rectangle> rectangles = new ArrayList<Rectangle>();
for (Pair<Integer> left : leftPairs) {
int y = left.first;
int height = left.second - left.first;
rectangles.add(new Rectangle(1, y, image.getWidth() - 2, height));
}
return rectangles;
}
private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
if (pairs.size() == 0) {
return new Pair<Integer>(0, 0);
} else if (pairs.size() == 1) {
if (pairs.get(0).first == 1) {
return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first, 0);
} else {
return new Pair<Integer>(0, pairs.get(0).second - pairs.get(0).first);
}
} else {
int index = pairs.size() - 1;
return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first,
pairs.get(index).second - pairs.get(index).first);
}
}
private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
List<Pair<Integer>> topPairs) {
List<Rectangle> rectangles = new ArrayList<Rectangle>();
for (Pair<Integer> left : leftPairs) {
int y = left.first;
int height = left.second - left.first;
for (Pair<Integer> top : topPairs) {
int x = top.first;
int width = top.second - top.first;
rectangles.add(new Rectangle(x, y, width, height));
}
}
return rectangles;
}
private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) {
int lastIndex = 1;
int lastPixel = pixels[1];
boolean first = true;
List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();
for (int i = 1; i < pixels.length - 1; i++) {
int pixel = pixels[i];
if (pixel != lastPixel) {
if (lastPixel == BLACK_TICK) {
if (first) startWithPatch[0] = true;
patches.add(new Pair<Integer>(lastIndex, i));
} else {
fixed.add(new Pair<Integer>(lastIndex, i));
}
first = false;
lastIndex = i;
lastPixel = pixel;
}
}
if (lastPixel == BLACK_TICK) {
if (first) startWithPatch[0] = true;
patches.add(new Pair<Integer>(lastIndex, pixels.length - 1));
} else {
fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1));
}
if (patches.size() == 0) {
patches.add(new Pair<Integer>(1, pixels.length - 1));
startWithPatch[0] = true;
fixed.clear();
}
return new Pair<List<Pair<Integer>>>(fixed, patches);
}
void setLockVisible(boolean visible) {
showLock = visible;
repaint();
}
}
static class Pair<E> {
E first;
E second;
Pair(E first, E second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "Pair[" + first + ", " + second + "]";
}
}
}