/* * Copyright (c) 2005-2016 Laf-Widget 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 Laf-Widget 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 org.pushingpixels.lafwidget.scroll; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Robot; import java.awt.event.ContainerAdapter; import java.awt.event.ContainerEvent; import java.awt.event.MouseEvent; import java.awt.geom.Area; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.MouseInputAdapter; import javax.swing.event.MouseInputListener; import org.pushingpixels.lafwidget.LafWidgetRepository; import org.pushingpixels.lafwidget.LafWidgetUtilities2; import org.pushingpixels.lafwidget.animation.AnimationConfigurationManager; import org.pushingpixels.lafwidget.contrib.intellij.UIUtil; import org.pushingpixels.lafwidget.preview.PreviewPainter; import org.pushingpixels.trident.Timeline; import org.pushingpixels.trident.Timeline.TimelineState; import org.pushingpixels.trident.callback.UIThreadTimelineCallbackAdapter; /** * ScrollPaneSelector is a little utility class that provides a means to quickly * scroll both vertically and horizontally on a single mouse click, by dragging * a selection rectangle over a "thumbnail" of the scrollPane's viewport view. * <p> * Once the selector is installed on a given JScrollPane instance, a little * button appears as soon as at least one of its scroll bars is made visible. * <p> * Contributed by the original author under BSD license. Also appears in the <a * href="https://jdnc-incubator.dev.java.net">JDNC Incubator</a>. * * @author weebib (Pierre LE LANNIC) * @author Kirill Grouchnikov (animations). */ public class ScrollPaneSelector extends JComponent { // static final fields private static final String COMPONENT_ORIENTATION = "componentOrientation"; // member fields private LayoutManager theFormerLayoutManager; private JScrollPane theScrollPane; private JComponent theComponent; private JPopupMenu thePopupMenu; private boolean toRestoreOriginal; private JButton theButton; private BufferedImage theImage; private Rectangle theStartRectangle; private Rectangle theRectangle; private Point theStartPoint; private Point thePrevPoint; private double theScale; private PropertyChangeListener propertyChangeListener; private ContainerAdapter theViewPortViewListener; // -- Constructor ------ ScrollPaneSelector() { setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); theScrollPane = null; theImage = null; theStartRectangle = null; theRectangle = null; theStartPoint = null; theScale = 0.0; theButton = new JButton(); LafWidgetRepository.getRepository().getLafSupport().markButtonAsFlat(theButton); theButton.setFocusable(false); theButton.setFocusPainted(false); MouseInputListener mil = new MouseInputAdapter() { @Override public void mousePressed(MouseEvent e) { Point p = e.getPoint(); SwingUtilities.convertPointToScreen(p, theButton); display(p); } @Override public void mouseReleased(MouseEvent e) { if (!thePopupMenu.isVisible()) return; toRestoreOriginal = false; thePopupMenu.setVisible(false); theStartRectangle = theRectangle; } @Override public void mouseDragged(MouseEvent e) { if (theStartPoint == null) return; if (!thePopupMenu.isShowing()) return; Point newPoint = e.getPoint(); SwingUtilities.convertPointToScreen(newPoint, (Component) e .getSource()); Rectangle popupScreenRect = new Rectangle(thePopupMenu .getLocationOnScreen(), thePopupMenu.getSize()); if (!popupScreenRect.contains(newPoint)) return; int deltaX = (int) ((newPoint.x - thePrevPoint.x) / theScale); int deltaY = (int) ((newPoint.y - thePrevPoint.y) / theScale); scroll(deltaX, deltaY, false); thePrevPoint = newPoint; } }; theButton.addMouseListener(mil); theButton.addMouseMotionListener(mil); setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); thePopupMenu = new JPopupMenu(); thePopupMenu.setLayout(new BorderLayout()); thePopupMenu.add(this, BorderLayout.CENTER); propertyChangeListener = (PropertyChangeEvent evt) -> { if (theScrollPane == null) return; if ("componentOrientation".equals(evt.getPropertyName())) { theScrollPane.setCorner(JScrollPane.LOWER_LEADING_CORNER, null); theScrollPane.setCorner(JScrollPane.LOWER_TRAILING_CORNER, theButton); } }; theViewPortViewListener = new ContainerAdapter() { @Override public void componentAdded(ContainerEvent e) { if (thePopupMenu.isVisible()) thePopupMenu.setVisible(false); Component comp = theScrollPane.getViewport().getView(); theComponent = (comp instanceof JComponent) ? (JComponent) comp : null; } }; thePopupMenu.addPropertyChangeListener((PropertyChangeEvent evt) -> { if ("visible".equals(evt.getPropertyName())) { if (!thePopupMenu.isVisible()) { setCursor(Cursor .getPredefinedCursor(Cursor.DEFAULT_CURSOR)); if (toRestoreOriginal) { int deltaX = (int) ((thePrevPoint.x - theStartPoint.x) / theScale); int deltaY = (int) ((thePrevPoint.y - theStartPoint.y) / theScale); scroll(-deltaX, -deltaY, true); } } } }); } // -- JComponent overriden methods ------ @Override public Dimension getPreferredSize() { if (theImage == null || theRectangle == null) return new Dimension(); Insets insets = getInsets(); int scaleFactor = UIUtil.getScaleFactor(); return new Dimension( theImage.getWidth() / scaleFactor + insets.left + insets.right, theImage.getHeight() / scaleFactor + insets.top + insets.bottom); } @Override protected void paintComponent(Graphics g1D) { if (theImage == null || theRectangle == null) return; Graphics2D g = (Graphics2D) g1D.create(); Insets insets = getInsets(); int xOffset = insets.left; int yOffset = insets.top; int availableWidth = getWidth() - insets.left - insets.right; int availableHeight = getHeight() - insets.top - insets.bottom; int scaleFactor = UIUtil.getScaleFactor(); g.drawImage(theImage, xOffset, yOffset, theImage.getWidth() / scaleFactor, theImage.getHeight() / scaleFactor, null); Color tmpColor = g.getColor(); Area area = new Area(new Rectangle(xOffset, yOffset, availableWidth, availableHeight)); area.subtract(new Area(theRectangle)); g.setColor(new Color(200, 200, 200, 128)); g.fill(area); g.setColor(Color.BLACK); g.draw(theRectangle); g.setColor(tmpColor); g.dispose(); } // -- Private methods ------ void installOnScrollPane(JScrollPane aScrollPane) { if (theScrollPane != null) uninstallFromScrollPane(); theScrollPane = aScrollPane; theFormerLayoutManager = theScrollPane.getLayout(); theScrollPane.setLayout(new TweakedScrollPaneLayout()); theScrollPane.firePropertyChange("layoutManager", false, true); theScrollPane.addPropertyChangeListener(COMPONENT_ORIENTATION, propertyChangeListener); theScrollPane.getViewport().addContainerListener( theViewPortViewListener); theScrollPane.setCorner(JScrollPane.LOWER_TRAILING_CORNER, theButton); Component comp = theScrollPane.getViewport().getView(); theComponent = (comp instanceof JComponent) ? (JComponent) comp : null; this.theButton.setIcon(LafWidgetRepository.getRepository() .getLafSupport().getSearchIcon( this.theButton, UIManager.getInt("ScrollBar.width") - 3, theScrollPane.getComponentOrientation())); theScrollPane.doLayout(); } void uninstallFromScrollPane() { if (theScrollPane == null) return; if (thePopupMenu.isVisible()) thePopupMenu.setVisible(false); theScrollPane.setCorner(JScrollPane.LOWER_TRAILING_CORNER, null); theScrollPane.removePropertyChangeListener(COMPONENT_ORIENTATION, propertyChangeListener); theScrollPane.getViewport().removeContainerListener( theViewPortViewListener); theScrollPane.setLayout(theFormerLayoutManager); theScrollPane.firePropertyChange("layoutManager", true, false); theScrollPane = null; } private void display(Point aPointOnScreen) { if (theComponent == null) return; PreviewPainter previewPainter = LafWidgetUtilities2 .getComponentPreviewPainter(theScrollPane); if (!previewPainter.hasPreview(theComponent.getParent(), theComponent, 0)) return; Dimension pDimension = previewPainter.getPreviewWindowDimension( theComponent.getParent(), theComponent, 0); double compWidth = theComponent.getWidth(); double compHeight = theComponent.getHeight(); double scaleX = pDimension.getWidth() / compWidth; double scaleY = pDimension.getHeight() / compHeight; theScale = Math.min(scaleX, scaleY); int previewWidth = (int) (theComponent.getWidth() * theScale); int previewHeight = (int) (theComponent.getHeight() * theScale); theImage = LafWidgetUtilities2.getBlankImage(previewWidth, previewHeight); Graphics2D g = theImage.createGraphics(); previewPainter.previewComponent(null, theComponent, 0, g, 0, 0, theImage.getWidth(), theImage.getHeight()); g.dispose(); theStartRectangle = theComponent.getVisibleRect(); Insets insets = getInsets(); theStartRectangle.x = (int) (theScale * theStartRectangle.x + insets.left); theStartRectangle.y = (int) (theScale * theStartRectangle.y + insets.right); theStartRectangle.width *= theScale; theStartRectangle.height *= theScale; theRectangle = theStartRectangle; Dimension pref = thePopupMenu.getPreferredSize(); Point buttonLocation = theButton.getLocationOnScreen(); Point popupLocation = new Point( (theButton.getWidth() - pref.width) / 2, (theButton.getHeight() - pref.height) / 2); Point centerPoint = new Point(buttonLocation.x + popupLocation.x + theRectangle.x + theRectangle.width / 2, buttonLocation.y + popupLocation.y + theRectangle.y + theRectangle.height / 2); try { // Attempt to move the mouse pointer to the center of the selector's // rectangle. new Robot().mouseMove(centerPoint.x, centerPoint.y); theStartPoint = centerPoint; } catch (Exception e) { // Since we cannot move the cursor, we'll move the popup instead. theStartPoint = aPointOnScreen; popupLocation.x += theStartPoint.x - centerPoint.x; popupLocation.y += theStartPoint.y - centerPoint.y; } thePrevPoint = new Point(theStartPoint); toRestoreOriginal = true; thePopupMenu.show(theButton, popupLocation.x, popupLocation.y); } private void syncRectangle() { JViewport viewport = this.theScrollPane.getViewport(); Rectangle viewRect = viewport.getViewRect(); Insets insets = getInsets(); Rectangle newRect = new Rectangle(); newRect.x = (int) (theScale * viewRect.x + insets.left); newRect.y = (int) (theScale * viewRect.y + insets.top); newRect.width = (int) (viewRect.width * theScale); newRect.height = (int) (viewRect.height * theScale); Rectangle clip = new Rectangle(); Rectangle.union(theRectangle, newRect, clip); clip.grow(2, 2); theRectangle = newRect; paintImmediately(clip); } private void scroll(final int aDeltaX, final int aDeltaY, boolean toAnimate) { if (theComponent == null) return; final Rectangle oldRectangle = theComponent.getVisibleRect(); final Rectangle newRectangle = new Rectangle(oldRectangle.x + aDeltaX, oldRectangle.y + aDeltaY, oldRectangle.width, oldRectangle.height); // Animate scrolling if (toAnimate) { Timeline scrollTimeline = new Timeline(theComponent); AnimationConfigurationManager.getInstance().configureTimeline( scrollTimeline); scrollTimeline.addCallback(new UIThreadTimelineCallbackAdapter() { @Override public void onTimelineStateChanged(TimelineState oldState, TimelineState newState, float durationFraction, float timelinePosition) { if ((oldState == TimelineState.DONE) && (newState == TimelineState.IDLE)) { theComponent.scrollRectToVisible(newRectangle); syncRectangle(); } } @Override public void onTimelinePulse(float durationFraction, float timelinePosition) { int x = (int) (oldRectangle.x + timelinePosition * aDeltaX); int y = (int) (oldRectangle.y + timelinePosition * aDeltaY); theComponent.scrollRectToVisible(new Rectangle(x, y, oldRectangle.width, oldRectangle.height)); syncRectangle(); } }); scrollTimeline.play(); } else { theComponent.scrollRectToVisible(newRectangle); syncRectangle(); } } }