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      */
 114     public static ComponentUI createUI(JComponent c) {
 115         return new SynthTabbedPaneUI();
 116     }
 117 
 118      private boolean scrollableTabLayoutEnabled() {
 119         return (tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT);
 120     }
 121 
 122     /**
 123      * {@inheritDoc}
 124      */
 125     @Override
 126     protected void installDefaults() {
 127         updateStyle(tabPane);
 128     }
 129 
 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         context.dispose();
 158 
 159         if (tabContext != null) {
 160             tabContext.dispose();
 161         }
 162         tabContext = getContext(c, Region.TABBED_PANE_TAB, ENABLED);
 163         this.tabStyle = SynthLookAndFeel.updateStyle(tabContext, this);
 164         tabInsets = tabStyle.getInsets(tabContext, null);
 165 
 166 
 167         if (tabAreaContext != null) {
 168             tabAreaContext.dispose();
 169         }
 170         tabAreaContext = getContext(c, Region.TABBED_PANE_TAB_AREA, ENABLED);
 171         this.tabAreaStyle = SynthLookAndFeel.updateStyle(tabAreaContext, this);
 172         tabAreaInsets = tabAreaStyle.getInsets(tabAreaContext, null);
 173 
 174 
 175         if (tabContentContext != null) {
 176             tabContentContext.dispose();
 177         }
 178         tabContentContext = getContext(c, Region.TABBED_PANE_CONTENT, ENABLED);
 179         this.tabContentStyle = SynthLookAndFeel.updateStyle(tabContentContext,
 180                                                             this);
 181         contentBorderInsets =
 182             tabContentStyle.getInsets(tabContentContext, null);
 183     }
 184 
 185     /**
 186      * {@inheritDoc}
 187      */
 188     @Override
 189     protected void installListeners() {
 190         super.installListeners();
 191         tabPane.addPropertyChangeListener(this);
 192     }
 193 
 194     /**
 195      * {@inheritDoc}
 196      */
 197     @Override
 198     protected void uninstallListeners() {
 199         super.uninstallListeners();
 200         tabPane.removePropertyChangeListener(this);
 201     }
 202 
 203     /**
 204      * {@inheritDoc}
 205      */
 206     @Override
 207     protected void uninstallDefaults() {
 208         SynthContext context = getContext(tabPane, ENABLED);
 209         style.uninstallDefaults(context);
 210         context.dispose();
 211         style = null;
 212 
 213         tabStyle.uninstallDefaults(tabContext);
 214         tabContext.dispose();
 215         tabContext = null;
 216         tabStyle = null;
 217 
 218         tabAreaStyle.uninstallDefaults(tabAreaContext);
 219         tabAreaContext.dispose();
 220         tabAreaContext = null;
 221         tabAreaStyle = null;
 222 
 223         tabContentStyle.uninstallDefaults(tabContentContext);
 224         tabContentContext.dispose();
 225         tabContentContext = null;
 226         tabContentStyle = null;
 227     }
 228 
 229     /**
 230      * {@inheritDoc}
 231      */
 232     @Override
 233     public SynthContext getContext(JComponent c) {
 234         return getContext(c, SynthLookAndFeel.getComponentState(c));
 235     }
 236 
 237     private SynthContext getContext(JComponent c, int state) {
 238         return SynthContext.getContext(c, style, state);
 239     }
 240 
 241     private SynthContext getContext(JComponent c, Region subregion, int state){
 242         SynthStyle style = null;
 243 
 244         if (subregion == Region.TABBED_PANE_TAB) {
 245             style = tabStyle;
 246         }
 247         else if (subregion == Region.TABBED_PANE_TAB_AREA) {
 248             style = tabAreaStyle;
 249         }
 250         else if (subregion == Region.TABBED_PANE_CONTENT) {
 251             style = tabContentStyle;
 252         }
 253         return SynthContext.getContext(c, subregion, style, state);
 254     }
 255 
 256     /**
 257      * {@inheritDoc}
 258      */
 259     @Override
 260     protected JButton createScrollButton(int direction) {
 261         // added for Nimbus LAF so that it can use the basic arrow buttons
 262         // UIManager is queried directly here because this is called before
 263         // updateStyle is called so the style can not be queried directly
 264         if (UIManager.getBoolean("TabbedPane.useBasicArrows")) {
 265             JButton btn = super.createScrollButton(direction);
 266             btn.setBorder(BorderFactory.createEmptyBorder());
 267             return btn;
 268         }
 269         return new SynthScrollableTabButton(direction);
 270     }
 271 
 272     /**
 273      * {@inheritDoc}
 274      */
 275     @Override
 276     public void propertyChange(PropertyChangeEvent e) {
 277         if (SynthLookAndFeel.shouldUpdateStyle(e)) {
 278             updateStyle(tabPane);
 279         }
 280     }
 281 
 282     /**
 283      * {@inheritDoc}
 284      *
 285      * Overridden to keep track of whether the selected tab is also pressed.
 286      */
 287     @Override
 288     protected MouseListener createMouseListener() {
 289         final MouseListener delegate = super.createMouseListener();
 290         final MouseMotionListener delegate2 = (MouseMotionListener)delegate;
 291         return new MouseListener() {
 292             public void mouseClicked(MouseEvent e) { delegate.mouseClicked(e); }
 293             public void mouseEntered(MouseEvent e) { delegate.mouseEntered(e); }
 294             public void mouseExited(MouseEvent e) { delegate.mouseExited(e); }
 295 
 296             public void mousePressed(MouseEvent e) {
 297                 if (!tabPane.isEnabled()) {
 298                     return;
 299                 }
 300 
 301                 int tabIndex = tabForCoordinate(tabPane, e.getX(), e.getY());
 302                 if (tabIndex >= 0 && tabPane.isEnabledAt(tabIndex)) {
 303                     if (tabIndex == tabPane.getSelectedIndex()) {
 304                         // Clicking on selected tab
 305                         selectedTabIsPressed = true;
 306                         //TODO need to just repaint the tab area!
 307                         tabPane.repaint();
 308                     }
 309                 }
 310 
 311                 //forward the event (this will set the selected index, or none at all
 312                 delegate.mousePressed(e);
 313             }
 314 
 315             public void mouseReleased(MouseEvent e) {
 316                 if (selectedTabIsPressed) {
 317                     selectedTabIsPressed = false;
 318                     //TODO need to just repaint the tab area!
 319                     tabPane.repaint();
 320                 }
 321                 //forward the event
 322                 delegate.mouseReleased(e);
 323 
 324                 //hack: The super method *should* be setting the mouse-over property correctly
 325                 //here, but it doesn't. That is, when the mouse is released, whatever tab is below the
 326                 //released mouse should be in rollover state. But, if you select a tab and don't
 327                 //move the mouse, this doesn't happen. Hence, forwarding the event.
 328                 delegate2.mouseMoved(e);
 329             }
 330         };
 331     }
 332 
 333     /**
 334      * {@inheritDoc}
 335      */
 336     @Override
 337     protected int getTabLabelShiftX(int tabPlacement, int tabIndex, boolean isSelected) {
 338         if (nudgeSelectedLabel) {
 339             return super.getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
 340         } else {
 341             return 0;
 342         }
 343     }
 344 
 345     /**
 346      * {@inheritDoc}
 347      */
 348     @Override
 349     protected int getTabLabelShiftY(int tabPlacement, int tabIndex, boolean isSelected) {
 350         if (nudgeSelectedLabel) {
 351             return super.getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
 352         } else {
 353             return 0;
 354         }
 355     }
 356 
 357     /**
 358      * Notifies this UI delegate to repaint the specified component.
 359      * This method paints the component background, then calls
 360      * the {@link #paint(SynthContext,Graphics)} method.
 361      *
 362      * <p>In general, this method does not need to be overridden by subclasses.
 363      * All Look and Feel rendering code should reside in the {@code paint} method.
 364      *
 365      * @param g the {@code Graphics} object used for painting
 366      * @param c the component being painted
 367      * @see #paint(SynthContext,Graphics)
 368      */
 369     @Override
 370     public void update(Graphics g, JComponent c) {
 371         SynthContext context = getContext(c);
 372 
 373         SynthLookAndFeel.update(context, g);
 374         context.getPainter().paintTabbedPaneBackground(context,
 375                           g, 0, 0, c.getWidth(), c.getHeight());
 376         paint(context, g);
 377         context.dispose();
 378     }
 379 
 380     /**
 381      * {@inheritDoc}
 382      */
 383     @Override
 384     protected int getBaseline(int tab) {
 385         if (tabPane.getTabComponentAt(tab) != null ||
 386                 getTextViewForTab(tab) != null) {
 387             return super.getBaseline(tab);
 388         }
 389         String title = tabPane.getTitleAt(tab);
 390         Font font = tabContext.getStyle().getFont(tabContext);
 391         FontMetrics metrics = getFontMetrics(font);
 392         Icon icon = getIconForTab(tab);
 393         textRect.setBounds(0, 0, 0, 0);
 394         iconRect.setBounds(0, 0, 0, 0);
 395         calcRect.setBounds(0, 0, Short.MAX_VALUE, maxTabHeight);
 396         tabContext.getStyle().getGraphicsUtils(tabContext).layoutText(
 397                 tabContext, metrics, title, icon, SwingUtilities.CENTER,
 398                 SwingUtilities.CENTER, SwingUtilities.LEADING,
 399                 SwingUtilities.CENTER, calcRect,
 400                 iconRect, textRect, textIconGap);
 401         return textRect.y + metrics.getAscent() + getBaselineOffset();
 402     }
 403 
 404     /**
 405      * {@inheritDoc}
 406      */
 407     @Override
 408     public void paintBorder(SynthContext context, Graphics g, int x,
 409                             int y, int w, int h) {
 410         context.getPainter().paintTabbedPaneBorder(context, g, x, y, w, h);
 411     }
 412 
 413     /**
 414      * Paints the specified component according to the Look and Feel.
 415      * <p>This method is not used by Synth Look and Feel.
 416      * Painting is handled by the {@link #paint(SynthContext,Graphics)} method.
 417      *
 418      * @param g the {@code Graphics} object used for painting
 419      * @param c the component being painted
 420      * @see #paint(SynthContext,Graphics)
 421      */
 422     @Override
 423     public void paint(Graphics g, JComponent c) {
 424         SynthContext context = getContext(c);
 425 
 426         paint(context, g);
 427         context.dispose();
 428     }
 429 
 430     /**
 431      * Paints the specified component.
 432      *
 433      * @param context context for the component being painted
 434      * @param g the {@code Graphics} object used for painting
 435      * @see #update(Graphics,JComponent)
 436      */
 437     protected void paint(SynthContext context, Graphics g) {
 438         int selectedIndex = tabPane.getSelectedIndex();
 439         int tabPlacement = tabPane.getTabPlacement();
 440 
 441         ensureCurrentLayout();
 442 
 443         // Paint tab area
 444         // If scrollable tabs are enabled, the tab area will be
 445         // painted by the scrollable tab panel instead.
 446         //
 447         if (!scrollableTabLayoutEnabled()) { // WRAP_TAB_LAYOUT
 448             Insets insets = tabPane.getInsets();
 449             int x = insets.left;
 450             int y = insets.top;
 451             int width = tabPane.getWidth() - insets.left - insets.right;
 452             int height = tabPane.getHeight() - insets.top - insets.bottom;
 453             int size;
 454             switch(tabPlacement) {
 455             case LEFT:
 456                 width = calculateTabAreaWidth(tabPlacement, runCount,
 457                                               maxTabWidth);
 458                 break;
 459             case RIGHT:
 460                 size = calculateTabAreaWidth(tabPlacement, runCount,
 461                                              maxTabWidth);
 462                 x = x + width - size;
 463                 width = size;
 464                 break;
 465             case BOTTOM:
 466                 size = calculateTabAreaHeight(tabPlacement, runCount,
 467                                               maxTabHeight);
 468                 y = y + height - size;
 469                 height = size;
 470                 break;
 471             case TOP:
 472             default:
 473                 height = calculateTabAreaHeight(tabPlacement, runCount,
 474                                                 maxTabHeight);
 475             }
 476 
 477             tabAreaBounds.setBounds(x, y, width, height);
 478 
 479             if (g.getClipBounds().intersects(tabAreaBounds)) {
 480                 paintTabArea(tabAreaContext, g, tabPlacement,
 481                          selectedIndex, tabAreaBounds);
 482             }
 483         }
 484 
 485         // Paint content border
 486         paintContentBorder(tabContentContext, g, tabPlacement, selectedIndex);
 487     }
 488 
 489     protected void paintTabArea(Graphics g, int tabPlacement,
 490                                 int selectedIndex) {
 491         // This can be invoked from ScrollabeTabPanel
 492         Insets insets = tabPane.getInsets();
 493         int x = insets.left;
 494         int y = insets.top;
 495         int width = tabPane.getWidth() - insets.left - insets.right;
 496         int height = tabPane.getHeight() - insets.top - insets.bottom;
 497 
 498         paintTabArea(tabAreaContext, g, tabPlacement, selectedIndex,
 499                      new Rectangle(x, y, width, height));
 500     }
 501 
 502     private void paintTabArea(SynthContext ss, Graphics g,
 503                                 int tabPlacement, int selectedIndex,
 504                                 Rectangle tabAreaBounds) {
 505         Rectangle clipRect = g.getClipBounds();
 506 
 507         //if the tab area's states should match that of the selected tab, then
 508         //first update the selected tab's states, then set the state
 509         //for the tab area to match
 510         //otherwise, restore the tab area's state to ENABLED (which is the
 511         //only supported state otherwise).
 512         if (tabAreaStatesMatchSelectedTab && selectedIndex >= 0) {
 513             updateTabContext(selectedIndex, true, selectedTabIsPressed,
 514                               (getRolloverTab() == selectedIndex),
 515                               (getFocusIndex() == selectedIndex));
 516             ss.setComponentState(tabContext.getComponentState());
 517         } else {
 518             ss.setComponentState(SynthConstants.ENABLED);
 519         }
 520 
 521         // Paint the tab area.
 522         SynthLookAndFeel.updateSubregion(ss, g, tabAreaBounds);
 523         ss.getPainter().paintTabbedPaneTabAreaBackground(ss, g,
 524              tabAreaBounds.x, tabAreaBounds.y, tabAreaBounds.width,
 525              tabAreaBounds.height, tabPlacement);
 526         ss.getPainter().paintTabbedPaneTabAreaBorder(ss, g, tabAreaBounds.x,
 527              tabAreaBounds.y, tabAreaBounds.width, tabAreaBounds.height,
 528              tabPlacement);
 529 
 530         int tabCount = tabPane.getTabCount();
 531 
 532         iconRect.setBounds(0, 0, 0, 0);
 533         textRect.setBounds(0, 0, 0, 0);
 534 
 535         // Paint tabRuns of tabs from back to front
 536         for (int i = runCount - 1; i >= 0; i--) {
 537             int start = tabRuns[i];
 538             int next = tabRuns[(i == runCount - 1)? 0 : i + 1];
 539             int end = (next != 0? next - 1: tabCount - 1);
 540             for (int j = start; j <= end; j++) {
 541                 if (rects[j].intersects(clipRect) && selectedIndex != j) {
 542                     paintTab(tabContext, g, tabPlacement, rects, j, iconRect,
 543                              textRect);
 544                 }
 545             }
 546         }
 547 
 548         if (selectedIndex >= 0) {
 549             if (rects[selectedIndex].intersects(clipRect)) {
 550                 paintTab(tabContext, g, tabPlacement, rects, selectedIndex,
 551                          iconRect, textRect);
 552             }
 553         }
 554     }
 555 
 556     /**
 557      * {@inheritDoc}
 558      */
 559     @Override
 560     protected void setRolloverTab(int index) {
 561         int oldRolloverTab = getRolloverTab();
 562         super.setRolloverTab(index);
 563 
 564         Rectangle r = null;
 565 
 566         if (oldRolloverTab != index && tabAreaStatesMatchSelectedTab) {
 567             //TODO need to just repaint the tab area!
 568             tabPane.repaint();
 569         } else {
 570             if ((oldRolloverTab >= 0) && (oldRolloverTab < tabPane.getTabCount())) {
 571                 r = getTabBounds(tabPane, oldRolloverTab);
 572                 if (r != null) {
 573                     tabPane.repaint(r);
 574                 }
 575             }
 576 
 577             if (index >= 0) {
 578                 r = getTabBounds(tabPane, index);
 579                 if (r != null) {
 580                     tabPane.repaint(r);
 581                 }
 582             }
 583         }
 584     }
 585 
 586     private void paintTab(SynthContext ss, Graphics g,
 587                             int tabPlacement, Rectangle[] rects, int tabIndex,
 588                             Rectangle iconRect, Rectangle textRect) {
 589         Rectangle tabRect = rects[tabIndex];
 590         int selectedIndex = tabPane.getSelectedIndex();
 591         boolean isSelected = selectedIndex == tabIndex;
 592         updateTabContext(tabIndex, isSelected, isSelected && selectedTabIsPressed,
 593                             (getRolloverTab() == tabIndex),
 594                             (getFocusIndex() == tabIndex));
 595 
 596         SynthLookAndFeel.updateSubregion(ss, g, tabRect);
 597         int x = tabRect.x;
 598         int y = tabRect.y;
 599         int height = tabRect.height;
 600         int width = tabRect.width;
 601         int placement = tabPane.getTabPlacement();
 602         if (extendTabsToBase && runCount > 1) {
 603             //paint this tab such that its edge closest to the base is equal to
 604             //edge of the selected tab closest to the base. In terms of the TOP
 605             //tab placement, this will cause the bottom of each tab to be
 606             //painted even with the bottom of the selected tab. This is because
 607             //in each tab placement (TOP, LEFT, BOTTOM, RIGHT) the selected tab
 608             //is closest to the base.
 609             if (selectedIndex >= 0) {
 610                 Rectangle r = rects[selectedIndex];
 611                 switch (placement) {
 612                     case TOP:
 613                         int bottomY = r.y + r.height;
 614                         height = bottomY - tabRect.y;
 615                         break;
 616                     case LEFT:
 617                         int rightX = r.x + r.width;
 618                         width = rightX - tabRect.x;
 619                         break;
 620                     case BOTTOM:
 621                         int topY = r.y;
 622                         height = (tabRect.y + tabRect.height) - topY;
 623                         y = topY;
 624                         break;
 625                     case RIGHT:
 626                         int leftX = r.x;
 627                         width = (tabRect.x + tabRect.width) - leftX;
 628                         x = leftX;
 629                         break;
 630                 }
 631             }
 632         }
 633         tabContext.getPainter().paintTabbedPaneTabBackground(tabContext, g,
 634                 x, y, width, height, tabIndex, placement);
 635         tabContext.getPainter().paintTabbedPaneTabBorder(tabContext, g,
 636                 x, y, width, height, tabIndex, placement);
 637 
 638         if (tabPane.getTabComponentAt(tabIndex) == null) {
 639             String title = tabPane.getTitleAt(tabIndex);
 640             Font font = ss.getStyle().getFont(ss);
 641             FontMetrics metrics = SwingUtilities2.getFontMetrics(tabPane, g, font);
 642             Icon icon = getIconForTab(tabIndex);
 643 
 644             layoutLabel(ss, tabPlacement, metrics, tabIndex, title, icon,
 645                     tabRect, iconRect, textRect, isSelected);
 646 
 647             paintText(ss, g, tabPlacement, font, metrics,
 648                     tabIndex, title, textRect, isSelected);
 649 
 650             paintIcon(g, tabPlacement, tabIndex, icon, iconRect, isSelected);
 651         }
 652     }
 653 
 654     private void layoutLabel(SynthContext ss, int tabPlacement,
 655                                FontMetrics metrics, int tabIndex,
 656                                String title, Icon icon,
 657                                Rectangle tabRect, Rectangle iconRect,
 658                                Rectangle textRect, boolean isSelected ) {
 659         View v = getTextViewForTab(tabIndex);
 660         if (v != null) {
 661             tabPane.putClientProperty("html", v);
 662         }
 663 
 664         textRect.x = textRect.y = iconRect.x = iconRect.y = 0;
 665 
 666         ss.getStyle().getGraphicsUtils(ss).layoutText(ss, metrics, title,
 667                          icon, SwingUtilities.CENTER, SwingUtilities.CENTER,
 668                          SwingUtilities.LEADING, SwingUtilities.CENTER,
 669                          tabRect, iconRect, textRect, textIconGap);
 670 
 671         tabPane.putClientProperty("html", null);
 672 
 673         int xNudge = getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
 674         int yNudge = getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
 675         iconRect.x += xNudge;
 676         iconRect.y += yNudge;
 677         textRect.x += xNudge;
 678         textRect.y += yNudge;
 679     }
 680 
 681     private void paintText(SynthContext ss,
 682                              Graphics g, int tabPlacement,
 683                              Font font, FontMetrics metrics, int tabIndex,
 684                              String title, Rectangle textRect,
 685                              boolean isSelected) {
 686         g.setFont(font);
 687 
 688         View v = getTextViewForTab(tabIndex);
 689         if (v != null) {
 690             // html
 691             v.paint(g, textRect);
 692         } else {
 693             // plain text
 694             int mnemIndex = tabPane.getDisplayedMnemonicIndexAt(tabIndex);
 695 
 696             g.setColor(ss.getStyle().getColor(ss, ColorType.TEXT_FOREGROUND));
 697             ss.getStyle().getGraphicsUtils(ss).paintText(ss, g, title,
 698                                   textRect, mnemIndex);
 699         }
 700     }
 701 
 702 
 703     private void paintContentBorder(SynthContext ss, Graphics g,
 704                                       int tabPlacement, int selectedIndex) {
 705         int width = tabPane.getWidth();
 706         int height = tabPane.getHeight();
 707         Insets insets = tabPane.getInsets();
 708 
 709         int x = insets.left;
 710         int y = insets.top;
 711         int w = width - insets.right - insets.left;
 712         int h = height - insets.top - insets.bottom;
 713 
 714         switch(tabPlacement) {
 715           case LEFT:
 716               x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
 717               w -= (x - insets.left);
 718               break;
 719           case RIGHT:
 720               w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
 721               break;
 722           case BOTTOM:
 723               h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
 724               break;
 725           case TOP:
 726           default:
 727               y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
 728               h -= (y - insets.top);
 729         }
 730         SynthLookAndFeel.updateSubregion(ss, g, new Rectangle(x, y, w, h));
 731         ss.getPainter().paintTabbedPaneContentBackground(ss, g, x, y,
 732                                                            w, h);
 733         ss.getPainter().paintTabbedPaneContentBorder(ss, g, x, y, w, h);
 734     }
 735 
 736     private void ensureCurrentLayout() {
 737         if (!tabPane.isValid()) {
 738             tabPane.validate();
 739         }
 740         /* If tabPane doesn't have a peer yet, the validate() call will
 741          * silently fail.  We handle that by forcing a layout if tabPane
 742          * is still invalid.  See bug 4237677.
 743          */
 744         if (!tabPane.isValid()) {
 745             TabbedPaneLayout layout = (TabbedPaneLayout)tabPane.getLayout();
 746             layout.calculateLayoutInfo();
 747         }
 748     }
 749 
 750     /**
 751      * {@inheritDoc}
 752      */
 753     @Override
 754     protected int calculateMaxTabHeight(int tabPlacement) {
 755         FontMetrics metrics = getFontMetrics(tabContext.getStyle().getFont(
 756                                              tabContext));
 757         int tabCount = tabPane.getTabCount();
 758         int result = 0;
 759         int fontHeight = metrics.getHeight();
 760         for(int i = 0; i < tabCount; i++) {
 761             result = Math.max(calculateTabHeight(tabPlacement, i, fontHeight), result);
 762         }
 763         return result;
 764     }
 765 
 766     /**
 767      * {@inheritDoc}
 768      */
 769     @Override
 770     protected int calculateTabWidth(int tabPlacement, int tabIndex,
 771                                     FontMetrics metrics) {
 772         Icon icon = getIconForTab(tabIndex);
 773         Insets tabInsets = getTabInsets(tabPlacement, tabIndex);
 774         int width = tabInsets.left + tabInsets.right;
 775         Component tabComponent = tabPane.getTabComponentAt(tabIndex);
 776         if (tabComponent != null) {
 777             width += tabComponent.getPreferredSize().width;
 778         } else {
 779             if (icon != null) {
 780                 width += icon.getIconWidth() + textIconGap;
 781             }
 782             View v = getTextViewForTab(tabIndex);
 783             if (v != null) {
 784                 // html
 785                 width += (int) v.getPreferredSpan(View.X_AXIS);
 786             } else {
 787                 // plain text
 788                 String title = tabPane.getTitleAt(tabIndex);
 789                 width += tabContext.getStyle().getGraphicsUtils(tabContext).
 790                         computeStringWidth(tabContext, metrics.getFont(),
 791                                 metrics, title);
 792             }
 793         }
 794         return width;
 795     }
 796 
 797     /**
 798      * {@inheritDoc}
 799      */
 800     @Override
 801     protected int calculateMaxTabWidth(int tabPlacement) {
 802         FontMetrics metrics = getFontMetrics(tabContext.getStyle().getFont(
 803                                      tabContext));
 804         int tabCount = tabPane.getTabCount();
 805         int result = 0;
 806         for(int i = 0; i < tabCount; i++) {
 807             result = Math.max(calculateTabWidth(tabPlacement, i, metrics),
 808                               result);
 809         }
 810         return result;
 811     }
 812 
 813     /**
 814      * {@inheritDoc}
 815      */
 816     @Override
 817     protected Insets getTabInsets(int tabPlacement, int tabIndex) {
 818         updateTabContext(tabIndex, false, false, false,
 819                           (getFocusIndex() == tabIndex));
 820         return tabInsets;
 821     }
 822 
 823     /**
 824      * {@inheritDoc}
 825      */
 826     @Override
 827     protected FontMetrics getFontMetrics() {
 828         return getFontMetrics(tabContext.getStyle().getFont(tabContext));
 829     }
 830 
 831     private FontMetrics getFontMetrics(Font font) {
 832         return tabPane.getFontMetrics(font);
 833     }
 834 
 835     private void updateTabContext(int index, boolean selected,
 836                                   boolean isMouseDown, boolean isMouseOver, boolean hasFocus) {
 837         int state = 0;
 838         if (!tabPane.isEnabled() || !tabPane.isEnabledAt(index)) {
 839             state |= SynthConstants.DISABLED;
 840             if (selected) {
 841                 state |= SynthConstants.SELECTED;
 842             }
 843         }
 844         else if (selected) {
 845             state |= (SynthConstants.ENABLED | SynthConstants.SELECTED);
 846             if (isMouseOver && UIManager.getBoolean("TabbedPane.isTabRollover")) {
 847                 state |= SynthConstants.MOUSE_OVER;
 848             }
 849         }
 850         else if (isMouseOver) {
 851             state |= (SynthConstants.ENABLED | SynthConstants.MOUSE_OVER);
 852         }
 853         else {
 854             state = SynthLookAndFeel.getComponentState(tabPane);
 855             state &= ~SynthConstants.FOCUSED; // don't use tabbedpane focus state
 856         }
 857         if (hasFocus && tabPane.hasFocus()) {
 858             state |= SynthConstants.FOCUSED; // individual tab has focus
 859         }
 860         if (isMouseDown) {
 861             state |= SynthConstants.PRESSED;
 862         }
 863 
 864         tabContext.setComponentState(state);
 865     }
 866 
 867     /**
 868      * {@inheritDoc}
 869      *
 870      * Overridden to create a TabbedPaneLayout subclass which takes into
 871      * account tabOverlap.
 872      */
 873     @Override
 874     protected LayoutManager createLayoutManager() {
 875         if (tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT) {
 876             return super.createLayoutManager();
 877         } else { /* WRAP_TAB_LAYOUT */
 878             return new TabbedPaneLayout() {
 879                 @Override
 880                 public void calculateLayoutInfo() {
 881                     super.calculateLayoutInfo();
 882                     //shift all the tabs, if necessary
 883                     if (tabOverlap != 0) {
 884                         int tabCount = tabPane.getTabCount();
 885                         //left-to-right/right-to-left only affects layout
 886                         //when placement is TOP or BOTTOM
 887                         boolean ltr = tabPane.getComponentOrientation().isLeftToRight();
 888                         for (int i = runCount - 1; i >= 0; i--) {
 889                             int start = tabRuns[i];
 890                             int next = tabRuns[(i == runCount - 1)? 0 : i + 1];
 891                             int end = (next != 0? next - 1: tabCount - 1);
 892                             for (int j = start+1; j <= end; j++) {
 893                                 // xshift and yshift represent the amount &
 894                                 // direction to shift the tab in their
 895                                 // respective axis.
 896                                 int xshift = 0;
 897                                 int yshift = 0;
 898                                 // configure xshift and y shift based on tab
 899                                 // position and ltr/rtl
 900                                 switch (tabPane.getTabPlacement()) {
 901                                     case JTabbedPane.TOP:
 902                                     case JTabbedPane.BOTTOM:
 903                                         xshift = ltr ? tabOverlap : -tabOverlap;
 904                                         break;
 905                                     case JTabbedPane.LEFT:
 906                                     case JTabbedPane.RIGHT:
 907                                         yshift = tabOverlap;
 908                                         break;
 909                                     default: //do nothing
 910                                 }
 911                                 rects[j].x += xshift;
 912                                 rects[j].y += yshift;
 913                                 rects[j].width += Math.abs(xshift);
 914                                 rects[j].height += Math.abs(yshift);
 915                             }
 916                         }
 917                     }
 918                 }
 919             };
 920         }
 921     }
 922 
 923     @SuppressWarnings("serial") // Superclass is not serializable across versions
 924     private class SynthScrollableTabButton extends SynthArrowButton implements
 925             UIResource {
 926         public SynthScrollableTabButton(int direction) {
 927             super(direction);
 928             setName("TabbedPane.button");
 929         }
 930     }
 931 }