blob: fca896f2c3b84ae4a2613800b09615734170117d [file] [log] [blame]
/*
*
* Copyright (C) 2013 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 java.awt.AWTEvent;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
public 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 final Color HIGHLIGHT_REGION_COLOR = new Color(0.5f, 0.5f, 0.5f, 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;
/** Default zoom level for the 9patch image. */
public static final int DEFAULT_ZOOM = 8;
/** Minimum zoom level for the 9patch image. */
public static final int MIN_ZOOM = 1;
/** Maximum zoom level for the 9patch image. */
public static final int MAX_ZOOM = 16;
/** Current 9patch zoom level, {@link #MIN_ZOOM} <= zoom <= {@link #MAX_ZOOM} */
private int zoom = DEFAULT_ZOOM;
private boolean showPatches;
private boolean showLock = false;
private final TexturePaint texture;
private final Container container;
private final StatusBar statusBar;
private final Dimension size;
private boolean locked;
private int lastPositionX;
private int lastPositionY;
private boolean showCursor;
private JLabel helpLabel;
private boolean eraseMode;
private JButton checkButton;
private List<Rectangle> corruptedPatches;
private boolean showBadPatches;
private JPanel helpPanel;
private boolean drawingLine;
private int lineFromX;
private int lineFromY;
private int lineToX;
private int lineToY;
private boolean showDrawingLine;
private final List<Rectangle> hoverHighlightRegions = new ArrayList<Rectangle>();
private String toolTipText;
/**
* Indicates whether we are currently in edit mode.
* All fields with the prefix 'edit' are valid only when in edit mode.
*/
private boolean isEditMode;
/** Region being edited. */
private UpdateRegion editRegion;
/**
* The start and end points corresponding to the region being edited.
* During an edit sequence, the start point is constant and the end varies based on the
* mouse location.
*/
private final Pair<Integer> editSegment = new Pair<Integer>(0, 0);
/** Regions to highlight based on the current edit. */
private final List<Rectangle> editHighlightRegions = new ArrayList<Rectangle>();
/** The actual patch location in the image being edited. */
private Rectangle editPatchRegion = new Rectangle();
private BufferedImage image;
private PatchInfo patchInfo;
/** The types of edit actions that can be performed on the image. */
private enum DrawMode {
PATCH, // drawing a patch or a padding
LAYOUT_BOUND, // drawing layout bounds
ERASE, // erasing whatever has been drawn
}
/**
* Current drawing mode. The mode is changed by using either the Shift or Ctrl keys while
* drawing.
*/
private DrawMode currentMode = DrawMode.PATCH;
ImageViewer(Container container, TexturePaint texture, BufferedImage image,
StatusBar statusBar) {
this.container = container;
this.texture = texture;
this.image = image;
this.statusBar = statusBar;
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.
setZoom(DEFAULT_ZOOM);
removeAncestorListener(this);
}
@Override
public void ancestorAdded(AncestorEvent event) {
}
});
updatePatchInfo();
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent event) {
// Update the drawing mode looking at the current state of modifier (shift/ctrl)
// keys. This is done 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).
updateDrawMode(event);
int x = imageXCoordinate(event.getX());
int y = imageYCoordinate(event.getY());
startDrawingLine(x, y);
if (currentMode == DrawMode.PATCH) {
startEditingRegion(x, y);
} else {
hoverHighlightRegions.clear();
setCursor(Cursor.getDefaultCursor());
repaint();
}
}
@Override
public void mouseReleased(MouseEvent event) {
int x = imageXCoordinate(event.getX());
int y = imageYCoordinate(event.getY());
endDrawingLine();
endEditingRegion(x, y);
resetDrawMode();
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent event) {
int x = imageXCoordinate(event.getX());
int y = imageYCoordinate(event.getY());
if (!checkLockedRegion(x, y)) {
// use the stored button, see note above
moveLine(x, y);
}
updateEditRegion(x, y);
}
@Override
public void mouseMoved(MouseEvent event) {
int x = imageXCoordinate(event.getX());
int y = imageYCoordinate(event.getY());
checkLockedRegion(x, y);
updateHoverRegion(x, y);
repaint();
}
});
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
hoverHighlightRegions.clear();
updateSize();
repaint();
}
});
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) {
corruptedPatches = CorruptPatch.findBadPatches(ImageViewer.this.image,
patchInfo);
checkButton.setText("Hide bad patches");
} else {
checkButton.setText("Show bad patches");
corruptedPatches = null;
}
repaint();
showBadPatches = !showBadPatches;
}
});
}
private void updateDrawMode(MouseEvent event) {
if (event.isShiftDown()) {
currentMode = DrawMode.ERASE;
} else if (event.isControlDown()) {
currentMode = DrawMode.LAYOUT_BOUND;
} else {
currentMode = DrawMode.PATCH;
}
}
private void resetDrawMode() {
currentMode = DrawMode.PATCH;
}
private enum UpdateRegion {
LEFT_PATCH,
TOP_PATCH,
RIGHT_PADDING,
BOTTOM_PADDING,
}
private static class UpdateRegionInfo {
public final UpdateRegion region;
public final Pair<Integer> segment;
private UpdateRegionInfo(UpdateRegion region, Pair<Integer> segment) {
this.region = region;
this.segment = segment;
}
}
private UpdateRegionInfo findVerticalPatch(int x, int y) {
List<Pair<Integer>> markers;
UpdateRegion region;
// Given the mouse x location, we need to determine if we need to map this edit to
// the patch info at the left, or the padding info at the right. We make this decision
// based on whichever is closer, so if the mouse x is in the left half of the image,
// we are editing the left patch, else the right padding.
if (x < image.getWidth() / 2) {
markers = patchInfo.verticalPatchMarkers;
region = UpdateRegion.LEFT_PATCH;
} else {
markers = patchInfo.verticalPaddingMarkers;
region = UpdateRegion.RIGHT_PADDING;
}
return getContainingPatch(markers, y, region);
}
private UpdateRegionInfo findHorizontalPatch(int x, int y) {
List<Pair<Integer>> markers;
UpdateRegion region;
if (y < image.getHeight() / 2) {
markers = patchInfo.horizontalPatchMarkers;
region = UpdateRegion.TOP_PATCH;
} else {
markers = patchInfo.horizontalPaddingMarkers;
region = UpdateRegion.BOTTOM_PADDING;
}
return getContainingPatch(markers, x, region);
}
private UpdateRegionInfo getContainingPatch(List<Pair<Integer>> patches, int a,
UpdateRegion region) {
for (Pair<Integer> p: patches) {
if (p.first <= a && p.second > a) {
return new UpdateRegionInfo(region, p);
}
if (p.first > a) {
break;
}
}
return new UpdateRegionInfo(region, null);
}
private void updateHoverRegion(int x, int y) {
// find regions to highlight based on the horizontal and vertical patches that
// cover this (x, y)
UpdateRegionInfo vertical = findVerticalPatch(x, y);
UpdateRegionInfo horizontal = findHorizontalPatch(x, y);
computeHoverHighlightRegions(vertical, horizontal);
computeHoverRegionTooltip(vertical, horizontal);
// change cursor if (x,y) is at the edge of either of the regions
UpdateRegionInfo updateRegion = pickUpdateRegion(x, y, vertical, horizontal);
setCursorForRegion(x, y, updateRegion);
}
private void startEditingRegion(int x, int y) {
hoverHighlightRegions.clear();
isEditMode = false;
editRegion = null;
UpdateRegionInfo vertical = findVerticalPatch(x, y);
UpdateRegionInfo horizontal = findHorizontalPatch(x, y);
UpdateRegionInfo updateRegion = pickUpdateRegion(x, y, vertical, horizontal);
setCursorForRegion(x, y, updateRegion);
if (updateRegion != null) { // edit an existing patch
editRegion = updateRegion.region;
isEditMode = true;
Edge e = null;
switch (this.editRegion) {
case LEFT_PATCH:
case RIGHT_PADDING:
e = getClosestEdge(y, updateRegion.segment);
break;
case TOP_PATCH:
case BOTTOM_PADDING:
e = getClosestEdge(x, updateRegion.segment);
break;
default:
assert false : this.editRegion;
}
int first = updateRegion.segment.first;
int second = updateRegion.segment.second;
// The edge being edited should always be the end point in editSegment.
boolean start = e == Edge.START;
editSegment.first = start ? second : first;
editSegment.second = start ? first : second;
// clear the current patch data
flushEditPatchData(0);
} else if ((editRegion = findNewPatchRegion(x, y)) != null) { // create a new patch
isEditMode = true;
boolean verticalPatch = editRegion == UpdateRegion.LEFT_PATCH
|| editRegion == UpdateRegion.RIGHT_PADDING;
x = clamp(x, 1, image.getWidth() - 1);
y = clamp(y, 1, image.getHeight() - 1);
editSegment.first = editSegment.second = verticalPatch ? y : x;
}
if (isEditMode) {
computeEditHighlightRegions();
}
repaint();
}
private void endEditingRegion(int x, int y) {
if (!isEditMode) {
return;
}
x = clamp(x, 1, image.getWidth() - 1);
y = clamp(y, 1, image.getHeight() - 1);
switch (editRegion) {
case LEFT_PATCH:
case RIGHT_PADDING:
editSegment.second = y;
break;
case TOP_PATCH:
case BOTTOM_PADDING:
editSegment.second = x;
break;
default:
assert false : editRegion;
}
flushEditPatchData(PatchInfo.BLACK_TICK);
hoverHighlightRegions.clear();
setCursor(Cursor.getDefaultCursor());
patchesChanged();
repaint();
isEditMode = false;
editRegion = null;
}
private void updateEditRegion(int x, int y) {
if (!isEditMode) {
return;
}
x = clamp(x, 1, image.getWidth() - 1);
y = clamp(y, 1, image.getHeight() - 1);
switch (editRegion) {
case LEFT_PATCH:
case RIGHT_PADDING:
editSegment.second = y;
break;
case TOP_PATCH:
case BOTTOM_PADDING:
editSegment.second = x;
break;
}
computeEditHighlightRegions();
repaint();
}
private int clamp(int i, int min, int max) {
if (i < min) {
return min;
}
if (i > max) {
return max;
}
return i;
}
/** Returns the type of patch that should be created given the initial mouse location. */
private UpdateRegion findNewPatchRegion(int x, int y) {
boolean verticalPatch = y >= 0 && y <= image.getHeight();
boolean horizontalPatch = x >= 0 && x <= image.getWidth();
// Heuristic: If the pointer is within the vertical bounds of the image,
// then we create a patch on the left or right depending on which side of the image
// the pointer is on
if (verticalPatch) {
if (x < 0) {
return UpdateRegion.LEFT_PATCH;
} else if (x > image.getWidth()) {
return UpdateRegion.RIGHT_PADDING;
}
}
// Similarly, if it is within the horizontal bounds of the image,
// then create a patch at the top or bottom depending on its location relative to the image
if (horizontalPatch) {
if (y < 0) {
return UpdateRegion.TOP_PATCH;
} else if (y > image.getHeight()) {
return UpdateRegion.BOTTOM_PADDING;
}
}
return null;
}
private void computeHoverHighlightRegions(UpdateRegionInfo vertical,
UpdateRegionInfo horizontal) {
hoverHighlightRegions.clear();
if (vertical != null && vertical.segment != null) {
hoverHighlightRegions.addAll(
getHorizontalHighlightRegions(0,
vertical.segment.first,
image.getWidth(),
vertical.segment.second - vertical.segment.first));
}
if (horizontal != null && horizontal.segment != null) {
hoverHighlightRegions.addAll(
getVerticalHighlightRegions(horizontal.segment.first,
0,
horizontal.segment.second - horizontal.segment.first,
image.getHeight()));
}
}
private void computeHoverRegionTooltip(UpdateRegionInfo vertical, UpdateRegionInfo horizontal) {
StringBuilder sb = new StringBuilder(50);
if (vertical != null && vertical.segment != null) {
if (vertical.region == UpdateRegion.LEFT_PATCH) {
sb.append("Vertical Patch: ");
} else {
sb.append("Vertical Padding: ");
}
sb.append(String.format("%d - %d px",
vertical.segment.first, vertical.segment.second));
}
if (horizontal != null && horizontal.segment != null) {
if (sb.length() > 0) {
sb.append(", ");
}
if (horizontal.region == UpdateRegion.TOP_PATCH) {
sb.append("Horizontal Patch: ");
} else {
sb.append("Horizontal Padding: ");
}
sb.append(String.format("%d - %d px",
horizontal.segment.first, horizontal.segment.second));
}
toolTipText = sb.length() > 0 ? sb.toString() : null;
}
private void computeEditHighlightRegions() {
editHighlightRegions.clear();
int f = editSegment.first;
int s = editSegment.second;
int min = Math.min(f, s);
int diff = Math.abs(f - s);
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
switch (editRegion) {
case LEFT_PATCH:
editPatchRegion = displayCoordinates(new Rectangle(0, min, 1, diff));
editHighlightRegions.addAll(
getHorizontalHighlightRegions(0, min, imageWidth, diff));
break;
case RIGHT_PADDING:
editPatchRegion = displayCoordinates(new Rectangle(imageWidth - 1, min, 1, diff));
editHighlightRegions.addAll(
getHorizontalHighlightRegions(0, min, imageWidth, diff));
break;
case TOP_PATCH:
editPatchRegion = displayCoordinates(new Rectangle(min, 0, diff, 1));
editHighlightRegions.addAll(
getVerticalHighlightRegions(min, 0, diff, imageHeight));
break;
case BOTTOM_PADDING:
editPatchRegion = displayCoordinates(new Rectangle(min, imageHeight - 1, diff, 1));
editHighlightRegions.addAll(
getVerticalHighlightRegions(min, 0, diff, imageHeight));
default:
assert false : editRegion;
}
}
private List<Rectangle> getHorizontalHighlightRegions(int x, int y, int w, int h) {
List<Rectangle> l = new ArrayList<Rectangle>(3);
// highlight the region within the image
Rectangle r = displayCoordinates(new Rectangle(x, y, w, h));
l.add(r);
// add a 1 pixel line at the top and bottom that extends outside the image
l.add(new Rectangle(0, r.y, getWidth(), 1));
l.add(new Rectangle(0, r.y + r.height, getWidth(), 1));
return l;
}
private List<Rectangle> getVerticalHighlightRegions(int x, int y, int w, int h) {
List<Rectangle> l = new ArrayList<Rectangle>(3);
// highlight the region within the image
Rectangle r = displayCoordinates(new Rectangle(x, y, w, h));
l.add(r);
// add a 1 pixel line at the top and bottom that extends outside the image
l.add(new Rectangle(r.x, 0, 1, getHeight()));
l.add(new Rectangle(r.x + r.width, 0, 1, getHeight()));
return l;
}
private void setCursorForRegion(int x, int y, UpdateRegionInfo region) {
if (region != null) {
Cursor c = getCursor(x, y, region);
setCursor(c);
} else {
setCursor(Cursor.getDefaultCursor());
}
}
private Cursor getCursor(int x, int y, UpdateRegionInfo editRegion) {
Edge e;
int cursor = Cursor.DEFAULT_CURSOR;
switch (editRegion.region) {
case LEFT_PATCH:
case RIGHT_PADDING:
e = getClosestEdge(y, editRegion.segment);
cursor = (e == Edge.START) ? Cursor.N_RESIZE_CURSOR : Cursor.S_RESIZE_CURSOR;
break;
case TOP_PATCH:
case BOTTOM_PADDING:
e = getClosestEdge(x, editRegion.segment);
cursor = (e == Edge.START) ? Cursor.W_RESIZE_CURSOR : Cursor.E_RESIZE_CURSOR;
break;
default:
assert false : this.editRegion;
}
return Cursor.getPredefinedCursor(cursor);
}
/**
* Returns whether the horizontal or the vertical region should be updated based on the
* mouse pointer's location relative to the edges of either region. If no edge is close to
* the mouse pointer, then it returns null.
*/
private UpdateRegionInfo pickUpdateRegion(int x, int y, UpdateRegionInfo vertical,
UpdateRegionInfo horizontal) {
if (vertical != null && vertical.segment != null) {
Edge e = getClosestEdge(y, vertical.segment);
if (e != Edge.NONE) {
return vertical;
}
}
if (horizontal != null && horizontal.segment != null) {
Edge e = getClosestEdge(x, horizontal.segment);
if (e != Edge.NONE) {
return horizontal;
}
}
return null;
}
private enum Edge {
START,
END,
NONE,
}
private static final int EDGE_DELTA = 1;
private Edge getClosestEdge(int x, Pair<Integer> range) {
if (Math.abs(x - range.first) <= EDGE_DELTA) {
return Edge.START;
} else if (Math.abs(range.second - x) <= EDGE_DELTA) {
return Edge.END;
} else {
return Edge.NONE;
}
}
private int imageYCoordinate(int y) {
int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
return (y - top) / zoom;
}
private int imageXCoordinate(int x) {
int left = (getWidth() - size.width) / 2;
return (x - left) / zoom;
}
private Point getImageOrigin() {
int left = (getWidth() - size.width) / 2;
int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
return new Point(left, top);
}
private Rectangle displayCoordinates(Rectangle r) {
Point imageOrigin = getImageOrigin();
int x = r.x * zoom + imageOrigin.x;
int y = r.y * zoom + imageOrigin.y;
int w = r.width * zoom;
int h = r.height * zoom;
return new Rectangle(x, y, w, h);
}
private void updatePatchInfo() {
patchInfo = new PatchInfo(image);
}
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 startDrawingLine(int x, int y) {
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))) {
drawingLine = true;
lineFromX = x;
lineFromY = y;
lineToX = x;
lineToY = y;
showDrawingLine = true;
showCursor = false;
repaint();
}
}
private void moveLine(int x, int y) {
if (!drawingLine) {
return;
}
int width = image.getWidth();
int height = image.getHeight();
showDrawingLine = false;
if (((x == lineFromX) && (y > 0 && y < height - 1))
|| ((x > 0 && x < width - 1) && (y == lineFromY))) {
lineToX = x;
lineToY = y;
showDrawingLine = true;
}
repaint();
}
private void endDrawingLine() {
if (!drawingLine) {
return;
}
drawingLine = false;
if (!showDrawingLine) {
return;
}
int color;
switch (currentMode) {
case PATCH:
color = PatchInfo.BLACK_TICK;
break;
case LAYOUT_BOUND:
color = PatchInfo.RED_TICK;
break;
case ERASE:
color = 0;
break;
default:
return;
}
setPatchData(color, lineFromX, lineFromY, lineToX, lineToY, true);
patchesChanged();
repaint();
}
/**
* Set the color of pixels on the line from (x1, y1) to (x2, y2) to given color.
* @param inclusive indicates whether the range is inclusive. If true, the last pixel (x2, y2)
* will be set to the given color as well.
*/
private void setPatchData(int color, int x1, int y1, int x2, int y2, boolean inclusive) {
int x = x1;
int y = y1;
int dx = 0;
int dy = 0;
if (x2 != x1) {
dx = x2 > x1 ? 1 : -1;
} else if (y2 != y1) {
dy = y2 > y1 ? 1 : -1;
}
while (x != x2 || y != y2) {
image.setRGB(x, y, color);
x += dx;
y += dy;
}
if (inclusive) {
image.setRGB(x, y, color);
}
}
/** Flushes current edit data to the image. */
private void flushEditPatchData(int color) {
int x1, y1, x2, y2;
x1 = x2 = y1 = y2 = 0;
int min = Math.min(editSegment.first, editSegment.second);
int max = Math.max(editSegment.first, editSegment.second);
switch (editRegion) {
case LEFT_PATCH:
x1 = x2 = 0;
y1 = min;
y2 = max;
break;
case RIGHT_PADDING:
x1 = x2 = image.getWidth() - 1;
y1 = min;
y2 = max;
break;
case TOP_PATCH:
x1 = min;
x2 = max;
y1 = y2 = 0;
break;
case BOTTOM_PADDING:
x1 = min;
x2 = max;
y1 = y2 = image.getHeight() - 1;
break;
default:
assert false : editRegion;
}
setPatchData(color, x1, y1, x2, y2, false);
}
private void patchesChanged() {
updatePatchInfo();
notifyPatchesUpdated();
if (showBadPatches) {
corruptedPatches = CorruptPatch.findBadPatches(image, patchInfo);
}
}
private boolean checkLockedRegion(int x, int y) {
int oldX = lastPositionX;
int oldY = lastPositionY;
lastPositionX = x;
lastPositionY = y;
int width = image.getWidth();
int height = image.getHeight();
statusBar.setPointerLocation(Math.max(0, Math.min(x, width - 1)),
Math.max(0, Math.min(y, height - 1)));
boolean previousLock = locked;
locked = x > 0 && x < width - 1 && y > 0 && y < height - 1;
boolean previousCursor = showCursor;
showCursor =
!drawingLine &&
( ((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 : patchInfo.patches) {
g2.fillRect(patch.x, patch.y, patch.width, patch.height);
}
g2.setColor(PATCH_ONEWAY_COLOR);
for (Rectangle patch : patchInfo.horizontalPatches) {
g2.fillRect(patch.x, patch.y, patch.width, patch.height);
}
for (Rectangle patch : patchInfo.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 (drawingLine && showDrawingLine) {
Graphics cursor = g.create();
cursor.setXORMode(Color.WHITE);
cursor.setColor(Color.BLACK);
x = Math.min(lineFromX, lineToX);
y = Math.min(lineFromY, lineToY);
int w = Math.abs(lineFromX - lineToX) + 1;
int h = Math.abs(lineFromY - lineToY) + 1;
x = x * zoom;
y = y * zoom;
w = w * zoom;
h = h * zoom;
int left = (getWidth() - size.width) / 2;
int top = helpPanel.getHeight() + (getHeight() - size.height)
/ 2;
x += left;
y += top;
cursor.drawRect(x, y, w, h);
cursor.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();
}
g2 = (Graphics2D) g.create();
g2.setColor(HIGHLIGHT_REGION_COLOR);
for (Rectangle r: hoverHighlightRegions) {
g2.fillRect(r.x, r.y, r.width, r.height);
}
if (!hoverHighlightRegions.isEmpty()) {
setToolTipText(toolTipText);
} else {
setToolTipText(null);
}
if (isEditMode && editRegion != null) {
g2.setColor(HIGHLIGHT_REGION_COLOR);
for (Rectangle r: editHighlightRegions) {
g2.fillRect(r.x, r.y, r.width, r.height);
}
g2.setColor(Color.BLACK);
g2.fillRect(editPatchRegion.x, editPatchRegion.y,
editPatchRegion.width, editPatchRegion.height);
}
g2.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) {
zoom = value;
updateSize();
if (!size.equals(getSize())) {
setSize(size);
container.validate();
repaint();
}
}
private void updateSize() {
int width = image.getWidth();
int height = image.getHeight();
if (size.height == 0 || (getHeight() - size.height) == 0) {
size.setSize(width * zoom, height * zoom + helpPanel.getHeight());
} else {
size.setSize(width * zoom, height * zoom);
}
}
void setPatchesVisible(boolean visible) {
showPatches = visible;
updatePatchInfo();
repaint();
}
void setLockVisible(boolean visible) {
showLock = visible;
repaint();
}
public void setImage(BufferedImage image) {
this.image = image;
}
public BufferedImage getImage() {
return image;
}
public PatchInfo getPatchInfo() {
return patchInfo;
}
public interface StatusBar {
void setPointerLocation(int x, int y);
}
public interface PatchUpdateListener {
void patchesUpdated();
}
private final Set<PatchUpdateListener> listeners = new HashSet<PatchUpdateListener>();
public void addPatchUpdateListener(PatchUpdateListener p) {
listeners.add(p);
}
private void notifyPatchesUpdated() {
for (PatchUpdateListener p: listeners) {
p.patchesUpdated();
}
}
}