LightScrollPane.java

package com.jsql.view.swing.scrollpane;

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

import javax.swing.*;
import javax.swing.plaf.basic.BasicScrollBarUI;
import java.awt.*;

public class LightScrollPane extends JComponent {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();
    
    protected int scrollBarAlpha = 25;
    
    protected int scrollBarAlphaRollover = 100;
    
    private static final int THUMB_BORDER_SIZE = 0;
    
    public static final int THUMB_SIZE = 15;
    
    protected Color colorThumb = Color.DARK_GRAY;

    public final JScrollPane scrollPane;
    
    private final JScrollBar verticalScrollBar;
    
    private final JScrollBar horizontalScrollBar;

    /**
     * Create a scrollpane with top and left border for default component and a slide one.
     * A component slided to the right will normally hide the left border, JScrollPanePixelBorder fix this.
     * @param top Border top size
     * @param left Border left size
     * @param bottom Border bottom size
     * @param right Border right size
     * @param c Component to decorate
     */
    public LightScrollPane(int top, int left, int bottom, int right, JComponent c) {
        
        this(c);
        this.setBorder(BorderFactory.createMatteBorder(top, left, bottom, right, UiUtil.COLOR_COMPONENT_BORDER));
    }
    
    public LightScrollPane(JComponent component) {
        
        this.scrollPane = new JScrollPanePixelBorder(component);
        this.scrollPane.setBorder(BorderFactory.createEmptyBorder());
        
        this.verticalScrollBar = this.scrollPane.getVerticalScrollBar();
        this.verticalScrollBar.setVisible(false);
        this.verticalScrollBar.setOpaque(false);
        this.verticalScrollBar.setUI(new MyScrollBarUI());

        this.horizontalScrollBar = this.scrollPane.getHorizontalScrollBar();
        this.horizontalScrollBar.setVisible(false);
        this.horizontalScrollBar.setOpaque(false);
        this.horizontalScrollBar.setUI(new MyScrollBarUI());

        var layeredPane = new JLayeredPane();
        layeredPane.setLayer(this.verticalScrollBar, JLayeredPane.PALETTE_LAYER);
        layeredPane.setLayer(this.horizontalScrollBar, JLayeredPane.PALETTE_LAYER);

        this.scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
        this.scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        this.scrollPane.setLayout(new ScrollPaneLayout() {
            
            @Override
            public void layoutContainer(Container parent) {
                
                // Fix #13412: NullPointerException on setBounds()
                // Fix #48549: IllegalStateException on setBounds()
                // Implementation by sun.swing.SwingUtilities2.getFontMetrics()
                try {
                    this.viewport.setBounds(0, 0, LightScrollPane.this.getWidth(), LightScrollPane.this.getHeight() - 1);
                } catch (NullPointerException e) {
                    LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
                }
                
                SwingUtilities.invokeLater(() -> LightScrollPane.this.displayScrollBarsIfNecessary(this.viewport));
            }
        });
        
        layeredPane.add(this.horizontalScrollBar);
        layeredPane.add(this.verticalScrollBar);
        layeredPane.add(this.scrollPane);

        this.setLayout(new BorderLayout() {
            
            @Override
            public void layoutContainer(Container target) {
                
                super.layoutContainer(target);
                int width = LightScrollPane.this.getWidth();
                int height = LightScrollPane.this.getHeight();
                
                LightScrollPane.this.scrollPane.setBounds(0, 0, width, height);

                int scrollBarSize = THUMB_SIZE;
                int cornerOffset =
                    LightScrollPane.this.verticalScrollBar.isVisible()
                    && LightScrollPane.this.horizontalScrollBar.isVisible()
                    ? scrollBarSize
                    : 0;
                
                if (LightScrollPane.this.verticalScrollBar.isVisible()) {
                    
                    LightScrollPane.this.verticalScrollBar.setBounds(
                        LightScrollPane.this.getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT
                        ? 0
                        : width - scrollBarSize,
                        0,
                        scrollBarSize,
                        height - cornerOffset
                    );
                }
                
                if (LightScrollPane.this.horizontalScrollBar.isVisible()) {
                    
                    LightScrollPane.this.horizontalScrollBar.setBounds(
                        0,
                        height - scrollBarSize,
                        width - cornerOffset,
                        scrollBarSize
                    );
                }
            }
        });
        this.add(layeredPane, BorderLayout.CENTER);
        layeredPane.setBackground(Color.BLUE);
    }

    private void displayScrollBarsIfNecessary(JViewport viewPort) {
        
        this.displayVerticalScrollBarIfNecessary(viewPort);
        this.displayHorizontalScrollBarIfNecessary(viewPort);
    }

    private void displayVerticalScrollBarIfNecessary(JViewport viewPort) {
        
        Rectangle viewRect = viewPort.getViewRect();
        Dimension viewSize = viewPort.getViewSize();
        boolean isDisplayingVerticalScrollBar = viewSize.getHeight() > viewRect.getHeight();
        
        this.verticalScrollBar.setVisible(isDisplayingVerticalScrollBar);
    }

    private void displayHorizontalScrollBarIfNecessary(JViewport viewPort) {
        
        Rectangle viewRect = viewPort.getViewRect();
        Dimension viewSize = viewPort.getViewSize();
        boolean isDisplayingHorizontalScrollBar = viewSize.getWidth() > viewRect.getWidth();
        
        this.horizontalScrollBar.setVisible(isDisplayingHorizontalScrollBar);
    }

    private static class MyScrollBarButton extends JButton {
        private MyScrollBarButton() {

            this.setOpaque(false);
            this.setFocusable(false);
            this.setFocusPainted(false);
            this.setBorderPainted(false);
            this.setBorder(BorderFactory.createEmptyBorder());
        }
    }

    private class MyScrollBarUI extends BasicScrollBarUI {
        
        @Override
        protected JButton createDecreaseButton(int orientation) {
            return new MyScrollBarButton();
        }

        @Override
        protected JButton createIncreaseButton(int orientation) {
            return new MyScrollBarButton();
        }

        @Override
        protected void paintTrack(Graphics g, JComponent c, Rectangle trackBounds) {
            // Nothing
        }

        @Override
        protected Dimension getMinimumThumbSize() {
            return new Dimension(THUMB_SIZE, 24 + 2*2);
        }
        
        @Override
        protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
            
            int alpha = this.isThumbRollover() ? LightScrollPane.this.scrollBarAlphaRollover : LightScrollPane.this.scrollBarAlpha;
            int orientation = this.scrollbar.getOrientation();
            int x = thumbBounds.x + THUMB_BORDER_SIZE;
            int y = thumbBounds.y + THUMB_BORDER_SIZE;

            int width = orientation == Adjustable.VERTICAL
                ? THUMB_SIZE
                : thumbBounds.width - THUMB_BORDER_SIZE * 2;

            width = Math.max(width, THUMB_SIZE);

            int height = orientation == Adjustable.VERTICAL
                ? thumbBounds.height - THUMB_BORDER_SIZE * 2
                : THUMB_SIZE;

            height = Math.max(height, THUMB_SIZE);

            var graphics2D = (Graphics2D) g.create();
            graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            
            // Fix macOS Color.DARK_GRAY and alpha incompatibility
            Color colorThumbAlpha;
            
            try {
                colorThumbAlpha = new Color(LightScrollPane.this.colorThumb.getRed(), LightScrollPane.this.colorThumb.getGreen(), LightScrollPane.this.colorThumb.getBlue(), alpha);
            } catch (NullPointerException e) {
                
                colorThumbAlpha = Color.GRAY;
                LOGGER.log(LogLevelUtil.IGNORE, e);
            }
            
            graphics2D.setColor(colorThumbAlpha);
            
            // Unhandled NoClassDefFoundError #65554: Could not initialize class sun.dc.pr.Rasterizer
            graphics2D.fillRect(x, y, width, height);
            
            graphics2D.dispose();
        }
    }
}