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

org.wings.SScrollPane Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2000,2005 wingS development team.
 *
 * This file is part of wingS (http://wingsframework.org).
 *
 * wingS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * Please see COPYING for the complete licence.
 */
package org.wings;

import java.awt.Adjustable;
import java.awt.LayoutManager;
import java.awt.Rectangle;

import javax.swing.BoundedRangeModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import org.wings.event.SViewportChangeEvent;
import org.wings.event.SViewportChangeListener;
import org.wings.plaf.ScrollPaneCG;

/**
 * A pane which allows to add {@link Scrollable} components on this pane
 * and display only a viewport of it. Can be used to scroll graphically
 * (default) or pages (using {@link SPageScroller} components.).
 *
 * @author Armin Haaf
 */
public class SScrollPane
        extends SContainer
        implements javax.swing.ScrollPaneConstants {

    public static final int MODE_SCROLLING = 0;
    public static final int MODE_COMPLETE = 1;
    public static final int MODE_PAGING = 2;

    /**
     * The element which should be scrolled.
     */
    protected Scrollable scrollable;

    protected Adjustable verticalScrollBar = null;

    protected Adjustable horizontalScrollBar = null;

    protected int horizontalScrollBarPolicy = HORIZONTAL_SCROLLBAR_AS_NEEDED;

    protected int verticalScrollBarPolicy = VERTICAL_SCROLLBAR_AS_NEEDED;

    protected int horizontalExtent = 10;

    protected int verticalExtent = 10;

    protected int mode = MODE_SCROLLING;

    /**
     * While the (horizontal) maximum of columns displayed by this scrollpane
     * will never be greater than the number of available columns in the model
     * of the contained scrollable, the (vertical) maximum of rows displayed by
     * this scrollpane CAN be greater than the number of available rows in the
     * model of the contained scrollable. For example this might be true if ...
     *   - MODE == SCROLLING and this scrollpane's vertical extent was set to
     *     something greater than the number of available rows in the model, or
     *   - MODE == PAGING and there are not enough rows in the model in order
     *     to fill up the last page so that the full vertical extent is reached.
     * In such cases the scrollable's CG fills the difference with empty lines.
     * The "virtualViewportHeight" equals der number of available rows in the
     * scrollable's model (typically "scrollable.getScrollableViewport()") PLUS
     * the number of empty lines needed to fill the viewport until the vertical
     * extent of this scrollpane is reached.
     * If MODE == COMPLETE this variable equals the number of available rows.
     */
    protected int virtualViewportHeight;

    /**
     * Holds the viewport that the scrollable had before adding it to this
     * scrollpane. The scrollable's viewport is reset to this value, if it
     * is removed from the this scrollpane.
     */
    protected Rectangle backupViewport;

    protected SViewportSynchronizationModel horizontalModel = new SViewportSynchronizationModel(true);

    protected SViewportSynchronizationModel verticalModel = new SViewportSynchronizationModel(false);


    public SScrollPane() {
        super(new SScrollPaneLayout());
        setHorizontalScrollBar(new SScrollBar(SConstants.HORIZONTAL));
        setVerticalScrollBar(new SScrollBar(SConstants.VERTICAL));
    }

    public SScrollPane(SComponent c) {
        this();
        setViewportView(c);
    }

    /**
     * Sets the element which should be scrolled.
     *
     * @param c the element which should be scrolled.
     */
    protected void setScrollable(SComponent c) {
        Scrollable oldVal = this.scrollable;
        if (scrollable != null) {
            // reset the scrollable's viewport to the one
            // it had before adding it to this scrollpane
            scrollable.setViewportSize(backupViewport);
        }

        if (c instanceof Scrollable) {
            scrollable = (Scrollable) c;
            horizontalModel.setScrollable(scrollable);
            verticalModel.setScrollable(scrollable);

            // keep the scrollable's original viewport - the
            // one it had before adding it to this scrollpane
            backupViewport = scrollable.getViewportSize();

            // apply new viewport to scrollable
            setInitialViewportSize();
        } else {
            scrollable = null;
        }

        reload();
        propertyChangeSupport.firePropertyChange("scrollable", oldVal, this.scrollable);
    }

    /**
     * Returns the element which should be scrolled.
     *
     * @return the element which should be scrolled.
     */
    public final Scrollable getScrollable() {
        return scrollable;
    }

    /**
     * Sets the scrollable.
     * If there is already one, it will be removed first.
     *
     * @param view the component to add to the viewport
     */
    public void setViewportView(SComponent view) {
        //SComponent oldVal = t.b.d.;
        add(view, SScrollPaneLayout.VIEWPORT);
        String oldVal = "oldValue";
        propertyChangeSupport.firePropertyChange("viewportView", oldVal , view);
    }
      
    /**
     * Only {@link Scrollable scrollables} are allowed here!
     */
    @Override
    public SComponent addComponent(SComponent c, Object constraint, int index) {
        if (c instanceof Scrollable || constraint == SScrollPaneLayout.VIEWPORT) {
            super.addComponent(c, SScrollPaneLayout.VIEWPORT, index);
            setScrollable(c);
        } else {
            super.addComponent(c, constraint, index);
        }
        return c;
    }

    protected SComponent addMe(SComponent c, Object constraint, int index) {
        return super.addComponent(c, constraint, index);
    }

    public int getMode() {
        return mode;
    }

    public void setMode(int mode) {
        int oldVal = this.mode;
        reloadIfChange(this.mode, mode);
        this.mode = mode;
        setInitialViewportSize();
        propertyChangeSupport.firePropertyChange("mode", oldVal, this.mode);
    }

    public void setCG(ScrollPaneCG cg) {
        super.setCG(cg);
    }

    /**
     * Returns the horizontal scroll bar.
     *
     * @return the scrollbar that controls the viewports horizontal view position
     */
    public Adjustable getHorizontalScrollBar() {
        return horizontalScrollBar;
    }

    /**
     * Sets the horizontal scroll bar.
     *
     * @param sb the scrollbar that controls the viewports horizontal view position
     */
    public void setHorizontalScrollBar(Adjustable sb) {
        setHorizontalScrollBar(sb, SScrollPaneLayout.SOUTH);
    }

    /**
     * Sets the horizontal scrollbar.
     *
     * @param sb         the scrollbar that controls the viewports horizontal view position
     * @param constraint the constraint for the {@link LayoutManager} of this {@link SContainer}.
     *                   The {@link LayoutManager} is per default {@link SScrollPaneLayout}.
     */
    public void setHorizontalScrollBar(Adjustable sb, String constraint) {
        Adjustable oldVal = this.horizontalScrollBar;
        if (horizontalScrollBar != null) {
            if (horizontalScrollBar instanceof SAbstractAdjustable)
                ((SAbstractAdjustable) horizontalScrollBar).setModel(new SDefaultBoundedRangeModel());

            if (horizontalScrollBar instanceof SComponent)
                super.remove((SComponent) horizontalScrollBar);
        }

        horizontalScrollBar = sb;

        if (horizontalScrollBar != null) {
            if (horizontalScrollBar instanceof SComponent)
                super.addComponent((SComponent) horizontalScrollBar, constraint, getComponentCount());

            if (horizontalScrollBar instanceof SAbstractAdjustable) {
                SAbstractAdjustable scrollbar = (SAbstractAdjustable) horizontalScrollBar;
                if (scrollbar.getOrientation() == SConstants.HORIZONTAL)
                    scrollbar.setModel(horizontalModel);
                else
                    scrollbar.setModel(verticalModel);
            }

            adoptScrollBarVisibility(horizontalScrollBar, horizontalScrollBarPolicy);
        }

        reload();
        propertyChangeSupport.firePropertyChange("horizontalScrollBar", oldVal, this.horizontalScrollBar);
    }

    /**
     * Returns the horizontal scroll bar policy value.
     *
     * @return the horizontal scrollbar policy.
     * @see #setHorizontalScrollBarPolicy(int)
     */
    public final int getHorizontalScrollBarPolicy() {
        return horizontalScrollBarPolicy;
    }

    /**
     * Returns the vertical scrollbar.
     *
     * @return the scrollbar that controls the viewports vertical view position
     */
    public final Adjustable getVerticalScrollBar() {
        return verticalScrollBar;
    }

    /**
     * Sets the vertical scroll bar.
     *
     * @param sb the scrollbar that controls the viewports vertical view position
     */
    public void setVerticalScrollBar(Adjustable sb) {
        setVerticalScrollBar(sb, SScrollPaneLayout.EAST);
    }

    /**
     * Sets the vertical scroll bar.
     *
     * @param sb         the scrollbar that controls the viewports vertical view position
     * @param constraint the constraint for the {@link LayoutManager} of this {@link SContainer}.
     *                   The {@link LayoutManager} is per default {@link SScrollPaneLayout}.
     */
    public void setVerticalScrollBar(Adjustable sb, String constraint) {
        Adjustable oldVal = this.verticalScrollBar;
        if (verticalScrollBar != null) {
            if (verticalScrollBar instanceof SAbstractAdjustable)
                ((SAbstractAdjustable) verticalScrollBar).setModel(new SDefaultBoundedRangeModel());

            if (verticalScrollBar instanceof SComponent)
                super.remove((SComponent) verticalScrollBar);
        }

        verticalScrollBar = sb;

        if (verticalScrollBar != null) {
            if (verticalScrollBar instanceof SComponent)
                super.addComponent((SComponent) verticalScrollBar, constraint, getComponentCount());

            if (verticalScrollBar instanceof SAbstractAdjustable) {
                SAbstractAdjustable scrollbar = (SAbstractAdjustable) verticalScrollBar;
                if (scrollbar.getOrientation() == SConstants.HORIZONTAL)
                    scrollbar.setModel(horizontalModel);
                else
                    scrollbar.setModel(verticalModel);
            }

            adoptScrollBarVisibility(verticalScrollBar, verticalScrollBarPolicy);
        }

        reload();
        propertyChangeSupport.firePropertyChange("verticalScrollBar", oldVal, this.verticalScrollBar);
    }

    /**
     * Returns the vertical scroll bar policy value.
     *
     * @return the vertical scrollbar policy.
     * @see #setVerticalScrollBarPolicy(int)
     */
    public final int getVerticalScrollBarPolicy() {
        return verticalScrollBarPolicy;
    }

    /**
     * Determines when the horizontal scrollbar appears in the scrollpane.
     * The options are:
     * 
  • SScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
  • *
  • SScrollPane.HORIZONTAL_SCROLLBAR_NEVER
  • *
  • SScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS
  • */ public void setHorizontalScrollBarPolicy(int policy) { if (policy != horizontalScrollBarPolicy) { int oldVal = this.horizontalScrollBarPolicy; horizontalScrollBarPolicy = policy; adoptScrollBarVisibility(horizontalScrollBar, policy); reload(); propertyChangeSupport.firePropertyChange("horizontalScrollBar", oldVal, this.horizontalScrollBarPolicy); } } /** * Determines when the vertical scrollbar appears in the scrollpane. * The options are: *
  • SScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
  • *
  • SScrollPane.VERTICAL_SCROLLBAR_NEVER
  • *
  • SScrollPane.VERTICAL_SCROLLBAR_ALWAYS
  • */ public void setVerticalScrollBarPolicy(int policy) { if (policy != verticalScrollBarPolicy) { int oldVal = this.verticalScrollBarPolicy; verticalScrollBarPolicy = policy; adoptScrollBarVisibility(verticalScrollBar, policy); reload(); propertyChangeSupport.firePropertyChange("verticalScrollBarPolicy", oldVal, this.verticalScrollBarPolicy); } } public void setHorizontalExtent(int horizontalExtent) { int oldVal = this.horizontalExtent; reloadIfChange(this.horizontalExtent, horizontalExtent); this.horizontalExtent = horizontalExtent; propertyChangeSupport.firePropertyChange("horizontalExtent", oldVal, this.horizontalExtent); horizontalModel.setExtent(horizontalExtent); } public final int getHorizontalExtent() { return horizontalExtent; } public void setVerticalExtent(int verticalExtent) { int oldVal = this.verticalExtent; reloadIfChange(this.verticalExtent, verticalExtent); this.verticalExtent = verticalExtent; verticalModel.setExtent(verticalExtent); propertyChangeSupport.firePropertyChange("verticalExtent", oldVal, this.verticalExtent); } public final int getVerticalExtent() { return verticalExtent; } @Override public void scrollRectToVisible(Rectangle aRect) { Rectangle viewport = scrollable.getScrollableViewportSize(); // This should never happen. If it happen we got a serious // problem, because we cannot determine what to scroll... if (viewport == null) { return; } Adjustable hbar = horizontalScrollBar; if (hbar != null && horizontalScrollBarPolicy != HORIZONTAL_SCROLLBAR_NEVER) { int nval = scrollValue(hbar.getValue(), horizontalExtent, aRect.x, aRect.width, hbar.getUnitIncrement()); if (nval != hbar.getValue()) { hbar.setValue(nval); } } Adjustable vbar = verticalScrollBar; if (vbar != null && verticalScrollBarPolicy != VERTICAL_SCROLLBAR_NEVER) { int nval = scrollValue(vbar.getValue(), verticalExtent, aRect.y, aRect.height, vbar.getUnitIncrement()); if (nval != vbar.getValue()) { vbar.setValue(nval); } } } /** * Calculate the best new position to show the given range. * * @param pos the current position * @param size the current visible amount * @param rpos the start-position of the range to expose * @param rsize the size of the range to expose * @param inc the unit-increment to advance pos * @return pos the new position */ protected static int scrollValue(int pos, int size, int rpos, int rsize, int inc) { if (pos <= rpos && (pos + size) >= (rpos + rsize)) { // nothing to do return pos; } if (pos > rpos) { // scroll backward - ignore rsize, either it fits or it doesn't, // just make sure the difference between pos and rpos is as // small as possible. while (pos > rpos) { pos -= inc; } } else { // scroll forward while ((pos + size) < (rpos + rsize) && (pos + inc) <= rpos) { pos += inc; } } return pos; } private void setInitialViewportSize() { if (scrollable == null) return; Rectangle oldVal = scrollable.getViewportSize(); if (mode == MODE_COMPLETE){ scrollable.setViewportSize(scrollable.getScrollableViewportSize()); } else { scrollable.setViewportSize(new Rectangle(0, 0, horizontalExtent, verticalExtent)); adoptScrollBarVisibility(horizontalScrollBar, horizontalScrollBarPolicy); adoptScrollBarVisibility(verticalScrollBar, verticalScrollBarPolicy); } propertyChangeSupport.firePropertyChange("initialViewportSize", oldVal, scrollable.getViewportSize()); } protected void adoptScrollBarVisibility(Adjustable scrollbar, int policy) { if (scrollbar != null && scrollable != null) { Rectangle maxVp = scrollable.getScrollableViewportSize(); Rectangle curVp = scrollable.getViewportSize(); if (maxVp != null && curVp != null) { boolean newVisibility; if (scrollbar.getOrientation() == SConstants.HORIZONTAL) { newVisibility = isScrollBarVisible(policy, maxVp.width, curVp.width); } else { newVisibility = isScrollBarVisible(policy, maxVp.height, curVp.height); } ((SComponent) scrollbar).setVisible(newVisibility); } } } private static boolean isScrollBarVisible(int policy, int maxRecords, int maxDisplayed) { return isPolicyAlways(policy) || (isPolicyAsNeeded(policy) && maxRecords > maxDisplayed); } private static boolean isPolicyAlways(int policy) { return policy == HORIZONTAL_SCROLLBAR_ALWAYS || policy == VERTICAL_SCROLLBAR_ALWAYS; } private static boolean isPolicyAsNeeded(int policy) { return policy == HORIZONTAL_SCROLLBAR_AS_NEEDED || policy == VERTICAL_SCROLLBAR_AS_NEEDED; } /** * A model synchronizing the scrollbar settings with the viewport of the given scrollable. */ protected class SViewportSynchronizationModel implements SBoundedRangeModel, SViewportChangeListener { private boolean horizontal; private Scrollable scrollable; private boolean isAdjusting = false; private boolean delayEvents = false; /** * Indicates if we have got a delayed Event */ protected boolean gotDelayedEvent = false; /** * Only one ChangeEvent is needed per model instance * since the event's only (read-only) state is the source property. */ protected transient ChangeEvent changeEvent = null; /** * The listeners waiting for model or viewport changes respectively. */ protected EventListenerList listenerList = new EventListenerList(); /** * Constructs a SViewportSynchronizationModel for either a horizontal or * vertical scrollbar that has to be synchronized with the scrollable. */ public SViewportSynchronizationModel(boolean horizontal) { this.horizontal = horizontal; } public Scrollable getScrollable() { return scrollable; } public void setScrollable(Scrollable scrollable) { if (this.scrollable != null) this.scrollable.removeViewportChangeListener(this); this.scrollable = scrollable; if (this.scrollable != null) this.scrollable.addViewportChangeListener(this); } @Override public int getValue() { if (!isViewportAvailable()) return 0; Rectangle curVp = scrollable.getViewportSize(); if (horizontal) return curVp.x; else return curVp.y; } @Override public void setValue(int newValue) { if (!isViewportAvailable()) return; Rectangle curVp = scrollable.getViewportSize(); Rectangle maxVp = scrollable.getScrollableViewportSize(); Rectangle newVp = (Rectangle)curVp.clone(); if (horizontal) { // Check range in order to prevent unnecessary update of views. // This is primarily useful to prevent reloads in case the user // wants to scroll further left/right than it's actually possible. newValue = Math.min(maxVp.width - curVp.width, newValue); newValue = Math.max(0, newValue); if (curVp.x != newValue) { newVp.x = newValue; updateViews(newVp); } } else { // Check range in order to prevent unnecessary update of views. // This is primarily useful to prevent reloads in case the user // wants to scroll further up/down than it's actually possible. newValue = Math.min(virtualViewportHeight - curVp.height, newValue); newValue = Math.max(0, newValue); if (curVp.y != newValue) { newVp.y = newValue; updateViews(newVp); } } } @Override public int getExtent() { if (!isViewportAvailable()) return 10; Rectangle curVp = scrollable.getViewportSize(); if (horizontal) return curVp.width; else return curVp.height; } @Override public void setExtent(int newExtent) { if (!isViewportAvailable()) return; Rectangle curVp = scrollable.getViewportSize(); Rectangle newVp = (Rectangle)curVp.clone(); if (horizontal && curVp.width != newExtent) { // Keep extent in sync with scrollpane if (horizontalExtent != newExtent) { horizontalExtent = newExtent; SScrollPane.this.reload(); } newVp.width = newExtent; updateViews(newVp); } else if (!horizontal && curVp.height != newExtent) { // Keep extent in sync with scrollpane if (verticalExtent != newExtent) { verticalExtent = newExtent; SScrollPane.this.reload(); } newVp.height = newExtent; updateViews(newVp); } } @Override public int getMinimum() { return 0; } @Override public void setMinimum(int newMinimum) { // minimum should always be zero } @Override public int getMaximum() { if (!isScrollableViewportAvailable()) return 100; Rectangle maxVp = scrollable.getScrollableViewportSize(); // The horizontal maximum should never be greater than the // number of available columns in the scrollable's model. // In contrast, the vertical maximum CAN be greater than the // number of available rows in the scrollable's model, i.e. // if this scrollpane's vertical extent was set to something // greater than the number of available rows. In such cases // the scrollable's CG fills the difference with empty lines. if (horizontal) return maxVp.width; else return virtualViewportHeight; } @Override public void setMaximum(int newMaximum) { if (!isScrollableViewportAvailable()) return; Rectangle maxVp = scrollable.getScrollableViewportSize(); if (horizontal && maxVp.width != newMaximum) { maxVp.width = newMaximum; updateViews(); } else if (maxVp.height != newMaximum) { maxVp.height = newMaximum; updateViews(); } } @Override public boolean getValueIsAdjusting() { return isAdjusting; } @Override public void setValueIsAdjusting(boolean b) { isAdjusting = b; } @Override public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) { setValue(value); setExtent(extent); setMaximum(max); isAdjusting = adjusting; } @Override public boolean getDelayEvents() { return delayEvents; } @Override public void setDelayEvents(boolean b) { delayEvents = b; } @Override public void fireDelayedIntermediateEvents() { // there are no intermediate events to fire } @Override public void fireDelayedFinalEvents() { if (!delayEvents && gotDelayedEvent) { // Reloads scrollbar fireStateChanged(); gotDelayedEvent = false; } } @Override public void viewportChanged(SViewportChangeEvent e) { if (mode == MODE_COMPLETE) { // In this mode we don't have to do anything special like adjusting // values or printing empty lines. We always want so see everything // - not more and not less! scrollable.setViewportSize(scrollable.getScrollableViewportSize()); } else if (horizontal == e.isHorizontalChange()) { // If the event was for THIS and not the other model ... Rectangle maxVp = scrollable.getScrollableViewportSize(); Rectangle curVp = scrollable.getViewportSize(); if (maxVp != null && curVp != null) { SComponent scrollbar; boolean newVisibility; if (horizontal) { // Do not show more than we want or more than we have. curVp.width = Math.min(horizontalExtent, maxVp.width); // Check range! This is primarily useful to prevent the // indices in the CGs to be out of bounds. An application // developer might accidentally set such a wrong viewport. curVp.x = Math.min(maxVp.width - curVp.width, curVp.x); curVp.x = Math.max(0, curVp.x); // Determine the new visibility of the horizontal scrollbar scrollbar = (SComponent) horizontalScrollBar; newVisibility = isScrollBarVisible(horizontalScrollBarPolicy, maxVp.width, curVp.width); } else { // Set the "virtualViewportHeight" if (mode == MODE_PAGING) { // Determine the number of empty cells that are needed in order to fill the last page int emptyCells = (verticalExtent - (maxVp.height % verticalExtent)) % verticalExtent; virtualViewportHeight = maxVp.height + emptyCells; } else virtualViewportHeight = Math.max(verticalExtent, maxVp.height); // Check range! This is primarily useful to prevent the // indices in the CGs to be out of bounds. An application // developer might accidentally set such a wrong viewport. curVp.y = Math.min(virtualViewportHeight - curVp.height, curVp.y); curVp.y = Math.max(0, curVp.y); // Determine the new visibility of the vertical scrollbar scrollbar = (SComponent) verticalScrollBar; newVisibility = isScrollBarVisible(verticalScrollBarPolicy, maxVp.height, curVp.height); } if (scrollbar != null) { // Prevent the scrollbar(s) from an unnecessary reload if (!scrollbar.isVisible() && newVisibility == false) { return; } // Apply the scrollbar's new visibility scrollbar.setVisible(newVisibility); } if (delayEvents) { gotDelayedEvent = true; } else { // Reloads scrollbar fireStateChanged(); } } } } /** * Adds a ChangeListener. * * @param l the ChangeListener to add * @see #removeChangeListener * @see BoundedRangeModel#addChangeListener */ @Override public void addChangeListener(ChangeListener l) { listenerList.add(ChangeListener.class, l); } /** * Removes a ChangeListener. * * @param l the ChangeListener to remove * @see #addChangeListener * @see BoundedRangeModel#removeChangeListener */ @Override public void removeChangeListener(ChangeListener l) { listenerList.remove(ChangeListener.class, l); } /** * Runs each ChangeListener's stateChanged method. * * @see #setRangeProperties * @see EventListenerList */ protected void fireStateChanged() { Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -=2) { if (listeners[i] == ChangeListener.class) { if (changeEvent == null) { changeEvent = new ChangeEvent(scrollable); } ((ChangeListener) listeners[i+1]).stateChanged(changeEvent); } } } private void updateViews(Rectangle newVp) { // Reload scrollbar and scrollable in order to display the changes viewportChanged(new SViewportChangeEvent(scrollable, horizontal)); //((SComponent) scrollable).reload(); scrollable.setViewportSize(newVp); } private void updateViews() { // Reload scrollbar and scrollable in order to display the changes viewportChanged(new SViewportChangeEvent(scrollable, horizontal)); ((SComponent) scrollable).reload(); } private boolean isViewportAvailable() { return scrollable != null && scrollable.getViewportSize() != null; } private boolean isScrollableViewportAvailable() { return scrollable != null && scrollable.getScrollableViewportSize() != null; } } }




    © 2015 - 2024 Weber Informatics LLC | Privacy Policy