/* * BackgroundImageWizardStepsPanel.java 8 juin 07 * * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.eteks.sweethome3d.swing; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.CardLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.EventQueue; 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.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import javax.imageio.ImageIO; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputAdapter; import com.eteks.sweethome3d.model.BackgroundImage; import com.eteks.sweethome3d.model.Content; import com.eteks.sweethome3d.model.RecorderException; import com.eteks.sweethome3d.model.UserPreferences; import com.eteks.sweethome3d.tools.OperatingSystem; import com.eteks.sweethome3d.tools.TemporaryURLContent; import com.eteks.sweethome3d.viewcontroller.BackgroundImageWizardController; import com.eteks.sweethome3d.viewcontroller.ContentManager; import com.eteks.sweethome3d.viewcontroller.View; /** * Wizard panel for background image choice. * @author Emmanuel Puybaret */ public class BackgroundImageWizardStepsPanel extends JPanel implements View { private final BackgroundImageWizardController controller; private final Executor imageLoader; private CardLayout cardLayout; private JLabel imageChoiceOrChangeLabel; private JButton imageChoiceOrChangeButton; private JLabel imageChoiceErrorLabel; private ScaledImageComponent imageChoicePreviewComponent; private JLabel scaleLabel; private JLabel scaleDistanceLabel; private JSpinner scaleDistanceSpinner; private ScaledImageComponent scalePreviewComponent; private JLabel originLabel; private JLabel xOriginLabel; private JSpinner xOriginSpinner; private JLabel yOriginLabel; private JSpinner yOriginSpinner; private ScaledImageComponent originPreviewComponent; private static BufferedImage waitImage; /** * Creates a view for background image choice, scale and origin. */ public BackgroundImageWizardStepsPanel(BackgroundImage backgroundImage, UserPreferences preferences, final BackgroundImageWizardController controller) { this.controller = controller; this.imageLoader = Executors.newSingleThreadExecutor(); createComponents(preferences, controller); setMnemonics(preferences); layoutComponents(); updateController(backgroundImage, preferences); controller.addPropertyChangeListener(BackgroundImageWizardController.Property.STEP, new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { updateStep(controller); } }); } /** * Creates components displayed by this panel. */ private void createComponents(final UserPreferences preferences, final BackgroundImageWizardController controller) { // Get unit name matching current unit String unitName = preferences.getLengthUnit().getName(); // Image choice panel components this.imageChoiceOrChangeLabel = new JLabel(); this.imageChoiceOrChangeButton = new JButton(); this.imageChoiceOrChangeButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { String imageName = showImageChoiceDialog(preferences, controller.getContentManager()); if (imageName != null) { updateController(imageName, preferences, controller.getContentManager()); } } }); this.imageChoiceErrorLabel = new JLabel(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "imageChoiceErrorLabel.text")); // Make imageChoiceErrorLabel visible only if an error occurred during image content loading this.imageChoiceErrorLabel.setVisible(false); this.imageChoicePreviewComponent = new ScaledImageComponent(); // Add a transfer handler to image preview component to let user drag and drop an image in component this.imageChoicePreviewComponent.setTransferHandler(new TransferHandler() { @Override public boolean canImport(JComponent comp, DataFlavor [] flavors) { return Arrays.asList(flavors).contains(DataFlavor.javaFileListFlavor); } @Override public boolean importData(JComponent comp, Transferable transferedFiles) { boolean success = true; try { List<File> files = (List<File>)transferedFiles.getTransferData(DataFlavor.javaFileListFlavor); final String imageName = files.get(0).getAbsolutePath(); EventQueue.invokeLater(new Runnable() { public void run() { updateController(imageName, preferences, controller.getContentManager()); } }); } catch (UnsupportedFlavorException ex) { success = false; } catch (IOException ex) { success = false; } if (!success) { EventQueue.invokeLater(new Runnable() { public void run() { JOptionPane.showMessageDialog(SwingUtilities.getRootPane(BackgroundImageWizardStepsPanel.this), preferences.getLocalizedString(BackgroundImageWizardStepsPanel.class, "imageChoiceError")); } }); } return success; } }); this.imageChoicePreviewComponent.setBorder(SwingTools.getDropableComponentBorder()); // Image scale panel components this.scaleLabel = new JLabel(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "scaleLabel.text")); this.scaleDistanceLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, BackgroundImageWizardStepsPanel.class, "scaleDistanceLabel.text", unitName)); final float maximumLength = preferences.getLengthUnit().getMaximumLength(); final NullableSpinner.NullableSpinnerLengthModel scaleDistanceSpinnerModel = new NullableSpinner.NullableSpinnerLengthModel(preferences, preferences.getLengthUnit().getMinimumLength(), maximumLength); this.scaleDistanceSpinner = new NullableSpinner(scaleDistanceSpinnerModel); this.scaleDistanceSpinner.getModel().addChangeListener(new ChangeListener () { public void stateChanged(ChangeEvent ev) { // If spinner value changes update controller controller.setScaleDistance( ((NullableSpinner.NullableSpinnerLengthModel)scaleDistanceSpinner.getModel()).getLength()); } }); controller.addPropertyChangeListener(BackgroundImageWizardController.Property.SCALE_DISTANCE, new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { // If scale distance changes updates scale spinner scaleDistanceSpinnerModel.setNullable(controller.getScaleDistance() == null); scaleDistanceSpinnerModel.setLength(controller.getScaleDistance()); } }); this.scalePreviewComponent = new ScaleImagePreviewComponent(controller); // Image origin panel components this.originLabel = new JLabel(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "originLabel.text")); this.xOriginLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, BackgroundImageWizardStepsPanel.class, "xOriginLabel.text", unitName)); this.yOriginLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, BackgroundImageWizardStepsPanel.class, "yOriginLabel.text", unitName)); final NullableSpinner.NullableSpinnerLengthModel xOriginSpinnerModel = new NullableSpinner.NullableSpinnerLengthModel(preferences, controller.getXOrigin(), -maximumLength, maximumLength); this.xOriginSpinner = new NullableSpinner(xOriginSpinnerModel); final NullableSpinner.NullableSpinnerLengthModel yOriginSpinnerModel = new NullableSpinner.NullableSpinnerLengthModel(preferences, controller.getYOrigin(), -maximumLength, maximumLength); this.yOriginSpinner = new NullableSpinner(yOriginSpinnerModel); ChangeListener originSpinnersListener = new ChangeListener () { public void stateChanged(ChangeEvent ev) { // If origin spinners value changes update controller controller.setOrigin(xOriginSpinnerModel.getLength(), yOriginSpinnerModel.getLength()); } }; xOriginSpinnerModel.addChangeListener(originSpinnersListener); yOriginSpinnerModel.addChangeListener(originSpinnersListener); controller.addPropertyChangeListener(BackgroundImageWizardController.Property.X_ORIGIN, new PropertyChangeListener () { public void propertyChange(PropertyChangeEvent ev) { // If origin values changes update x origin spinner xOriginSpinnerModel.setLength(controller.getXOrigin()); } }); controller.addPropertyChangeListener(BackgroundImageWizardController.Property.Y_ORIGIN, new PropertyChangeListener () { public void propertyChange(PropertyChangeEvent ev) { // If origin values changes update y origin spinner yOriginSpinnerModel.setLength(controller.getYOrigin()); } }); this.originPreviewComponent = new OriginImagePreviewComponent(controller); } /** * Sets components mnemonics and label / component associations. */ private void setMnemonics(UserPreferences preferences) { if (!OperatingSystem.isMacOSX()) { this.scaleDistanceLabel.setDisplayedMnemonic( KeyStroke.getKeyStroke(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "scaleDistanceLabel.mnemonic")).getKeyCode()); this.scaleDistanceLabel.setLabelFor(this.scaleDistanceSpinner); this.xOriginLabel.setDisplayedMnemonic( KeyStroke.getKeyStroke(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "xOriginLabel.mnemonic")).getKeyCode()); this.xOriginLabel.setLabelFor(this.xOriginSpinner); this.yOriginLabel.setDisplayedMnemonic( KeyStroke.getKeyStroke(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "yOriginLabel.mnemonic")).getKeyCode()); this.yOriginLabel.setLabelFor(this.yOriginSpinner); } } /** * Layouts components in 3 panels added to this panel as cards. */ private void layoutComponents() { this.cardLayout = new CardLayout(); setLayout(this.cardLayout); JPanel imageChoiceTopPanel = new JPanel(new GridBagLayout()); imageChoiceTopPanel.add(this.imageChoiceOrChangeLabel, new GridBagConstraints( 0, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(5, 0, 5, 0), 0, 0)); imageChoiceTopPanel.add(this.imageChoiceOrChangeButton, new GridBagConstraints( 0, 1, 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); imageChoiceTopPanel.add(this.imageChoiceErrorLabel, new GridBagConstraints( 0, 2, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 0, 0, 0), 0, 0)); JPanel imageChoicePanel = new JPanel(new ProportionalLayout()); imageChoicePanel.add(imageChoiceTopPanel, ProportionalLayout.Constraints.TOP); imageChoicePanel.add(this.imageChoicePreviewComponent, ProportionalLayout.Constraints.BOTTOM); JPanel scaleTopPanel = new JPanel(new GridBagLayout()); scaleTopPanel.add(this.scaleLabel, new GridBagConstraints( 0, 0, 2, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(5, 0, 5, 0), 0, 0)); scaleTopPanel.add(this.scaleDistanceLabel, new GridBagConstraints( 0, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0)); scaleTopPanel.add(this.scaleDistanceSpinner, new GridBagConstraints( 1, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0)); JPanel scalePanel = new JPanel(new ProportionalLayout()); scalePanel.add(scaleTopPanel, ProportionalLayout.Constraints.TOP); scalePanel.add(this.scalePreviewComponent, ProportionalLayout.Constraints.BOTTOM); JPanel originTopPanel = new JPanel(new GridBagLayout()); originTopPanel.add(this.originLabel, new GridBagConstraints( 0, 0, 4, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(5, 0, 5, 0), 0, 0)); originTopPanel.add(this.xOriginLabel, new GridBagConstraints( 0, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0)); originTopPanel.add(this.xOriginSpinner, new GridBagConstraints( 1, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 10), -10, 0)); originTopPanel.add(this.yOriginLabel, new GridBagConstraints( 2, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0)); originTopPanel.add(this.yOriginSpinner, new GridBagConstraints( 3, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), -10, 0)); JPanel originPanel = new JPanel(new ProportionalLayout()); originPanel.add(originTopPanel, ProportionalLayout.Constraints.TOP); originPanel.add(this.originPreviewComponent, ProportionalLayout.Constraints.BOTTOM); add(imageChoicePanel, BackgroundImageWizardController.Step.CHOICE.name()); add(scalePanel, BackgroundImageWizardController.Step.SCALE.name()); add(originPanel, BackgroundImageWizardController.Step.ORIGIN.name()); } /** * Switches to the view card matching current step. */ public void updateStep(BackgroundImageWizardController controller) { BackgroundImageWizardController.Step step = controller.getStep(); this.cardLayout.show(this, step.name()); switch (step) { case CHOICE: this.imageChoiceOrChangeButton.requestFocusInWindow(); break; case SCALE: ((JSpinner.DefaultEditor)this.scaleDistanceSpinner.getEditor()).getTextField().requestFocusInWindow(); break; case ORIGIN: ((JSpinner.DefaultEditor)this.xOriginSpinner.getEditor()).getTextField().requestFocusInWindow(); break; } } /** * Updates controller initial values from <code>backgroundImage</code>. */ private void updateController(final BackgroundImage backgroundImage, final UserPreferences preferences) { if (backgroundImage == null) { setImageChoiceTexts(preferences); updatePreviewComponentsImage(null); } else { setImageChangeTexts(preferences); // Read image in imageLoader executor this.imageLoader.execute(new Runnable() { public void run() { BufferedImage image = null; try { image = readImage(backgroundImage.getImage(), preferences); } catch (IOException ex) { // image is null } final BufferedImage readImage = image; // Update components in dispatch thread EventQueue.invokeLater(new Runnable() { public void run() { if (readImage != null) { controller.setImage(backgroundImage.getImage()); controller.setScaleDistance(backgroundImage.getScaleDistance()); controller.setScaleDistancePoints(backgroundImage.getScaleDistanceXStart(), backgroundImage.getScaleDistanceYStart(), backgroundImage.getScaleDistanceXEnd(), backgroundImage.getScaleDistanceYEnd()); controller.setOrigin(backgroundImage.getXOrigin(), backgroundImage.getYOrigin()); } else { controller.setImage(null); setImageChoiceTexts(preferences); imageChoiceErrorLabel.setVisible(true); } } }); } }); } } /** * Reads image from <code>imageName</code> and updates controller values. */ private void updateController(final String imageName, final UserPreferences preferences, final ContentManager contentManager) { // Read image in imageLoader executor this.imageLoader.execute(new Runnable() { public void run() { Content imageContent = null; try { // Copy image to a temporary content to keep a safe access to it until home is saved imageContent = TemporaryURLContent.copyToTemporaryURLContent( contentManager.getContent(imageName)); } catch (RecorderException ex) { // Error message displayed below } catch (IOException ex) { // Error message displayed below } if (imageContent == null) { EventQueue.invokeLater(new Runnable() { public void run() { JOptionPane.showMessageDialog(SwingUtilities.getRootPane(BackgroundImageWizardStepsPanel.this), preferences.getLocalizedString(BackgroundImageWizardStepsPanel.class, "imageChoiceError", imageName)); } }); return; } BufferedImage image = null; try { image = readImage(imageContent, preferences); } catch (IOException ex) { // image is null } final BufferedImage readImage = image; final Content readContent = imageContent; // Update components in dispatch thread EventQueue.invokeLater(new Runnable() { public void run() { if (readImage != null) { controller.setImage(readContent); setImageChangeTexts(preferences); imageChoiceErrorLabel.setVisible(false); // Initialize distance and origin with default values controller.setScaleDistance(null); float scaleDistanceXStart = readImage.getWidth() * 0.1f; float scaleDistanceYStart = readImage.getHeight() / 2f; float scaleDistanceXEnd = readImage.getWidth() * 0.9f; controller.setScaleDistancePoints(scaleDistanceXStart, scaleDistanceYStart, scaleDistanceXEnd, scaleDistanceYStart); controller.setOrigin(0, 0); } else if (isShowing()){ controller.setImage(null); setImageChoiceTexts(preferences); JOptionPane.showMessageDialog(SwingUtilities.getRootPane(BackgroundImageWizardStepsPanel.this), preferences.getLocalizedString(BackgroundImageWizardStepsPanel.class, "imageChoiceFormatError")); } } }); } }); } /** * Reads image from <code>imageContent</code>. * Caution : this method must be thread safe because it's called from image loader executor. */ private BufferedImage readImage(Content imageContent, UserPreferences preferences) throws IOException { try { // Display a waiting image while loading if (waitImage == null) { waitImage = ImageIO.read(BackgroundImageWizardStepsPanel.class. getResource(preferences.getLocalizedString(BackgroundImageWizardStepsPanel.class, "waitIcon"))); } updatePreviewComponentsImage(waitImage); // Read the image content InputStream contentStream = imageContent.openStream(); BufferedImage image = ImageIO.read(contentStream); contentStream.close(); if (image != null) { updatePreviewComponentsImage(image); return image; } else { throw new IOException(); } } catch (IOException ex) { updatePreviewComponentsImage(null); throw ex; } } /** * Updates the <code>image</code> displayed by preview components. */ private void updatePreviewComponentsImage(BufferedImage image) { this.imageChoicePreviewComponent.setImage(image); this.scalePreviewComponent.setImage(image); this.originPreviewComponent.setImage(image); } /** * Sets the texts of label and button of image choice panel with * change texts. */ private void setImageChangeTexts(UserPreferences preferences) { this.imageChoiceOrChangeLabel.setText(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "imageChangeLabel.text")); this.imageChoiceOrChangeButton.setText(SwingTools.getLocalizedLabelText(preferences, BackgroundImageWizardStepsPanel.class, "imageChangeButton.text")); if (!OperatingSystem.isMacOSX()) { this.imageChoiceOrChangeButton.setMnemonic( KeyStroke.getKeyStroke(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "imageChangeButton.mnemonic")).getKeyCode()); } } /** * Sets the texts of label and button of image choice panel with * choice texts. */ private void setImageChoiceTexts(UserPreferences preferences) { this.imageChoiceOrChangeLabel.setText(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "imageChoiceLabel.text")); this.imageChoiceOrChangeButton.setText(SwingTools.getLocalizedLabelText(preferences, BackgroundImageWizardStepsPanel.class, "imageChoiceButton.text")); if (!OperatingSystem.isMacOSX()) { this.imageChoiceOrChangeButton.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "imageChoiceButton.mnemonic")).getKeyCode()); } } /** * Returns an image chosen for a content chooser dialog. */ private String showImageChoiceDialog(UserPreferences preferences, ContentManager contentManager) { return contentManager.showOpenDialog(this,preferences.getLocalizedString( BackgroundImageWizardStepsPanel.class, "imageChoiceDialog.title"), ContentManager.ContentType.IMAGE); } /** * Returns the selection color used in preview components. */ private static Color getSelectionColor() { Color selectionColor = OperatingSystem.isMacOSXLeopardOrSuperior() ? UIManager.getColor("List.selectionBackground") : UIManager.getColor("textHighlight"); float [] hsb = new float [3]; Color.RGBtoHSB(selectionColor.getRed(), selectionColor.getGreen(), selectionColor.getBlue(), hsb); if (hsb [1] < 0.4f) { // If color is too gray, return a default blue color selectionColor = new Color(40, 89, 208); } return selectionColor; } /** * Preview component for image scale distance choice. */ private static class ScaleImagePreviewComponent extends ScaledImageComponent { private final BackgroundImageWizardController controller; public ScaleImagePreviewComponent(BackgroundImageWizardController controller) { this.controller = controller; addChangeListeners(controller); addMouseListeners(controller); } /** * Adds listeners to <code>controller</code> * to update the scale distance points of the origin drawn by this component. */ private void addChangeListeners(final BackgroundImageWizardController controller) { controller.addPropertyChangeListener(BackgroundImageWizardController.Property.SCALE_DISTANCE_POINTS, new PropertyChangeListener () { public void propertyChange(PropertyChangeEvent ev) { // If origin values changes update displayed origin repaint(); } }); } /** * Adds to this component a mouse listeners that allows the user to move the start point * or the end point of the scale distance line. */ public void addMouseListeners(final BackgroundImageWizardController controller) { MouseInputAdapter mouseListener = new MouseInputAdapter() { private int lastX; private int lastY; private boolean distanceStartPoint; private boolean distanceEndPoint; @Override public void mousePressed(MouseEvent ev) { if (!ev.isPopupTrigger()) { mouseMoved(ev); if (this.distanceStartPoint || this.distanceEndPoint) { this.lastX = ev.getX(); this.lastY = ev.getY(); } } } @Override public void mouseDragged(MouseEvent ev) { if (isPointInImage(ev.getX(), ev.getY())) { float [][] scaleDistancePoints = controller.getScaleDistancePoints(); float scale = getImageScale(); if (this.distanceStartPoint) { // Compute start point of distance line scaleDistancePoints [0][0] += (ev.getX() - this.lastX) / scale; scaleDistancePoints [0][1] += (ev.getY() - this.lastY) / scale; } else if (this.distanceEndPoint) { // Compute end point of distance line scaleDistancePoints [1][0] += (ev.getX() - this.lastX) / scale; scaleDistancePoints [1][1] += (ev.getY() - this.lastY) / scale; } // Accept new points only if distance is greater that 2 pixels if ((this.distanceStartPoint || this.distanceEndPoint) && Point2D.distanceSq(scaleDistancePoints [0][0] * scale, scaleDistancePoints [0][1] * scale, scaleDistancePoints [1][0] * scale, scaleDistancePoints [1][1] * scale) >= 4) { controller.setScaleDistancePoints( scaleDistancePoints [0][0], scaleDistancePoints [0][1], scaleDistancePoints [1][0], scaleDistancePoints [1][1]); repaint(); } this.lastX = ev.getX(); this.lastY = ev.getY(); } } @Override public void mouseMoved(MouseEvent ev) { this.distanceStartPoint = this.distanceEndPoint = false; if (isPointInImage(ev.getX(), ev.getY())) { float [][] scaleDistancePoints = controller.getScaleDistancePoints(); Point translation = getImageTranslation(); float scale = getImageScale(); // Check if user clicked on start or end point of distance line if (Math.abs(scaleDistancePoints [0][0] * scale - ev.getX() + translation.x) < 2 && Math.abs(scaleDistancePoints [0][1] * scale - ev.getY() + translation.y) < 2) { this.distanceStartPoint = true; } else if (Math.abs(scaleDistancePoints [1][0] * scale - ev.getX() + translation.x) < 2 && Math.abs(scaleDistancePoints [1][1] * scale - ev.getY() + translation.y) < 2) { this.distanceEndPoint = true; } } if (this.distanceStartPoint || this.distanceEndPoint) { setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } else { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } }; addMouseListener(mouseListener); addMouseMotionListener(mouseListener); } @Override protected void paintComponent(Graphics g) { if (getImage() != null) { Graphics2D g2D = (Graphics2D)g; g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Point translation = getImageTranslation(); float scale = getImageScale(); // Fill image background g2D.setColor(UIManager.getColor("window")); g2D.fillRect(translation.x, translation.y, (int)(getImage().getWidth() * scale), (int)(getImage().getHeight() * scale)); // Paint image with a 0.5 alpha paintImage(g2D, AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); g2D.setPaint(getSelectionColor()); AffineTransform oldTransform = g2D.getTransform(); Stroke oldStroke = g2D.getStroke(); // Use same origin and scale as image drawing in super class g2D.translate(translation.x, translation.y); g2D.scale(scale, scale); // Draw a scale distance line g2D.setStroke(new BasicStroke(5 / scale, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); float [][] scaleDistancePoints = this.controller.getScaleDistancePoints(); g2D.draw(new Line2D.Float(scaleDistancePoints [0][0], scaleDistancePoints [0][1], scaleDistancePoints [1][0], scaleDistancePoints [1][1])); // Draw start point line g2D.setStroke(new BasicStroke(1 / scale, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); double angle = Math.atan2(scaleDistancePoints [1][1] - scaleDistancePoints [0][1], scaleDistancePoints [1][0] - scaleDistancePoints [0][0]); AffineTransform oldTransform2 = g2D.getTransform(); g2D.translate(scaleDistancePoints [0][0], scaleDistancePoints [0][1]); g2D.rotate(angle); Shape endLine = new Line2D.Double(0, 5 / scale, 0, -5 / scale); g2D.draw(endLine); g2D.setTransform(oldTransform2); // Draw end point line g2D.translate(scaleDistancePoints [1][0], scaleDistancePoints [1][1]); g2D.rotate(angle); g2D.draw(endLine); g2D.setTransform(oldTransform); g2D.setStroke(oldStroke); } } } /** * Preview component for image scale distance choice. */ private static class OriginImagePreviewComponent extends ScaledImageComponent { private final BackgroundImageWizardController controller; public OriginImagePreviewComponent(BackgroundImageWizardController controller) { this.controller = controller; addChangeListeners(controller); addMouseListener(controller); } /** * Adds listeners to <code>controller</code> * to update the location of the origin drawn by this component. */ private void addChangeListeners(final BackgroundImageWizardController controller) { PropertyChangeListener originChangeListener = new PropertyChangeListener () { public void propertyChange(PropertyChangeEvent ev) { // If origin values changes update displayed origin repaint(); } }; controller.addPropertyChangeListener(BackgroundImageWizardController.Property.X_ORIGIN, originChangeListener); controller.addPropertyChangeListener(BackgroundImageWizardController.Property.Y_ORIGIN, originChangeListener); } /** * Adds a mouse listener to this component to update the origin stored * by <code>controller</code> when the user clicks in component. */ public void addMouseListener(final BackgroundImageWizardController controller) { MouseInputAdapter mouseAdapter = new MouseInputAdapter() { @Override public void mousePressed(MouseEvent ev) { if (!ev.isPopupTrigger() && isPointInImage(ev.getX(), ev.getY())) { Point translation = getImageTranslation(); float [][] scaleDistancePoints = controller.getScaleDistancePoints(); float rescale = getImageScale() / BackgroundImage.getScale(controller.getScaleDistance(), scaleDistancePoints [0][0], scaleDistancePoints [0][1], scaleDistancePoints [1][0], scaleDistancePoints [1][1]); float xOrigin = Math.round((ev.getX() - translation.x) / rescale * 10) / 10.f; float yOrigin = Math.round((ev.getY() - translation.y) / rescale * 10) / 10.f; controller.setOrigin(xOrigin, yOrigin); } } @Override public void mouseDragged(MouseEvent ev) { mouseMoved(ev); mousePressed(ev); } @Override public void mouseMoved(MouseEvent ev) { if (isPointInImage(ev.getX(), ev.getY())) { setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } else { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } }; addMouseListener(mouseAdapter); addMouseMotionListener(mouseAdapter); } @Override protected void paintComponent(Graphics g) { if (getImage() != null) { Graphics2D g2D = (Graphics2D)g; g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Point translation = getImageTranslation(); // Fill image background g2D.setColor(UIManager.getColor("window")); g2D.fillRect(translation.x, translation.y, (int)(getImage().getWidth() * getImageScale()), (int)(getImage().getHeight() * getImageScale())); // Paint image with a 0.5 alpha paintImage(g, AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); g2D.setPaint(getSelectionColor()); AffineTransform oldTransform = g2D.getTransform(); Stroke oldStroke = g2D.getStroke(); g2D.translate(translation.x, translation.y); // Rescale according to scale distance float [][] scaleDistancePoints = this.controller.getScaleDistancePoints(); float scale = getImageScale() / BackgroundImage.getScale(this.controller.getScaleDistance(), scaleDistancePoints [0][0], scaleDistancePoints [0][1], scaleDistancePoints [1][0], scaleDistancePoints [1][1]); g2D.scale(scale, scale); // Draw a dot at origin g2D.translate(this.controller.getXOrigin(), this.controller.getYOrigin()); float originRadius = 4 / scale; g2D.fill(new Ellipse2D.Float(-originRadius, -originRadius, originRadius * 2, originRadius * 2)); // Draw a cross g2D.setStroke(new BasicStroke(1 / scale, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); g2D.draw(new Line2D.Double(8 / scale, 0, -8 / scale, 0)); g2D.draw(new Line2D.Double(0, 8 / scale, 0, -8 / scale)); g2D.setTransform(oldTransform); g2D.setStroke(oldStroke); } } } }