alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 *
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.netbeans.swing.tabcontrol.plaf;

import org.netbeans.swing.tabcontrol.TabDataModel;

import javax.swing.*;
import java.awt.*;
import java.util.Arrays;

/*
 * ScrollingTabLayoutModel.java
 *
 * Created on December 5, 2003, 5:16 PM
 */

/**
 * Layout model which manages an offset into a set of scrollable tabs, and
 * recalculates its layout on a change.  Also handles adding extra pixels to the
 * selected tab if necessary.  Basics of how it works:
 * 

* Wrapppers a DefaultTabLayoutModel, which can simply calculate tab widths and * 0 based positions. Listens to the data model for changes, and sets a flag * when a change happens to mark the cached widths and positions as dirty. On * any call to fetch sizes, first checks if the cached values are good, * recalculates if needed, and returns the result. * * @author Tim Boudreau */ public final class ScrollingTabLayoutModel implements TabLayoutModel { /** * The index of the first clipped, visible tab, or -1 if the first tab * should not be clippped */ private int offset = -1; /** * The wrapped DefaultTabLayoutModel which will give us pure numbers for the * desired width of tabs */ private TabLayoutModel wrapped; /** * Flag indicating that any call to get a value should trigger recalculation * of the cached values */ private boolean changed = true; /** * The tabDataModel, which we occasionally need to get data from */ TabDataModel mdl; /** * The selection model we will get the current selection from when we need * to ensure it is visible */ SingleSelectionModel sel; /** * Holds the value of the tab that should be made visible if makeVisible is * called before the component has a valid (>0) width. If not -1, a call to * setWidth() will trigger a call to makeVisible with this value. */ private int makeVisibleTab = -1; /** * Integer count of pixels that should be added to the width of the selected * tab. They will be subtracted from the surrounding tabs */ int pixelsToAddToSelection = 0; /** * Stores the value of whether the final tab is clipped. Recalculated in * change() */ private boolean lastTabClipped = false; /** * Cached index of the first visible tab */ private int firstVisibleTab = -1; /** * Cached index of the last visible tab */ private int lastVisibleTab = -1; /** * The last known width for which values were calculated */ private int width = -1; /** * Cache of the widths of tabs that *are* onscreen. This will always have a * length of (lastVisibleTab + 1) - firstVisibleTab. */ private int[] widths = null; /** * Creates a new instance of ScrollingTabLayoutModel */ public ScrollingTabLayoutModel(TabLayoutModel wrapped, SingleSelectionModel sel, TabDataModel mdl) { this.wrapped = wrapped; this.mdl = mdl; this.sel = sel; } public ScrollingTabLayoutModel(TabLayoutModel wrapped, SingleSelectionModel sel, TabDataModel mdl, int minimumXposition) { this(wrapped, sel, mdl); this.minimumXposition = minimumXposition; } public void setMinimumXposition (int x) { this.minimumXposition = x; setChanged(true); } /** * Some UIs will want to make the selected tab a little wider than the * rest. * @param i */ public void setPixelsToAddToSelection (int i) { pixelsToAddToSelection = i; setChanged (true); } private int minimumXposition = 0; /** * External operations on the selection or data model can invalidate cached * widths. The UI will listen for such changes and call this method if the * data we have cached is probably no good anymore. */ public void clearCachedData() { setChanged(true); } /** * Convenience getter for the "wrapped" model which will give us "pure" * numbers regarding the widths of tabs */ private TabLayoutModel getWrapped() { return wrapped; } /** * Get the offset - the number of tabs that are scrolled over. The default * value is -1, which means no tabs are scrolled off to the left. 0 means * the first tab is visible but clipped...and so forth */ public int getOffset() { if (mdl.size() <= 1) { return -1; } return offset; } /** * Called to recalculate cached values the first time a value that needs to * be calculated is requested, after some change that invalidates the cached * values */ private void change() { if (mdl.size() == 0) { //no tabs, do nothing widths = new int[0]; updateActions(); setChanged(false); return; } //Create an array that will hold precalculated widths until something //changes if (widths == null || widths.length != mdl.size()) { widths = new int[mdl.size()]; //Fill our array with 0's - any tabs not visible should get 0 width } Arrays.fill(widths, 0); if (widths.length == 1) { //there's only one tab, get rid of any offset - otherwise there's //no way to ever make the close button show because it won't be //able to be scrolled offset = -1; } //Handle throws case where we don't really even have enough room to //display one tab, by centering the clipped selected tag on what little //space we have. The UI will make sure it looks clipped. if (width < getMinimumLeftClippedWidth()) { int toBeShown = makeVisibleTab != -1 ? makeVisibleTab : sel.getSelectedIndex(); if (toBeShown != -1) { widths[toBeShown] = width; } else { widths[0] = width; } firstVisibleTab = toBeShown; lastVisibleTab = toBeShown; setChanged(false); return; } //init an index to the current position while looping int x = minimumXposition; //Find the starting point, the first visible tab int start = offset >= 0 ? offset : 0; //Holds a count of pixels to redistribute among other tabs, if we don't //quite have room to fit the last tab, so we'll stretch the one next //to it, but we don't want to make it huge int toRedistribute = -1; //Reset stored value for the last visible tab, returned from //getLastVisibleTab() lastVisibleTab = -1; //Reset stored value for first visible tab, returned from //getFirstVisibleTab() firstVisibleTab = start; //Reset the lastTabClipped flag returned by isLastTabClipped() lastTabClipped = false; //Special case - if the last tab the starting tab and there's not enough room for //it, show as much of it as possible if (start == mdl.size() - 1 && width < getWrapped().getW(start) + getMinimumLeftClippedWidth()) { lastVisibleTab = start; if (start != 0) { firstVisibleTab = start - 1; widths[start] = width - getMinimumLeftClippedWidth(); widths[start - 1] = getMinimumLeftClippedWidth(); lastTabClipped = width - getMinimumLeftClippedWidth() < getWrapped().getW(start); } else { firstVisibleTab = start; widths[start] = width; lastTabClipped = width < getWrapped().getW(start); } return; } for (int i = start; i < widths.length; i++) { int w; if (i == offset) { //If it's the first tab and it's an offset, it will use the //fixed width w = getMinimumLeftClippedWidth(); } else { //Get a dynamic width from the underlying model, which tells us //how wide that tab wants to be w = getWrapped().getW(i); } //See if we've overshot the space available for tabs. If we have, //we'll need to display this tab as right-clipped if (x + w > width) { if (width - x < getMinimumRightClippedWidth() && i != start) { //There's not enough space to fit the current tab. Add all //the extra space to the previous one (we'll redistribute //it later - this just makes the algorithm work even if //you comment out the redistribution code) widths[i - 1] += (width - x) - 1; //Now we know how many extra pixels we'll have to redistribute toRedistribute = (width - x); //Decrement the last visible tab so it will show a correct //value lastVisibleTab = i - 1; //Set the width of the tab that wouldn't fit to 0 widths[i] = 0; } else { //Okay, there's enough space for this last tab as a clipped //tab. Truncate it at the last possible pixel a tab can //occupy widths[i] = (width - x) - 1; //set this to the last visible tab lastVisibleTab = i; } //Set the clipped flag - the UI will use this to decide what //border to give the last tab lastTabClipped = true; //We're done looping - this tab will be clipped, so it's the last break; } //Okay, we're just iterating through a tab in the middle. Set its //width to whatever its measurements are and move on widths[i] = w; x += w; //make sure the last visible tab is really set correctly if there //is no right clipped tab if (i == widths.length - 1) { lastVisibleTab = widths.length - 1; } } //Some UIs want to make the selected tab bigger. So try to do that here. //Get the selection from the selection model int selected = sel.getSelectedIndex(); //See if we have to add some pixels to the selected tab, but ignore if //it's the first or last clipped tabs if (pixelsToAddToSelection != 0 && selected > start && selected < lastVisibleTab) { //Add the pixels to the selected index widths[selected] += pixelsToAddToSelection; //Get the average number of pixels per tab to remove. If a small //number, it may round to 0. Note we are intentionally dividing //by the number of tabs-1 because the selected tab doesn't count. int perTab = pixelsToAddToSelection - 1 / (lastVisibleTab - start); //In case it does round to 0, keep an exact count int pixels = pixelsToAddToSelection - 1; //Iterate all the tabs, skipping the selected one for (int i = start; i <= lastVisibleTab; i++) { if (i != selected) { //if it rounded to 0, we'll just subtract 2 until we get //there - this will work most of the time and be harmless //the rest if (perTab == 0) { //remove 2 pixels from the tab width widths[i] -= 2; pixels -= 2; if (pixels <= 0) { //if we'return out of pixels, stop break; } } else { //Okay, we have an exact (+/- rounding errors) number of //pixels to remove. Remove them, widths[i] -= perTab; //Subtract from our exact count, it will avoid rounding //errors showing up pixels -= perTab; //if we'return out of pixels, stop if (pixels <= 0) { break; } } } } } //Now, do we have some spare pixels in the last tab that we need to redistribute //so we don't have a huge last tab? Only do this if there are > 2 tabs, //or there's really no point - both are clipped if (toRedistribute != -1 && lastVisibleTab != start && lastVisibleTab != start + 1) { //Similar algorithm as above int perTab = toRedistribute / ((lastVisibleTab + 1) - start); for (int i = start; i < lastVisibleTab; i++) { if (perTab != 0) { widths[i] += perTab; widths[lastVisibleTab] -= perTab; } else { int use = toRedistribute > 2 ? 2 : toRedistribute; widths[i] += use; widths[lastVisibleTab] -= use; toRedistribute -= use; if (toRedistribute <= 0) { //out of pixels, quit break; } } } } updateActions(); //set the changed flag so we won't recalculate all this again until //the next time something warrants it setChanged(false); } private void setChanged(boolean val) { if (changed != val) { changed = val; } } /** * Some look and feel specs require that the selected tab be wider. This * method sets the number of pixels to add to its width. It is important * that the underlying layout model's padX property include enough padding * that 1-2 pixels may be stolen without causing overlap problems. The * default is 0. */ public int getPixelsToAddToSelection() { return pixelsToAddToSelection; } /** * Returns true if the last tab displayed is clipped and should therefore be * painted as a clipped tab */ public boolean isLastTabClipped() { if (width < getMinimumLeftClippedWidth()) { return true; } return lastTabClipped; } /** * Make a tab visible, according to the rules of the spec. Returns whether * or not a repaint of the entire control is required. The width of the tab * view is passed to this method, so that it can tell if the width has * changed (in which case it needs to recalculate tab bounds), or if it can * use the existing cached values. *

* This method will not trigger a repaint - it just adjusts the cached withs * and positions of tabs so that the next repaint will paint correctly. It * may be called as part of a more complex operation which would not want to * trigger spurious repaints - but the return value should be noted, and if * the return value is true, the caller should repaint the tab displayer * whenever it is done doing what it is doing. */ public boolean makeVisible (int index, int width) { if (width < 0) { setWidth (width); makeVisibleTab = index; return false; } boolean resized = width != this.width || recentlyResized; recentlyResized = false; //First, make sure we have an accurate first/last visible tab setWidth(width); if (index == -1) { return false; } //Special case a single tab model - the index should always be 0 if (mdl.size() == 1) { setOffset (-1); return changed; } //Special case two tabs in a very small area - try to show them both if (mdl.size() == 2) { int totalWidth = getWrapped().getW(0) + getWrapped().getW(1); if (totalWidth > width) { setOffset (0); return changed; } } if (changed) { change(); } //Special case index 0 - it will always get -1 if (index == 0) { int off = setOffset(-1); return off != -1; } int widthForRequestedTab = getWrapped().getW(index); //Special case a single tab which is wider than the entire //tab displayer area if (widthForRequestedTab > width) { //It will be left clipped, but what can you do... setOffset (index); return changed; } //If it's the last tab and it's already not clipped, don't //do anything if (index == mdl.size() - 1 && !isLastTabClipped() && !resized) { return false; } int newOffset = -2; int currW = 0; boolean isOffBack = false; boolean result = changed; //If it's after the last tab, we'll find it's width, then count //backward until we're out of tabs or out of space if (index >= getLastVisibleTab(width)) { int selIdx = sel.getSelectedIndex(); //Find the width of this tab, and count back currW = getWrapped().getW(index); if (currW == selIdx) { currW += pixelsToAddToSelection; } int firstTab = index; //Count backward from the requested tab until we're out of space do { firstTab--; if (firstTab > -1) { if (firstTab == selIdx) { currW += pixelsToAddToSelection; } int wid = getWrapped().getW(firstTab); currW += wid; } } while (currW <= width && firstTab >= -1); newOffset = firstTab + 1; } else if (index <= getFirstVisibleTab(width)) { isOffBack = true; newOffset = index-1; } if (resized || !isOffBack || index == mdl.size() && getFirstVisibleTab(width) == index) { if (newOffset != -2) { setOffset (newOffset); } result = ensureAvailableSpaceUsed(false); } else { if (newOffset != -2) { int old = offset; int nue = setOffset (Math.min (mdl.size(), newOffset)); result = old != nue; } } return result; } boolean ensureAvailableSpaceUsed(boolean useCached) { if (mdl.size() == 0) { return false; } boolean result = false; if (changed && !useCached) { result = true; change(); } int last = mdl.size() -1; int lastTab = useCached ? getCachedLastVisibleTab() : getLastVisibleTab(width); if (lastTab == last || lastTab == mdl.size() && last > -1) { //one has been removed int off = offset; int availableWidth = width - (getX(last) + getW(last)); while (availableWidth > 0 && off > -1) { availableWidth -= getWrapped().getW(off); if (availableWidth > 0) { off--; } } setOffset (off); if (changed) { result = true; change(); } } return result; } /** * Probably these should be made into constructor arguments. The minimum * space to be used for a right-clipped tab */ int getMinimumRightClippedWidth() { return 40; } /** * Probably these should be made into constructor arguments. The minimum * space to be used for a left-clipped tab */ int getMinimumLeftClippedWidth() { return 40; } /** * Sets the current cached width the model thinks it has for displaying * tabs. This is used to trigger a recalculation if it differs from the * previously passed value */ public void setWidth(int width) { if (this.width != width) { recentlyResized = true; //see if someone called makeVisible before the component was shown - //we'll want to do that now if (width < this.width) { //Ensure that the current selection stays visible in a resize makeVisibleTab = sel.getSelectedIndex(); } boolean needMakeVisible = (width > 0 && this.width < 0 && makeVisibleTab != -1); this.width = width - minimumXposition; setChanged(width > getMinimumLeftClippedWidth()); if (changed && needMakeVisible && width > getMinimumLeftClippedWidth()) { makeVisible(makeVisibleTab, width); makeVisibleTab = -1; } } } private boolean recentlyResized = true; /** * Set the offset - the number of tabs that should be hidden to the left. * The default is -1 - tab 0 is showing. If set to 0, tab 0 still shows but * is clipped, and so forth. */ public int setOffset(int i) { int prevOffset = offset; if (mdl.size() == 1) { if (offset > -1) { offset = -1; setChanged(true); } return prevOffset; } if (mdl.size() == 2 && width < getMinimumLeftClippedWidth() + getMinimumRightClippedWidth()) { offset = -1; setChanged(false); return prevOffset; } if (i < -1) { //repeated action calls can do this i = -1; } if (i != offset) { setChanged(true); offset = i; } return prevOffset; } /** * Returns the index of the first tab that is visible (may be clipped - if * it == getOffset() then it is */ public int getFirstVisibleTab(int width) { setWidth(width); if (mdl.size() == 0) { return -1; } if (width < getMinimumLeftClippedWidth()) { int first = makeVisibleTab == -1 ? sel.getSelectedIndex() : makeVisibleTab; return first; } if (changed) { change(); } return firstVisibleTab; } /** * Return the number of tabs currently visible */ public int countVisibleTabs(int width) { return (getLastVisibleTab(width) + 1) - getFirstVisibleTab(width); } /** * Returns the last visible tab, which may or may not be clipped */ public int getLastVisibleTab(int width) { setWidth(width); if (mdl.size() == 0) { return -1; } if (width < getMinimumLeftClippedWidth()) { int first = makeVisibleTab == -1 ? sel.getSelectedIndex() : makeVisibleTab; return first; } if (changed) { change(); } return lastVisibleTab; } /** * Used when components are deleted, so that if the user scrolls to close * some tabs, and the selection is offscreen, we don't infuriatingly * re-scroll away from the end tabs. */ int getCachedLastVisibleTab() { return lastVisibleTab; } /** * Used when components are deleted, so that if the user scrolls to close * some tabs, and the selection is offscreen, we don't infuriatingly * re-scroll away from the end tabs. */ int getCachedFirstVisibleTab() { return firstVisibleTab; } public int dropIndexOfPoint(int x, int y) { if (changed) { change(); } int first = getFirstVisibleTab(width); int last = getLastVisibleTab(width); int pos = 0; //XXX - may not be 0 with insets for (int i = first; i <= last; i++) { int lastPos = pos; pos += getW(i); int h = getH(i); int ay = getY(i); if (y < 0 || y > ay + h) { return -1; } if (i == last && x > lastPos + (getW(i) / 2)) { return last + 1; } if (x >= lastPos && x <= pos) { return i; } } return -1; } public void setPadding(Dimension d) { getWrapped().setPadding(d); setChanged (true); } public int getH(int index) { if (changed) { change(); } return getWrapped().getH(index); } /** * Returns a cached width, after checking the changed flag and calling * change() if recalculation is needed */ public int getW(int index) { //widths can be null on OS-X if component is instantiated with //0 size (some bug with reloading winsys) and has never been painted if (changed || widths == null) { change(); } return widths[index]; } public int getX(int index) { if (changed) { change(); } int result = minimumXposition; for (int i = 0; i < index; i++) { result += getW(i); } return result; } public int getY(int index) { if (changed) { change(); } return getWrapped().getY(index); } public int indexOfPoint(int x, int y) { if (changed) { change(); } int pos = minimumXposition; int lastPos; for (int i = offset == -1 ? 0 : offset; i < mdl.size(); i++) { lastPos = pos; int w = getW(i); pos += w; if (w == 0) { break; } int h = getH(i); int ay = getY(i); if (y < 0 || y > ay + h) { return -1; } if (x > lastPos && x < pos) { return i; } } return -1; } private Action fAction = null; private Action bAction = null; /** * Returns an Action that the control buttons can call to scroll forward */ public Action getForwardAction() { if (fAction == null) { fAction = new ForwardAction(); } return fAction; } /** * Returns an Action that the control buttons can call to scroll backward */ public Action getBackwardAction() { if (bAction == null) { bAction = new BackwardAction(); } return bAction; } /** * Update the enabled state of the button actions if the state of the layout * has changed in a way that affects them */ private void updateActions() { if (width <= getMinimumLeftClippedWidth()) { bAction.setEnabled(false); fAction.setEnabled(false); } if (bAction != null) { bAction.setEnabled(mdl.size() > 1 && offset > -1); } if (fAction != null) { fAction.setEnabled(isLastTabClipped() && mdl.size() > 2); } } /** * An action which will scroll forward */ private class ForwardAction extends AbstractAction { public void actionPerformed(java.awt.event.ActionEvent e) { setOffset(getOffset() + 1); Component jc = (Component) getValue("control"); //NOI18N //Use a convenient hack to get the control to paint if (jc != null) { jc.repaint(); } } } /** * An action which will scroll backward */ private class BackwardAction extends AbstractAction { public void actionPerformed(java.awt.event.ActionEvent e) { setOffset(getOffset() - 1); //Use a convenient hack to get the control to paint Component jc = (Component) getValue("control"); //NOI18N if (jc != null) { jc.repaint(); } } } }

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.