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

org.netbeans.swing.tabcontrol.plaf.ScrollingTabLayoutModel Maven / Gradle / Ivy

There is a newer version: RELEASE230
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 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(); toBeShown = Math.min(widths.length-1, toBeShown); 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); } updateActions(); //set the changed flag so we won't recalculate all this again until //the next time something warrants it setChanged(false); 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 cachedWidthOfRequestedTab = getW( index ); 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-1); 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 && cachedWidthOfRequestedTab == width) { return false; } int newOffset = -2; int currW = 0; boolean isOffBack = false; boolean result = changed; boolean switchForward = false; //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(); switchForward = index >= selIdx; //Find the width of this tab, and count back currW = getWrapped().getW(index); if (index == 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; if( currW <= width || switchForward ) { newOffset++; if( getOffset() == -1 && newOffset == -1 ) newOffset = 0; } } 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(); } try { return getWrapped().getH(index); } catch (IndexOutOfBoundsException e) { //The tab was just removed, and the selection model was notified, //by the data model, but not everything else has been notified yet return 0; } } /** * 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 || index > widths.length) { change(); } if (index >= widths.length) { //If a tab has just been removed, there may be a request to //repaint it return 0; } 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 && (lastVisibleTab-firstVisibleTab > 1 //special case when a tab is too wide || lastVisibleTab < mdl.size()-1)); } } /** * 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(); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy