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: /*
042: * BasicScrollingTabDisplayerUI.java
043: *
044: * Created on March 19, 2004, 1:08 PM
045: */
046:
047: package org.netbeans.swing.tabcontrol.plaf;
048:
049: import org.netbeans.swing.tabcontrol.TabDisplayer;
050:
051: import javax.swing.*;
052: import java.awt.*;
053: import java.awt.event.*;
054: import java.awt.image.BufferedImage;
055: import java.lang.ref.SoftReference;
056:
057: /**
058: * Base class for tab displayers that have scrollable tabs.
059: *
060: * @author Tim Boudreau
061: */
062: public abstract class BasicScrollingTabDisplayerUI extends
063: BasicTabDisplayerUI {
064: private Rectangle scratch = new Rectangle();
065:
066: private JPanel controlButtons;
067:
068: private TabControlButton btnScrollLeft;
069: private TabControlButton btnScrollRight;
070: private TabControlButton btnDropDown;
071: private TabControlButton btnMaximizeRestore;
072:
073: /**
074: * Creates a new instance of BasicScrollingTabDisplayerUI
075: */
076: public BasicScrollingTabDisplayerUI(TabDisplayer displayer) {
077: super (displayer);
078: }
079:
080: protected final TabLayoutModel createLayoutModel() {
081: DefaultTabLayoutModel dtlm = new DefaultTabLayoutModel(
082: displayer.getModel(), displayer);
083: return new ScrollingTabLayoutModel(dtlm, selectionModel,
084: displayer.getModel());
085: }
086:
087: protected TabState createTabState() {
088: return new ScrollingTabState();
089: }
090:
091: protected HierarchyListener createHierarchyListener() {
092: return new ScrollingHierarchyListener();
093: }
094:
095: public void makeTabVisible(int tab) {
096: if (scroll().makeVisible(tab, getTabsAreaWidth())) {
097: getTabsVisibleArea(scratch);
098: displayer.repaint(scratch.x, scratch.y, scratch.width,
099: scratch.height);
100: }
101: }
102:
103: /**
104: * Returns the width of the tabs area
105: */
106: protected final int getTabsAreaWidth() {
107: int result = displayer.getWidth();
108: Insets ins = getTabAreaInsets();
109: return result - (ins.left + ins.right);
110: }
111:
112: public Insets getTabAreaInsets() {
113: return new Insets(0, 0, 0, getControlButtons()
114: .getPreferredSize().width + 5);
115: }
116:
117: protected final int getLastVisibleTab() {
118: if (displayer.getModel().size() == 0) {
119: return -1;
120: }
121: return scroll().getLastVisibleTab(getTabsAreaWidth());
122: }
123:
124: protected final int getFirstVisibleTab() {
125: if (displayer.getModel().size() == 0) {
126: return -1;
127: }
128: return scroll().getFirstVisibleTab(getTabsAreaWidth());
129: }
130:
131: protected void install() {
132: super .install();
133: installControlButtons();
134: ((ScrollingTabLayoutModel) layoutModel)
135: .setPixelsToAddToSelection(defaultRenderer
136: .getPixelsToAddToSelection());
137: }
138:
139: protected void uninstall() {
140: super .uninstall();
141: displayer.setLayout(null);
142: displayer.removeAll();
143: }
144:
145: protected LayoutManager createLayout() {
146: return new WCLayout();
147: }
148:
149: /**
150: * @return A component that holds control buttons (scroll left/right, drop down menu)
151: * that are displayed to right of the tab area.
152: */
153: protected Component getControlButtons() {
154: if (null == controlButtons) {
155: JPanel buttonsPanel = new JPanel(null);
156: buttonsPanel.setOpaque(false);
157:
158: int width = 0;
159: int height = 0;
160:
161: final boolean isGTK = "GTK".equals(UIManager
162: .getLookAndFeel().getID());
163:
164: //create scroll-left button
165: Action a = scroll().getBackwardAction();
166: a.putValue("control", displayer); //NO18N
167: btnScrollLeft = TabControlButtonFactory
168: .createScrollLeftButton(displayer, a, isGTK);
169: buttonsPanel.add(btnScrollLeft);
170: Dimension prefDim = btnScrollLeft.getPreferredSize();
171: btnScrollLeft.setBounds(width, 0, prefDim.width,
172: prefDim.height);
173: width += prefDim.width;
174: height = prefDim.height;
175:
176: //create scroll-right button
177: a = scroll().getForwardAction();
178: a.putValue("control", displayer); //NO18N
179: btnScrollRight = TabControlButtonFactory
180: .createScrollRightButton(displayer, a, isGTK);
181: buttonsPanel.add(btnScrollRight);
182: prefDim = btnScrollRight.getPreferredSize();
183: btnScrollRight.setBounds(width, 0, prefDim.width,
184: prefDim.height);
185: width += prefDim.width;
186: height = Math.max(height, prefDim.height);
187:
188: //create drop down button
189: btnDropDown = TabControlButtonFactory.createDropDownButton(
190: displayer, isGTK);
191: buttonsPanel.add(btnDropDown);
192:
193: width += 3;
194: prefDim = btnDropDown.getPreferredSize();
195: btnDropDown.setBounds(width, 0, prefDim.width,
196: prefDim.height);
197: width += prefDim.width;
198: height = Math.max(height, prefDim.height);
199:
200: //maximize / restore button
201: if (null != displayer.getWinsysInfo()) {
202: width += 3;
203: btnMaximizeRestore = TabControlButtonFactory
204: .createMaximizeRestoreButton(displayer, isGTK);
205: buttonsPanel.add(btnMaximizeRestore);
206: prefDim = btnMaximizeRestore.getPreferredSize();
207: btnMaximizeRestore.setBounds(width, 0, prefDim.width,
208: prefDim.height);
209: width += prefDim.width;
210: height = Math.max(height, prefDim.height);
211: }
212:
213: Dimension size = new Dimension(width, height);
214: buttonsPanel.setMinimumSize(size);
215: buttonsPanel.setSize(size);
216: buttonsPanel.setPreferredSize(size);
217: buttonsPanel.setMaximumSize(size);
218:
219: controlButtons = buttonsPanel;
220: }
221: return controlButtons;
222: }
223:
224: protected ComponentListener createComponentListener() {
225: return new ScrollingDisplayerComponentListener();
226: }
227:
228: private int lastKnownModelSize = Integer.MAX_VALUE;
229:
230: /** Overrides <code>modelChanged()</code> to clear the transient information in the
231: * state model, which may now contain tab indices that don't exist, and also
232: * to clear cached width/last-visible-tab data in the layout model, and ensure that
233: * the selected tab is visible.
234: */
235: protected void modelChanged() {
236: scroll().clearCachedData();
237: int index = selectionModel.getSelectedIndex();
238:
239: //If the user has intentionally scrolled the selected tab offscreen, do ensure space is
240: //optimally used, but don't volunteer to radically change the scroll point
241: if (index >= scroll().getCachedFirstVisibleTab()
242: && index < scroll().getCachedLastVisibleTab()) {
243: makeTabVisible(selectionModel.getSelectedIndex());
244: }
245:
246: int modelSize = displayer.getModel().size();
247: if (modelSize < lastKnownModelSize) {
248: //When closing tabs, make sure we resync the state, so the
249: //user doesn't end up with a huge gap due to closed tabs
250: scroll().ensureAvailableSpaceUsed(true);
251: }
252: lastKnownModelSize = modelSize;
253: super .modelChanged();
254: }
255:
256: protected void installControlButtons() {
257: displayer.setLayout(createLayout());
258: displayer.add(getControlButtons());
259: }
260:
261: public Dimension getMinimumSize(JComponent c) {
262: return getPreferredSize(c);
263: }
264:
265: /**
266: * Convenience getter for the layout model as an instance of
267: * ScrollingTabLayoutModel
268: */
269: protected final ScrollingTabLayoutModel scroll() {
270: return (ScrollingTabLayoutModel) layoutModel;
271: }
272:
273: /**
274: * Overridden to update the offset of the ScrollingTabLayoutModel on mouse
275: * wheel events
276: */
277: protected void processMouseWheelEvent(MouseWheelEvent e) {
278: int i = e.getWheelRotation();
279: //clear the mouse-in-tab index so we don't occasionally have
280: //tabs the mouse is not in scrolling away looking as if the mouse
281: //is in them
282: tabState.clearTransientStates();
283: int offset = scroll().getOffset();
284: if (i > 0 && (offset < displayer.getModel().size() - 1)) {
285: if (scroll().isLastTabClipped()) {
286: scroll().setOffset(offset + 1);
287: }
288: } else if (i < 0) {
289: if (offset >= 0) {
290: scroll().setOffset(offset - 1);
291: }
292: } else {
293: return;
294: }
295:
296: //tabState.repaintAllTabs();
297: //XXX should optimize this - need to make sure the space below the tabs
298: //is painted on metal and win classic
299: displayer.repaint();
300: }
301:
302: protected class ScrollingTabState extends BasicTabState {
303: public int getState(int tabIndex) {
304: int result = super .getState(tabIndex);
305: int first = getFirstVisibleTab();
306: int last = getLastVisibleTab();
307:
308: if (tabIndex < first || tabIndex > last) {
309: return TabState.NOT_ONSCREEN;
310: }
311: if (first == last && first == tabIndex
312: && displayer.getModel().size() > 1) {
313: //We have a very small area to fit tabs - smaller than even the
314: //minimum clip width, probably < 40 pixels. Definitely don't
315: //want to display a close button or much of anything else
316: result |= TabState.CLIP_LEFT | TabState.CLIP_RIGHT;
317:
318: } else if (getTabsAreaWidth() < scroll()
319: .getMinimumLeftClippedWidth()
320: + scroll().getMinimumRightClippedWidth()
321: && tabIndex == first
322: && last == first - 1
323: && displayer.getModel().size() > 1
324: && scroll().isLastTabClipped()) {
325: //when we're displaying two tabs in less than enough room,
326: //make sure a truncated tab is never displayed with a close button
327: result |= TabState.CLIP_LEFT;
328: } else {
329: if (tabIndex == first && scroll().getOffset() == first) {
330: result |= TabState.CLIP_LEFT;
331: }
332: if (tabIndex == last && scroll().isLastTabClipped()) {
333: result |= TabState.CLIP_RIGHT;
334: }
335: }
336: return result;
337: }
338: }
339:
340: protected class ScrollingDisplayerComponentListener extends
341: ComponentAdapter {
342: public void componentResized(ComponentEvent e) {
343: //Notify the layout model that its cached sizes are invalid
344: makeTabVisible(selectionModel.getSelectedIndex());
345: }
346: }
347:
348: protected class ScrollingHierarchyListener extends
349: DisplayerHierarchyListener {
350: public void hierarchyChanged(HierarchyEvent e) {
351: super .hierarchyChanged(e);
352: if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
353: if (displayer.isShowing()) {
354: //#47850 - for some reason, uninstall can be called on the Ui class, before this gets processed.
355: // check for null values just to be sure.
356: if (tabState != null && selectionModel != null) {
357: tabState.setActive(displayer.isActive());
358: makeTabVisible(selectionModel
359: .getSelectedIndex());
360: }
361: }
362: }
363: }
364: }
365:
366: static SoftReference<BufferedImage> ctx = null;
367:
368: /**
369: * Provides an offscreen graphics context so that widths based on character
370: * size can be calculated correctly before the component is shown
371: */
372: public static Graphics2D getOffscreenGraphics() {
373: BufferedImage result = null;
374: //XXX multi-monitors w/ different resolution may have problems;
375: //Better to call Toolkit to create a screen graphics
376: if (ctx != null) {
377: result = ctx.get();
378: }
379: if (result == null) {
380: result = new BufferedImage(10, 10,
381: BufferedImage.TYPE_INT_RGB);
382: ctx = new SoftReference<BufferedImage>(result);
383: }
384: return (Graphics2D) result.getGraphics();
385: }
386:
387: /**
388: * @return Bounds for the control buttons in the tab displayer container.
389: */
390: protected Rectangle getControlButtonsRectangle(Container parent) {
391: Component c = getControlButtons();
392: return new Rectangle(parent.getWidth() - c.getWidth(), 0, c
393: .getWidth(), c.getHeight());
394: }
395:
396: /**
397: * Layout manager for the tab displayer to make sure that control buttons
398: * are always displayed at the end of the tab list.
399: */
400: private class WCLayout implements LayoutManager {
401:
402: public void addLayoutComponent(String name, Component comp) {
403: }
404:
405: public void layoutContainer(java.awt.Container parent) {
406:
407: Rectangle r = getControlButtonsRectangle(parent);
408: Component c = getControlButtons();
409: c.setBounds(r);
410: }
411:
412: public Dimension minimumLayoutSize(Container parent) {
413: return getPreferredSize((JComponent) parent);
414: }
415:
416: public Dimension preferredLayoutSize(Container parent) {
417: return getPreferredSize((JComponent) parent);
418: }
419:
420: public void removeLayoutComponent(java.awt.Component comp) {
421: }
422: }
423: }
|