001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.swing.tabcontrol.plaf;
042:
043: import org.netbeans.swing.tabcontrol.TabDataModel;
044:
045: import javax.swing.*;
046: import java.awt.*;
047: import java.util.Arrays;
048:
049: /*
050: * ScrollingTabLayoutModel.java
051: *
052: * Created on December 5, 2003, 5:16 PM
053: */
054:
055: /**
056: * Layout model which manages an offset into a set of scrollable tabs, and
057: * recalculates its layout on a change. Also handles adding extra pixels to the
058: * selected tab if necessary. Basics of how it works:
059: * <p>
060: * Wrapppers a DefaultTabLayoutModel, which can simply calculate tab widths and
061: * 0 based positions. Listens to the data model for changes, and sets a flag
062: * when a change happens to mark the cached widths and positions as dirty. On
063: * any call to fetch sizes, first checks if the cached values are good,
064: * recalculates if needed, and returns the result.
065: *
066: * @author Tim Boudreau
067: */
068: public final class ScrollingTabLayoutModel implements TabLayoutModel {
069: /**
070: * The index of the first clipped, visible tab, or -1 if the first tab
071: * should not be clippped
072: */
073: private int offset = -1;
074: /**
075: * The wrapped DefaultTabLayoutModel which will give us pure numbers for the
076: * desired width of tabs
077: */
078: private TabLayoutModel wrapped;
079: /**
080: * Flag indicating that any call to get a value should trigger recalculation
081: * of the cached values
082: */
083: private boolean changed = true;
084: /**
085: * The tabDataModel, which we occasionally need to get data from
086: */
087: TabDataModel mdl;
088: /**
089: * The selection model we will get the current selection from when we need
090: * to ensure it is visible
091: */
092: SingleSelectionModel sel;
093: /**
094: * Holds the value of the tab that should be made visible if makeVisible is
095: * called before the component has a valid (>0) width. If not -1, a call to
096: * setWidth() will trigger a call to makeVisible with this value.
097: */
098: private int makeVisibleTab = -1;
099: /**
100: * Integer count of pixels that should be added to the width of the selected
101: * tab. They will be subtracted from the surrounding tabs
102: */
103: int pixelsToAddToSelection = 0;
104: /**
105: * Stores the value of whether the final tab is clipped. Recalculated in
106: * <code>change()</code>
107: */
108: private boolean lastTabClipped = false;
109: /**
110: * Cached index of the first visible tab
111: */
112: private int firstVisibleTab = -1;
113: /**
114: * Cached index of the last visible tab
115: */
116: private int lastVisibleTab = -1;
117: /**
118: * The last known width for which values were calculated
119: */
120: private int width = -1;
121: /**
122: * Cache of the widths of tabs that *are* onscreen. This will always have a
123: * length of (lastVisibleTab + 1) - firstVisibleTab.
124: */
125: private int[] widths = null;
126:
127: /**
128: * Creates a new instance of ScrollingTabLayoutModel
129: */
130: public ScrollingTabLayoutModel(TabLayoutModel wrapped,
131: SingleSelectionModel sel, TabDataModel mdl) {
132: this .wrapped = wrapped;
133: this .mdl = mdl;
134: this .sel = sel;
135: }
136:
137: public ScrollingTabLayoutModel(TabLayoutModel wrapped,
138: SingleSelectionModel sel, TabDataModel mdl,
139: int minimumXposition) {
140: this (wrapped, sel, mdl);
141: this .minimumXposition = minimumXposition;
142: }
143:
144: public void setMinimumXposition(int x) {
145: this .minimumXposition = x;
146: setChanged(true);
147: }
148:
149: /**
150: * Some UIs will want to make the selected tab a little wider than the
151: * rest.
152: * @param i
153: */
154: public void setPixelsToAddToSelection(int i) {
155: pixelsToAddToSelection = i;
156: setChanged(true);
157: }
158:
159: private int minimumXposition = 0;
160:
161: /**
162: * External operations on the selection or data model can invalidate cached
163: * widths. The UI will listen for such changes and call this method if the
164: * data we have cached is probably no good anymore.
165: */
166: public void clearCachedData() {
167: setChanged(true);
168: }
169:
170: /**
171: * Convenience getter for the "wrapped" model which will give us "pure"
172: * numbers regarding the widths of tabs
173: */
174: private TabLayoutModel getWrapped() {
175: return wrapped;
176: }
177:
178: /**
179: * Get the offset - the number of tabs that are scrolled over. The default
180: * value is -1, which means no tabs are scrolled off to the left. 0 means
181: * the first tab is visible but clipped...and so forth
182: */
183: public int getOffset() {
184: if (mdl.size() <= 1) {
185: return -1;
186: }
187: return offset;
188: }
189:
190: /**
191: * Called to recalculate cached values the first time a value that needs to
192: * be calculated is requested, after some change that invalidates the cached
193: * values
194: */
195: private void change() {
196: if (mdl.size() == 0) {
197: //no tabs, do nothing
198: widths = new int[0];
199: updateActions();
200: setChanged(false);
201: return;
202: }
203: //Create an array that will hold precalculated widths until something
204: //changes
205: if (widths == null || widths.length != mdl.size()) {
206: widths = new int[mdl.size()];
207: //Fill our array with 0's - any tabs not visible should get 0 width
208: }
209: Arrays.fill(widths, 0);
210:
211: if (widths.length == 1) {
212: //there's only one tab, get rid of any offset - otherwise there's
213: //no way to ever make the close button show because it won't be
214: //able to be scrolled
215: offset = -1;
216: }
217:
218: //Handle throws case where we don't really even have enough room to
219: //display one tab, by centering the clipped selected tag on what little
220: //space we have. The UI will make sure it looks clipped.
221: if (width < getMinimumLeftClippedWidth()) {
222: int toBeShown = makeVisibleTab != -1 ? makeVisibleTab : sel
223: .getSelectedIndex();
224: if (toBeShown != -1) {
225: widths[toBeShown] = width;
226: } else {
227: widths[0] = width;
228: }
229: firstVisibleTab = toBeShown;
230: lastVisibleTab = toBeShown;
231: setChanged(false);
232: return;
233: }
234:
235: //init an index to the current position while looping
236: int x = minimumXposition;
237: //Find the starting point, the first visible tab
238: int start = offset >= 0 ? offset : 0;
239: //Holds a count of pixels to redistribute among other tabs, if we don't
240: //quite have room to fit the last tab, so we'll stretch the one next
241: //to it, but we don't want to make it huge
242: int toRedistribute = -1;
243: //Reset stored value for the last visible tab, returned from
244: //getLastVisibleTab()
245: lastVisibleTab = -1;
246: //Reset stored value for first visible tab, returned from
247: //getFirstVisibleTab()
248: firstVisibleTab = start;
249: //Reset the lastTabClipped flag returned by isLastTabClipped()
250: lastTabClipped = false;
251:
252: //Special case - if the last tab the starting tab and there's not enough room for
253: //it, show as much of it as possible
254: if (start == mdl.size() - 1
255: && width < getWrapped().getW(start)
256: + getMinimumLeftClippedWidth()) {
257: lastVisibleTab = start;
258: if (start != 0) {
259: firstVisibleTab = start - 1;
260: widths[start] = width - getMinimumLeftClippedWidth();
261: widths[start - 1] = getMinimumLeftClippedWidth();
262: lastTabClipped = width - getMinimumLeftClippedWidth() < getWrapped()
263: .getW(start);
264: } else {
265: firstVisibleTab = start;
266: widths[start] = width;
267: lastTabClipped = width < getWrapped().getW(start);
268: }
269: updateActions();
270: //set the changed flag so we won't recalculate all this again until
271: //the next time something warrants it
272: setChanged(false);
273:
274: return;
275: }
276:
277: for (int i = start; i < widths.length; i++) {
278: int w;
279: if (i == offset) {
280: //If it's the first tab and it's an offset, it will use the
281: //fixed width
282: w = getMinimumLeftClippedWidth();
283: } else {
284: //Get a dynamic width from the underlying model, which tells us
285: //how wide that tab wants to be
286: w = getWrapped().getW(i);
287: }
288: //See if we've overshot the space available for tabs. If we have,
289: //we'll need to display this tab as right-clipped
290: if (x + w > width) {
291: if (width - x < getMinimumRightClippedWidth()
292: && i != start) {
293: //There's not enough space to fit the current tab. Add all
294: //the extra space to the previous one (we'll redistribute
295: //it later - this just makes the algorithm work even if
296: //you comment out the redistribution code)
297: widths[i - 1] += (width - x) - 1;
298: //Now we know how many extra pixels we'll have to redistribute
299: toRedistribute = (width - x);
300: //Decrement the last visible tab so it will show a correct
301: //value
302: lastVisibleTab = i - 1;
303: //Set the width of the tab that wouldn't fit to 0
304: widths[i] = 0;
305: } else {
306: //Okay, there's enough space for this last tab as a clipped
307: //tab. Truncate it at the last possible pixel a tab can
308: //occupy
309: widths[i] = (width - x) - 1;
310: //set this to the last visible tab
311: lastVisibleTab = i;
312: }
313: //Set the clipped flag - the UI will use this to decide what
314: //border to give the last tab
315: lastTabClipped = true;
316: //We're done looping - this tab will be clipped, so it's the last
317: break;
318: }
319: //Okay, we're just iterating through a tab in the middle. Set its
320: //width to whatever its measurements are and move on
321: widths[i] = w;
322: x += w;
323: //make sure the last visible tab is really set correctly if there
324: //is no right clipped tab
325: if (i == widths.length - 1) {
326: lastVisibleTab = widths.length - 1;
327: }
328: }
329:
330: //Some UIs want to make the selected tab bigger. So try to do that here.
331: //Get the selection from the selection model
332: int selected = sel.getSelectedIndex();
333: //See if we have to add some pixels to the selected tab, but ignore if
334: //it's the first or last clipped tabs
335: if (pixelsToAddToSelection != 0 && selected > start
336: && selected < lastVisibleTab) {
337: //Add the pixels to the selected index
338: widths[selected] += pixelsToAddToSelection;
339: //Get the average number of pixels per tab to remove. If a small
340: //number, it may round to 0. Note we are intentionally dividing
341: //by the number of tabs-1 because the selected tab doesn't count.
342: int perTab = pixelsToAddToSelection - 1
343: / (lastVisibleTab - start);
344: //In case it does round to 0, keep an exact count
345: int pixels = pixelsToAddToSelection - 1;
346: //Iterate all the tabs, skipping the selected one
347: for (int i = start; i <= lastVisibleTab; i++) {
348: if (i != selected) {
349: //if it rounded to 0, we'll just subtract 2 until we get
350: //there - this will work most of the time and be harmless
351: //the rest
352: if (perTab == 0) {
353: //remove 2 pixels from the tab width
354: widths[i] -= 2;
355: pixels -= 2;
356: if (pixels <= 0) {
357: //if we'return out of pixels, stop
358: break;
359: }
360: } else {
361: //Okay, we have an exact (+/- rounding errors) number of
362: //pixels to remove. Remove them,
363: widths[i] -= perTab;
364: //Subtract from our exact count, it will avoid rounding
365: //errors showing up
366: pixels -= perTab;
367: //if we'return out of pixels, stop
368: if (pixels <= 0) {
369: break;
370: }
371: }
372: }
373: }
374: }
375:
376: //Now, do we have some spare pixels in the last tab that we need to redistribute
377: //so we don't have a huge last tab? Only do this if there are > 2 tabs,
378: //or there's really no point - both are clipped
379: if (toRedistribute != -1 && lastVisibleTab != start
380: && lastVisibleTab != start + 1) {
381: //Similar algorithm as above
382: int perTab = toRedistribute
383: / ((lastVisibleTab + 1) - start);
384: for (int i = start; i < lastVisibleTab; i++) {
385: if (perTab != 0) {
386: widths[i] += perTab;
387: widths[lastVisibleTab] -= perTab;
388: } else {
389: int use = toRedistribute > 2 ? 2 : toRedistribute;
390: widths[i] += use;
391: widths[lastVisibleTab] -= use;
392: toRedistribute -= use;
393: if (toRedistribute <= 0) {
394: //out of pixels, quit
395: break;
396: }
397: }
398: }
399: }
400: updateActions();
401: //set the changed flag so we won't recalculate all this again until
402: //the next time something warrants it
403: setChanged(false);
404: }
405:
406: private void setChanged(boolean val) {
407: if (changed != val) {
408: changed = val;
409: }
410: }
411:
412: /**
413: * Some look and feel specs require that the selected tab be wider. This
414: * method sets the number of pixels to add to its width. It is important
415: * that the underlying layout model's padX property include enough padding
416: * that 1-2 pixels may be stolen without causing overlap problems. The
417: * default is 0.
418: */
419: public int getPixelsToAddToSelection() {
420: return pixelsToAddToSelection;
421: }
422:
423: /**
424: * Returns true if the last tab displayed is clipped and should therefore be
425: * painted as a clipped tab
426: */
427: public boolean isLastTabClipped() {
428: if (width < getMinimumLeftClippedWidth()) {
429: return true;
430: }
431: return lastTabClipped;
432: }
433:
434: /**
435: * Make a tab visible, according to the rules of the spec. Returns whether
436: * or not a repaint of the entire control is required. The width of the tab
437: * view is passed to this method, so that it can tell if the width has
438: * changed (in which case it needs to recalculate tab bounds), or if it can
439: * use the existing cached values.
440: * <p>
441: * This method will not trigger a repaint - it just adjusts the cached withs
442: * and positions of tabs so that the next repaint will paint correctly. It
443: * may be called as part of a more complex operation which would not want to
444: * trigger spurious repaints - but the return value should be noted, and if
445: * the return value is true, the caller should repaint the tab displayer
446: * whenever it is done doing what it is doing.
447: */
448: public boolean makeVisible(int index, int width) {
449: if (width < 0) {
450: setWidth(width);
451: makeVisibleTab = index;
452: return false;
453: }
454:
455: boolean resized = width != this .width || recentlyResized;
456: recentlyResized = false;
457:
458: //First, make sure we have an accurate first/last visible tab
459: setWidth(width);
460:
461: if (index == -1) {
462: return false;
463: }
464:
465: //Special case a single tab model - the index should always be 0
466: if (mdl.size() == 1) {
467: setOffset(-1);
468: return changed;
469: }
470:
471: //Special case two tabs in a very small area - try to show them both
472: if (mdl.size() == 2) {
473: int totalWidth = getWrapped().getW(0)
474: + getWrapped().getW(1);
475: if (totalWidth > width) {
476: setOffset(0);
477: return changed;
478: }
479: }
480:
481: if (changed) {
482: change();
483: }
484:
485: //Special case index 0 - it will always get -1
486: if (index == 0) {
487: int off = setOffset(-1);
488: return off != -1;
489: }
490: int widthForRequestedTab = getWrapped().getW(index);
491:
492: //Special case a single tab which is wider than the entire
493: //tab displayer area
494: if (widthForRequestedTab > width) {
495: //It will be left clipped, but what can you do...
496: setOffset(index - 1);
497: return changed;
498: }
499:
500: //If it's the last tab and it's already not clipped, don't
501: //do anything
502: if (index == mdl.size() - 1 && !isLastTabClipped() && !resized) {
503: return false;
504: }
505:
506: int newOffset = -2;
507:
508: int currW = 0;
509: boolean isOffBack = false;
510: boolean result = changed;
511: boolean switchForward = false;
512: //If it's after the last tab, we'll find it's width, then count
513: //backward until we're out of tabs or out of space
514: if (index >= getLastVisibleTab(width)) {
515: int selIdx = sel.getSelectedIndex();
516: switchForward = index >= selIdx;
517:
518: //Find the width of this tab, and count back
519: currW = getWrapped().getW(index);
520: if (index == selIdx) {
521: currW += pixelsToAddToSelection;
522: }
523: int firstTab = index;
524: //Count backward from the requested tab until we're out of space
525: do {
526: firstTab--;
527: if (firstTab > -1) {
528: if (firstTab == selIdx) {
529: currW += pixelsToAddToSelection;
530: }
531: int wid = getWrapped().getW(firstTab);
532: currW += wid;
533: }
534: } while (currW <= width && firstTab >= -1);
535: newOffset = firstTab;
536: if (currW <= width || switchForward) {
537: newOffset++;
538: if (getOffset() == -1 && newOffset == -1)
539: newOffset = 0;
540: }
541: } else if (index <= getFirstVisibleTab(width)) {
542: isOffBack = true;
543: newOffset = index - 1;
544: }
545:
546: if (resized || !isOffBack || index == mdl.size()
547: && getFirstVisibleTab(width) == index) {
548: if (newOffset != -2) {
549: setOffset(newOffset);
550: }
551: result = ensureAvailableSpaceUsed(false);
552:
553: } else {
554: if (newOffset != -2) {
555: int old = offset;
556: int nue = setOffset(Math.min(mdl.size(), newOffset));
557: result = old != nue;
558: }
559: }
560: return result;
561: }
562:
563: boolean ensureAvailableSpaceUsed(boolean useCached) {
564: if (mdl.size() == 0) {
565: return false;
566: }
567: boolean result = false;
568: if (changed && !useCached) {
569: result = true;
570: change();
571: }
572: int last = mdl.size() - 1;
573: int lastTab = useCached ? getCachedLastVisibleTab()
574: : getLastVisibleTab(width);
575: if (lastTab == last || lastTab == mdl.size() && last > -1) { //one has been removed
576: int off = offset;
577: int availableWidth = width - (getX(last) + getW(last));
578:
579: while (availableWidth > 0 && off > -1) {
580: availableWidth -= getWrapped().getW(off);
581: if (availableWidth > 0) {
582: off--;
583: }
584: }
585: setOffset(off);
586: if (changed) {
587: result = true;
588: change();
589: }
590: }
591: return result;
592: }
593:
594: /**
595: * Probably these should be made into constructor arguments. The minimum
596: * space to be used for a right-clipped tab
597: */
598: int getMinimumRightClippedWidth() {
599: return 40;
600: }
601:
602: /**
603: * Probably these should be made into constructor arguments. The minimum
604: * space to be used for a left-clipped tab
605: */
606: int getMinimumLeftClippedWidth() {
607: return 40;
608: }
609:
610: /**
611: * Sets the current cached width the model thinks it has for displaying
612: * tabs. This is used to trigger a recalculation if it differs from the
613: * previously passed value
614: */
615: public void setWidth(int width) {
616: if (this .width != width) {
617: recentlyResized = true;
618: //see if someone called makeVisible before the component was shown -
619: //we'll want to do that now
620: if (width < this .width) {
621: //Ensure that the current selection stays visible in a resize
622: makeVisibleTab = sel.getSelectedIndex();
623: }
624: boolean needMakeVisible = (width > 0 && this .width < 0 && makeVisibleTab != -1);
625: this .width = width - minimumXposition;
626: setChanged(width > getMinimumLeftClippedWidth());
627: if (changed && needMakeVisible
628: && width > getMinimumLeftClippedWidth()) {
629: makeVisible(makeVisibleTab, width);
630: makeVisibleTab = -1;
631: }
632: }
633: }
634:
635: private boolean recentlyResized = true;
636:
637: /**
638: * Set the offset - the number of tabs that should be hidden to the left.
639: * The default is -1 - tab 0 is showing. If set to 0, tab 0 still shows but
640: * is clipped, and so forth.
641: */
642: public int setOffset(int i) {
643: int prevOffset = offset;
644: if (mdl.size() == 1) {
645: if (offset > -1) {
646: offset = -1;
647: setChanged(true);
648: }
649: return prevOffset;
650: }
651:
652: if (mdl.size() == 2
653: && width < getMinimumLeftClippedWidth()
654: + getMinimumRightClippedWidth()) {
655: offset = -1;
656: setChanged(false);
657: return prevOffset;
658: }
659:
660: if (i < -1) {
661: //repeated action calls can do this
662: i = -1;
663: }
664: if (i != offset) {
665: setChanged(true);
666: offset = i;
667: }
668: return prevOffset;
669: }
670:
671: /**
672: * Returns the index of the first tab that is visible (may be clipped - if
673: * it == getOffset() then it is
674: */
675: public int getFirstVisibleTab(int width) {
676: setWidth(width);
677: if (mdl.size() == 0) {
678: return -1;
679: }
680: if (width < getMinimumLeftClippedWidth()) {
681: int first = makeVisibleTab == -1 ? sel.getSelectedIndex()
682: : makeVisibleTab;
683: return first;
684: }
685: if (changed) {
686: change();
687: }
688: return firstVisibleTab;
689: }
690:
691: /**
692: * Return the number of tabs currently visible
693: */
694: public int countVisibleTabs(int width) {
695: return (getLastVisibleTab(width) + 1)
696: - getFirstVisibleTab(width);
697: }
698:
699: /**
700: * Returns the last visible tab, which may or may not be clipped
701: */
702: public int getLastVisibleTab(int width) {
703: setWidth(width);
704: if (mdl.size() == 0) {
705: return -1;
706: }
707: if (width < getMinimumLeftClippedWidth()) {
708: int first = makeVisibleTab == -1 ? sel.getSelectedIndex()
709: : makeVisibleTab;
710: return first;
711: }
712: if (changed) {
713: change();
714: }
715: return lastVisibleTab;
716: }
717:
718: /**
719: * Used when components are deleted, so that if the user scrolls to close
720: * some tabs, and the selection is offscreen, we don't infuriatingly
721: * re-scroll away from the end tabs.
722: */
723: int getCachedLastVisibleTab() {
724: return lastVisibleTab;
725: }
726:
727: /**
728: * Used when components are deleted, so that if the user scrolls to close
729: * some tabs, and the selection is offscreen, we don't infuriatingly
730: * re-scroll away from the end tabs.
731: */
732: int getCachedFirstVisibleTab() {
733: return firstVisibleTab;
734: }
735:
736: public int dropIndexOfPoint(int x, int y) {
737: if (changed) {
738: change();
739: }
740: int first = getFirstVisibleTab(width);
741: int last = getLastVisibleTab(width);
742: int pos = 0; //XXX - may not be 0 with insets
743: for (int i = first; i <= last; i++) {
744: int lastPos = pos;
745: pos += getW(i);
746: int h = getH(i);
747: int ay = getY(i);
748: if (y < 0 || y > ay + h) {
749: return -1;
750: }
751: if (i == last && x > lastPos + (getW(i) / 2)) {
752: return last + 1;
753: }
754: if (x >= lastPos && x <= pos) {
755: return i;
756: }
757: }
758: return -1;
759: }
760:
761: public void setPadding(Dimension d) {
762: getWrapped().setPadding(d);
763: setChanged(true);
764: }
765:
766: public int getH(int index) {
767: if (changed) {
768: change();
769: }
770: try {
771: return getWrapped().getH(index);
772: } catch (IndexOutOfBoundsException e) {
773: //The tab was just removed, and the selection model was notified,
774: //by the data model, but not everything else has been notified yet
775: return 0;
776: }
777: }
778:
779: /**
780: * Returns a cached width, after checking the changed flag and calling
781: * change() if recalculation is needed
782: */
783: public int getW(int index) {
784: //widths can be null on OS-X if component is instantiated with
785: //0 size (some bug with reloading winsys) and has never been painted
786: if (changed || widths == null || index > widths.length) {
787: change();
788: }
789: if (index >= widths.length) {
790: //If a tab has just been removed, there may be a request to
791: //repaint it
792: return 0;
793: }
794: return widths[index];
795: }
796:
797: public int getX(int index) {
798: if (changed) {
799: change();
800: }
801: int result = minimumXposition;
802: for (int i = 0; i < index; i++) {
803: result += getW(i);
804: }
805: return result;
806: }
807:
808: public int getY(int index) {
809: if (changed) {
810: change();
811: }
812: return getWrapped().getY(index);
813: }
814:
815: public int indexOfPoint(int x, int y) {
816: if (changed) {
817: change();
818: }
819: int pos = minimumXposition;
820: int lastPos;
821: for (int i = offset == -1 ? 0 : offset; i < mdl.size(); i++) {
822: lastPos = pos;
823: int w = getW(i);
824: pos += w;
825: if (w == 0) {
826: break;
827: }
828: int h = getH(i);
829: int ay = getY(i);
830: if (y < 0 || y > ay + h) {
831: return -1;
832: }
833: if (x > lastPos && x < pos) {
834: return i;
835: }
836: }
837: return -1;
838: }
839:
840: private Action fAction = null;
841: private Action bAction = null;
842:
843: /**
844: * Returns an Action that the control buttons can call to scroll forward
845: */
846: public Action getForwardAction() {
847: if (fAction == null) {
848: fAction = new ForwardAction();
849: }
850: return fAction;
851: }
852:
853: /**
854: * Returns an Action that the control buttons can call to scroll backward
855: */
856: public Action getBackwardAction() {
857: if (bAction == null) {
858: bAction = new BackwardAction();
859: }
860: return bAction;
861: }
862:
863: /**
864: * Update the enabled state of the button actions if the state of the layout
865: * has changed in a way that affects them
866: */
867: private void updateActions() {
868: if (width <= getMinimumLeftClippedWidth()) {
869: bAction.setEnabled(false);
870: fAction.setEnabled(false);
871: }
872: if (bAction != null) {
873: bAction.setEnabled(mdl.size() > 1 && offset > -1);
874: }
875: if (fAction != null) {
876: fAction.setEnabled(isLastTabClipped() && mdl.size() > 2
877: && (lastVisibleTab - firstVisibleTab > 1 //special case when a tab is too wide
878: || lastVisibleTab < mdl.size() - 1));
879: }
880: }
881:
882: /**
883: * An action which will scroll forward
884: */
885: private class ForwardAction extends AbstractAction {
886: public void actionPerformed(java.awt.event.ActionEvent e) {
887: setOffset(getOffset() + 1);
888: Component jc = (Component) getValue("control"); //NOI18N
889: //Use a convenient hack to get the control to paint
890: if (jc != null) {
891: jc.repaint();
892: }
893: }
894: }
895:
896: /**
897: * An action which will scroll backward
898: */
899: private class BackwardAction extends AbstractAction {
900: public void actionPerformed(java.awt.event.ActionEvent e) {
901: setOffset(getOffset() - 1);
902: //Use a convenient hack to get the control to paint
903: Component jc = (Component) getValue("control"); //NOI18N
904: if (jc != null) {
905: jc.repaint();
906: }
907: }
908: }
909: }
|