1 /*
2  * Copyright (c) 2002, 2014, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package javax.swing.plaf.synth;
27 
28 import javax.swing.*;
29 import javax.swing.plaf.*;
30 import javax.swing.plaf.basic.*;
31 import javax.swing.text.View;
32 
33 import java.awt.*;
34 import java.awt.event.*;
35 import java.beans.PropertyChangeListener;
36 import java.beans.PropertyChangeEvent;
37 import sun.swing.SwingUtilities2;
38 
39 /**
40  * Provides the Synth L&F UI delegate for
41  * {@link javax.swing.JTabbedPane}.
42  *
43  * <p>Looks up the {@code selectedTabPadInsets} property from the Style,
44  * which represents additional insets for the selected tab.
45  *
46  * @author Scott Violet
47  * @since 1.7
48  */
49 public class SynthTabbedPaneUI extends BasicTabbedPaneUI
50                                implements PropertyChangeListener, SynthUI {
51 
52     /**
53      * <p>If non-zero, tabOverlap indicates the amount that the tab bounds
54      * should be altered such that they would overlap with a tab on either the
55      * leading or trailing end of a run (ie: in TOP, this would be on the left
56      * or right).</p>
57 
58      * <p>A positive overlap indicates that tabs should overlap right/down,
59      * while a negative overlap indicates tha tabs should overlap left/up.</p>
60      *
61      * <p>When tabOverlap is specified, it both changes the x position and width
62      * of the tab if in TOP or BOTTOM placement, and changes the y position and
63      * height if in LEFT or RIGHT placement.</p>
64      *
65      * <p>This is done for the following reason. Consider a run of 10 tabs.
66      * There are 9 gaps between these tabs. If you specified a tabOverlap of
67      * "-1", then each of the tabs "x" values will be shifted left. This leaves
68      * 9 pixels of space to the right of the right-most tab unpainted. So, each
69      * tab's width is also extended by 1 pixel to make up the difference.</p>
70      *
71      * <p>This property respects the RTL component orientation.</p>
72      */
73     private int tabOverlap = 0;
74 
75     /**
76      * When a tabbed pane has multiple rows of tabs, this indicates whether
77      * the tabs in the upper row(s) should extend to the base of the tab area,
78      * or whether they should remain at their normal tab height. This does not
79      * affect the bounds of the tabs, only the bounds of area painted by the
80      * tabs. The text position does not change. The result is that the bottom
81      * border of the upper row of tabs becomes fully obscured by the lower tabs,
82      * resulting in a cleaner look.
83      */
84     private boolean extendTabsToBase = false;
85 
86     private SynthContext tabAreaContext;
87     private SynthContext tabContext;
88     private SynthContext tabContentContext;
89 
90     private SynthStyle style;
91     private SynthStyle tabStyle;
92     private SynthStyle tabAreaStyle;
93     private SynthStyle tabContentStyle;
94 
95     private Rectangle textRect = new Rectangle();
96     private Rectangle iconRect = new Rectangle();
97 
98     private Rectangle tabAreaBounds = new Rectangle();
99 
100     //added for the Nimbus look and feel, where the tab area is painted differently depending on the
101     //state for the selected tab
102     private boolean tabAreaStatesMatchSelectedTab = false;
103     //added for the Nimbus LAF to ensure that the labels don't move whether the tab is selected or not
104     private boolean nudgeSelectedLabel = true;
105 
106     private boolean selectedTabIsPressed = false;
107 
108     /**
109      * Creates a new UI object for the given component.
110      *
111      * @param c component to create UI object for
112      * @return the UI object
113      */
createUI(JComponent c)114     public static ComponentUI createUI(JComponent c) {
115         return new SynthTabbedPaneUI();
116     }
117 
scrollableTabLayoutEnabled()118      private boolean scrollableTabLayoutEnabled() {
119         return (tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT);
120     }
121 
122     /**
123      * {@inheritDoc}
124      */
125     @Override
installDefaults()126     protected void installDefaults() {
127         updateStyle(tabPane);
128     }
129 
updateStyle(JTabbedPane c)130     private void updateStyle(JTabbedPane c) {
131         SynthContext context = getContext(c, ENABLED);
132         SynthStyle oldStyle = style;
133         style = SynthLookAndFeel.updateStyle(context, this);
134         // Add properties other than JComponent colors, Borders and
135         // opacity settings here:
136         if (style != oldStyle) {
137             tabRunOverlay =
138                 style.getInt(context, "TabbedPane.tabRunOverlay", 0);
139             tabOverlap = style.getInt(context, "TabbedPane.tabOverlap", 0);
140             extendTabsToBase = style.getBoolean(context,
141                     "TabbedPane.extendTabsToBase", false);
142             textIconGap = style.getInt(context, "TabbedPane.textIconGap", 0);
143             selectedTabPadInsets = (Insets)style.get(context,
144                 "TabbedPane.selectedTabPadInsets");
145             if (selectedTabPadInsets == null) {
146                 selectedTabPadInsets = new Insets(0, 0, 0, 0);
147             }
148             tabAreaStatesMatchSelectedTab = style.getBoolean(context,
149                     "TabbedPane.tabAreaStatesMatchSelectedTab", false);
150             nudgeSelectedLabel = style.getBoolean(context,
151                     "TabbedPane.nudgeSelectedLabel", true);
152             if (oldStyle != null) {
153                 uninstallKeyboardActions();
154                 installKeyboardActions();
155             }
156         }
157 
158         tabContext = getContext(c, Region.TABBED_PANE_TAB, ENABLED);
159         this.tabStyle = SynthLookAndFeel.updateStyle(tabContext, this);
160         tabInsets = tabStyle.getInsets(tabContext, null);
161 
162 
163         tabAreaContext = getContext(c, Region.TABBED_PANE_TAB_AREA, ENABLED);
164         this.tabAreaStyle = SynthLookAndFeel.updateStyle(tabAreaContext, this);
165         tabAreaInsets = tabAreaStyle.getInsets(tabAreaContext, null);
166 
167 
168         tabContentContext = getContext(c, Region.TABBED_PANE_CONTENT, ENABLED);
169         this.tabContentStyle = SynthLookAndFeel.updateStyle(tabContentContext,
170                                                             this);
171         contentBorderInsets =
172             tabContentStyle.getInsets(tabContentContext, null);
173     }
174 
175     /**
176      * {@inheritDoc}
177      */
178     @Override
installListeners()179     protected void installListeners() {
180         super.installListeners();
181         tabPane.addPropertyChangeListener(this);
182     }
183 
184     /**
185      * {@inheritDoc}
186      */
187     @Override
uninstallListeners()188     protected void uninstallListeners() {
189         super.uninstallListeners();
190         tabPane.removePropertyChangeListener(this);
191     }
192 
193     /**
194      * {@inheritDoc}
195      */
196     @Override
uninstallDefaults()197     protected void uninstallDefaults() {
198         SynthContext context = getContext(tabPane, ENABLED);
199         style.uninstallDefaults(context);
200         style = null;
201 
202         tabStyle.uninstallDefaults(tabContext);
203         tabContext = null;
204         tabStyle = null;
205 
206         tabAreaStyle.uninstallDefaults(tabAreaContext);
207         tabAreaContext = null;
208         tabAreaStyle = null;
209 
210         tabContentStyle.uninstallDefaults(tabContentContext);
211         tabContentContext = null;
212         tabContentStyle = null;
213     }
214 
215     /**
216      * {@inheritDoc}
217      */
218     @Override
getContext(JComponent c)219     public SynthContext getContext(JComponent c) {
220         return getContext(c, SynthLookAndFeel.getComponentState(c));
221     }
222 
getContext(JComponent c, int state)223     private SynthContext getContext(JComponent c, int state) {
224         return SynthContext.getContext(c, style, state);
225     }
226 
getContext(JComponent c, Region subregion, int state)227     private SynthContext getContext(JComponent c, Region subregion, int state){
228         SynthStyle style = null;
229 
230         if (subregion == Region.TABBED_PANE_TAB) {
231             style = tabStyle;
232         }
233         else if (subregion == Region.TABBED_PANE_TAB_AREA) {
234             style = tabAreaStyle;
235         }
236         else if (subregion == Region.TABBED_PANE_CONTENT) {
237             style = tabContentStyle;
238         }
239         return SynthContext.getContext(c, subregion, style, state);
240     }
241 
242     /**
243      * {@inheritDoc}
244      */
245     @Override
createScrollButton(int direction)246     protected JButton createScrollButton(int direction) {
247         // added for Nimbus LAF so that it can use the basic arrow buttons
248         // UIManager is queried directly here because this is called before
249         // updateStyle is called so the style can not be queried directly
250         if (UIManager.getBoolean("TabbedPane.useBasicArrows")) {
251             JButton btn = super.createScrollButton(direction);
252             btn.setBorder(BorderFactory.createEmptyBorder());
253             return btn;
254         }
255         return new SynthScrollableTabButton(direction);
256     }
257 
258     /**
259      * {@inheritDoc}
260      */
261     @Override
propertyChange(PropertyChangeEvent e)262     public void propertyChange(PropertyChangeEvent e) {
263         if (SynthLookAndFeel.shouldUpdateStyle(e)) {
264             updateStyle(tabPane);
265         }
266     }
267 
268     /**
269      * {@inheritDoc}
270      *
271      * Overridden to keep track of whether the selected tab is also pressed.
272      */
273     @Override
createMouseListener()274     protected MouseListener createMouseListener() {
275         final MouseListener delegate = super.createMouseListener();
276         final MouseMotionListener delegate2 = (MouseMotionListener)delegate;
277         return new MouseListener() {
278             public void mouseClicked(MouseEvent e) { delegate.mouseClicked(e); }
279             public void mouseEntered(MouseEvent e) { delegate.mouseEntered(e); }
280             public void mouseExited(MouseEvent e) { delegate.mouseExited(e); }
281 
282             public void mousePressed(MouseEvent e) {
283                 if (!tabPane.isEnabled()) {
284                     return;
285                 }
286 
287                 int tabIndex = tabForCoordinate(tabPane, e.getX(), e.getY());
288                 if (tabIndex >= 0 && tabPane.isEnabledAt(tabIndex)) {
289                     if (tabIndex == tabPane.getSelectedIndex()) {
290                         // Clicking on selected tab
291                         selectedTabIsPressed = true;
292                         //TODO need to just repaint the tab area!
293                         tabPane.repaint();
294                     }
295                 }
296 
297                 //forward the event (this will set the selected index, or none at all
298                 delegate.mousePressed(e);
299             }
300 
301             public void mouseReleased(MouseEvent e) {
302                 if (selectedTabIsPressed) {
303                     selectedTabIsPressed = false;
304                     //TODO need to just repaint the tab area!
305                     tabPane.repaint();
306                 }
307                 //forward the event
308                 delegate.mouseReleased(e);
309 
310                 //hack: The super method *should* be setting the mouse-over property correctly
311                 //here, but it doesn't. That is, when the mouse is released, whatever tab is below the
312                 //released mouse should be in rollover state. But, if you select a tab and don't
313                 //move the mouse, this doesn't happen. Hence, forwarding the event.
314                 delegate2.mouseMoved(e);
315             }
316         };
317     }
318 
319     /**
320      * {@inheritDoc}
321      */
322     @Override
323     protected int getTabLabelShiftX(int tabPlacement, int tabIndex, boolean isSelected) {
324         if (nudgeSelectedLabel) {
325             return super.getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
326         } else {
327             return 0;
328         }
329     }
330 
331     /**
332      * {@inheritDoc}
333      */
334     @Override
335     protected int getTabLabelShiftY(int tabPlacement, int tabIndex, boolean isSelected) {
336         if (nudgeSelectedLabel) {
337             return super.getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
338         } else {
339             return 0;
340         }
341     }
342 
343     /**
344      * Notifies this UI delegate to repaint the specified component.
345      * This method paints the component background, then calls
346      * the {@link #paint(SynthContext,Graphics)} method.
347      *
348      * <p>In general, this method does not need to be overridden by subclasses.
349      * All Look and Feel rendering code should reside in the {@code paint} method.
350      *
351      * @param g the {@code Graphics} object used for painting
352      * @param c the component being painted
353      * @see #paint(SynthContext,Graphics)
354      */
355     @Override
356     public void update(Graphics g, JComponent c) {
357         SynthContext context = getContext(c);
358 
359         SynthLookAndFeel.update(context, g);
360         context.getPainter().paintTabbedPaneBackground(context,
361                           g, 0, 0, c.getWidth(), c.getHeight());
362         paint(context, g);
363     }
364 
365     /**
366      * {@inheritDoc}
367      */
368     @Override
369     protected int getBaseline(int tab) {
370         if (tabPane.getTabComponentAt(tab) != null ||
371                 getTextViewForTab(tab) != null) {
372             return super.getBaseline(tab);
373         }
374         String title = tabPane.getTitleAt(tab);
375         Font font = tabContext.getStyle().getFont(tabContext);
376         FontMetrics metrics = getFontMetrics(font);
377         Icon icon = getIconForTab(tab);
378         textRect.setBounds(0, 0, 0, 0);
379         iconRect.setBounds(0, 0, 0, 0);
380         calcRect.setBounds(0, 0, Short.MAX_VALUE, maxTabHeight);
381         tabContext.getStyle().getGraphicsUtils(tabContext).layoutText(
382                 tabContext, metrics, title, icon, SwingUtilities.CENTER,
383                 SwingUtilities.CENTER, SwingUtilities.LEADING,
384                 SwingUtilities.CENTER, calcRect,
385                 iconRect, textRect, textIconGap);
386         return textRect.y + metrics.getAscent() + getBaselineOffset();
387     }
388 
389     /**
390      * {@inheritDoc}
391      */
392     @Override
393     public void paintBorder(SynthContext context, Graphics g, int x,
394                             int y, int w, int h) {
395         context.getPainter().paintTabbedPaneBorder(context, g, x, y, w, h);
396     }
397 
398     /**
399      * Paints the specified component according to the Look and Feel.
400      * <p>This method is not used by Synth Look and Feel.
401      * Painting is handled by the {@link #paint(SynthContext,Graphics)} method.
402      *
403      * @param g the {@code Graphics} object used for painting
404      * @param c the component being painted
405      * @see #paint(SynthContext,Graphics)
406      */
407     @Override
408     public void paint(Graphics g, JComponent c) {
409         SynthContext context = getContext(c);
410 
411         paint(context, g);
412     }
413 
414     /**
415      * Paints the specified component.
416      *
417      * @param context context for the component being painted
418      * @param g the {@code Graphics} object used for painting
419      * @see #update(Graphics,JComponent)
420      */
421     protected void paint(SynthContext context, Graphics g) {
422         int selectedIndex = tabPane.getSelectedIndex();
423         int tabPlacement = tabPane.getTabPlacement();
424 
425         ensureCurrentLayout();
426 
427         // Paint tab area
428         // If scrollable tabs are enabled, the tab area will be
429         // painted by the scrollable tab panel instead.
430         //
431         if (!scrollableTabLayoutEnabled()) { // WRAP_TAB_LAYOUT
432             Insets insets = tabPane.getInsets();
433             int x = insets.left;
434             int y = insets.top;
435             int width = tabPane.getWidth() - insets.left - insets.right;
436             int height = tabPane.getHeight() - insets.top - insets.bottom;
437             int size;
438             switch(tabPlacement) {
439             case LEFT:
440                 width = calculateTabAreaWidth(tabPlacement, runCount,
441                                               maxTabWidth);
442                 break;
443             case RIGHT:
444                 size = calculateTabAreaWidth(tabPlacement, runCount,
445                                              maxTabWidth);
446                 x = x + width - size;
447                 width = size;
448                 break;
449             case BOTTOM:
450                 size = calculateTabAreaHeight(tabPlacement, runCount,
451                                               maxTabHeight);
452                 y = y + height - size;
453                 height = size;
454                 break;
455             case TOP:
456             default:
457                 height = calculateTabAreaHeight(tabPlacement, runCount,
458                                                 maxTabHeight);
459             }
460 
461             tabAreaBounds.setBounds(x, y, width, height);
462 
463             if (g.getClipBounds().intersects(tabAreaBounds)) {
464                 paintTabArea(tabAreaContext, g, tabPlacement,
465                          selectedIndex, tabAreaBounds);
466             }
467         }
468 
469         // Paint content border
470         paintContentBorder(tabContentContext, g, tabPlacement, selectedIndex);
471     }
472 
473     protected void paintTabArea(Graphics g, int tabPlacement,
474                                 int selectedIndex) {
475         // This can be invoked from ScrollabeTabPanel
476         Insets insets = tabPane.getInsets();
477         int x = insets.left;
478         int y = insets.top;
479         int width = tabPane.getWidth() - insets.left - insets.right;
480         int height = tabPane.getHeight() - insets.top - insets.bottom;
481 
482         paintTabArea(tabAreaContext, g, tabPlacement, selectedIndex,
483                      new Rectangle(x, y, width, height));
484     }
485 
486     private void paintTabArea(SynthContext ss, Graphics g,
487                                 int tabPlacement, int selectedIndex,
488                                 Rectangle tabAreaBounds) {
489         Rectangle clipRect = g.getClipBounds();
490 
491         //if the tab area's states should match that of the selected tab, then
492         //first update the selected tab's states, then set the state
493         //for the tab area to match
494         //otherwise, restore the tab area's state to ENABLED (which is the
495         //only supported state otherwise).
496         if (tabAreaStatesMatchSelectedTab && selectedIndex >= 0) {
497             updateTabContext(selectedIndex, true, selectedTabIsPressed,
498                               (getRolloverTab() == selectedIndex),
499                               (getFocusIndex() == selectedIndex));
500             ss.setComponentState(tabContext.getComponentState());
501         } else {
502             ss.setComponentState(SynthConstants.ENABLED);
503         }
504 
505         // Paint the tab area.
506         SynthLookAndFeel.updateSubregion(ss, g, tabAreaBounds);
507         ss.getPainter().paintTabbedPaneTabAreaBackground(ss, g,
508              tabAreaBounds.x, tabAreaBounds.y, tabAreaBounds.width,
509              tabAreaBounds.height, tabPlacement);
510         ss.getPainter().paintTabbedPaneTabAreaBorder(ss, g, tabAreaBounds.x,
511              tabAreaBounds.y, tabAreaBounds.width, tabAreaBounds.height,
512              tabPlacement);
513 
514         int tabCount = tabPane.getTabCount();
515 
516         iconRect.setBounds(0, 0, 0, 0);
517         textRect.setBounds(0, 0, 0, 0);
518 
519         // Paint tabRuns of tabs from back to front
520         for (int i = runCount - 1; i >= 0; i--) {
521             int start = tabRuns[i];
522             int next = tabRuns[(i == runCount - 1)? 0 : i + 1];
523             int end = (next != 0? next - 1: tabCount - 1);
524             for (int j = start; j <= end; j++) {
525                 if (rects[j].intersects(clipRect) && selectedIndex != j) {
526                     paintTab(tabContext, g, tabPlacement, rects, j, iconRect,
527                              textRect);
528                 }
529             }
530         }
531 
532         if (selectedIndex >= 0) {
533             if (rects[selectedIndex].intersects(clipRect)) {
534                 paintTab(tabContext, g, tabPlacement, rects, selectedIndex,
535                          iconRect, textRect);
536             }
537         }
538     }
539 
540     /**
541      * {@inheritDoc}
542      */
543     @Override
544     protected void setRolloverTab(int index) {
545         int oldRolloverTab = getRolloverTab();
546         super.setRolloverTab(index);
547 
548         Rectangle r = null;
549 
550         if (oldRolloverTab != index && tabAreaStatesMatchSelectedTab) {
551             //TODO need to just repaint the tab area!
552             tabPane.repaint();
553         } else {
554             if ((oldRolloverTab >= 0) && (oldRolloverTab < tabPane.getTabCount())) {
555                 r = getTabBounds(tabPane, oldRolloverTab);
556                 if (r != null) {
557                     tabPane.repaint(r);
558                 }
559             }
560 
561             if (index >= 0) {
562                 r = getTabBounds(tabPane, index);
563                 if (r != null) {
564                     tabPane.repaint(r);
565                 }
566             }
567         }
568     }
569 
570     private void paintTab(SynthContext ss, Graphics g,
571                             int tabPlacement, Rectangle[] rects, int tabIndex,
572                             Rectangle iconRect, Rectangle textRect) {
573         Rectangle tabRect = rects[tabIndex];
574         int selectedIndex = tabPane.getSelectedIndex();
575         boolean isSelected = selectedIndex == tabIndex;
576         updateTabContext(tabIndex, isSelected, isSelected && selectedTabIsPressed,
577                             (getRolloverTab() == tabIndex),
578                             (getFocusIndex() == tabIndex));
579 
580         SynthLookAndFeel.updateSubregion(ss, g, tabRect);
581         int x = tabRect.x;
582         int y = tabRect.y;
583         int height = tabRect.height;
584         int width = tabRect.width;
585         int placement = tabPane.getTabPlacement();
586         if (extendTabsToBase && runCount > 1) {
587             //paint this tab such that its edge closest to the base is equal to
588             //edge of the selected tab closest to the base. In terms of the TOP
589             //tab placement, this will cause the bottom of each tab to be
590             //painted even with the bottom of the selected tab. This is because
591             //in each tab placement (TOP, LEFT, BOTTOM, RIGHT) the selected tab
592             //is closest to the base.
593             if (selectedIndex >= 0) {
594                 Rectangle r = rects[selectedIndex];
595                 switch (placement) {
596                     case TOP:
597                         int bottomY = r.y + r.height;
598                         height = bottomY - tabRect.y;
599                         break;
600                     case LEFT:
601                         int rightX = r.x + r.width;
602                         width = rightX - tabRect.x;
603                         break;
604                     case BOTTOM:
605                         int topY = r.y;
606                         height = (tabRect.y + tabRect.height) - topY;
607                         y = topY;
608                         break;
609                     case RIGHT:
610                         int leftX = r.x;
611                         width = (tabRect.x + tabRect.width) - leftX;
612                         x = leftX;
613                         break;
614                 }
615             }
616         }
617         tabContext.getPainter().paintTabbedPaneTabBackground(tabContext, g,
618                 x, y, width, height, tabIndex, placement);
619         tabContext.getPainter().paintTabbedPaneTabBorder(tabContext, g,
620                 x, y, width, height, tabIndex, placement);
621 
622         if (tabPane.getTabComponentAt(tabIndex) == null) {
623             String title = tabPane.getTitleAt(tabIndex);
624             String clippedTitle = title;
625             Font font = ss.getStyle().getFont(ss);
626             FontMetrics metrics = SwingUtilities2.getFontMetrics(tabPane, g, font);
627             Icon icon = getIconForTab(tabIndex);
628 
629             layoutLabel(ss, tabPlacement, metrics, tabIndex, title, icon,
630                     tabRect, iconRect, textRect, isSelected);
631             clippedTitle = SwingUtilities2.clipStringIfNecessary(null, metrics,
632                            title, textRect.width);
633             paintText(ss, g, tabPlacement, font, metrics,
634                     tabIndex, clippedTitle, textRect, isSelected);
635 
636             paintIcon(g, tabPlacement, tabIndex, icon, iconRect, isSelected);
637         }
638     }
639 
640     private void layoutLabel(SynthContext ss, int tabPlacement,
641                                FontMetrics metrics, int tabIndex,
642                                String title, Icon icon,
643                                Rectangle tabRect, Rectangle iconRect,
644                                Rectangle textRect, boolean isSelected ) {
645         View v = getTextViewForTab(tabIndex);
646         if (v != null) {
647             tabPane.putClientProperty("html", v);
648         }
649 
650         textRect.x = textRect.y = iconRect.x = iconRect.y = 0;
651 
652         ss.getStyle().getGraphicsUtils(ss).layoutText(ss, metrics, title,
653                          icon, SwingUtilities.CENTER, SwingUtilities.CENTER,
654                          SwingUtilities.LEADING, SwingUtilities.CENTER,
655                          tabRect, iconRect, textRect, textIconGap);
656 
657         tabPane.putClientProperty("html", null);
658 
659         int xNudge = getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
660         int yNudge = getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
661         iconRect.x += xNudge;
662         iconRect.y += yNudge;
663         textRect.x += xNudge;
664         textRect.y += yNudge;
665     }
666 
667     private void paintText(SynthContext ss,
668                              Graphics g, int tabPlacement,
669                              Font font, FontMetrics metrics, int tabIndex,
670                              String title, Rectangle textRect,
671                              boolean isSelected) {
672         g.setFont(font);
673 
674         View v = getTextViewForTab(tabIndex);
675         if (v != null) {
676             // html
677             v.paint(g, textRect);
678         } else {
679             // plain text
680             int mnemIndex = tabPane.getDisplayedMnemonicIndexAt(tabIndex);
681 
682             g.setColor(ss.getStyle().getColor(ss, ColorType.TEXT_FOREGROUND));
683             ss.getStyle().getGraphicsUtils(ss).paintText(ss, g, title,
684                                   textRect, mnemIndex);
685         }
686     }
687 
688 
689     private void paintContentBorder(SynthContext ss, Graphics g,
690                                       int tabPlacement, int selectedIndex) {
691         int width = tabPane.getWidth();
692         int height = tabPane.getHeight();
693         Insets insets = tabPane.getInsets();
694 
695         int x = insets.left;
696         int y = insets.top;
697         int w = width - insets.right - insets.left;
698         int h = height - insets.top - insets.bottom;
699 
700         switch(tabPlacement) {
701           case LEFT:
702               x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
703               w -= (x - insets.left);
704               break;
705           case RIGHT:
706               w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
707               break;
708           case BOTTOM:
709               h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
710               break;
711           case TOP:
712           default:
713               y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
714               h -= (y - insets.top);
715         }
716         SynthLookAndFeel.updateSubregion(ss, g, new Rectangle(x, y, w, h));
717         ss.getPainter().paintTabbedPaneContentBackground(ss, g, x, y,
718                                                            w, h);
719         ss.getPainter().paintTabbedPaneContentBorder(ss, g, x, y, w, h);
720     }
721 
722     private void ensureCurrentLayout() {
723         if (!tabPane.isValid()) {
724             tabPane.validate();
725         }
726         /* If tabPane doesn't have a peer yet, the validate() call will
727          * silently fail.  We handle that by forcing a layout if tabPane
728          * is still invalid.  See bug 4237677.
729          */
730         if (!tabPane.isValid()) {
731             TabbedPaneLayout layout = (TabbedPaneLayout)tabPane.getLayout();
732             layout.calculateLayoutInfo();
733         }
734     }
735 
736     /**
737      * {@inheritDoc}
738      */
739     @Override
740     protected int calculateMaxTabHeight(int tabPlacement) {
741         FontMetrics metrics = getFontMetrics(tabContext.getStyle().getFont(
742                                              tabContext));
743         int tabCount = tabPane.getTabCount();
744         int result = 0;
745         int fontHeight = metrics.getHeight();
746         for(int i = 0; i < tabCount; i++) {
747             result = Math.max(calculateTabHeight(tabPlacement, i, fontHeight), result);
748         }
749         return result;
750     }
751 
752     /**
753      * {@inheritDoc}
754      */
755     @Override
756     protected int calculateTabWidth(int tabPlacement, int tabIndex,
757                                     FontMetrics metrics) {
758         Icon icon = getIconForTab(tabIndex);
759         Insets tabInsets = getTabInsets(tabPlacement, tabIndex);
760         int width = tabInsets.left + tabInsets.right;
761         Component tabComponent = tabPane.getTabComponentAt(tabIndex);
762         if (tabComponent != null) {
763             width += tabComponent.getPreferredSize().width;
764         } else {
765             if (icon != null) {
766                 width += icon.getIconWidth() + textIconGap;
767             }
768             View v = getTextViewForTab(tabIndex);
769             if (v != null) {
770                 // html
771                 width += (int) v.getPreferredSpan(View.X_AXIS);
772             } else {
773                 // plain text
774                 String title = tabPane.getTitleAt(tabIndex);
775                 width += tabContext.getStyle().getGraphicsUtils(tabContext).
776                         computeStringWidth(tabContext, metrics.getFont(),
777                                 metrics, title);
778             }
779         }
780         return width;
781     }
782 
783     /**
784      * {@inheritDoc}
785      */
786     @Override
787     protected int calculateMaxTabWidth(int tabPlacement) {
788         FontMetrics metrics = getFontMetrics(tabContext.getStyle().getFont(
789                                      tabContext));
790         int tabCount = tabPane.getTabCount();
791         int result = 0;
792         for(int i = 0; i < tabCount; i++) {
793             result = Math.max(calculateTabWidth(tabPlacement, i, metrics),
794                               result);
795         }
796         return result;
797     }
798 
799     /**
800      * {@inheritDoc}
801      */
802     @Override
803     protected Insets getTabInsets(int tabPlacement, int tabIndex) {
804         updateTabContext(tabIndex, false, false, false,
805                           (getFocusIndex() == tabIndex));
806         return tabInsets;
807     }
808 
809     /**
810      * {@inheritDoc}
811      */
812     @Override
813     protected FontMetrics getFontMetrics() {
814         return getFontMetrics(tabContext.getStyle().getFont(tabContext));
815     }
816 
817     private FontMetrics getFontMetrics(Font font) {
818         return tabPane.getFontMetrics(font);
819     }
820 
821     private void updateTabContext(int index, boolean selected,
822                                   boolean isMouseDown, boolean isMouseOver, boolean hasFocus) {
823         int state = 0;
824         if (!tabPane.isEnabled() || !tabPane.isEnabledAt(index)) {
825             state |= SynthConstants.DISABLED;
826             if (selected) {
827                 state |= SynthConstants.SELECTED;
828             }
829         }
830         else if (selected) {
831             state |= (SynthConstants.ENABLED | SynthConstants.SELECTED);
832             if (isMouseOver && UIManager.getBoolean("TabbedPane.isTabRollover")) {
833                 state |= SynthConstants.MOUSE_OVER;
834             }
835         }
836         else if (isMouseOver) {
837             state |= (SynthConstants.ENABLED | SynthConstants.MOUSE_OVER);
838         }
839         else {
840             state = SynthLookAndFeel.getComponentState(tabPane);
841             state &= ~SynthConstants.FOCUSED; // don't use tabbedpane focus state
842         }
843         if (hasFocus && tabPane.hasFocus()) {
844             state |= SynthConstants.FOCUSED; // individual tab has focus
845         }
846         if (isMouseDown) {
847             state |= SynthConstants.PRESSED;
848         }
849 
850         tabContext.setComponentState(state);
851     }
852 
853     /**
854      * {@inheritDoc}
855      *
856      * Overridden to create a TabbedPaneLayout subclass which takes into
857      * account tabOverlap.
858      */
859     @Override
860     protected LayoutManager createLayoutManager() {
861         if (tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT) {
862             return super.createLayoutManager();
863         } else { /* WRAP_TAB_LAYOUT */
864             return new TabbedPaneLayout() {
865                 @Override
866                 public void calculateLayoutInfo() {
867                     super.calculateLayoutInfo();
868                     //shift all the tabs, if necessary
869                     if (tabOverlap != 0) {
870                         int tabCount = tabPane.getTabCount();
871                         //left-to-right/right-to-left only affects layout
872                         //when placement is TOP or BOTTOM
873                         boolean ltr = tabPane.getComponentOrientation().isLeftToRight();
874                         for (int i = runCount - 1; i >= 0; i--) {
875                             int start = tabRuns[i];
876                             int next = tabRuns[(i == runCount - 1)? 0 : i + 1];
877                             int end = (next != 0? next - 1: tabCount - 1);
878                             for (int j = start+1; j <= end; j++) {
879                                 // xshift and yshift represent the amount &
880                                 // direction to shift the tab in their
881                                 // respective axis.
882                                 int xshift = 0;
883                                 int yshift = 0;
884                                 // configure xshift and y shift based on tab
885                                 // position and ltr/rtl
886                                 switch (tabPane.getTabPlacement()) {
887                                     case JTabbedPane.TOP:
888                                     case JTabbedPane.BOTTOM:
889                                         xshift = ltr ? tabOverlap : -tabOverlap;
890                                         break;
891                                     case JTabbedPane.LEFT:
892                                     case JTabbedPane.RIGHT:
893                                         yshift = tabOverlap;
894                                         break;
895                                     default: //do nothing
896                                 }
897                                 rects[j].x += xshift;
898                                 rects[j].y += yshift;
899                                 rects[j].width += Math.abs(xshift);
900                                 rects[j].height += Math.abs(yshift);
901                             }
902                         }
903                     }
904                 }
905             };
906         }
907     }
908 
909     @SuppressWarnings("serial") // Superclass is not serializable across versions
910     private class SynthScrollableTabButton extends SynthArrowButton implements
911             UIResource {
912         public SynthScrollableTabButton(int direction) {
913             super(direction);
914             setName("TabbedPane.button");
915         }
916     }
917 }
918