All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tascape.reactor.ui.SmartScroller Maven / Gradle / Ivy

/*
 * Copyright 2015- 2016 Nebula Bay.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.tascape.reactor.ui;

import java.awt.Component;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.BoundedRangeModel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.text.DefaultCaret;
import javax.swing.text.JTextComponent;

/**
 * The SmartScroller will attempt to keep the viewport positioned based on
 * the users interaction with the scrollbar. The normal behaviour is to keep
 * the viewport positioned to see new data as it is dynamically added.
 * https://tips4java.wordpress.com/2013/03/03/smart-scrolling/
 *
 * Assuming vertical scrolling and data is added to the bottom:
 *
 * - when the viewport is at the bottom and new data is added,
 * then automatically scroll the viewport to the bottom
 * - when the viewport is not at the bottom and new data is added,
 * then do nothing with the viewport
 *
 * Assuming vertical scrolling and data is added to the top:
 *
 * - when the viewport is at the top and new data is added,
 * then do nothing with the viewport
 * - when the viewport is not at the top and new data is added, then adjust
 * the viewport to the relative position it was at before the data was added
 *
 * Similiar logic would apply for horizontal scrolling.
 */
public class SmartScroller implements AdjustmentListener {

    public static final int HORIZONTAL = 0;

    public static final int VERTICAL = 1;

    public static final int START = 0;

    public static final int END = 1;

    private int viewportPosition;

    private JScrollBar scrollBar;

    private boolean adjustScrollBar = true;

    private int previousValue = -1;

    private int previousMaximum = -1;

    /**
     * Convenience constructor.
     * Scroll direction is VERTICAL and viewport position is at the END.
     *
     * @param scrollPane the scroll pane to monitor
     */
    public SmartScroller(JScrollPane scrollPane) {
        this(scrollPane, VERTICAL, END);
    }

    /**
     * Convenience constructor.
     * Scroll direction is VERTICAL.
     *
     * @param scrollPane       the scroll pane to monitor
     * @param viewportPosition valid values are START and END
     */
    public SmartScroller(JScrollPane scrollPane, int viewportPosition) {
        this(scrollPane, VERTICAL, viewportPosition);
    }

    /**
     * Specify how the SmartScroller will function.
     *
     * @param scrollPane       the scroll pane to monitor
     * @param scrollDirection  indicates which JScrollBar to monitor.
     *                         Valid values are HORIZONTAL and VERTICAL.
     * @param viewportPosition indicates where the viewport will normally be
     *                         positioned as data is added.
     *                         Valid values are START and END
     */
    public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition) {
        if (scrollDirection != HORIZONTAL && scrollDirection != VERTICAL) {
            throw new IllegalArgumentException("invalid scroll direction specified");
        }
        if (viewportPosition != START && viewportPosition != END) {
            throw new IllegalArgumentException("invalid viewport position specified");
        }
        this.viewportPosition = viewportPosition;
        if (scrollDirection == HORIZONTAL) {
            scrollBar = scrollPane.getHorizontalScrollBar();
        } else {
            scrollBar = scrollPane.getVerticalScrollBar();
        }
        scrollBar.addAdjustmentListener(this);
        //  Turn off automatic scrolling for text components
        Component view = scrollPane.getViewport().getView();
        if (view instanceof JTextComponent) {
            JTextComponent textComponent = (JTextComponent) view;
            DefaultCaret caret = (DefaultCaret) textComponent.getCaret();
            caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
        }
    }

    @Override
    public void adjustmentValueChanged(final AdjustmentEvent e) {
        SwingUtilities.invokeLater(() -> {
            checkScrollBar(e);
        });
    }

    /*
     * Analyze every adjustment event to determine when the viewport
     * needs to be repositioned.
     */
    private void checkScrollBar(AdjustmentEvent e) {
        //  The scroll bar listModel contains information needed to determine
        //  whether the viewport should be repositioned or not.
        JScrollBar jsb = (JScrollBar) e.getSource();
        BoundedRangeModel listModel = jsb.getModel();
        int value = listModel.getValue();
        int extent = listModel.getExtent();
        int maximum = listModel.getMaximum();
        boolean valueChanged = previousValue != value;
        boolean maximumChanged = previousMaximum != maximum;
        //  Check if the user has manually repositioned the scrollbar
        if (valueChanged && !maximumChanged) {
            if (viewportPosition == START) {
                adjustScrollBar = value != 0;
            } else {
                adjustScrollBar = value + extent >= maximum;
            }
        }
        //  Reset the "value" so we can reposition the viewport and
        //  distinguish between a user scroll and a program scroll.
        //  (ie. valueChanged will be false on a program scroll)
        if (adjustScrollBar && viewportPosition == END) {
            //  Scroll the viewport to the end.
            jsb.removeAdjustmentListener(this);
            value = maximum - extent;
            jsb.setValue(value);
            jsb.addAdjustmentListener(this);
        }
        if (adjustScrollBar && viewportPosition == START) {
            //  Keep the viewport at the same relative viewportPosition
            jsb.removeAdjustmentListener(this);
            value = value + maximum - previousMaximum;
            jsb.setValue(value);
            jsb.addAdjustmentListener(this);
        }
        previousValue = value;
        previousMaximum = maximum;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy