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: * AbstractTabCellRenderer.java
043: *
044: * Created on December 2, 2003, 4:13 PM
045: */
046:
047: package org.netbeans.swing.tabcontrol.plaf;
048:
049: import org.netbeans.swing.tabcontrol.TabData;
050: import org.netbeans.swing.tabcontrol.TabDisplayer;
051:
052: import org.openide.awt.HtmlRenderer;
053:
054: import javax.swing.*;
055: import javax.swing.border.Border;
056: import java.awt.*;
057: import java.awt.event.ContainerListener;
058: import java.awt.event.HierarchyBoundsListener;
059: import java.awt.event.HierarchyListener;
060: import java.awt.event.MouseEvent;
061:
062: /**
063: * Base class for tab renderers for the tab control. This is a support class
064: * which will allow authors who want to provide a different look or behavior for
065: * tabbed controls with a minimum of coding. The main methods of interest are
066: * <ul><li><code>stateChanged()</code> - where the component should be
067: * configured to render a given tab</li><li><code>getState()</code> - where the
068: * current state is to be found at the time stateChanged is called</li>
069: * </ul>.
070: * <p>
071: * Typical usage is to pass one or more TabPainter objects to the constructor
072: * which will be responsible for doing the actual painting, calling the convenience
073: * getters in this class (such as <code>isSelected</code>) to determine how
074: * to paint.
075: *
076: *
077: * @author Tim Boudreau
078: */
079: public abstract class AbstractTabCellRenderer extends JLabel implements
080: TabCellRenderer {
081: private int state = TabState.NOT_ONSCREEN;
082: TabPainter leftBorder;
083: TabPainter rightBorder;
084: TabPainter normalBorder;
085: private Dimension padding;
086:
087: /**
088: * Creates a new instance of AbstractTabCellRenderer
089: */
090: public AbstractTabCellRenderer(TabPainter leftClip,
091: TabPainter noClip, TabPainter rightClip, Dimension padding) {
092: setOpaque(false);
093: setFocusable(false);
094: setBorder(noClip);
095: normalBorder = noClip;
096: leftBorder = leftClip;
097: rightBorder = rightClip;
098: this .padding = padding;
099: }
100:
101: public AbstractTabCellRenderer(TabPainter painter, Dimension padding) {
102: this (painter, painter, painter, padding);
103: }
104:
105: private boolean showClose = true;
106:
107: public final void setShowCloseButton(boolean b) {
108: showClose = b;
109: }
110:
111: public final boolean isShowCloseButton() {
112: return showClose;
113: }
114:
115: private Rectangle scratch = new Rectangle();
116:
117: public String getCommandAtPoint(Point p, int tabState,
118: Rectangle bounds) {
119: setBounds(bounds);
120: setState(tabState);
121: if (supportsCloseButton(getBorder()) && isShowCloseButton()) {
122: TabPainter cbp = (TabPainter) getBorder();
123: cbp.getCloseButtonRectangle(this , scratch, bounds);
124: if (getClass() != AquaEditorTabCellRenderer.class) {
125: //#47408 - hit test area of close button is too small
126: scratch.x -= 3;
127: scratch.y -= 3;
128: scratch.width += 6;
129: scratch.height += 6;
130: }
131: if (scratch.contains(p)) {
132: return TabDisplayer.COMMAND_CLOSE;
133: }
134: }
135: Polygon tabShape = getTabShape(tabState, bounds);
136: if (tabShape.contains(p)) {
137: return TabDisplayer.COMMAND_SELECT;
138: }
139: return null;
140: }
141:
142: public String getCommandAtPoint(Point p, int tabState,
143: Rectangle bounds, int mouseButton, int eventType,
144: int modifiers) {
145: String result = null;
146: if (mouseButton == MouseEvent.BUTTON2
147: && eventType == MouseEvent.MOUSE_RELEASED) {
148: result = TabDisplayer.COMMAND_CLOSE;
149: } else {
150: result = getCommandAtPoint(p, tabState, bounds);
151: }
152: if (result != null) {
153: if (TabDisplayer.COMMAND_SELECT == result) {
154: boolean clipped = isClipLeft() || isClipRight();
155: if ((clipped && eventType == MouseEvent.MOUSE_RELEASED && mouseButton == MouseEvent.BUTTON1)
156: || (!clipped
157: && eventType == MouseEvent.MOUSE_PRESSED && mouseButton == MouseEvent.BUTTON1)) {
158:
159: return result;
160: }
161: } else if (TabDisplayer.COMMAND_CLOSE == result
162: && eventType == MouseEvent.MOUSE_RELEASED
163: && isShowCloseButton()) {
164: if ((modifiers & MouseEvent.SHIFT_DOWN_MASK) != 0) {
165: return TabDisplayer.COMMAND_CLOSE_ALL;
166: } else if ((modifiers & MouseEvent.ALT_DOWN_MASK) != 0
167: && mouseButton != MouseEvent.BUTTON2) {
168: return TabDisplayer.COMMAND_CLOSE_ALL_BUT_THIS;
169: }
170: return result;
171: }
172: }
173: return null;
174: }
175:
176: //********************** Subclass convenience API methods*****************
177:
178: /**
179: * Convenience getter to determine if the current state includes the armed
180: * state (the mouse is in the tab the component is currently configured to
181: * render).
182: */
183: protected final boolean isArmed() {
184: return isPressed() || (state & TabState.ARMED) != 0;
185: }
186:
187: /**
188: * Convenience getter to determine if the current state includes the active
189: * state (a component in the container or the container itself has keyboard
190: * focus)
191: */
192: protected final boolean isActive() {
193: return (state & TabState.ACTIVE) != 0;
194: }
195:
196: /**
197: * Convenience getter to determine if the current state includes the pressed
198: * state (the mouse is in the tab this component is currently configured to
199: * render, and the mouse button is currently down)
200: */
201: protected final boolean isPressed() {
202: return (state & TabState.PRESSED) != 0;
203: }
204:
205: /**
206: * Convenience getter to determine if the current state includes the
207: * selected state (the tab this component is currently configured to render
208: * is the selected tab in a container)
209: */
210: protected final boolean isSelected() {
211: return (state & TabState.SELECTED) != 0;
212: }
213:
214: /**
215: * Convenience getter to determine if the current state includes the
216: * right-clipped state (the right hand side of the tab is not visible).
217: */
218: protected final boolean isClipRight() {
219: return (state & TabState.CLIP_RIGHT) != 0;
220: }
221:
222: /**
223: * Convenience getter to determine if the current state includes the
224: * left-clipped state (the right hand side of the tab is not visible).
225: */
226: protected final boolean isClipLeft() {
227: return (state & TabState.CLIP_LEFT) != 0;
228: }
229:
230: /**
231: * Convenience getter to determine if the current state indicates
232: * that the renderer is currently configured as the leftmost (non-clipped).
233: */
234: protected final boolean isLeftmost() {
235: return (state & TabState.LEFTMOST) != 0;
236: }
237:
238: /**
239: * Convenience getter to determine if the current state indicates
240: * that the renderer is currently configured as the rightmost (non-clipped).
241: */
242: protected final boolean isRightmost() {
243: return (state & TabState.RIGHTMOST) != 0;
244: }
245:
246: protected final boolean isAttention() {
247: return (state & TabState.ATTENTION) != 0;
248: }
249:
250: /**
251: * Convenience getter to determine if the current state indicates
252: * that the renderer is currently configured appears to the left of
253: * the selected tab.
254: */
255: protected final boolean isNextTabSelected() {
256: return (state & TabState.BEFORE_SELECTED) != 0;
257: }
258:
259: /**
260: * Convenience getter to determine if the current state indicates
261: * that the renderer is currently configured appears to the left of
262: * the armed tab.
263: */
264: protected final boolean isNextTabArmed() {
265: return (state & TabState.BEFORE_ARMED) != 0;
266: }
267:
268: /**
269: * Convenience getter to determine if the current state indicates
270: * that the renderer is currently configured appears to the right of
271: * the selected tab.
272: */
273: protected final boolean isPreviousTabSelected() {
274: return (state & TabState.AFTER_SELECTED) != 0;
275: }
276:
277: public Dimension getPadding() {
278: return new Dimension(padding);
279: }
280:
281: private int phash = -1;
282:
283: /** Set the state of the renderer, in preparation for painting it or evaluating a condition
284: * (such as the position of the close button) for which it must be correctly configured).
285: * This method will call stateChanged(), allowing the renderer to reconfigure itself if
286: * necessary, when the state changes.
287: *
288: * @param state
289: */
290: protected final void setState(int state) {
291: //System.err.println("Renderer SetState " + TabState.stateToString(state));
292: boolean needChange = this .state != state;
293: if (needChange) {
294: int old = this .state;
295: //Set the state value here, so isArmed(), etc. will return
296: //correct values in stateChanged(), so subclasses can set
297: //up colors correctly
298: this .state = state;
299: int newState = stateChanged(old, state);
300: if ((newState & this .state) != state) {
301: this .state = state;
302: throw new IllegalStateException(
303: "StateChanged may add, but not remove bits from the "
304: + "state bitmask. Expected state: "
305: + TabState.stateToString(state)
306: + " but got "
307: + TabState.stateToString(this .state));
308: }
309: this .state = newState;
310: }
311: }
312:
313: /**
314: * Returns the state as set up in getRendererComponent
315: */
316: public final int getState() {
317: return state;
318: }
319:
320: /**
321: * Implementation of getRendererComponent from TabCellRenderer. This
322: * method is final, and will configure the text, bounds and icon correctly
323: * according to the passed values, and call setState to set the state of the
324: * tab. Implementers must implement <code>stateChanged()</code> to handle
325: * any changes (background color, border, etc) necessary to reflect the
326: * current state as returned by <code>getState()</code>.
327: */
328: public final javax.swing.JComponent getRendererComponent(
329: TabData data, Rectangle bounds, int state) {
330: setBounds(bounds);
331: setText(data.getText());
332: setIcon(data.getIcon());
333: setState(state);
334: return this ;
335: }
336:
337: //***************SPI METHODS********************************************
338: /*
339: * Implementations of this method <strong>may not remove</strong> state bits
340: * that were passed in. A runtime check of the result will be performed,
341: * and in the case that some states were removed, a runtime exception will
342: * be thrown after this method exits.
343: */
344:
345: protected int stateChanged(int oldState, int newState) {
346: Color bg = isSelected() ? isActive() ? getSelectedActivatedBackground()
347: : getSelectedBackground()
348: : UIManager.getColor("control");
349: Color fg = isSelected() ? isActive() ? getSelectedActivatedForeground()
350: : getSelectedForeground()
351: : UIManager.getColor("textText");
352:
353: if (isArmed() && isPressed() && (isClipLeft() || isClipRight())) {
354: //Create an armed appearance for clipped, pressed tabs, which will respond
355: //to mouseReleased, not mousePressed
356: bg = getSelectedActivatedBackground();
357: fg = getSelectedActivatedForeground();
358: }
359:
360: if (isClipLeft()) {
361: setIcon(null);
362: setBorder(leftBorder);
363: } else if (isClipRight()) {
364: setBorder(rightBorder);
365: } else {
366: setBorder(normalBorder);
367: }
368:
369: setBackground(bg);
370: setForeground(fg);
371: return newState;
372: }
373:
374: /** Overridden to be a no-op for performance reasons */
375: public void revalidate() {
376: //do nothing - performance
377: }
378:
379: /** Overridden to be a no-op for performance reasons */
380: public void repaint() {
381: //do nothing - performance
382: }
383:
384: /** Overridden to be a no-op for performance reasons */
385: public void validate() {
386: //do nothing - performance
387: }
388:
389: /** Overridden to be a no-op for performance reasons */
390: public void repaint(long tm) {
391: //do nothing - performance
392: }
393:
394: /** Overridden to be a no-op for performance reasons */
395: public void repaint(long tm, int x, int y, int w, int h) {
396: //do nothing - performance
397: }
398:
399: /** Overridden to be a no-op for performance reasons */
400: protected final void firePropertyChange(String s, Object a, Object b) {
401: //do nothing - performance
402: }
403:
404: /** Overridden to be a no-op for performance reasons */
405: public final void addHierarchyBoundsListener(
406: HierarchyBoundsListener hbl) {
407: //do nothing
408: }
409:
410: /** Overridden to be a no-op for performance reasons */
411: public final void addHierarchyListener(HierarchyListener hl) {
412: //do nothing
413: }
414:
415: /** Overridden to be a no-op for performance reasons */
416: public final void addContainerListener(ContainerListener cl) {
417: //do nothing
418: }
419:
420: /**
421: * Overridden to paint the interior of the polygon if the border is an instance of TabPainter.
422: */
423: public void paintComponent(Graphics g) {
424: g.setColor(getBackground());
425: if (getBorder() instanceof TabPainter) {
426: ((TabPainter) getBorder()).paintInterior(g, this );
427: }
428: paintIconAndText(g);
429: }
430:
431: /** Return non-zero to shift the text up or down by the specified number of pixels when painting.
432: *
433: * @return A positive or negative number of pixels
434: */
435: protected int getCaptionYAdjustment() {
436: return -1;
437: }
438:
439: /** Return non-zero to shift the icon up or down by the specified number of pixels when painting.
440: *
441: * @return A positive or negative number of pixels
442: */
443: protected int getIconYAdjustment() {
444: return -1;
445: }
446:
447: /**
448: * Actually paints the icon and text (using the lightweight HTML renderer)
449: *
450: * @param g The graphics context
451: */
452: protected void paintIconAndText(Graphics g) {
453: g.setFont(getFont());
454: FontMetrics fm = g.getFontMetrics(getFont());
455: //Find out what height we need
456: int txtH = fm.getHeight();
457: Insets ins = getInsets();
458: //find out the available height
459: int availH = getHeight() - (ins.top + ins.bottom);
460: int txtY;
461: if (availH > txtH) {
462: txtY = txtH + ins.top + ((availH / 2) - (txtH / 2)) - 3;
463: } else {
464: txtY = txtH + ins.top;
465: }
466: int txtX;
467:
468: int centeringToAdd = getPixelsToAddToSelection() != 0 ? getPixelsToAddToSelection() / 2
469: : 0;
470:
471: Icon icon = getIcon();
472: //Check the icon non-null and height (see TabData.NO_ICON for why)
473: if (!isClipLeft() && icon != null && icon.getIconWidth() > 0
474: && icon.getIconHeight() > 0) {
475: int iconY;
476: if (availH > icon.getIconHeight()) {
477: //add 2 to make sure icon top pixels are not cut off by outline
478: iconY = ins.top
479: + ((availH / 2) - (icon.getIconHeight() / 2))
480: + 2;
481: } else {
482: //add 2 to make sure icon top pixels are not cut off by outline
483: iconY = ins.top + 2;
484: }
485: int iconX = ins.left + centeringToAdd;
486:
487: iconY += getIconYAdjustment();
488:
489: icon.paintIcon(this , g, iconX, iconY);
490: txtX = iconX + icon.getIconWidth() + getIconTextGap();
491: } else {
492: txtX = ins.left + centeringToAdd;
493: }
494:
495: if (icon != null && icon.getIconWidth() == 0) {
496: //Add some spacing so the text isn't flush for, e.g., the
497: //welcome screen tab
498: txtX += 5;
499: }
500:
501: txtY += getCaptionYAdjustment();
502:
503: //Get the available horizontal pixels for text
504: int txtW = getWidth() - (txtX + ins.right);
505: if (isClipLeft()) {
506: //fiddle with the string to get "...blah"
507: String s = preTruncateString(getText(), g, txtW - 4); //subtract 4 so it's not flush w/ tab edge
508: HtmlRenderer.renderString(s, g, txtX, txtY, txtW, txtH,
509: getFont(), getForeground(),
510: HtmlRenderer.STYLE_CLIP, true);
511: } else {
512: String s;
513: if (isClipRight()) {
514: //Jano wants to always show a "..." for cases where a tab is truncated,
515: //even if we've really painted all the text.
516: s = getText() + "..."; //NOI18N
517: } else {
518: s = getText();
519: }
520: HtmlRenderer.renderString(s, g, txtX, txtY, txtW, txtH,
521: getFont(), getForeground(),
522: HtmlRenderer.STYLE_TRUNCATE, true);
523: }
524: }
525:
526: static String preTruncateString(String s, Graphics g,
527: int availPixels) {
528: if (s.length() < 3) {
529: return s;
530: }
531: s = stripHTML(s);
532: if (s.length() < 2) {
533: return "..." + s; //NOI18N
534: }
535: FontMetrics fm = g.getFontMetrics();
536: int dotsWidth = fm.stringWidth("..."); //NOI18N
537: int beginIndex = s.length() - 2;
538: String test = s.substring(beginIndex);
539: String result = test;
540: while (fm.stringWidth(test) + dotsWidth < availPixels) {
541: beginIndex--;
542: if (beginIndex <= 0) {
543: break;
544: } else {
545: result = test;
546: test = s.substring(beginIndex);
547: }
548: }
549: return "..." + result; //NOI18N
550: }
551:
552: static boolean isHTML(String s) {
553: boolean result = s.startsWith("<html>")
554: || s.startsWith("<HTML>"); //NOI18N
555: return result;
556: }
557:
558: static String stripHTML(String s) {
559: if (isHTML(s)) {
560: StringBuffer result = new StringBuffer(s.length());
561: char[] c = s.toCharArray();
562: boolean inTag = false;
563: for (int i = 0; i < c.length; i++) {
564: //XXX need to handle entity includes
565: boolean wasInTag = inTag;
566: if (!inTag) {
567: if (c[i] == '<') {
568: inTag = true;
569: }
570: } else {
571: if (c[i] == '>') {
572: inTag = false;
573: }
574: }
575: if (!inTag && wasInTag == inTag) {
576: result.append(c[i]);
577: }
578: }
579: return result.toString();
580: } else {
581: return s;
582: }
583: }
584:
585: /**
586: * Get the shape of the tab. The implementation here will check if the
587: * border is an instance of TabPainter, and if so, use the polygon it
588: * returns, translating it to the position of the passed-in rectangle. If
589: * you are subclassing but do not intend to use TabPainter, you need to
590: * override this method
591: */
592: public Polygon getTabShape(int tabState, Rectangle bounds) {
593: setBounds(bounds);
594: setState(tabState);
595: if (getBorder() instanceof TabPainter) {
596: TabPainter pb = (TabPainter) getBorder();
597: Polygon p = pb.getInteriorPolygon(this );
598: p.translate(bounds.x, bounds.y);
599: return p;
600: } else {
601: //punt and return the bounds as a polygon - what else to do?
602: return new Polygon(new int[] { bounds.x,
603: bounds.x + bounds.width - 1,
604: bounds.x + bounds.width - 1, bounds.x }, new int[] {
605: bounds.y, bounds.y, bounds.y + bounds.height - 1,
606: bounds.y + bounds.height - 1 }, 4);
607: }
608: }
609:
610: public Color getSelectedBackground() {
611: Color base = UIManager.getColor("control"); //NOI18N
612: Color towards = UIManager.getColor("controlHighlight"); //NOI18N
613:
614: if (base == null) {
615: base = Color.GRAY;
616: }
617: if (towards == null) {
618: towards = Color.WHITE;
619: }
620:
621: Color result = ColorUtil.adjustTowards(base, 30, towards);
622: return result;
623: }
624:
625: public Color getSelectedActivatedBackground() {
626: return UIManager
627: .getColor("TabRenderer.selectedActivatedBackground");
628: }
629:
630: public Color getSelectedActivatedForeground() {
631: return UIManager
632: .getColor("TabRenderer.selectedActivatedForeground");
633: }
634:
635: public Color getSelectedForeground() {
636: return UIManager.getColor("TabRenderer.selectedForeground");
637: }
638:
639: protected boolean inCloseButton() {
640: return (state & TabState.CLOSE_BUTTON_ARMED) != 0;
641: }
642:
643: /**
644: * Subclasses which want to make the selected tab wider than it would otherwise be should return a value
645: * greater than 0 here. The default implementation returns 0.
646: */
647: public int getPixelsToAddToSelection() {
648: return 0;
649: }
650:
651: private boolean supportsCloseButton(Border b) {
652: if (b instanceof TabPainter) {
653: return ((TabPainter) b).supportsCloseButton(this );
654: } else {
655: return false;
656: }
657: }
658: }
|