/* * Copyright 2017 Laszlo Balazs-Csiki * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU * General Public License, version 3 as published by the Free * Software Foundation. * * Pixelitor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Pixelitor. If not, see <http://www.gnu.org/licenses/>. */ package pixelitor.gui; import org.jdesktop.swingx.painter.CheckerboardPainter; import pixelitor.Canvas; import pixelitor.Composition; import pixelitor.gui.utils.DialogBuilder; import pixelitor.menus.view.ZoomMenu; import pixelitor.utils.ImageSwitchListener; import pixelitor.utils.ImageUtils; import javax.swing.*; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.AdjustmentListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; public class Navigator extends JComponent implements MouseListener, MouseMotionListener, ImageSwitchListener { private static final BasicStroke STROKE = new BasicStroke(3); private static final CheckerboardPainter checkerBoardPainter = ImageUtils.createCheckerboardPainter(); private static final int DEFAULT_SIZE = 300; private ImageComponent ic; // can be null if all images are closed private boolean dragging = false; private double scaling; private Rectangle redRect; private Point dragStartPoint; private Point origRectLoc; // the red rectangle location before starting the drag private int thumbWidth; private int thumbHeight; private int viewWidth; private int viewHeight; private JScrollPane scrollPane; private final AdjustmentListener adjListener; private static JDialog dialog; private Navigator(ImageComponent ic) { adjListener = e -> SwingUtilities.invokeLater(this::updateRedRectanglePosition); refreshSizeCalc(ic, true, true, true); addMouseListener(this); addMouseMotionListener(this); setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); ImageComponents.addImageSwitchListener(this); addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { if (Navigator.this.ic != null) { // it is null if all images are closed refreshSizeCalc(Navigator.this.ic, false, false, true); } } }); addMouseWheelListener(e -> { if (e.isControlDown()) { if (e.getWheelRotation() < 0) { // up, away from the user this.ic.increaseZoom(); } else { // down, towards the user this.ic.decreaseZoom(); } } }); ZoomMenu.setupZoomKeys(this); } public static void showInDialog(PixelitorWindow pw) { ImageComponent ic = ImageComponents.getActiveIC(); Navigator navigator = new Navigator(ic); if(dialog != null && dialog.isVisible()) { dialog.setVisible(false); dialog.dispose(); } dialog = new DialogBuilder() .title("Navigator") .parent(pw) .form(navigator) .notModal() .noOKButton() .noCancelButton() .noGlobalKeyChange() .okAction(navigator::dispose) .show(); } public void refreshSizeCalc(ImageComponent ic, boolean newIC, boolean newICSize, boolean newOwnSize) { if (this.ic != null && newIC) { releaseImage(); } this.ic = ic; scrollPane = ic.getFrame().getScrollPane(); if (newIC) { reCalcScaling(ic, DEFAULT_SIZE, DEFAULT_SIZE); } else if (newICSize || newOwnSize) { reCalcScaling(ic, getWidth(), getHeight()); } setPreferredSize(new Dimension(thumbWidth, thumbHeight)); updateRedRectanglePosition(); if (newIC) { ic.setNavigator(this); scrollPane.getHorizontalScrollBar().addAdjustmentListener(adjListener); scrollPane.getVerticalScrollBar().addAdjustmentListener(adjListener); } if (newICSize) { Window window = SwingUtilities.getWindowAncestor(this); if (window != null) { window.pack(); } } } private void releaseImage() { ic.setNavigator(null); scrollPane.getHorizontalScrollBar().removeAdjustmentListener(adjListener); scrollPane.getVerticalScrollBar().removeAdjustmentListener(adjListener); ic = null; } // updates the red rectangle position based on the ic private void updateRedRectanglePosition() { if (dragging) { // no need to update the rectangle if the change // was caused by this navigator return; } JViewport viewport = scrollPane.getViewport(); Rectangle viewRect = viewport.getViewRect(); Dimension d = viewport.getViewSize(); viewWidth = d.width; viewHeight = d.height; double scaleX = (double) thumbWidth / viewWidth; double scaleY = (double) thumbHeight / viewHeight; int redX = (int) (viewRect.x * scaleX); int redY = (int) (viewRect.y * scaleY); int redWidth = (int) (viewRect.width * scaleX); int redHeight = (int) (viewRect.height * scaleY); redRect = new Rectangle(redX, redY, redWidth, redHeight); repaint(); } // scrolls the image component based on the red rectangle position private void scrollIC() { double scaleX = (double) viewWidth / thumbWidth; double scaleY = (double) viewHeight / thumbHeight; int bigX = (int) (redRect.x * scaleX); int bigY = (int) (redRect.y * scaleY); int bigWidth = (int) (redRect.width * scaleX); int bigHeight = (int) (redRect.height * scaleY); ic.scrollRectToVisible(new Rectangle(bigX, bigY, bigWidth, bigHeight)); } @Override protected void paintComponent(Graphics g) { if (ic == null) { return; } Graphics2D g2 = (Graphics2D) g; checkerBoardPainter.paint(g2, null, thumbWidth, thumbHeight); AffineTransform origTX = g2.getTransform(); g2.scale(scaling, scaling); g2.drawImage(ic.getComp().getCompositeImage(), 0, 0, null); g2.setTransform(origTX); g2.setStroke(STROKE); g2.setColor(Color.RED); g2.draw(redRect); } @Override public void mousePressed(MouseEvent e) { Point point = e.getPoint(); if (redRect.contains(point) && ic != null) { dragStartPoint = point; origRectLoc = redRect.getLocation(); } } @Override public void mouseReleased(MouseEvent e) { dragStartPoint = null; dragging = false; } @Override public void mouseDragged(MouseEvent e) { if (dragStartPoint != null) { dragging = true; Point eventPoint = e.getPoint(); int relX = eventPoint.x - dragStartPoint.x; int relY = eventPoint.y - dragStartPoint.y; int newRedX = origRectLoc.x + relX; int newRedY = origRectLoc.y + relY; // make sure that the red rectangle does not leave the thumb if (newRedX < 0) { newRedX = 0; } if (newRedY < 0) { newRedY = 0; } if (newRedX + redRect.width > thumbWidth) { newRedX = thumbWidth - redRect.width; } if (newRedY + redRect.height > thumbHeight) { newRedY = thumbHeight - redRect.height; } updateRedRectLocation(newRedX, newRedY); } } private void updateRedRectLocation(int newRedX, int newRedY) { if (newRedX != redRect.x || newRedY != redRect.y) { redRect.setLocation(newRedX, newRedY); repaint(); scrollIC(); } } private void reCalcScaling(ImageComponent ic, int width, int height) { Canvas canvas = ic.getCanvas(); int imgWidth = canvas.getWidth(); int imgHeight = canvas.getHeight(); double xScaling = width / (double) imgWidth; double yScaling = height / (double) imgHeight; scaling = Math.min(xScaling, yScaling); thumbWidth = (int) (imgWidth * scaling); thumbHeight = (int) (imgHeight * scaling); } @Override public void mouseClicked(MouseEvent e) { // not interested } @Override public void mouseEntered(MouseEvent e) { // not interested } @Override public void mouseExited(MouseEvent e) { // not interested } @Override public void mouseMoved(MouseEvent e) { // not interested } @Override public void noOpenImageAnymore() { releaseImage(); repaint(); } @Override public void newImageOpened(Composition comp) { // not necessary to implement, since activeImageHasChanged is also called } @Override public void activeImageHasChanged(ImageComponent oldIC, ImageComponent newIC) { refreshSizeCalc(newIC, true, true, false); } // called when this navigator instance is no longer needed private void dispose() { ImageComponents.removeImageSwitchListener(this); } }