JScrollIndicator.java

package com.jsql.view.swing.scrollpane;

import com.jsql.util.LogLevelUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.*;
import javax.swing.plaf.ScrollBarUI;
import javax.swing.plaf.basic.BasicScrollBarUI;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

/**
 * A scrollpane like component, where the scroll bars are floating over the
 * scrollable view to indicate the current scroll positions.
 * The scroll indicators appear smoothly during scroll events and disappear
 * smoothly afterward.
 * <p>
 * The scrollbars can be dragged just as normal.</p>
 * <p>
 * The usage is similar to a classic scrollpane.</p>
 *
 * @author Jolly Littlebottom
 */
public class JScrollIndicator extends JLayeredPane {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();

    private static final int SCROLL_BAR_ALPHA_ROLLOVER = 100;
    private static final int SCROLL_BAR_ALPHA = 25;

    private static final Color THUMB_COLOR = Color.DARK_GRAY;
    private static final int THUMB_THICKNESS = 15;
    private static final int THUMB_MIN_SIZE = 48;
    private static final int THUMB_MARGIN = 0;

    private final JScrollPane scrollPane;
    private final ControlPanel controlPanel;

    /**
     * Creates a <code>JScrollIndicator</code> that displays the contents of the
     * specified component, where both horizontal and vertical scrollbars appear
     * whenever the component's contents are larger than the view and scrolling
     * in underway or the mouse is over the scrollbar position.
     *
     * see #setViewportView
     * @param view the component to display in the scrollpane's viewport
     */
    public JScrollIndicator(final JComponent view) {
        this(view, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }
    
    public JScrollIndicator(final JComponent view, int scrollPaneConstants) {
        this(view, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, scrollPaneConstants);
    }

    /**
     * Creates a <code>JScrollIndicator</code> that displays the view component
     * in a viewport whose view position can be controlled with a pair of
     * scrollbars.
     * The scrollbar policies specify when the scrollbars are displayed,
     * For example, if <code>vsbPolicy</code> is
     * <code>JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED</code>
     * then the vertical scrollbar only appears if the view doesn't fit
     * vertically. The available policy settings are listed at
     * {link #JScrollPane.setVerticalScrollBarPolicy} and
     * {link #JScrollPane.setHorizontalScrollBarPolicy}.
     *
     * @param view the component to display in the scrollpanes viewport
     * @param vsbPolicy an integer that specifies the vertical scrollbar policy
     * @param hsbPolicy an integer that specifies the horizontal scrollbar policy
     */
    public JScrollIndicator(final JComponent view, int vsbPolicy, int hsbPolicy) {
        
        this.scrollPane = new JScrollPane(view, vsbPolicy, hsbPolicy);
        this.scrollPane.setBorder(BorderFactory.createEmptyBorder());
        this.add(this.scrollPane, JLayeredPane.DEFAULT_LAYER);

        this.controlPanel = new ControlPanel(this.scrollPane);
        this.add(this.controlPanel, JLayeredPane.PALETTE_LAYER);

        this.addComponentListener(
            new ComponentAdapter() {
                
                @Override
                public void componentResized(ComponentEvent e) {
                    
                    // listen to changes of JLayeredPane size
                    JScrollIndicator.this.scrollPane.setSize(JScrollIndicator.this.getSize());
                    JScrollIndicator.this.scrollPane.getViewport().revalidate();
                    JScrollIndicator.this.controlPanel.setSize(JScrollIndicator.this.getSize());
                    JScrollIndicator.this.controlPanel.revalidate();
                }
            }
        );
    }

    /**
     * Returns the scroll pane used by this scroll indicator.
     * Use carefully (e.g. to set unit increments) because not all changes have an
     * effect. You have to write listeners in these cases (e.g. for changing the
     * scrollbar policy)
     * 
     * @return
     */
    public JScrollPane getScrollPane() {
        return this.scrollPane;
    }

    private class ControlPanel extends JPanel {

        private ControlPanel(JScrollPane scrollPane) {
            
            this.setLayout(new BorderLayout());
            this.setOpaque(false);

            JMyScrollBar vScrollBar = new JMyScrollBar(Adjustable.VERTICAL);
            scrollPane.setVerticalScrollBar(vScrollBar);
            scrollPane.remove(vScrollBar);
            
            if (scrollPane.getVerticalScrollBarPolicy() != ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER) {
                this.add(vScrollBar, BorderLayout.EAST);
            }

            JMyScrollBar hScrollBar = new JMyScrollBar(Adjustable.HORIZONTAL);
            scrollPane.setHorizontalScrollBar(hScrollBar);
            scrollPane.remove(hScrollBar);
            
            if (scrollPane.getHorizontalScrollBarPolicy() != ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER) {
                this.add(hScrollBar, BorderLayout.SOUTH);
            }
        }
    }

    private class JMyScrollBar extends JScrollBar {

        protected final transient MyScrollBarUI scrollUI;

        public JMyScrollBar(int direction) {
            
            super(direction);

            this.scrollUI = new MyScrollBarUI(this);
            super.setUI(this.scrollUI);
            this.setUnitIncrement(64);
            int size = THUMB_THICKNESS + THUMB_MARGIN;
            this.setPreferredSize(new Dimension(size, size));
            this.scrollUI.setVisible();
            
            this.addMouseListener(new MouseAdapter() {
                
                @Override
                public void mouseEntered(MouseEvent e) {
                    JMyScrollBar.this.scrollUI.setVisible();
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    JMyScrollBar.this.scrollUI.setVisible();
                }
            });

            this.addAdjustmentListener(adjustmentEvent -> this.scrollUI.setVisible());
        }

        @Override
        public void setUI(ScrollBarUI ui) {
            // Nothing
        }

        @Override
        public void updateUI() {
            // Nothing
        }

        @Override
        public void paint(Graphics g) {
            this.scrollUI.paintThumb(g); // just the thumb
        }

        @Override
        public void repaint(Rectangle r) {
            
            JScrollIndicator scrollIndicator = JScrollIndicator.this;
            
            // Fix #15956: NullPointerException on convertRectangle()
            try {
                var rect = SwingUtilities.convertRectangle(this, r, scrollIndicator);
                rect.grow(1, 1);
                // ensure for a translucent thumb, that the view is first painted
                scrollIndicator.repaint(rect);
                
            } catch (NullPointerException e) {
                LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
            }
        }
    }

    public class MyScrollBarUI extends BasicScrollBarUI {
        
        private final JMyScrollBar myScrollBar;
        private int alpha = 0;

        private MyScrollBarUI(JMyScrollBar scrollBar) {
            this.myScrollBar = scrollBar;
        }

        @Override
        protected void installComponents() {
            
            this.incrButton = new JButton();
            this.decrButton = new JButton();
            
            if (this.myScrollBar.getOrientation() == Adjustable.HORIZONTAL) {
                
                int size = THUMB_THICKNESS + THUMB_MARGIN; // let lower right corner empty
                this.incrButton.setPreferredSize(new Dimension(size, size));
                
            } else {
                this.incrButton.setPreferredSize(new Dimension(THUMB_MARGIN, THUMB_MARGIN));
            }
            
            this.decrButton.setPreferredSize(new Dimension(THUMB_MARGIN, THUMB_MARGIN));
        }

        @Override
        protected void installDefaults() {
            
            super.installDefaults();

            // ensure the minimum size of the thumb
            int w = this.minimumThumbSize.width;
            int h = this.minimumThumbSize.height;
            
            if (this.myScrollBar.getOrientation() == Adjustable.VERTICAL) {
                h = Math.max(h, Math.min(this.maximumThumbSize.height, THUMB_MIN_SIZE));
            } else {
                w = Math.max(w, Math.min(this.maximumThumbSize.width, THUMB_MIN_SIZE));
            }
            
            this.minimumThumbSize = new Dimension(w, h);
        }

        private void paintThumb(Graphics g) {
            
            int alphaThumb = this.isThumbRollover()
                ? SCROLL_BAR_ALPHA_ROLLOVER
                : SCROLL_BAR_ALPHA;

            g.setColor(
                new Color(
                    this.getAlphaColor().getRed(),
                    this.getAlphaColor().getGreen(),
                    this.getAlphaColor().getBlue(),
                    alphaThumb
                )
            );
            
            Rectangle thumbBounds = this.getThumbBounds();

            int x = thumbBounds.x;
            int y = thumbBounds.y;
            int w = thumbBounds.width;
            int h = thumbBounds.height;

            if (this.myScrollBar.getOrientation() == Adjustable.VERTICAL) {
                w -= THUMB_MARGIN;
            } else {
                h -= THUMB_MARGIN;
            }

            g.fillRect(x, y, w, h);
        }

        private Color getAlphaColor() {
            
            if (this.alpha == 100) {
                return JScrollIndicator.THUMB_COLOR;
            }
            
            int rgb = JScrollIndicator.THUMB_COLOR.getRGB() & 0xFFFFFF; // color without alpha values
            rgb |= (this.alpha / 100 * 255) << 24; // add alpha value
            
            return new Color(rgb, true);
        }

        public void setAlpha(int alpha) {
            
            this.alpha = alpha;
            this.myScrollBar.repaint(this.getThumbBounds());
        }

        public void setVisible() {
            this.myScrollBar.repaint(this.getThumbBounds());
        }
    }
}