/*
* Copyright (c) 2005-2016 Substance Kirill Grouchnikov. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of Substance Kirill Grouchnikov nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package tools.electra;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.util.*;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.*;
import org.pushingpixels.lafwidget.utils.RenderingUtils;
import org.pushingpixels.substance.api.SubstanceLookAndFeel;
import tools.common.JImageComponent;
public class JElectrifiedImageComponent extends JComponent {
private JImageComponent originalImageComponent;
private BufferedImage originalImage;
private static final int WIDTH = 720;
private BufferedImage electrifiedImage;
private float scaleFactor;
private int imageOffsetX;
private int imageOffsetY;
private List<ZoomBubble> zoomBubbles = new ArrayList<ZoomBubble>();
private JTextField captionEditor;
private MouseDragHandler mouseDragHandler;
private static final float RIM_THICKNESS = 3.0f;
private static final float OUTER_SHADOW_THICKNESS = 8.0f;
private static final float INNER_SHADOW_THICKNESS = 3.0f;
private static final int TEXT_OUTER_SHADOW_THICKNESS = 6;
private static class ZoomBubble {
double centerX;
double centerY;
double radius;
boolean isSelected;
boolean isInTextEdit;
String caption;
double captionOffsetX;
double captionOffsetY;
Rectangle captionRectangle;
boolean isInverted;
}
private static interface MouseDragHandler {
void onStart(Point point);
void onDrag(Point point);
void onEnd(Point point);
}
private class ZoomBubbleDragHandler implements MouseDragHandler {
private ZoomBubble zoomBubble;
private Point lastDragPoint;
public ZoomBubbleDragHandler(ZoomBubble zoomBubble) {
this.zoomBubble = zoomBubble;
}
@Override
public void onStart(Point point) {
this.lastDragPoint = point;
}
@Override
public void onDrag(Point point) {
double dx = ((point.x - lastDragPoint.x) / scaleFactor);
double dy = ((point.y - lastDragPoint.y) / scaleFactor);
zoomBubble.centerX += dx;
zoomBubble.centerY += dy;
lastDragPoint = point;
repaint();
}
@Override
public void onEnd(Point point) {
}
}
private class ZoomBubbleResizeHandler implements MouseDragHandler {
private ZoomBubble zoomBubble;
private Point lastDragPoint;
public ZoomBubbleResizeHandler(ZoomBubble zoomBubble) {
this.zoomBubble = zoomBubble;
}
@Override
public void onStart(Point point) {
this.lastDragPoint = point;
}
@Override
public void onDrag(Point point) {
Point2D bubbleCenterView = originalToView(new Point2D.Double(
zoomBubble.centerX, zoomBubble.centerY));
double ndx = point.x - bubbleCenterView.getX();
double ndy = point.y - bubbleCenterView.getY();
double newRadius = Math.sqrt(ndx * ndx + ndy * ndy);
double odx = lastDragPoint.x - bubbleCenterView.getX();
double ody = lastDragPoint.y - bubbleCenterView.getY();
double oldRadius = Math.sqrt(odx * odx + ody * ody);
zoomBubble.radius += (newRadius - oldRadius);
lastDragPoint = point;
repaint();
}
@Override
public void onEnd(Point point) {
}
}
private class ZoomBubbleCaptionDragHandler implements MouseDragHandler {
private ZoomBubble zoomBubble;
private Point lastDragPoint;
public ZoomBubbleCaptionDragHandler(ZoomBubble zoomBubble) {
this.zoomBubble = zoomBubble;
}
@Override
public void onStart(Point point) {
this.lastDragPoint = point;
}
@Override
public void onDrag(Point point) {
int dx = point.x - lastDragPoint.x;
int dy = point.y - lastDragPoint.y;
zoomBubble.captionOffsetX += dx;
zoomBubble.captionOffsetY += dy;
lastDragPoint = point;
repaint();
}
@Override
public void onEnd(Point point) {
}
}
private enum DragType {
BUBBLE_DRAG, BUBBLE_RESIZE, CAPTION_DRAG;
}
private class BubbleDragPair {
ZoomBubble zoomBubble;
DragType dragType;
public BubbleDragPair(ZoomBubble zoomBubble, DragType dragType) {
this.zoomBubble = zoomBubble;
this.dragType = dragType;
}
}
public JElectrifiedImageComponent(JImageComponent originalImageComponent) {
this.originalImageComponent = originalImageComponent;
this.originalImageComponent
.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("image".equals(evt.getPropertyName())) {
originalImage = (BufferedImage) evt.getNewValue();
reset();
}
if ("file".equals(evt.getPropertyName())) {
File file = (File) evt.getNewValue();
File layers = new File(file.getParent(), file
.getName()
+ ".layers");
if (layers.exists()) {
loadBubbles(layers);
}
}
}
});
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (!e.isPopupTrigger()) {
// stop editing if any
stopCaptionEdit(true);
for (ZoomBubble zoomBubble : zoomBubbles) {
zoomBubble.isSelected = false;
}
BubbleDragPair pressed = getInfoForDrag(e.getPoint());
if (pressed == null) {
repaint();
return;
}
pressed.zoomBubble.isSelected = true;
switch (pressed.dragType) {
case BUBBLE_DRAG:
mouseDragHandler = new ZoomBubbleDragHandler(
pressed.zoomBubble);
break;
case BUBBLE_RESIZE:
mouseDragHandler = new ZoomBubbleResizeHandler(
pressed.zoomBubble);
break;
case CAPTION_DRAG:
mouseDragHandler = new ZoomBubbleCaptionDragHandler(
pressed.zoomBubble);
break;
}
mouseDragHandler.onStart(e.getPoint());
repaint();
} else {
final BubbleDragPair pressed = getInfoForDrag(e.getPoint());
if (pressed == null) {
return;
}
JPopupMenu popupMenu = new JPopupMenu();
popupMenu.add(new AbstractAction("remove") {
@Override
public void actionPerformed(ActionEvent e) {
zoomBubbles.remove(pressed.zoomBubble);
repaint();
}
});
popupMenu.add(new AbstractAction("invert caption colors") {
@Override
public void actionPerformed(ActionEvent e) {
pressed.zoomBubble.isInverted = !pressed.zoomBubble.isInverted;
repaint();
}
});
Point pt = SwingUtilities.convertPoint(e.getComponent(), e
.getPoint(), JElectrifiedImageComponent.this);
popupMenu.show(JElectrifiedImageComponent.this, pt.x, pt.y);
}
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() != 2)
return;
for (ZoomBubble zoomBubble : zoomBubbles) {
if ((zoomBubble.isSelected) && (zoomBubble.caption == null)) {
if (zoomBubble.centerX < originalImage.getWidth() / 2) {
zoomBubble.captionOffsetX = zoomBubble.radius + 30;
} else {
zoomBubble.captionOffsetX = -zoomBubble.radius - 30;
}
zoomBubble.captionOffsetY = 0;
startCaptionEdit(zoomBubble);
return;
}
}
for (ZoomBubble zoomBubble : zoomBubbles) {
if (zoomBubble.captionRectangle != null) {
if (zoomBubble.captionRectangle.contains(e.getPoint())) {
startCaptionEdit(zoomBubble);
break;
}
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (mouseDragHandler != null) {
mouseDragHandler.onEnd(e.getPoint());
mouseDragHandler = null;
}
}
});
this.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (mouseDragHandler != null) {
mouseDragHandler.onDrag(e.getPoint());
}
}
@Override
public void mouseMoved(MouseEvent e) {
BubbleDragPair underMouse = getInfoForDrag(e.getPoint());
if (underMouse == null) {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
return;
}
switch (underMouse.dragType) {
case BUBBLE_DRAG:
case CAPTION_DRAG:
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
break;
case BUBBLE_RESIZE:
setCursor(Cursor
.getPredefinedCursor(Cursor.S_RESIZE_CURSOR));
break;
}
}
});
this.captionEditor = new JTextField(25);
InputMap im = this.captionEditor.getInputMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "escape");
ActionMap am = this.captionEditor.getActionMap();
am.put("enter", new AbstractAction() {
public void actionPerformed(ActionEvent ae) {
stopCaptionEdit(true);
}
});
am.put("escape", new AbstractAction() {
public void actionPerformed(ActionEvent ae) {
stopCaptionEdit(false);
}
});
this.captionEditor.setVisible(false);
this.add(this.captionEditor);
this.setLayout(null);
}
BubbleDragPair getInfoForDrag(Point viewPoint) {
for (ZoomBubble zoomBubble : zoomBubbles) {
Point2D zoomViewCenter = originalToView(new Point2D.Double(
zoomBubble.centerX, zoomBubble.centerY));
double diffX = zoomViewCenter.getX() - viewPoint.x;
double diffY = zoomViewCenter.getY() - viewPoint.y;
double distFromCenter = Math.sqrt(diffX * diffX + diffY * diffY);
if (distFromCenter < zoomBubble.radius / 2) {
return new BubbleDragPair(zoomBubble, DragType.BUBBLE_DRAG);
}
if (distFromCenter < zoomBubble.radius) {
return new BubbleDragPair(zoomBubble, DragType.BUBBLE_RESIZE);
}
}
// caption?
for (ZoomBubble zoomBubble : zoomBubbles) {
if (zoomBubble.captionRectangle != null) {
if (zoomBubble.captionRectangle.contains(viewPoint)) {
return new BubbleDragPair(zoomBubble, DragType.CAPTION_DRAG);
}
}
}
return null;
}
/**
* Convenience method that returns a scaled instance of the provided {@code
* BufferedImage}. Adopted from <a href="http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html"
* >article by Chris Campbell</a>.
*
* @param img
* the original image to be scaled
* @param targetWidth
* the desired width of the scaled instance, in pixels
* @param targetHeight
* the desired height of the scaled instance, in pixels
* @param hint
* one of the rendering hints that corresponds to {@code
* RenderingHints.KEY_INTERPOLATION} (e.g. {@code
* RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR}, {@code
* RenderingHints.VALUE_INTERPOLATION_BILINEAR}, {@code
* RenderingHints.VALUE_INTERPOLATION_BICUBIC})
* @param higherQuality
* if true, this method will use a multi-step scaling technique
* that provides higher quality than the usual one-step technique
* (only useful in downscaling cases, where {@code targetWidth}
* or {@code targetHeight} is smaller than the original
* dimensions, and generally only when the {@code BILINEAR} hint
* is specified)
* @return a scaled version of the original {@code BufferedImage}
*/
static BufferedImage getScaledInstance(BufferedImage img, int targetWidth,
int targetHeight) {
int type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB
: BufferedImage.TYPE_INT_ARGB;
BufferedImage ret = (BufferedImage) img;
int w, h;
// Use multi-step technique: start with original size, then
// scale down in multiple passes with drawImage()
// until the target size is reached
w = img.getWidth();
h = img.getHeight();
do {
if (w > targetWidth) {
w /= 2;
if (w < targetWidth) {
w = targetWidth;
}
}
if (h > targetHeight) {
h /= 2;
if (h < targetHeight) {
h = targetHeight;
}
}
BufferedImage tmp = new BufferedImage(w, h, type);
Graphics2D g2 = tmp.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2.drawImage(ret, 0, 0, w, h, null);
g2.dispose();
ret = tmp;
} while (w != targetWidth || h != targetHeight);
return ret;
}
private void reset() {
if (originalImage == null) {
electrifiedImage = null;
} else {
// scale down
this.scaleFactor = (float) WIDTH / (float) originalImage.getWidth();
if (this.scaleFactor < 1.0f) {
electrifiedImage = getScaledInstance(originalImage, WIDTH,
(int) (this.scaleFactor * originalImage.getHeight()));
} else {
this.scaleFactor = 1.0f;
electrifiedImage = originalImage;
}
// and blur
int kernelSide = 3;
float[] kernelData = new float[kernelSide * kernelSide];
for (int i = 0; i < kernelData.length; i++)
kernelData[i] = 1.0f / kernelData.length;
Kernel kernel = new Kernel(kernelSide, kernelSide, kernelData);
electrifiedImage = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP,
null).filter(electrifiedImage, null);
setPreferredSize(new Dimension(getSize().width, electrifiedImage
.getHeight() + 50));
((JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class,
this)).revalidate();
}
zoomBubbles.clear();
repaint();
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
if (electrifiedImage != null) {
int width = getWidth();
int height = getHeight();
this.imageOffsetX = (width - electrifiedImage.getWidth()) / 2;
this.imageOffsetY = (height - electrifiedImage.getHeight()) / 2;
paintElectrified(g2d, true, this.imageOffsetX, this.imageOffsetY);
}
g2d.dispose();
}
private void paintElectrified(Graphics2D g2d, boolean isOnScreen,
int offsetX, int offsetY) {
if (electrifiedImage != null) {
g2d.drawImage(this.electrifiedImage, offsetX, offsetY, null);
g2d.setColor(new Color(0, 0, 0, 32));
g2d.fillRect(offsetX, offsetY, this.electrifiedImage.getWidth(),
this.electrifiedImage.getHeight());
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// bubbles
for (ZoomBubble zoomBubble : zoomBubbles) {
double bubbleCenterViewX = offsetX + zoomBubble.centerX
* this.scaleFactor;
double bubbleCenterViewY = offsetY + zoomBubble.centerY
* this.scaleFactor;
Shape currClip = g2d.getClip();
int centerViewX = (int) bubbleCenterViewX;
int centerViewY = (int) bubbleCenterViewY;
g2d.clip(new Ellipse2D.Double(centerViewX - zoomBubble.radius,
centerViewY - zoomBubble.radius, 2 * zoomBubble.radius,
2 * zoomBubble.radius));
int sx1 = (int) (zoomBubble.centerX - zoomBubble.radius);
int dx1 = (int) (centerViewX - zoomBubble.radius);
int sx2 = (int) (zoomBubble.centerX + zoomBubble.radius);
int dx2 = dx1 + (sx2 - sx1);
if (sx1 < 0) {
dx1 = dx1 - sx1;
sx1 = 0;
}
if (sx2 > originalImage.getWidth()) {
dx2 = dx2 - (sx2 - originalImage.getWidth());
sx2 = originalImage.getWidth();
}
int sy1 = (int) (zoomBubble.centerY - zoomBubble.radius);
int dy1 = (int) (centerViewY - zoomBubble.radius);
int sy2 = (int) (zoomBubble.centerY + zoomBubble.radius);
int dy2 = dy1 + (sy2 - sy1);
if (sy1 < 0) {
dy1 = dy1 - sy1;
sy1 = 0;
}
if (sy2 > originalImage.getHeight()) {
dy2 = dy2 - (sy2 - originalImage.getHeight());
sy2 = originalImage.getHeight();
}
if ((sx2 - sx1) != (dx2 - dx1)) {
throw new IllegalStateException();
}
if ((sy2 - sy1) != (dy2 - dy1)) {
throw new IllegalStateException();
}
g2d.drawImage(this.originalImage, dx1, dy1, dx2, dy2, sx1, sy1,
sx2, sy2, null);
g2d.setClip(currClip);
float totalRadius = (float) (zoomBubble.radius + RIM_THICKNESS
/ 2 + OUTER_SHADOW_THICKNESS);
RadialGradientPaint rimPaint = new RadialGradientPaint(
centerViewX,
centerViewY,
totalRadius,
new float[] {
0.0f,
(float) ((zoomBubble.radius - RIM_THICKNESS / 2 - INNER_SHADOW_THICKNESS) / totalRadius),
(float) ((zoomBubble.radius - RIM_THICKNESS / 2) / totalRadius),
(float) ((zoomBubble.radius + RIM_THICKNESS / 2) / totalRadius),
(float) ((zoomBubble.radius + RIM_THICKNESS / 2 + OUTER_SHADOW_THICKNESS / 3) / totalRadius),
1.0f }, new Color[] { new Color(0, 0, 0, 0),
new Color(0, 0, 0, 0), new Color(0, 0, 0, 128),
new Color(0, 0, 0, 128),
new Color(0, 0, 0, 32), new Color(0, 0, 0, 0) });
g2d.setPaint(rimPaint);
g2d.fill(new Ellipse2D.Double(centerViewX - totalRadius,
centerViewY - totalRadius, 2 * totalRadius,
2 * totalRadius));
g2d.setColor(Color.white);
g2d.setStroke(new BasicStroke(RIM_THICKNESS));
g2d.draw(new Ellipse2D.Double(centerViewX - zoomBubble.radius,
centerViewY - zoomBubble.radius, 2 * zoomBubble.radius,
2 * zoomBubble.radius));
if (zoomBubble.isSelected && isOnScreen) {
g2d.setColor(new Color(0, 0, 0, 196));
int selectionCornerSide = 6;
int selectionLeftX = (int) (centerViewX - zoomBubble.radius - RIM_THICKNESS / 2);
int selectionRightX = (int) (centerViewX
+ zoomBubble.radius + RIM_THICKNESS / 2);
int selectionTopY = (int) (centerViewY - zoomBubble.radius - RIM_THICKNESS / 2);
int selectionBottomY = (int) (centerViewY
+ zoomBubble.radius + RIM_THICKNESS / 2);
g2d.setStroke(new BasicStroke(1.2f));
g2d.drawRect(
(int) (selectionLeftX - selectionCornerSide / 2),
(int) (selectionTopY - selectionCornerSide / 2),
selectionCornerSide, selectionCornerSide);
g2d.drawRect(
(int) (selectionLeftX - selectionCornerSide / 2),
(int) (selectionBottomY - selectionCornerSide / 2),
selectionCornerSide, selectionCornerSide);
g2d.drawRect(
(int) (selectionRightX - selectionCornerSide / 2),
(int) (selectionTopY - selectionCornerSide / 2),
selectionCornerSide, selectionCornerSide);
g2d.drawRect(
(int) (selectionRightX - selectionCornerSide / 2),
(int) (selectionBottomY - selectionCornerSide / 2),
selectionCornerSide, selectionCornerSide);
g2d.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_ROUND, 0.0f, new float[] { 2.0f,
1.0f }, 0.0f));
g2d.drawLine(
(int) (selectionLeftX + selectionCornerSide / 2),
selectionTopY,
(int) (selectionRightX - selectionCornerSide / 2),
selectionTopY);
g2d.drawLine(
(int) (selectionLeftX + selectionCornerSide / 2),
selectionBottomY,
(int) (selectionRightX - selectionCornerSide / 2),
selectionBottomY);
g2d.drawLine(selectionLeftX,
(int) (selectionTopY + selectionCornerSide / 2),
selectionLeftX,
(int) (selectionBottomY - selectionCornerSide / 2));
g2d.drawLine(selectionRightX,
(int) (selectionTopY + selectionCornerSide / 2),
selectionRightX,
(int) (selectionBottomY - selectionCornerSide / 2));
}
// caption
if (zoomBubble.caption != null && !zoomBubble.isInTextEdit) {
Font font = SubstanceLookAndFeel.getFontPolicy()
.getFontSet("Substance", null).getControlFont();
g2d.setFont(font);
int strWidth = g2d.getFontMetrics().stringWidth(
zoomBubble.caption);
int fontHeight = g2d.getFontMetrics().getHeight();
int captionHeight = fontHeight + 8;
int captionWidth = strWidth + 8;
int radius = 3;
int x = (int) (centerViewX + zoomBubble.captionOffsetX);
int y = (int) (centerViewY + zoomBubble.captionOffsetY);
Shape outerContour = (zoomBubble.captionOffsetX < 0) ? getCaptionOutlinePointingToRight(
captionHeight, captionWidth, radius, 0)
: getCaptionOutlinePointingToLeft(captionHeight,
captionWidth, radius, 0);
Shape innerContour = (zoomBubble.captionOffsetX < 0) ? getCaptionOutlinePointingToRight(
captionHeight, captionWidth, radius, 1)
: getCaptionOutlinePointingToLeft(captionHeight,
captionWidth, radius, 1);
boolean isInverted = zoomBubble.isInverted;
g2d.translate(x, y);
g2d.setPaint(new GradientPaint(0, 0,
isInverted ? new Color(224, 224, 224, 240)
: new Color(32, 32, 32, 240), 0,
captionHeight, isInverted ? new Color(255, 255,
255, 240) : new Color(0, 0, 0, 240)));
g2d.fill(outerContour);
for (int i = TEXT_OUTER_SHADOW_THICKNESS; i >= 0; i--) {
g2d.setColor(new Color(0, 0, 0, 12));
g2d.setStroke(new BasicStroke(i));
g2d.draw(outerContour);
}
g2d.setColor(isInverted ? new Color(255, 255, 255, 196)
: new Color(0, 0, 0, 196));
g2d.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
g2d.draw(outerContour);
g2d.setPaint(new LinearGradientPaint(0, 0, 0,
captionHeight, new float[] { 0.0f, 0.8f, 1.0f },
new Color[] {
isInverted ? new Color(64, 64, 64, 64)
: new Color(192, 192, 192, 64),
isInverted ? new Color(64, 64, 64, 48)
: new Color(192, 192, 192, 48),
isInverted ? new Color(64, 64, 64, 16)
: new Color(192, 192, 192, 16) }));
g2d.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
g2d.draw(innerContour);
g2d.translate(-x, -y);
RenderingUtils.installDesktopHints(g2d, this);
int textY = y + 4 + g2d.getFontMetrics().getAscent();
int textX = (zoomBubble.captionOffsetX < 0) ? x
+ captionHeight / 6 + 4 : x + captionHeight / 3 + 4;
g2d.setColor(isInverted ? new Color(255, 255, 255, 128)
: new Color(0, 0, 0, 196));
g2d.drawString(zoomBubble.caption, textX - 1, textY);
g2d.drawString(zoomBubble.caption, textX + 1, textY);
g2d.drawString(zoomBubble.caption, textX, textY - 1);
g2d.drawString(zoomBubble.caption, textX, textY + 1);
g2d.setColor(isInverted ? new Color(0, 0, 0) : new Color(
224, 224, 224));
g2d.drawString(zoomBubble.caption, textX, textY);
if (zoomBubble.captionOffsetX < 0) {
zoomBubble.captionRectangle = new Rectangle(x
+ captionHeight / 6, y, captionWidth,
captionHeight);
} else {
zoomBubble.captionRectangle = new Rectangle(x
+ captionHeight / 3, y, captionWidth,
captionHeight);
}
}
}
}
}
private Shape getCaptionOutlinePointingToRight(int captionHeight,
int captionWidth, int radius, int insets) {
GeneralPath contour = new GeneralPath();
contour.moveTo(radius, insets);
contour.lineTo(captionWidth, insets);
contour.lineTo(captionWidth + captionHeight / 2 - insets,
captionHeight / 2);
contour.lineTo(captionWidth, captionHeight - insets);
// bottom left corner
contour.append(new Arc2D.Double(insets, captionHeight - 2 * radius
+ insets, 2 * radius - 2 * insets, 2 * radius - 2 * insets,
270, -90, Arc2D.OPEN), true);
contour.lineTo(insets, radius);
// top left corner
contour.append(new Arc2D.Double(insets, insets,
2 * radius - 2 * insets, 2 * radius - 2 * insets, 180, -90,
Arc2D.OPEN), true);
contour.closePath();
return contour;
}
private Shape getCaptionOutlinePointingToLeft(int captionHeight,
int captionWidth, int radius, int insets) {
GeneralPath contour = new GeneralPath();
contour.moveTo(insets, captionHeight / 2);
contour.lineTo(captionHeight / 2, insets);
contour.lineTo(captionWidth + captionHeight / 2 - radius, insets);
// top right corner
contour.append(new Arc2D.Double(captionWidth + captionHeight / 2 - 2
* radius + insets, insets, 2 * radius - 2 * insets, 2 * radius
- 2 * insets, 90, -90, Arc2D.OPEN), true);
contour.lineTo(captionWidth + captionHeight / 2 - insets, captionHeight
- radius - insets);
// bottom right corner
contour.append(new Arc2D.Double(captionWidth + captionHeight / 2 - 2
* radius + insets, captionHeight - 2 * radius + insets, 2
* radius - 2 * insets, 2 * radius - 2 * insets, 0, -90,
Arc2D.OPEN), true);
contour.lineTo(captionHeight / 2, captionHeight - insets);
contour.closePath();
return contour;
}
void addZoomBubble(int x, int y, int radius) {
ZoomBubble zoomBubble = new ZoomBubble();
zoomBubble.centerX = x;
zoomBubble.centerY = y;
zoomBubble.radius = radius;
zoomBubble.isInverted = false;
this.zoomBubbles.add(zoomBubble);
repaint();
}
Point2D originalToView(Point2D original) {
double viewX = this.imageOffsetX + original.getX() * this.scaleFactor;
double viewY = this.imageOffsetY + original.getY() * this.scaleFactor;
return new Point2D.Double(viewX, viewY);
}
Point2D viewToOriginal(Point2D view) {
double origX = (view.getX() - imageOffsetX) / scaleFactor;
double origY = (view.getY() - imageOffsetY) / scaleFactor;
return new Point2D.Double(origX, origY);
}
void startCaptionEdit(final ZoomBubble bubble) {
bubble.isInTextEdit = true;
if (bubble.caption != null) {
captionEditor.setText(bubble.caption);
captionEditor.selectAll();
} else {
captionEditor.setText("");
}
Dimension pref = captionEditor.getPreferredSize();
Point2D bubbleCenterView = originalToView(new Point2D.Double(
bubble.centerX, bubble.centerY));
captionEditor.setBounds(
(int) (bubbleCenterView.getX() + bubble.captionOffsetX),
(int) (bubbleCenterView.getY() + bubble.captionOffsetY),
pref.width, pref.height);
captionEditor.setVisible(true);
captionEditor.requestFocus();
repaint();
}
void stopCaptionEdit(boolean saveChanges) {
if (!this.captionEditor.isVisible())
return;
// get the text
String text = captionEditor.getText();
if (text.length() == 0) {
text = null;
}
for (ZoomBubble zoomBubble : zoomBubbles) {
if (zoomBubble.isInTextEdit) {
zoomBubble.caption = text;
zoomBubble.isInTextEdit = false;
}
}
captionEditor.setVisible(false);
repaint();
}
void save(File originalFile) {
int extraTop = 0;
int extraBottom = 0;
int extraLeft = 0;
int extraRight = 0;
for (ZoomBubble zoomBubble : zoomBubbles) {
Point2D bubbleCenterView = originalToView(new Point2D.Double(
zoomBubble.centerX, zoomBubble.centerY));
double l = bubbleCenterView.getX() - zoomBubble.radius
- imageOffsetX - RIM_THICKNESS / 2 - OUTER_SHADOW_THICKNESS;
double r = bubbleCenterView.getX() + zoomBubble.radius
- imageOffsetX + RIM_THICKNESS / 2 + OUTER_SHADOW_THICKNESS;
double t = bubbleCenterView.getY() - zoomBubble.radius
- imageOffsetY - RIM_THICKNESS / 2 - OUTER_SHADOW_THICKNESS;
double b = bubbleCenterView.getY() + zoomBubble.radius
- imageOffsetY + RIM_THICKNESS / 2 + OUTER_SHADOW_THICKNESS;
if (zoomBubble.captionRectangle != null) {
l = Math.min(l, zoomBubble.captionRectangle.getMinX()
- imageOffsetX - TEXT_OUTER_SHADOW_THICKNESS);
r = Math.max(r, zoomBubble.captionRectangle.getMaxX()
- imageOffsetX + TEXT_OUTER_SHADOW_THICKNESS);
t = Math.min(t, zoomBubble.captionRectangle.getMinY()
- imageOffsetY - TEXT_OUTER_SHADOW_THICKNESS);
b = Math.max(b, zoomBubble.captionRectangle.getMaxY()
- imageOffsetY + TEXT_OUTER_SHADOW_THICKNESS);
}
if (l < 0) {
extraLeft = Math.max(extraLeft, (int) Math.ceil(-l));
}
if (r > WIDTH) {
extraRight = Math.max(extraRight, (int) Math.ceil(r - WIDTH));
}
if (t < 0) {
extraTop = Math.max(extraTop, (int) Math.ceil(-t));
}
if (b > electrifiedImage.getHeight()) {
extraBottom = Math.max(extraBottom, (int) Math.ceil(b
- electrifiedImage.getHeight()));
}
}
int finalWidth = WIDTH + extraLeft + extraRight;
int finalHeight = electrifiedImage.getHeight() + extraTop + extraBottom;
GraphicsEnvironment e = GraphicsEnvironment
.getLocalGraphicsEnvironment();
GraphicsDevice d = e.getDefaultScreenDevice();
GraphicsConfiguration c = d.getDefaultConfiguration();
BufferedImage compatibleImage = c.createCompatibleImage(finalWidth,
finalHeight, Transparency.TRANSLUCENT);
Graphics2D g2d = compatibleImage.createGraphics();
g2d.translate(extraLeft, extraTop);
this.paintElectrified(g2d, false, 0, 0);
g2d.dispose();
try {
String origFileName = originalFile.getName();
String targetFileName = origFileName.substring(0, origFileName
.lastIndexOf('.'))
+ ".electra.png";
ImageIO.write(compatibleImage, "png", new File(originalFile
.getParentFile(), targetFileName));
} catch (IOException ioe) {
ioe.printStackTrace();
}
saveBubbles(new File(originalFile.getParentFile(), originalFile
.getName()
+ ".layers"));
}
private void saveBubbles(File file) {
try {
PrintWriter pw = new PrintWriter(file);
pw.println("count=" + this.zoomBubbles.size());
for (int i = 0; i < this.zoomBubbles.size(); i++) {
ZoomBubble zoomBubble = this.zoomBubbles.get(i);
pw.println("bubble" + i + ".centerX=" + zoomBubble.centerX);
pw.println("bubble" + i + ".centerY=" + zoomBubble.centerY);
pw.println("bubble" + i + ".radius=" + zoomBubble.radius);
if (zoomBubble.caption != null) {
pw.println("bubble" + i + ".caption=" + zoomBubble.caption);
pw.println("bubble" + i + ".captionOffsetX="
+ zoomBubble.captionOffsetX);
pw.println("bubble" + i + ".captionOffsetY="
+ zoomBubble.captionOffsetY);
}
pw.println("bubble" + i + ".isInverted="
+ zoomBubble.isInverted);
}
pw.flush();
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private void loadBubbles(File file) {
try {
Properties props = new Properties();
props.load(new FileReader(file));
zoomBubbles.clear();
int count = Integer.parseInt(props.getProperty("count"));
for (int i = 0; i < count; i++) {
ZoomBubble zoomBubble = new ZoomBubble();
zoomBubble.centerX = Double.parseDouble(props
.getProperty("bubble" + i + ".centerX"));
zoomBubble.centerY = Double.parseDouble(props
.getProperty("bubble" + i + ".centerY"));
zoomBubble.radius = Double.parseDouble(props
.getProperty("bubble" + i + ".radius"));
zoomBubble.caption = props.getProperty("bubble" + i
+ ".caption");
if (zoomBubble.caption != null) {
zoomBubble.captionOffsetX = Double.parseDouble(props
.getProperty("bubble" + i + ".captionOffsetX"));
zoomBubble.captionOffsetY = Double.parseDouble(props
.getProperty("bubble" + i + ".captionOffsetY"));
}
String invertedKey = "bubble" + i + ".isInverted";
if (props.containsKey(invertedKey)) {
zoomBubble.isInverted = Boolean.parseBoolean(props
.getProperty(invertedKey));
} else {
zoomBubble.isInverted = false;
}
zoomBubbles.add(zoomBubble);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}