/* * Copyright (c) 2005-2016 Substance Kirill Grouchnikov. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * o Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * o Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * o Neither the name of Substance Kirill Grouchnikov nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.pushingpixels.substance.internal.ui; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ContainerAdapter; import java.awt.event.ContainerEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.ButtonModel; import javax.swing.DefaultButtonModel; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JTabbedPane; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicTabbedPaneUI; import javax.swing.text.View; import org.pushingpixels.lafwidget.LafWidget; import org.pushingpixels.lafwidget.LafWidgetRepository; import org.pushingpixels.lafwidget.LafWidgetUtilities; import org.pushingpixels.lafwidget.animation.AnimationConfigurationManager; import org.pushingpixels.lafwidget.animation.AnimationFacet; import org.pushingpixels.lafwidget.contrib.intellij.UIUtil; import org.pushingpixels.lafwidget.utils.RenderingUtils; import org.pushingpixels.substance.api.ColorSchemeAssociationKind; import org.pushingpixels.substance.api.ComponentState; import org.pushingpixels.substance.api.ComponentStateFacet; import org.pushingpixels.substance.api.SubstanceColorScheme; import org.pushingpixels.substance.api.SubstanceConstants; import org.pushingpixels.substance.api.SubstanceConstants.Side; import org.pushingpixels.substance.api.SubstanceConstants.TabCloseKind; import org.pushingpixels.substance.api.SubstanceConstants.TabContentPaneBorderKind; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.SubstanceSkin; import org.pushingpixels.substance.api.painter.border.SubstanceBorderPainter; import org.pushingpixels.substance.api.painter.fill.SubstanceFillPainter; import org.pushingpixels.substance.api.shaper.ClassicButtonShaper; import org.pushingpixels.substance.api.shaper.SubstanceButtonShaper; import org.pushingpixels.substance.api.tabbed.BaseTabCloseListener; import org.pushingpixels.substance.api.tabbed.MultipleTabCloseListener; import org.pushingpixels.substance.api.tabbed.TabCloseCallback; import org.pushingpixels.substance.api.tabbed.TabCloseListener; import org.pushingpixels.substance.api.tabbed.VetoableMultipleTabCloseListener; import org.pushingpixels.substance.api.tabbed.VetoableTabCloseListener; import org.pushingpixels.substance.internal.animation.StateTransitionMultiTracker; import org.pushingpixels.substance.internal.animation.StateTransitionTracker; import org.pushingpixels.substance.internal.animation.StateTransitionTracker.StateContributionInfo; import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils; import org.pushingpixels.substance.internal.utils.HashMapKey; import org.pushingpixels.substance.internal.utils.LazyResettableHashMap; import org.pushingpixels.substance.internal.utils.SubstanceColorSchemeUtilities; import org.pushingpixels.substance.internal.utils.SubstanceColorUtilities; import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; import org.pushingpixels.substance.internal.utils.SubstanceImageCreator; import org.pushingpixels.substance.internal.utils.SubstanceOutlineUtilities; import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils; import org.pushingpixels.substance.internal.utils.SubstanceTextUtilities; import org.pushingpixels.substance.internal.utils.icon.TransitionAwareIcon; import org.pushingpixels.substance.internal.utils.scroll.SubstanceScrollButton; import org.pushingpixels.trident.Timeline; import org.pushingpixels.trident.Timeline.RepeatBehavior; import org.pushingpixels.trident.Timeline.TimelineState; import org.pushingpixels.trident.callback.UIThreadTimelineCallbackAdapter; /** * UI for tabbed panes in <b>Substance</b> look and feel. * * @author Kirill Grouchnikov */ public class SubstanceTabbedPaneUI extends BasicTabbedPaneUI { /** * Current mouse location. */ protected Point substanceMouseLocation; /** * Hash map for storing already computed backgrounds. */ private static LazyResettableHashMap<BufferedImage> backgroundMap = new LazyResettableHashMap<BufferedImage>( "SubstanceTabbedPaneUI.background"); /** * Hash map for storing already computed backgrounds. */ private static LazyResettableHashMap<BufferedImage> closeButtonMap = new LazyResettableHashMap<BufferedImage>( "SubstanceTabbedPaneUI.closeButton"); /** * Key - tab component. Value - the looping timeline that animates the tab * component when it's marked as modified (with * {@link SubstanceLookAndFeel#WINDOW_MODIFIED} property). */ private Map<Component, Timeline> modifiedTimelines; /** * Currently selected index (for selection animations). */ private int currSelectedIndex; private StateTransitionMultiTracker<Integer> stateTransitionMultiTracker; // private JPanel tabStrip; // // private JScrollablePanel<JPanel> scrollableTabStrip; private Set<LafWidget> lafWidgets; /* * (non-Javadoc) * * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent) */ public static ComponentUI createUI(JComponent comp) { SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp); return new SubstanceTabbedPaneUI(); } /** * Mouse handler for rollover effects. */ protected MouseRolloverHandler substanceRolloverHandler; /** * Tracks changes to the tabbed pane contents. Each tab component is tracked * for changes on the {@link SubstanceLookAndFeel#WINDOW_MODIFIED} property. */ protected TabbedContainerListener substanceContainerListener; /** * Listener for animation effects on tab selection. */ protected ChangeListener substanceSelectionListener; private boolean substanceContentOpaque; /** * Tracks changes to the tabbed pane contents. Each tab component is tracked * for changes on the {@link SubstanceLookAndFeel#WINDOW_MODIFIED} property. * * @author Kirill Grouchnikov */ protected final class TabbedContainerListener extends ContainerAdapter { /** * Property change listeners on the tab components. * <p/> * Fixes defect 135 - memory leaks on tabbed panes. */ private Map<Component, List<PropertyChangeListener>> listeners = new HashMap<Component, List<PropertyChangeListener>>(); /** * Creates a new container listener. */ public TabbedContainerListener() { } /** * Tracks all existing tab component. */ protected void trackExistingTabs() { // register listeners on all existing tabs for (int i = 0; i < SubstanceTabbedPaneUI.this.tabPane .getTabCount(); i++) { this.trackTab(SubstanceTabbedPaneUI.this.tabPane .getComponentAt(i)); } } /** * Tracks changes in a single tab component. * * @param tabComponent * Tab component. */ protected void trackTab(final Component tabComponent) { if (tabComponent == null) return; PropertyChangeListener tabModifiedListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { if (SubstanceLookAndFeel.WINDOW_MODIFIED.equals(evt .getPropertyName())) { Object oldValue = evt.getOldValue(); Object newValue = evt.getNewValue(); boolean wasModified = Boolean.TRUE.equals(oldValue); boolean isModified = Boolean.TRUE.equals(newValue); if (wasModified) { if (!isModified) { Timeline modifiedTimeline = modifiedTimelines .get(tabComponent); modifiedTimeline.cancel(); modifiedTimelines.remove(tabComponent); } } else { if (isModified) { int tabIndex = SubstanceTabbedPaneUI.this.tabPane .indexOfComponent(tabComponent); if (tabIndex >= 0) { trackTabModification(tabIndex, tabComponent); } } } } } }; tabComponent.addPropertyChangeListener(tabModifiedListener); // fix for defect 135 - memory leaks on tabbed panes List<PropertyChangeListener> currList = this.listeners .get(tabComponent); if (currList == null) currList = new LinkedList<PropertyChangeListener>(); currList.add(tabModifiedListener); // System.err.println(this.hashCode() + " adding for " + // tabComponent.hashCode()); this.listeners.put(tabComponent, currList); // Fix for defect 104 - a 'modified' component is added to // the tabbed pane. In this case it should be animated from the // beginning. if (tabComponent instanceof JComponent) { if (Boolean.TRUE .equals(((JComponent) tabComponent) .getClientProperty(SubstanceLookAndFeel.WINDOW_MODIFIED))) { int tabIndex = SubstanceTabbedPaneUI.this.tabPane .indexOfComponent(tabComponent); if (tabIndex >= 0) { trackTabModification(tabIndex, tabComponent); } } } } /** * Stops tracking changes to a single tab component. * * @param tabComponent * Tab component. */ protected void stopTrackTab(final Component tabComponent) { if (tabComponent == null) return; List<PropertyChangeListener> pclList = this.listeners .get(tabComponent); if (pclList != null) { for (PropertyChangeListener pcl : pclList) tabComponent.removePropertyChangeListener(pcl); } this.listeners.put(tabComponent, null); } /** * Stops tracking all tab components. */ protected void stopTrackExistingTabs() { // register listeners on all existing tabs for (int i = 0; i < SubstanceTabbedPaneUI.this.tabPane .getTabCount(); i++) { this.stopTrackTab(SubstanceTabbedPaneUI.this.tabPane .getComponentAt(i)); } } /* * (non-Javadoc) * * @seejava.awt.event.ContainerAdapter#componentAdded(java.awt.event. * ContainerEvent) */ @Override public void componentAdded(final ContainerEvent e) { final Component tabComponent = e.getChild(); if (tabComponent instanceof UIResource) return; this.trackTab(tabComponent); } /* * (non-Javadoc) * * @seejava.awt.event.ContainerAdapter#componentRemoved(java.awt.event. * ContainerEvent) */ @Override public void componentRemoved(ContainerEvent e) { // fix for defect 135 - memory leaks on tabbed panes final Component tabComponent = e.getChild(); if (tabComponent == null) return; // System.err.println(this.hashCode() + " removing for " + // tabComponent.hashCode()); if (tabComponent instanceof UIResource) return; for (PropertyChangeListener pcl : this.listeners.get(tabComponent)) tabComponent.removePropertyChangeListener(pcl); this.listeners.get(tabComponent).clear(); this.listeners.remove(tabComponent); // has running timeline? Timeline timeline = modifiedTimelines.get(tabComponent); if (timeline != null) { timeline.cancel(); modifiedTimelines.remove(tabComponent); } // this.cleanListeners(tabComponent); } } /** * Listener for rollover animation effects. * * @author Kirill Grouchnikov */ protected class MouseRolloverHandler implements MouseListener, MouseMotionListener { /** * Index of the tab that was rolloed over on the previous mouse event. */ int prevRolledOver = -1; /** * Indicates whether the previous mouse event was located in a close * button. */ boolean prevInCloseButton = false; /** * Tab index of the last mouse pressed event that happened in a close * button. */ int tabOfPressedCloseButton = -1; /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) */ public void mouseClicked(final MouseEvent e) { final int tabIndex = SubstanceTabbedPaneUI.this.tabForCoordinate( SubstanceTabbedPaneUI.this.tabPane, e.getX(), e.getY()); TabCloseCallback closeCallback = SubstanceCoreUtilities .getTabCloseCallback(e, SubstanceTabbedPaneUI.this.tabPane, tabIndex); if (closeCallback == null) return; final TabCloseKind tabCloseKind = closeCallback.onAreaClick( SubstanceTabbedPaneUI.this.tabPane, tabIndex, e); if (tabCloseKind == TabCloseKind.NONE) return; SwingUtilities.invokeLater(() -> SubstanceTabbedPaneUI.this.tryCloseTabs(tabIndex, tabCloseKind)); } /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent * ) */ public void mouseDragged(MouseEvent e) { this.handleMouseMoveDrag(e); } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) */ public void mouseEntered(MouseEvent e) { setRolloverTab(tabForCoordinate(tabPane, e.getX(), e.getY())); } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) */ public void mousePressed(MouseEvent e) { if (!tabPane.isEnabled()) { return; } int tabIndex = tabForCoordinate(tabPane, e.getX(), e.getY()); if (tabIndex >= 0 && tabPane.isEnabledAt(tabIndex)) { Rectangle rect = new Rectangle(); rect = getTabBounds(tabIndex, rect); Rectangle close = getCloseButtonRectangleForEvents(tabIndex, rect.x, rect.y, rect.width, rect.height); boolean inCloseButton = close.contains(e.getPoint()); this.tabOfPressedCloseButton = inCloseButton ? tabIndex : -1; if (tabIndex != tabPane.getSelectedIndex()) { // enhancement 307 - don't select tab on pressing its // close button if (inCloseButton) { return; } // Clicking on unselected tab, change selection, do NOT // request focus. // This will trigger the focusIndex to change by way // of stateChanged. tabPane.setSelectedIndex(tabIndex); } else if (tabPane.isRequestFocusEnabled()) { // Clicking on selected tab, try and give the tabbedpane // focus. Repaint will occur in focusGained. tabPane.requestFocus(); } } } /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent * ) */ public void mouseMoved(MouseEvent e) { this.handleMouseMoveDrag(e); } /** * Handles the move and drag mouse events. * * @param e * Mouse event to handle. */ private void handleMouseMoveDrag(MouseEvent e) { if (e.getSource() != tabPane) return; setRolloverTab(tabForCoordinate(tabPane, e.getX(), e.getY())); if (!AnimationConfigurationManager.getInstance() .isAnimationAllowed(AnimationFacet.ROLLOVER, tabPane)) { return; } SubstanceTabbedPaneUI.this.substanceMouseLocation = e.getPoint(); int currRolledOver = SubstanceTabbedPaneUI.this.getRolloverTab(); TabCloseCallback tabCloseCallback = SubstanceCoreUtilities .getTabCloseCallback(e, tabPane, currRolledOver); // System.err.println("Mouse moved " + currRolledOver + ":" + // prevRolledOver); if (currRolledOver == this.prevRolledOver) { if (currRolledOver >= 0) { Rectangle rect = new Rectangle(); rect = getTabBounds(currRolledOver, rect); Rectangle close = getCloseButtonRectangleForEvents( currRolledOver, rect.x, rect.y, rect.width, rect.height); // System.out.println("move " + rect + " " + close + " " // + e.getPoint()); boolean inCloseButton = close.contains(e.getPoint()); if (this.prevInCloseButton == inCloseButton) return; this.prevInCloseButton = inCloseButton; if (tabCloseCallback != null) { if (inCloseButton) { String closeButtonTooltip = tabCloseCallback .getCloseButtonTooltip(tabPane, currRolledOver); tabPane.setToolTipTextAt(currRolledOver, closeButtonTooltip); } else { String areaTooltip = tabCloseCallback .getAreaTooltip(tabPane, currRolledOver); tabPane.setToolTipTextAt(currRolledOver, areaTooltip); } } if ((currRolledOver >= 0) && (currRolledOver < tabPane.getTabCount())) { StateTransitionTracker tracker = getTracker( currRolledOver, true, currRolledOver == currSelectedIndex); tracker.getModel().setRollover(false); tracker.endTransition(); } } } else { if ((this.prevRolledOver >= 0) && (this.prevRolledOver < tabPane.getTabCount()) && tabPane.isEnabledAt(this.prevRolledOver)) { getTracker(prevRolledOver, true, prevRolledOver == currSelectedIndex). getModel().setRollover(false); } if ((currRolledOver >= 0) && (currRolledOver < tabPane.getTabCount()) && tabPane.isEnabledAt(currRolledOver)) { getTracker(currRolledOver, false, currRolledOver == currSelectedIndex). getModel().setRollover(true); } } this.prevRolledOver = currRolledOver; } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) */ public void mouseExited(MouseEvent e) { setRolloverTab(-1); // fix for bug 69 - non-selected non-rollover tab // may remain with close button after moving mouse quickly // to inner JTabbedPane if ((this.prevRolledOver >= 0) && (this.prevRolledOver < SubstanceTabbedPaneUI.this.tabPane .getTabCount()) && SubstanceTabbedPaneUI.this.tabPane .isEnabledAt(this.prevRolledOver)) { // only the previously rolled-over tab needs to be // repainted (fade-out) instead of repainting the // whole tab as before. getTracker(prevRolledOver, true, prevRolledOver == currSelectedIndex). getModel().setRollover(false); if (SubstanceCoreUtilities.getTabCloseCallback(e, tabPane, this.prevRolledOver) != null) { tabPane.setToolTipTextAt(this.prevRolledOver, null); } } this.prevRolledOver = -1; } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) */ public void mouseReleased(final MouseEvent e) { // enhancement 307 - moving the tab close to be on mouse release // and not on mouse press. final int tabIndex = SubstanceTabbedPaneUI.this.tabForCoordinate( SubstanceTabbedPaneUI.this.tabPane, e.getX(), e.getY()); // check that the mouse release is on the same tab as // mouse press, and that the tab has close button if (SubstanceCoreUtilities.hasCloseButton( SubstanceTabbedPaneUI.this.tabPane, tabIndex) && (tabIndex == this.tabOfPressedCloseButton)) { SwingUtilities.invokeLater(new Runnable() { public void run() { if ((tabIndex >= 0) && SubstanceTabbedPaneUI.this.tabPane .isEnabledAt(tabIndex)) { Rectangle rect = new Rectangle(); rect = SubstanceTabbedPaneUI.this.getTabBounds( tabIndex, rect); Rectangle close = SubstanceTabbedPaneUI.this .getCloseButtonRectangleForEvents(tabIndex, rect.x, rect.y, rect.width, rect.height); // System.out.println("press " + close + " " // + e.getPoint()); if (close.contains(e.getPoint())) { TabCloseCallback closeCallback = SubstanceCoreUtilities .getTabCloseCallback( e, SubstanceTabbedPaneUI.this.tabPane, tabIndex); TabCloseKind tabCloseKind = (closeCallback == null) ? TabCloseKind.THIS : closeCallback .onCloseButtonClick( SubstanceTabbedPaneUI.this.tabPane, tabIndex, e); SubstanceTabbedPaneUI.this.tryCloseTabs( tabIndex, tabCloseKind); } } } }); this.tabOfPressedCloseButton = -1; } } } /** * Creates the new UI delegate. */ public SubstanceTabbedPaneUI() { super(); this.stateTransitionMultiTracker = new StateTransitionMultiTracker<Integer>(); this.currSelectedIndex = -1; } @Override public void installUI(JComponent c) { this.lafWidgets = LafWidgetRepository.getRepository().getMatchingWidgets(c); super.installUI(c); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installUI(); } } @Override public void uninstallUI(JComponent c) { for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallUI(); } super.uninstallUI(c); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#installListeners() */ @Override protected void installListeners() { super.installListeners(); // Install listener to repaint the tabbed pane // on mouse move (for rollover effects). this.substanceRolloverHandler = new MouseRolloverHandler(); this.tabPane.addMouseMotionListener(this.substanceRolloverHandler); this.tabPane.addMouseListener(this.substanceRolloverHandler); // Add container listener to wire property change listener // on each tab in the tabbed pane. this.substanceContainerListener = new TabbedContainerListener(); this.substanceContainerListener.trackExistingTabs(); for (int i = 0; i < this.tabPane.getTabCount(); i++) { Component tabComp = this.tabPane.getComponentAt(i); if (SubstanceCoreUtilities.isTabModified(tabComp)) { trackTabModification(i, tabComp); } } this.tabPane.addContainerListener(this.substanceContainerListener); this.substanceSelectionListener = (ChangeEvent e) -> { SwingUtilities.invokeLater(() -> { if (SubstanceTabbedPaneUI.this.tabPane == null) return; int selected = SubstanceTabbedPaneUI.this.tabPane .getSelectedIndex(); // fix for issue 437 - track the selection change, // fading out the previously selected tab if ((currSelectedIndex >= 0) && (currSelectedIndex < SubstanceTabbedPaneUI.this.tabPane .getTabCount()) && SubstanceTabbedPaneUI.this.tabPane .isEnabledAt(currSelectedIndex)) { StateTransitionTracker tracker = getTracker( currSelectedIndex, getRolloverTabIndex() == currSelectedIndex, true); tracker.getModel().setSelected(false); } currSelectedIndex = selected; if ((selected >= 0) && (selected < SubstanceTabbedPaneUI.this.tabPane .getTabCount()) && SubstanceTabbedPaneUI.this.tabPane .isEnabledAt(selected)) { StateTransitionTracker tracker = getTracker( selected, getRolloverTabIndex() == selected, false); tracker.getModel().setSelected(true); } }); }; this.tabPane.getModel().addChangeListener(this.substanceSelectionListener); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installListeners(); } } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#uninstallListeners() */ @Override protected void uninstallListeners() { super.uninstallListeners(); if (this.substanceRolloverHandler != null) { this.tabPane .removeMouseMotionListener(this.substanceRolloverHandler); this.tabPane.removeMouseListener(this.substanceRolloverHandler); this.substanceRolloverHandler = null; } if (this.substanceContainerListener != null) { for (Map.Entry<Component, List<PropertyChangeListener>> entry : this.substanceContainerListener.listeners .entrySet()) { Component comp = entry.getKey(); // System.out.println(this.containerListener.hashCode() +" // removing all for" + comp.hashCode()); for (PropertyChangeListener pcl : entry.getValue()) { comp.removePropertyChangeListener(pcl); } } this.substanceContainerListener.listeners.clear(); this.tabPane .removeContainerListener(this.substanceContainerListener); this.substanceContainerListener = null; } this.tabPane.getModel().removeChangeListener( this.substanceSelectionListener); this.substanceSelectionListener = null; for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallListeners(); } } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#installDefaults() */ @Override protected void installDefaults() { super.installDefaults(); this.substanceContentOpaque = UIManager .getBoolean("TabbedPane.contentOpaque"); this.modifiedTimelines = new HashMap<Component, Timeline>(); this.currSelectedIndex = this.tabPane.getSelectedIndex(); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installDefaults(); } } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#uninstallDefaults() */ @Override protected void uninstallDefaults() { for (Timeline timeline : this.modifiedTimelines.values()) timeline.cancel(); this.modifiedTimelines.clear(); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallDefaults(); } super.uninstallDefaults(); } @Override protected void installComponents() { super.installComponents(); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installComponents(); } } @Override protected void uninstallComponents() { for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallComponents(); } super.uninstallComponents(); } /** * Retrieves tab background. * * @param tabPane * Tabbed pane. * @param width * Tab width. * @param height * Tab height. * @param isSelected * Indication whether the tab is selected. * @param cyclePos * Tab cycle position (for rollover effects). * @param tabPlacement * Tab placement. * @param side * Tab open side. * @param fillScheme * Color scheme for coloring the background. * @param borderScheme * Color scheme for coloring the border. * @param paintOnlyBorder * If <code>true</code>, only the border will be painted. * @return Tab background of specified parameters. */ private static BufferedImage getTabBackground(JTabbedPane tabPane, int width, int height, int tabPlacement, SubstanceColorScheme fillScheme, SubstanceColorScheme borderScheme, boolean paintOnlyBorder) { SubstanceFillPainter fillPainter = SubstanceCoreUtilities .getFillPainter(tabPane); SubstanceBorderPainter borderPainter = SubstanceCoreUtilities .getBorderPainter(tabPane); SubstanceButtonShaper shaper = SubstanceCoreUtilities .getButtonShaper(tabPane); float borderDelta = 2.0f * SubstanceSizeUtils.getBorderStrokeWidth(); float borderInsets = SubstanceSizeUtils.getBorderStrokeWidth() / 2.0f; int dy = (int) (2 + borderDelta); Set<Side> straightSides = EnumSet.of(Side.BOTTOM); float cornerRadius = height / 3.0f; if (shaper instanceof ClassicButtonShaper) { cornerRadius = SubstanceSizeUtils .getClassicButtonCornerRadius(SubstanceSizeUtils .getComponentFontSize(tabPane)); width -= 1; } GeneralPath contour = SubstanceOutlineUtilities.getBaseOutline(width, height + dy, cornerRadius, straightSides, borderInsets); BufferedImage result = SubstanceCoreUtilities.getBlankImage(width, height); Graphics2D resGraphics = result.createGraphics(); if (!paintOnlyBorder) { fillPainter.paintContourBackground(resGraphics, tabPane, width, height + dy, contour, false, fillScheme, true); } float borderThickness = SubstanceSizeUtils.getBorderStrokeWidth(); GeneralPath contourInner = borderPainter.isPaintingInnerContour() ? SubstanceOutlineUtilities.getBaseOutline(width, height + dy, cornerRadius - borderThickness, straightSides, borderThickness + borderInsets) : null; borderPainter.paintBorder(resGraphics, tabPane, width, height + dy, contour, contourInner, borderScheme); resGraphics.dispose(); return result; } /** * Retrieves tab background that will be shown on the screen. Unlike * {@link #getTabBackground(JTabbedPane, int, int, boolean, float, int, SubstanceColorScheme, SubstanceColorScheme, SubstanceColorScheme, SubstanceColorScheme, boolean)} * , the result is rotated as necessary (for {@link SwingConstants#LEFT} and * {@link SwingConstants#RIGHT} placement) and blended for selected tabs. * * @param tabPane * Tabbed pane. * @param tabIndex * Tab index. * @param width * Tab width. * @param height * Tab height. * @param isSelected * Indication whether the tab is selected. * @param cyclePos * Tab cycle position (for rollover effects). * @param tabPlacement * Tab placement. * @param side * Tab open side. * @param colorScheme * Color scheme for coloring the background. * @param borderScheme * Color scheme for coloring the border. * @param paintOnlyBorder * If <code>true</code>, only the border will be painted. * @return Tab background of specified parameters. */ private static BufferedImage getFinalTabBackgroundImage( JTabbedPane tabPane, int tabIndex, int x, int y, int width, int height, int tabPlacement, SubstanceConstants.Side side, SubstanceColorScheme colorScheme, SubstanceColorScheme borderScheme) { SubstanceFillPainter fillPainter = SubstanceCoreUtilities .getFillPainter(tabPane); SubstanceBorderPainter borderPainter = SubstanceCoreUtilities .getBorderPainter(tabPane); SubstanceButtonShaper shaper = SubstanceCoreUtilities .getButtonShaper(tabPane); Component compForBackground = tabPane.getTabComponentAt(tabIndex); if (compForBackground == null) compForBackground = tabPane.getComponentAt(tabIndex); if (compForBackground == null) compForBackground = tabPane; Color tabColor = compForBackground.getBackground(); if (tabColor instanceof UIResource) { // special handling of tabs placed in decoration areas tabColor = SubstanceColorUtilities.getBackgroundFillColor(compForBackground); } HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height, tabPlacement, fillPainter.getDisplayName(), borderPainter.getDisplayName(), shaper.getDisplayName(), tabPlacement == SwingConstants.BOTTOM, side.name(), colorScheme .getDisplayName(), borderScheme.getDisplayName(), tabColor); SubstanceSkin skin = SubstanceCoreUtilities.getSkin(tabPane); BufferedImage result = SubstanceTabbedPaneUI.backgroundMap.get(key); if (result == null) { BufferedImage backgroundImage = null; switch (tabPlacement) { case BOTTOM: BufferedImage unrotated = getFinalTabBackgroundImage(tabPane, tabIndex, x, y, width, height, SwingConstants.TOP, side, colorScheme, borderScheme); BufferedImage rotated = SubstanceImageCreator.getRotated(unrotated, 2, true); return rotated; case TOP: case LEFT: case RIGHT: backgroundImage = SubstanceTabbedPaneUI.getTabBackground( tabPane, width, height, SwingConstants.TOP, colorScheme, borderScheme, false); int fw = backgroundImage.getWidth(); int fh = backgroundImage.getHeight(); int factor = UIUtil.getScaleFactor(); BufferedImage fade = SubstanceCoreUtilities.getBlankImage( fw / factor, fh / factor); Graphics2D fadeGraphics = fade.createGraphics(); fadeGraphics.setColor(tabColor); fadeGraphics.fillRect(0, 0, fw, fh); if (skin.getWatermark() != null) { fadeGraphics.translate(-x, -y); skin.getWatermark().drawWatermarkImage(fadeGraphics, tabPane, x, y, fw, fh); fadeGraphics.translate(x, y); } BufferedImage background = SubstanceTabbedPaneUI .getTabBackground(tabPane, width, height, tabPlacement, colorScheme, borderScheme, true); fadeGraphics.drawImage(background, 0, 0, background.getWidth() / factor, background.getHeight() / factor, null); backgroundImage = SubstanceCoreUtilities.blendImagesVertical( backgroundImage, fade, skin.getTabFadeStart(), skin.getTabFadeEnd()); } SubstanceTabbedPaneUI.backgroundMap.put(key, backgroundImage); } return backgroundMap.get(key); } /** * Retrieves the image of the close button. * * @param tabPane * Tabbed pane. * @param width * Close button width. * @param height * Close button height. * @param cyclePos * Tab cycle position (for rollover effects). * @param toPaintBorder * Indication whether the button background (including contour) * needs to be painted. * @param fillScheme * Color scheme for coloring the background. * @param fillScheme2 * Second color scheme for coloring the background. * @param markScheme * Color scheme for painting the close mark. * @param markScheme2 * Second color scheme for painting the close mark. * @return Image of the close button of specified parameters. */ private static BufferedImage getCloseButtonImage(JTabbedPane tabPane, int width, int height, boolean toPaintBorder, SubstanceColorScheme fillScheme, SubstanceColorScheme markScheme) { SubstanceFillPainter fillPainter = SubstanceCoreUtilities .getFillPainter(tabPane); if (fillPainter == null) return null; HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height, toPaintBorder, fillPainter.getDisplayName(), fillScheme.getDisplayName(), markScheme.getDisplayName()); BufferedImage result = SubstanceTabbedPaneUI.closeButtonMap.get(key); if (result == null) { result = SubstanceCoreUtilities.getBlankImage(width, height); Graphics2D finalGraphics = (Graphics2D) result.getGraphics().create(); if (toPaintBorder) { GeneralPath contour = SubstanceOutlineUtilities.getBaseOutline( width, height, 1, null); fillPainter.paintContourBackground(finalGraphics, tabPane, width, height, contour, false, fillScheme, true); // finalGraphics.drawImage(background, 0, 0, null); SubstanceBorderPainter borderPainter = SubstanceCoreUtilities .getBorderPainter(tabPane); finalGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); borderPainter.paintBorder(finalGraphics, tabPane, width, height, contour, null, markScheme); } finalGraphics .setStroke(new BasicStroke(SubstanceSizeUtils.getTabCloseButtonStrokeWidth())); int delta = (int) (Math.floor(SubstanceSizeUtils.getBorderStrokeWidth())); if (delta % 2 != 0) delta--; int iconSize = width - delta; Icon closeIcon = SubstanceImageCreator.getCloseIcon(iconSize, markScheme, fillScheme); finalGraphics.translate(delta / 2, delta / 2); closeIcon.paintIcon(tabPane, finalGraphics, 0, 0); finalGraphics.dispose(); SubstanceTabbedPaneUI.closeButtonMap.put(key, result); } return result; } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintTabBackground(java.awt. * Graphics, int, int, int, int, int, int, boolean) */ @Override protected void paintTabBackground(Graphics g, int tabPlacement, final int tabIndex, final int x, final int y, int w, int h, boolean isSelected) { Graphics2D graphics = (Graphics2D) g.create(); graphics.setComposite(LafWidgetUtilities.getAlphaComposite( this.tabPane, g)); boolean isEnabled = this.tabPane.isEnabledAt(tabIndex); ComponentState currState = this.getTabState(tabIndex, false); StateTransitionTracker.ModelStateInfo modelStateInfo = this.getModelStateInfo(tabIndex); boolean isRollover = (this.getRolloverTab() == tabIndex); if (!isSelected && !isRollover && (modelStateInfo == null)) { return; } SubstanceColorScheme baseBorderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, tabIndex, ColorSchemeAssociationKind.TAB_BORDER, currState); SubstanceColorScheme baseColorScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, tabIndex, ColorSchemeAssociationKind.TAB, currState); BufferedImage fullOpacity = null; int scaleFactor = UIUtil.getScaleFactor(); // Slightly reduce the tab width to create "gaps" between tab visuals w -= 1; // check if have windowModified property Component comp = this.tabPane.getComponentAt(tabIndex); boolean isWindowModified = SubstanceCoreUtilities.isTabModified(comp); boolean toMarkModifiedCloseButton = SubstanceCoreUtilities .toAnimateCloseIconOfModifiedTab(this.tabPane, tabIndex); if (isWindowModified && isEnabled && !toMarkModifiedCloseButton) { SubstanceColorScheme colorScheme2 = SubstanceColorSchemeUtilities.YELLOW; SubstanceColorScheme colorScheme = SubstanceColorSchemeUtilities.ORANGE; float cyclePos = this.modifiedTimelines.get(comp) .getTimelinePosition(); BufferedImage layer1 = SubstanceTabbedPaneUI .getFinalTabBackgroundImage(this.tabPane, tabIndex, x, y, w, h, tabPlacement, SubstanceConstants.Side.BOTTOM, colorScheme, baseBorderScheme); BufferedImage layer2 = SubstanceTabbedPaneUI .getFinalTabBackgroundImage(this.tabPane, tabIndex, x, y, w, h, tabPlacement, SubstanceConstants.Side.BOTTOM, colorScheme2, baseBorderScheme); fullOpacity = SubstanceCoreUtilities.getBlankImage(w, h); Graphics2D g2d = fullOpacity.createGraphics(); if (cyclePos < 1.0f) g2d.drawImage(layer1, 0, 0, layer1.getWidth() / scaleFactor, layer1.getHeight() / scaleFactor, null); if (cyclePos > 0.0f) { g2d.setComposite(AlphaComposite.SrcOver.derive(cyclePos)); g2d.drawImage(layer2, 0, 0, layer2.getWidth() / scaleFactor, layer2.getHeight() / scaleFactor, null); } g2d.dispose(); } else { BufferedImage layerBase = SubstanceTabbedPaneUI .getFinalTabBackgroundImage(this.tabPane, tabIndex, x, y, w, h, tabPlacement, SubstanceConstants.Side.BOTTOM, baseColorScheme, baseBorderScheme); if ((modelStateInfo == null) || currState.isDisabled() || (modelStateInfo.getStateContributionMap().size() == 1)) { fullOpacity = layerBase; } else { fullOpacity = SubstanceCoreUtilities.getBlankImage(w, h); Graphics2D g2d = fullOpacity.createGraphics(); // draw the base layer g2d.drawImage(layerBase, 0, 0, layerBase.getWidth() / scaleFactor, layerBase.getHeight() / scaleFactor, null); // draw the other active layers for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : modelStateInfo .getStateContributionMap().entrySet()) { ComponentState activeState = activeEntry.getKey(); if (activeState == currState) continue; float stateContribution = activeEntry.getValue() .getContribution(); if (stateContribution > 0.0f) { g2d.setComposite(AlphaComposite.SrcOver .derive(stateContribution)); SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, tabIndex, ColorSchemeAssociationKind.TAB, activeState); SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, tabIndex, ColorSchemeAssociationKind.TAB_BORDER, activeState); BufferedImage layer = SubstanceTabbedPaneUI .getFinalTabBackgroundImage(this.tabPane, tabIndex, x, y, w, h, tabPlacement, SubstanceConstants.Side.BOTTOM, fillScheme, borderScheme); g2d.drawImage(layer, 0, 0, layer.getWidth() / scaleFactor, layer.getHeight() / scaleFactor, null); } } } } // at this point the 'fillOpacity' has all the relevant layers for the // fill + border // Special handling of selected tabs under skins that show partial visuals boolean isCloseMarkOnParentBackground = (SubstanceCoreUtilities.getSkin(tabPane).getTabFadeEnd() <= 0.5); ComponentState markState = currState; if (isCloseMarkOnParentBackground) { // Ignore all other "aspects" of tab's state markState = this.getTabState(tabIndex, true); // this.tabPane.isEnabledAt(tabIndex) ? ComponentState.ENABLED // : ComponentState.DISABLED_UNSELECTED; } SubstanceColorScheme baseMarkScheme = SubstanceColorSchemeUtilities.getColorScheme( this.tabPane, tabIndex, isCloseMarkOnParentBackground ? ColorSchemeAssociationKind.FILL : ColorSchemeAssociationKind.MARK, markState); // fix for defect 138 graphics.clip(new Rectangle(x, y, w, h)); float finalAlpha = 0.0f; StateTransitionTracker tabTracker = this.stateTransitionMultiTracker.getTracker(tabIndex); if (!isSelected && (modelStateInfo != null)) { finalAlpha += tabTracker.getFacetStrength(ComponentStateFacet.ROLLOVER); } else { if (isRollover || isSelected) { finalAlpha = 1.0f; } } finalAlpha *= SubstanceColorSchemeUtilities.getAlpha(this.tabPane .getComponentAt(tabIndex), currState); graphics.setComposite(LafWidgetUtilities.getAlphaComposite( this.tabPane, finalAlpha, g)); graphics.drawImage(fullOpacity, x, y, fullOpacity.getWidth() / scaleFactor, fullOpacity.getHeight() / scaleFactor, null); // Check if requested to paint close buttons. if (SubstanceCoreUtilities.hasCloseButton(this.tabPane, tabIndex) && isEnabled) { float alpha = (isSelected || isRollover) ? 1.0f : 0.0f; if (!isSelected) { if (tabTracker != null) { alpha = tabTracker.getFacetStrength(ComponentStateFacet.ROLLOVER); } } if (alpha > 0.0) { graphics.setComposite(LafWidgetUtilities.getAlphaComposite( this.tabPane, finalAlpha * alpha, g)); // paint close button Rectangle orig = this.getCloseButtonRectangleForDraw(tabIndex, x, y, w, h); boolean toPaintCloseBorder = false; if (isRollover) { if (this.substanceMouseLocation != null) { Rectangle bounds = new Rectangle(); bounds = this.getTabBounds(tabIndex, bounds); if (toRotateTabsOnPlacement(tabPlacement)) { bounds = new Rectangle(bounds.x, bounds.y, bounds.height, bounds.width); } Rectangle rect = this.getCloseButtonRectangleForEvents( tabIndex, bounds.x, bounds.y, bounds.width, bounds.height); // System.out.println("paint " + bounds + " " + rect +" // " // + mouseLocation); if (rect.contains(this.substanceMouseLocation)) { toPaintCloseBorder = true; } } } if (isWindowModified && isEnabled && toMarkModifiedCloseButton) { SubstanceColorScheme colorScheme2 = SubstanceColorSchemeUtilities.YELLOW; SubstanceColorScheme colorScheme = SubstanceColorSchemeUtilities.ORANGE; float cyclePos = this.modifiedTimelines.get(comp) .getTimelinePosition(); BufferedImage layer1 = SubstanceTabbedPaneUI .getCloseButtonImage(this.tabPane, orig.width, orig.height, toPaintCloseBorder, colorScheme, baseMarkScheme); BufferedImage layer2 = SubstanceTabbedPaneUI .getCloseButtonImage(this.tabPane, orig.width, orig.height, toPaintCloseBorder, colorScheme2, baseMarkScheme); if (cyclePos < 1.0f) { graphics.drawImage(layer1, orig.x, orig.y, layer1.getWidth() / scaleFactor, layer1.getHeight() / scaleFactor, null); } if (cyclePos > 0.0f) { graphics.setComposite(AlphaComposite.SrcOver .derive(cyclePos)); graphics.drawImage(layer2, orig.x, orig.y, layer1.getWidth() / scaleFactor, layer1.getHeight() / scaleFactor, null); } } else { BufferedImage layerBase = SubstanceTabbedPaneUI .getCloseButtonImage(this.tabPane, orig.width, orig.height, toPaintCloseBorder, baseColorScheme, baseMarkScheme); if ((modelStateInfo == null) || currState.isDisabled() || (modelStateInfo.getStateContributionMap().size() == 1)) { graphics.drawImage(layerBase, orig.x, orig.y, layerBase.getWidth() / scaleFactor, layerBase.getHeight() / scaleFactor, null); } else { BufferedImage complete = SubstanceCoreUtilities .getBlankImage(orig.width, orig.height); Graphics2D g2d = complete.createGraphics(); // draw the base layer g2d.drawImage(layerBase, 0, 0, layerBase.getWidth() / scaleFactor, layerBase.getHeight() / scaleFactor, null); // draw the other active layers Map<ComponentState, StateContributionInfo> contributionInfoMap = isCloseMarkOnParentBackground ? modelStateInfo.getStateNoSelectionContributionMap() : modelStateInfo.getStateContributionMap(); for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : contributionInfoMap.entrySet()) { ComponentState activeState = activeEntry.getKey(); if (activeState == currState) continue; float stateContribution = activeEntry.getValue().getContribution(); if (stateContribution > 0.0f) { g2d.setComposite(AlphaComposite.SrcOver .derive(stateContribution)); SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, tabIndex, ColorSchemeAssociationKind.TAB, activeState); SubstanceColorScheme markScheme = SubstanceColorSchemeUtilities .getColorScheme( this.tabPane, tabIndex, isCloseMarkOnParentBackground ? ColorSchemeAssociationKind.FILL : ColorSchemeAssociationKind.MARK, activeState); BufferedImage layer = SubstanceTabbedPaneUI .getCloseButtonImage(this.tabPane, orig.width, orig.height, toPaintCloseBorder, fillScheme, markScheme); g2d.drawImage(layer, 0, 0, layer.getWidth() / scaleFactor, layer.getHeight() / scaleFactor, null); } } g2d.dispose(); graphics.drawImage(complete, orig.x, orig.y, complete.getWidth() / scaleFactor, complete.getHeight() / scaleFactor, null); } } } } graphics.dispose(); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintFocusIndicator(java.awt * .Graphics, int, java.awt.Rectangle[], int, java.awt.Rectangle, * java.awt.Rectangle, boolean) */ @Override protected void paintFocusIndicator(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect, boolean isSelected) { // empty to remove Basic functionality } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintTabBorder(java.awt.Graphics * , int, int, int, int, int, int, boolean) */ @Override protected void paintTabBorder(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) { // empty to remove Basic functionality } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#createScrollButton(int) */ @Override protected JButton createScrollButton(final int direction) { SubstanceScrollButton ssb = new SubstanceScrollButton(direction); Icon icon = new TransitionAwareIcon(ssb, (SubstanceColorScheme scheme) -> { // fix for defect 279 - tab pane might not yet have the font installed. int fontSize = SubstanceSizeUtils.getComponentFontSize(tabPane); return SubstanceImageCreator.getArrowIcon(fontSize, direction, scheme); }, "substance.tabbedpane.scroll." + direction); ssb.setIcon(icon); return ssb; } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#calculateTabHeight(int, * int, int) */ @Override protected int calculateTabHeight(int tabPlacement, int tabIndex, int fontHeight) { boolean toSwap = toRotateTabsOnPlacement(tabPlacement); if (toSwap) return this.getTabExtraWidth(tabPlacement, tabIndex) + super.calculateTabWidth(tabPlacement, tabIndex, this .getFontMetrics()); return super.calculateTabHeight(tabPlacement, tabIndex, fontHeight); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#calculateTabWidth(int, int, * java.awt.FontMetrics) */ @Override protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) { boolean toSwap = toRotateTabsOnPlacement(tabPlacement); if (toSwap) return super.calculateTabHeight(tabPlacement, tabIndex, metrics .getHeight()); int result = this.getTabExtraWidth(tabPlacement, tabIndex) + super.calculateTabWidth(tabPlacement, tabIndex, metrics); return result; } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#calculateMaxTabHeight(int) */ @Override protected int calculateMaxTabHeight(int tabPlacement) { if (toRotateTabsOnPlacement(tabPlacement)) return super.calculateMaxTabHeight(tabPlacement); int result = 0; for (int i = 0; i < this.tabPane.getTabCount(); i++) result = Math.max(result, this.calculateTabHeight(tabPlacement, i, this.getFontMetrics().getHeight())); return result; } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#getTabRunOverlay(int) */ @Override protected int getTabRunOverlay(int tabPlacement) { boolean toSwap = this.toRotateTabsOnPlacement(tabPlacement); if (toSwap) return super.getTabRunOverlay(tabPlacement); return 0; } @Override public void paint(Graphics g, JComponent c) { int selectedIndex = tabPane.getSelectedIndex(); int tabPlacement = tabPane.getTabPlacement(); ensureCurrentLayout(); // If scrollable tabs are enabled, the tab area will be // painted by the scrollable tab panel instead. if (tabPane.getLayout().getClass() == TabbedPaneLayout.class) { paintTabArea(g, tabPlacement, selectedIndex); } int width = tabPane.getWidth(); int height = tabPane.getHeight(); Insets insets = tabPane.getInsets(); int x = insets.left; int y = insets.top; int w = width - insets.right - insets.left; int h = height - insets.top - insets.bottom; switch (tabPlacement) { case LEFT: x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth); w -= (x - insets.left); break; case RIGHT: w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth); break; case BOTTOM: h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight); break; case TOP: default: y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight); h -= (y - insets.top); } Graphics2D g2d = (Graphics2D) g.create(x, y, w, h); BackgroundPaintingUtils.update(g2d, c, false); paintContentBorder(g, tabPlacement, selectedIndex); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#paintTab(java.awt.Graphics, * int, java.awt.Rectangle[], int, java.awt.Rectangle, java.awt.Rectangle) */ @Override protected void paintTab(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect) { boolean toSwap = toRotateTabsOnPlacement(tabPlacement); if (toSwap) { Graphics2D tempG = (Graphics2D) g.create(); Rectangle tabRect = rects[tabIndex]; Rectangle correctRect = new Rectangle(tabRect.x, tabRect.y, tabRect.height, tabRect.width); if (tabPlacement == SwingConstants.LEFT) { // rotate 90 degrees counterclockwise for LEFT orientation tempG.rotate(-Math.PI / 2, tabRect.x, tabRect.y); tempG.translate(-tabRect.height, 0); } else { // rotate 90 degrees clockwise for RIGHT orientation tempG.rotate(Math.PI / 2, tabRect.x, tabRect.y); tempG.translate(0, -tabRect.getWidth()); } tempG.setColor(Color.red); rects[tabIndex] = correctRect; super.paintTab(tempG, tabPlacement, rects, tabIndex, iconRect, textRect); rects[tabIndex] = tabRect; tempG.dispose(); } else { if (tabPane.getLayout().getClass() == TabbedPaneLayout.class) { super.paintTab(g, tabPlacement, rects, tabIndex, iconRect, textRect); } else { // scrolled tabs are painted by // BasicTabbedPaneUI.ScrollableTabPanel // which does not have the right rendering hints Graphics2D g2d = (Graphics2D) g.create(); RenderingUtils.installDesktopHints(g2d, tabPane); super.paintTab(g2d, tabPlacement, rects, tabIndex, iconRect, textRect); g2d.dispose(); } } } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintTabArea(java.awt.Graphics, * int, int) */ @Override protected void paintTabArea(Graphics g, int tabPlacement, int selectedIndex) { if (this.substanceContentOpaque) { int width = calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth); if ((tabPlacement == SwingConstants.TOP) || (tabPlacement == SwingConstants.BOTTOM)) width = Math.max(width, tabPane.getWidth()); int height = calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight); if (toRotateTabsOnPlacement(tabPlacement)) height = Math.max(height, tabPane.getHeight()); // restrict the painting to the tab area only Graphics2D g2d = (Graphics2D) g.create(0, 0, width, height); BackgroundPaintingUtils.update(g2d, this.tabPane, true); g2d.dispose(); } super.paintTabArea(g, tabPlacement, selectedIndex); } /** * Retrieves the close button rectangle for drawing purposes. * * @param tabIndex * Tab index. * @param x * X coordinate of the tab. * @param y * Y coordinate of the tab. * @param width * The tab width. * @param height * The tab height. * @return The close button rectangle. */ protected Rectangle getCloseButtonRectangleForDraw(int tabIndex, int x, int y, int width, int height) { int dimension = SubstanceCoreUtilities.getCloseButtonSize(this.tabPane, tabIndex); int borderDelta = (int) Math.ceil(3.0f + SubstanceSizeUtils.getBorderStrokeWidth()); int xs = this.tabPane.getComponentOrientation().isLeftToRight() ? x + width - dimension - borderDelta : x + borderDelta; int ys = y + (height - dimension) / 2 + 1; if (this.tabPane.getTabPlacement() == SwingUtilities.BOTTOM) { ys -= 2; } return new Rectangle(xs, ys, dimension, dimension); } /** * Retrieves the close button rectangle for event handling. * * @param tabIndex * Tab index. * @param x * X coordinate of the tab. * @param y * Y coordinate of the tab. * @param w * The tab width. * @param h * The tab height. * @return The close button rectangle. */ protected Rectangle getCloseButtonRectangleForEvents(int tabIndex, int x, int y, int w, int h) { int tabPlacement = this.tabPane.getTabPlacement(); boolean toSwap = toRotateTabsOnPlacement(tabPlacement); if (!toSwap) { return this.getCloseButtonRectangleForDraw(tabIndex, x, y, w, h); } int dimension = SubstanceCoreUtilities.getCloseButtonSize(this.tabPane, tabIndex); Point2D transCorner = null; Rectangle rectForDraw = this.getCloseButtonRectangleForDraw(tabIndex, x, y, h, w); if (tabPlacement == SwingConstants.LEFT) { AffineTransform trans = new AffineTransform(); trans.rotate(-Math.PI / 2, x, y); trans.translate(-h, 0); Point2D.Double origCorner = new Point2D.Double(rectForDraw .getMaxX(), rectForDraw.getMinY()); transCorner = trans.transform(origCorner, null); } else { // rotate 90 degrees clockwise for RIGHT orientation AffineTransform trans = new AffineTransform(); trans.rotate(Math.PI / 2, x, y); trans.translate(0, -w); Point2D.Double origCorner = new Point2D.Double(rectForDraw .getMinX(), rectForDraw.getMaxY()); transCorner = trans.transform(origCorner, null); } return new Rectangle((int) transCorner.getX(), (int) transCorner.getY(), dimension, dimension); } /** * Implementation of the fade tracker callback that repaints a single tab. * * @author Kirill Grouchnikov */ protected class TabRepaintCallback extends UIThreadTimelineCallbackAdapter { /** * The associated tabbed pane. */ protected JTabbedPane tabbedPane; /** * The associated tab index. */ protected int tabIndex; /** * Creates new tab repaint callback. * * @param tabPane * The associated tabbed pane. * @param tabIndex * The associated tab index. */ public TabRepaintCallback(JTabbedPane tabPane, int tabIndex) { this.tabbedPane = tabPane; this.tabIndex = tabIndex; } @Override public void onTimelinePulse(float durationFraction, float timelinePosition) { this.repaintTab(); } @Override public void onTimelineStateChanged(TimelineState oldState, TimelineState newState, float durationFraction, float timelinePosition) { this.repaintTab(); } /** * Repaints the relevant tab. */ protected void repaintTab() { SwingUtilities.invokeLater(() -> { if (SubstanceTabbedPaneUI.this.tabPane == null) { // may happen if the LAF was switched in the meantime return; } SubstanceTabbedPaneUI.this.ensureCurrentLayout(); int tabCount = SubstanceTabbedPaneUI.this.tabPane .getTabCount(); if ((tabCount > 0) && (TabRepaintCallback.this.tabIndex < tabCount) && (TabRepaintCallback.this.tabIndex < SubstanceTabbedPaneUI.this.rects.length)) { // need to retrieve the tab rectangle since the tabs // can be moved while animating (especially when the // current layout is SCROLL_LAYOUT) Rectangle rect = SubstanceTabbedPaneUI.this .getTabBounds( SubstanceTabbedPaneUI.this.tabPane, TabRepaintCallback.this.tabIndex); // System.out.println("Repainting " + tabIndex); SubstanceTabbedPaneUI.this.tabPane.repaint(rect); } }); } } /** * Ensures the current layout. */ protected void ensureCurrentLayout() { if (!this.tabPane.isValid()) { this.tabPane.validate(); } /* * If tabPane doesn't have a peer yet, the validate() call will silently * fail. We handle that by forcing a layout if tabPane is still invalid. * See bug 4237677. */ if (!this.tabPane.isValid()) { LayoutManager lm = this.tabPane.getLayout(); if (lm instanceof BasicTabbedPaneUI.TabbedPaneLayout) { BasicTabbedPaneUI.TabbedPaneLayout layout = (BasicTabbedPaneUI.TabbedPaneLayout) lm; layout.calculateLayoutInfo(); } } } /** * Tries closing tabs based on the specified tab index and tab close kind. * * @param tabIndex * Tab index. * @param tabCloseKind * Tab close kind. */ protected void tryCloseTabs(int tabIndex, TabCloseKind tabCloseKind) { if (tabCloseKind == null) return; if (tabCloseKind == TabCloseKind.NONE) return; if (tabCloseKind == TabCloseKind.ALL_BUT_THIS) { // close all but this Set<Integer> indexes = new HashSet<Integer>(); for (int i = 0; i < this.tabPane.getTabCount(); i++) if (i != tabIndex) indexes.add(i); this.tryCloseTabs(indexes); return; } if (tabCloseKind == TabCloseKind.ALL) { // close all Set<Integer> indexes = new HashSet<Integer>(); for (int i = 0; i < this.tabPane.getTabCount(); i++) indexes.add(i); this.tryCloseTabs(indexes); return; } this.tryCloseTab(tabIndex); } /** * Tries closing a single tab. * * @param tabIndex * Tab index. */ protected void tryCloseTab(int tabIndex) { Component component = this.tabPane.getComponentAt(tabIndex); Set<Component> componentSet = new HashSet<Component>(); componentSet.add(component); // check if there's at least one listener // that vetoes the closing boolean isVetoed = false; for (BaseTabCloseListener listener : SubstanceLookAndFeel .getAllTabCloseListeners(this.tabPane)) { if (listener instanceof VetoableTabCloseListener) { VetoableTabCloseListener vetoableListener = (VetoableTabCloseListener) listener; isVetoed = isVetoed || vetoableListener.vetoTabClosing(this.tabPane, component); } if (listener instanceof VetoableMultipleTabCloseListener) { VetoableMultipleTabCloseListener vetoableListener = (VetoableMultipleTabCloseListener) listener; isVetoed = isVetoed || vetoableListener.vetoTabsClosing(this.tabPane, componentSet); } } if (isVetoed) return; for (BaseTabCloseListener listener : SubstanceLookAndFeel .getAllTabCloseListeners(this.tabPane)) { if (listener instanceof TabCloseListener) ((TabCloseListener) listener).tabClosing(this.tabPane, component); if (listener instanceof MultipleTabCloseListener) ((MultipleTabCloseListener) listener).tabsClosing(this.tabPane, componentSet); } this.tabPane.remove(tabIndex); if (this.tabPane.getTabCount() > 0) { this.selectPreviousTab(0); this.selectNextTab(this.tabPane.getSelectedIndex()); } this.tabPane.repaint(); for (BaseTabCloseListener listener : SubstanceLookAndFeel .getAllTabCloseListeners(this.tabPane)) { if (listener instanceof TabCloseListener) ((TabCloseListener) listener) .tabClosed(this.tabPane, component); if (listener instanceof MultipleTabCloseListener) ((MultipleTabCloseListener) listener).tabsClosed(this.tabPane, componentSet); } } /** * Tries closing the specified tabs. * * @param tabIndexes * Tab indexes. */ protected void tryCloseTabs(Set<Integer> tabIndexes) { Set<Component> componentSet = new HashSet<Component>(); for (int tabIndex : tabIndexes) { componentSet.add(this.tabPane.getComponentAt(tabIndex)); } // check if there's at least one listener // that vetoes the closing boolean isVetoed = false; for (BaseTabCloseListener listener : SubstanceLookAndFeel .getAllTabCloseListeners(this.tabPane)) { if (listener instanceof VetoableMultipleTabCloseListener) { VetoableMultipleTabCloseListener vetoableListener = (VetoableMultipleTabCloseListener) listener; isVetoed = isVetoed || vetoableListener.vetoTabsClosing(this.tabPane, componentSet); } } if (isVetoed) return; for (BaseTabCloseListener listener : SubstanceLookAndFeel .getAllTabCloseListeners(this.tabPane)) { if (listener instanceof MultipleTabCloseListener) ((MultipleTabCloseListener) listener).tabsClosing(this.tabPane, componentSet); } for (Component toRemove : componentSet) { this.tabPane.remove(toRemove); } if (this.tabPane.getTabCount() > 0) { this.selectPreviousTab(0); this.selectNextTab(this.tabPane.getSelectedIndex()); } this.tabPane.repaint(); for (BaseTabCloseListener listener : SubstanceLookAndFeel .getAllTabCloseListeners(this.tabPane)) { if (listener instanceof MultipleTabCloseListener) ((MultipleTabCloseListener) listener).tabsClosed(this.tabPane, componentSet); } } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#getTabLabelShiftX(int, int, * boolean) */ @Override protected int getTabLabelShiftX(int tabPlacement, int tabIndex, boolean isSelected) { int delta = 0; if (SubstanceCoreUtilities.hasCloseButton(this.tabPane, tabIndex)) { if (this.tabPane.getComponentOrientation().isLeftToRight()) { delta = 5 - SubstanceCoreUtilities.getCloseButtonSize( this.tabPane, tabIndex); } else { delta = SubstanceCoreUtilities.getCloseButtonSize(this.tabPane, tabIndex) - 5; } } return delta + super.getTabLabelShiftX(tabPlacement, tabIndex, isSelected); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#getTabLabelShiftY(int, int, * boolean) */ @Override protected int getTabLabelShiftY(int tabPlacement, int tabIndex, boolean isSelected) { int result = 0; if (tabPlacement == SwingConstants.BOTTOM) result = -1; else result = 1; return result; } /** * Returns extra width for the specified tab. * * @param tabPlacement * Tab placement. * @param tabIndex * Tab index. * @return Extra width for the specified tab. */ protected int getTabExtraWidth(int tabPlacement, int tabIndex) { int extraWidth = 0; SubstanceButtonShaper shaper = SubstanceCoreUtilities .getButtonShaper(this.tabPane); if (shaper instanceof ClassicButtonShaper) extraWidth = (int) (2.0 * SubstanceSizeUtils .getClassicButtonCornerRadius(SubstanceSizeUtils .getComponentFontSize(this.tabPane))); else extraWidth = super.calculateTabHeight(tabPlacement, tabIndex, this .getFontMetrics().getHeight()) / 3; if (SubstanceCoreUtilities.hasCloseButton(this.tabPane, tabIndex) && this.tabPane.isEnabledAt(tabIndex)) { extraWidth += (4 + SubstanceCoreUtilities.getCloseButtonSize( this.tabPane, tabIndex)); } // System.out.println(tabPane.getTitleAt(tabIndex) + ":" + extraWidth); return extraWidth; } /** * Returns the index of the tab currently being rolled-over. * * @return Index of the tab currently being rolled-over. */ public int getRolloverTabIndex() { return this.getRolloverTab(); } /** * Sets new value for tab area insets. * * @param insets * Tab area insets. */ public void setTabAreaInsets(Insets insets) { Insets old = this.tabAreaInsets; this.tabAreaInsets = insets; // Fire a property change event so that the tabbed // pane can revalidate itself LafWidgetUtilities.firePropertyChangeEvent(this.tabPane, "tabAreaInsets", old, tabAreaInsets); } /** * Returns tab area insets. * * @return Tab area insets. */ public Insets getTabAreaInsets() { return this.tabAreaInsets; } /** * Returns the tab rectangle for the specified tab. * * @param tabIndex * Index of a tab. * @return The tab rectangle for the specified parameters. */ public Rectangle getTabRectangle(int tabIndex) { return this.rects[tabIndex]; } /** * Returns the memory usage string. * * @return The memory usage string. */ public static String getMemoryUsage() { StringBuffer sb = new StringBuffer(); sb.append("SubstanceTabbedPaneUI: \n"); sb.append("\t" + SubstanceTabbedPaneUI.backgroundMap.size() + " backgrounds"); return sb.toString(); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#shouldPadTabRun(int, int) */ @Override protected boolean shouldPadTabRun(int tabPlacement, int run) { // Don't pad last run return this.runCount > 1 && run < this.runCount - 1; } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#createLayoutManager() */ @Override protected LayoutManager createLayoutManager() { if (this.tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT) { return super.createLayoutManager(); } return new TabbedPaneLayout(); } /** * Layout for the tabbed pane. * * @author Kirill Grouchnikov */ public class TabbedPaneLayout extends BasicTabbedPaneUI.TabbedPaneLayout { /** * Creates a new layout. */ public TabbedPaneLayout() { SubstanceTabbedPaneUI.this.super(); } /* * (non-Javadoc) * * @seejavax.swing.plaf.basic.BasicTabbedPaneUI$TabbedPaneLayout# * normalizeTabRuns(int, int, int, int) */ @Override protected void normalizeTabRuns(int tabPlacement, int tabCount, int start, int max) { // Only normalize the runs for top & bottom; normalizing // doesn't look right for Metal's vertical tabs // because the last run isn't padded and it looks odd to have // fat tabs in the first vertical runs, but slimmer ones in the // last (this effect isn't noticeable for horizontal tabs). if (tabPlacement == TOP || tabPlacement == BOTTOM) { super.normalizeTabRuns(tabPlacement, tabCount, start, max); } } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI$TabbedPaneLayout#rotateTabRuns * (int, int) */ @Override protected void rotateTabRuns(int tabPlacement, int selectedRun) { // Don't rotate runs! } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI$TabbedPaneLayout#padSelectedTab * (int, int) */ @Override protected void padSelectedTab(int tabPlacement, int selectedIndex) { // Don't pad selected tab } } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTabbedPaneUI#getContentBorderInsets(int) */ @Override protected Insets getContentBorderInsets(int tabPlacement) { Insets insets = SubstanceSizeUtils .getTabbedPaneContentInsets(); TabContentPaneBorderKind kind = SubstanceCoreUtilities .getContentBorderKind(this.tabPane); boolean isDouble = (kind == TabContentPaneBorderKind.DOUBLE_FULL) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); boolean isPlacement = (kind == TabContentPaneBorderKind.SINGLE_PLACEMENT) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); int delta = isDouble ? (int) Math.ceil(SubstanceSizeUtils.getBorderStrokeWidth() + 1.5f) : 0; if (isPlacement) { switch (tabPlacement) { case TOP: return new Insets(insets.top + delta, 0, 0, 0); case LEFT: return new Insets(0, insets.left + delta, 0, 0); case RIGHT: return new Insets(0, 0, 0, insets.right + delta); case BOTTOM: return new Insets(0, 0, insets.bottom + delta, 0); } } else { switch (tabPlacement) { case TOP: return new Insets(insets.top + delta, insets.left, insets.bottom, insets.right); case LEFT: return new Insets(insets.top, insets.left + delta, insets.bottom, insets.right); case RIGHT: return new Insets(insets.top, insets.left, insets.bottom, insets.right + delta); case BOTTOM: return new Insets(insets.top, insets.left, insets.bottom + delta, insets.right); } } return insets; } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintContentBorder(java.awt. * Graphics, int, int) */ @Override protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) { SubstanceColorScheme scheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, selectedIndex, ColorSchemeAssociationKind.TAB, ComponentState.ENABLED); this.highlight = scheme.isDark() ? SubstanceColorUtilities .getAlphaColor(scheme.getUltraDarkColor(), 100) : scheme .getLightColor(); super.paintContentBorder(g, tabPlacement, selectedIndex); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintContentBorderBottomEdge * (java.awt.Graphics, int, int, int, int, int, int) */ @Override protected void paintContentBorderBottomEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) { TabContentPaneBorderKind kind = SubstanceCoreUtilities .getContentBorderKind(this.tabPane); boolean isDouble = (kind == TabContentPaneBorderKind.DOUBLE_FULL) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); boolean isPlacement = (kind == TabContentPaneBorderKind.SINGLE_PLACEMENT) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); if (isPlacement) { if (tabPlacement != SwingConstants.BOTTOM) return; } Rectangle selRect = selectedIndex < 0 ? null : this.getTabBounds( selectedIndex, this.calcRect); Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); float strokeWidth = SubstanceSizeUtils.getBorderStrokeWidth(); int joinKind = BasicStroke.JOIN_ROUND; int capKind = BasicStroke.CAP_BUTT; g2d.setStroke(new BasicStroke(strokeWidth, capKind, joinKind)); int offset = (int) (strokeWidth / 2.0); int ribbonDelta = (int) Math.ceil(strokeWidth + 1.5f); boolean isUnbroken = (tabPlacement != BOTTOM || selectedIndex < 0 || (selRect.y - 1 > h) || (selRect.x < x || selRect.x > x + w)); x += offset; y += offset; w -= 2 * offset; h -= 2 * offset; // Draw unbroken line if tabs are not on BOTTOM, OR // selected tab is not in run adjacent to content, OR // selected tab is not visible (SCROLL_TAB_LAYOUT) SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, selectedIndex, ColorSchemeAssociationKind.TAB_BORDER, ComponentState.SELECTED); Color darkShadowColor = SubstanceColorUtilities .getMidBorderColor(borderScheme); if (isUnbroken) { g2d.setColor(this.highlight); g2d.drawLine(x, y + h - 1, x + w, y + h - 1); } else { // Break line to show visual connection to selected tab SubstanceButtonShaper shaper = SubstanceCoreUtilities .getButtonShaper(this.tabPane); int delta = (shaper instanceof ClassicButtonShaper) ? 1 : 0; float borderInsets = SubstanceSizeUtils.getBorderStrokeWidth() / 2.0f; GeneralPath bottomOutline = new GeneralPath(); bottomOutline.moveTo(x, y + h - 1); bottomOutline.lineTo(selRect.x + borderInsets, y + h - 1); int bumpHeight = super.calculateTabHeight(tabPlacement, 0, SubstanceSizeUtils.getComponentFontSize(this.tabPane)) / 2; bottomOutline.lineTo(selRect.x + borderInsets, y + h + bumpHeight); if (selRect.x + selRect.width < x + w) { float selectionEndX = selRect.x + selRect.width - delta - 1 - borderInsets; bottomOutline.lineTo(selectionEndX, y + h - 1 + bumpHeight); bottomOutline.lineTo(selectionEndX, y + h - 1); bottomOutline.lineTo(x + w, y + h - 1); } g2d.setPaint(new GradientPaint(x, y + h - 1, darkShadowColor, x, y + h - 1 + bumpHeight, SubstanceColorUtilities .getAlphaColor(darkShadowColor, 0))); g2d.draw(bottomOutline); } if (isDouble) { if (tabPlacement == BOTTOM) { g2d.setColor(this.highlight); g2d.setColor(darkShadowColor); g2d.drawLine(x, y + h - 1 - ribbonDelta, x + w, y + h - 1 - ribbonDelta); } if (tabPlacement == LEFT) { g2d.setPaint(new GradientPaint(x, y + h - 1, darkShadowColor, x + 4 * ribbonDelta, y + h - 1, this.highlight)); g2d.drawLine(x, y + h - 1, x + 4 * ribbonDelta, y + h - 1); } if (tabPlacement == RIGHT) { g2d.setPaint(new GradientPaint(x + w - 1 - 4 * ribbonDelta, y + h - 1, this.highlight, x + w - 1, y + h - 1, darkShadowColor)); g2d.drawLine(x + w - 1 - 4 * ribbonDelta, y + h - 1, x + w - 1, y + h - 1); } } g2d.dispose(); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintContentBorderLeftEdge(java * .awt.Graphics, int, int, int, int, int, int) */ @Override protected void paintContentBorderLeftEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) { TabContentPaneBorderKind kind = SubstanceCoreUtilities .getContentBorderKind(this.tabPane); boolean isDouble = (kind == TabContentPaneBorderKind.DOUBLE_FULL) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); boolean isPlacement = (kind == TabContentPaneBorderKind.SINGLE_PLACEMENT) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); if (isPlacement) { if (tabPlacement != SwingConstants.LEFT) return; } int ribbonDelta = (int) (SubstanceSizeUtils.getBorderStrokeWidth() + 1.5f); Rectangle selRect = selectedIndex < 0 ? null : this.getTabBounds( selectedIndex, this.calcRect); Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); float strokeWidth = SubstanceSizeUtils.getBorderStrokeWidth(); int joinKind = BasicStroke.JOIN_ROUND; int capKind = BasicStroke.CAP_BUTT; g2d.setStroke(new BasicStroke(strokeWidth, capKind, joinKind)); int offset = (int) (strokeWidth / 2.0); boolean isUnbroken = (tabPlacement != LEFT || selectedIndex < 0 || (selRect.x + selRect.width + 1 < x) || (selRect.y < y || selRect.y > y + h)); x += offset; y += offset; // w -= 2 * offset; h -= 2 * offset; // Draw unbroken line if tabs are not on LEFT, OR // selected tab is not in run adjacent to content, OR // selected tab is not visible (SCROLL_TAB_LAYOUT) SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, selectedIndex, ColorSchemeAssociationKind.TAB_BORDER, ComponentState.SELECTED); Color darkShadowColor = SubstanceColorUtilities .getMidBorderColor(borderScheme); if (isUnbroken) { g2d.setColor(this.highlight); g2d.drawLine(x, y, x, y + h); } else { // Break line to show visual connection to selected tab SubstanceButtonShaper shaper = SubstanceCoreUtilities .getButtonShaper(this.tabPane); int delta = (shaper instanceof ClassicButtonShaper) ? 1 : 0; float borderInsets = SubstanceSizeUtils.getBorderStrokeWidth() / 2.0f; GeneralPath leftOutline = new GeneralPath(); leftOutline.moveTo(x, y); leftOutline.lineTo(x, selRect.y + borderInsets + delta + 1); int bumpWidth = super.calculateTabHeight(tabPlacement, 0, SubstanceSizeUtils.getComponentFontSize(this.tabPane)) / 2; leftOutline.lineTo(x - bumpWidth, selRect.y + borderInsets + delta + 1); if (selRect.y + selRect.height < y + h) { float selectionEndY = selRect.y + selRect.height - borderInsets; leftOutline.lineTo(x - bumpWidth, selectionEndY); leftOutline.lineTo(x, selectionEndY); leftOutline.lineTo(x, y + h); } g2d.setPaint(new GradientPaint(x, y, darkShadowColor, x - bumpWidth, y, SubstanceColorUtilities.getAlphaColor( darkShadowColor, 0))); g2d.draw(leftOutline); } if (isDouble) { if (tabPlacement == LEFT) { g2d.setColor(darkShadowColor); g2d.drawLine(x + ribbonDelta, y, x + ribbonDelta, y + h); // g2d.setColor(this.highlight); // g2d.drawLine(x + 1 + ribbonDelta, y + 1, x + 1 + ribbonDelta, // y + // h - 1); } if (tabPlacement == TOP) { g2d.setPaint(new GradientPaint(x, y, darkShadowColor, x, y + 4 * ribbonDelta, this.highlight)); g2d.drawLine(x, y, x, y + 4 * ribbonDelta); } if (tabPlacement == BOTTOM) { g2d.setPaint(new GradientPaint(x, y + h - 1 - 4 * ribbonDelta, this.highlight, x, y + h - 1, darkShadowColor)); g2d.drawLine(x, y + h - 1 - 4 * ribbonDelta, x, y + h - 1); } } g2d.dispose(); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintContentBorderRightEdge( * java.awt.Graphics, int, int, int, int, int, int) */ @Override protected void paintContentBorderRightEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) { TabContentPaneBorderKind kind = SubstanceCoreUtilities .getContentBorderKind(this.tabPane); boolean isDouble = (kind == TabContentPaneBorderKind.DOUBLE_FULL) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); boolean isPlacement = (kind == TabContentPaneBorderKind.SINGLE_PLACEMENT) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); if (isPlacement) { if (tabPlacement != SwingConstants.RIGHT) return; } int ribbonDelta = (int) (SubstanceSizeUtils.getBorderStrokeWidth() + 1.5f); Rectangle selRect = selectedIndex < 0 ? null : this.getTabBounds( selectedIndex, this.calcRect); Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); float strokeWidth = SubstanceSizeUtils.getBorderStrokeWidth(); int joinKind = BasicStroke.JOIN_ROUND; int capKind = BasicStroke.CAP_BUTT; g2d.setStroke(new BasicStroke(strokeWidth, capKind, joinKind)); int offset = (int) (strokeWidth / 2.0); boolean isUnbroken = (tabPlacement != RIGHT || selectedIndex < 0 || (selRect.x - 1 > w) || (selRect.y < y || selRect.y > y + h)); x += offset; y += offset; w -= 2 * offset; h -= 2 * offset; // Draw unbroken line if tabs are not on RIGHT, OR // selected tab is not in run adjacent to content, OR // selected tab is not visible (SCROLL_TAB_LAYOUT) SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, selectedIndex, ColorSchemeAssociationKind.TAB_BORDER, ComponentState.SELECTED); Color darkShadowColor = SubstanceColorUtilities .getMidBorderColor(borderScheme); if (isUnbroken) { g2d.setColor(this.highlight); g2d.drawLine(x + w - 1, y, x + w - 1, y + h); } else { // Break line to show visual connection to selected tab SubstanceButtonShaper shaper = SubstanceCoreUtilities .getButtonShaper(this.tabPane); int delta = (shaper instanceof ClassicButtonShaper) ? 1 : 0; float borderInsets = SubstanceSizeUtils.getBorderStrokeWidth() / 2.0f; GeneralPath rightOutline = new GeneralPath(); rightOutline.moveTo(x + w - 1, y); rightOutline.lineTo(x + w - 1, selRect.y + borderInsets); int bumpWidth = super.calculateTabHeight(tabPlacement, 0, SubstanceSizeUtils.getComponentFontSize(this.tabPane)) / 2; rightOutline .lineTo(x + w - 1 + bumpWidth, selRect.y + borderInsets); if (selRect.y + selRect.height < y + h) { float selectionEndY = selRect.y + selRect.height - delta - 1 - borderInsets; rightOutline.lineTo(x + w - 1 + bumpWidth, selectionEndY); rightOutline.lineTo(x + w - 1, selectionEndY); rightOutline.lineTo(x + w - 1, y + h); } g2d.setPaint(new GradientPaint(x + w - 1, y, darkShadowColor, x + w - 1 + bumpWidth, y, SubstanceColorUtilities.getAlphaColor( darkShadowColor, 0))); g2d.draw(rightOutline); } if (isDouble) { if (tabPlacement == RIGHT) { g2d.setColor(this.highlight); // g2d.drawLine(x + w - 2 - ribbonDelta, y + 1, x + w - 2 - // ribbonDelta, y + h - 1); g2d.setColor(darkShadowColor); g2d.drawLine(x + w - 1 - ribbonDelta, y, x + w - 1 - ribbonDelta, y + h); } if (tabPlacement == TOP) { g2d.setPaint(new GradientPaint(x + w - 1, y, darkShadowColor, x + w - 1, y + 4 * ribbonDelta, this.highlight)); g2d.drawLine(x + w - 1, y, x + w - 1, y + 4 * ribbonDelta); } if (tabPlacement == BOTTOM) { g2d.setPaint(new GradientPaint(x + w - 1, y + h - 1 - 4 * ribbonDelta, this.highlight, x + w - 1, y + h - 1, darkShadowColor)); g2d.drawLine(x + w - 1, y + h - 1 - 4 * ribbonDelta, x + w - 1, y + h - 1); } } g2d.dispose(); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintContentBorderTopEdge(java * .awt.Graphics, int, int, int, int, int, int) */ @Override protected void paintContentBorderTopEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) { TabContentPaneBorderKind kind = SubstanceCoreUtilities .getContentBorderKind(this.tabPane); boolean isDouble = (kind == TabContentPaneBorderKind.DOUBLE_FULL) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); boolean isPlacement = (kind == TabContentPaneBorderKind.SINGLE_PLACEMENT) || (kind == TabContentPaneBorderKind.DOUBLE_PLACEMENT); if (isPlacement) { if (tabPlacement != SwingConstants.TOP) return; } Rectangle selRect = selectedIndex < 0 ? null : this.getTabBounds( selectedIndex, this.calcRect); Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); float strokeWidth = SubstanceSizeUtils.getBorderStrokeWidth(); int joinKind = BasicStroke.JOIN_ROUND; int capKind = BasicStroke.CAP_BUTT; g2d.setStroke(new BasicStroke(strokeWidth, capKind, joinKind)); int offset = (int) (strokeWidth / 2.0); int ribbonDelta = (int) Math.ceil(strokeWidth + 1.5f); boolean isUnbroken = (tabPlacement != TOP || selectedIndex < 0 || (selRect.y + selRect.height + 1 < y) || (selRect.x < x || selRect.x > x + w)); x += offset; y += offset; w -= 2 * offset; // h -= 2 * offset; // Draw unbroken line if tabs are not on TOP, OR // selected tab is not in run adjacent to content, OR // selected tab is not visible (SCROLL_TAB_LAYOUT) SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tabPane, selectedIndex, ColorSchemeAssociationKind.TAB_BORDER, ComponentState.SELECTED); Color darkShadowColor = SubstanceColorUtilities .getMidBorderColor(borderScheme); if (isUnbroken) { g2d.setColor(this.highlight); g2d.drawLine(x, y, x + w, y); } else { // Break line to show visual connection to selected tab SubstanceButtonShaper shaper = SubstanceCoreUtilities .getButtonShaper(this.tabPane); int delta = (shaper instanceof ClassicButtonShaper) ? 1 : 0; float borderInsets = SubstanceSizeUtils.getBorderStrokeWidth() / 2.0f; GeneralPath topOutline = new GeneralPath(); topOutline.moveTo(x, y); topOutline.lineTo(selRect.x + borderInsets, y); int bumpHeight = super.calculateTabHeight(tabPlacement, 0, SubstanceSizeUtils.getComponentFontSize(this.tabPane)) / 2; topOutline.lineTo(selRect.x + borderInsets, y - bumpHeight); if (selRect.x + selRect.width < x + w) { float selectionEndX = selRect.x + selRect.width - delta - 1 - borderInsets; topOutline.lineTo(selectionEndX, y - bumpHeight); topOutline.lineTo(selectionEndX, y); topOutline.lineTo(x + w, y); } g2d.setPaint(new GradientPaint(x, y, darkShadowColor, x, y - bumpHeight, SubstanceColorUtilities.getAlphaColor( darkShadowColor, 0))); g2d.draw(topOutline); } if (isDouble) { if (tabPlacement == TOP) { g2d.setColor(darkShadowColor); g2d.drawLine(x, y + ribbonDelta, x + w, y + ribbonDelta); } if (tabPlacement == LEFT) { g2d.setPaint(new GradientPaint(x, y, darkShadowColor, x + 4 * ribbonDelta, y, this.highlight)); g2d.drawLine(x, y, x + 4 * ribbonDelta, y); } if (tabPlacement == RIGHT) { g2d.setPaint(new GradientPaint(x + w - 1 - 4 * ribbonDelta, y, this.highlight, x + w - 1, y, darkShadowColor)); g2d.drawLine(x + w - 1 - 4 * ribbonDelta, y, x + w - 1, y); } } g2d.dispose(); } @Override public Rectangle getTabBounds(JTabbedPane pane, int i) { this.ensureCurrentLayout(); Rectangle tabRect = new Rectangle(); return this.getTabBounds(i, tabRect); } private StateTransitionTracker.ModelStateInfo getModelStateInfo(int tabIndex) { if (this.stateTransitionMultiTracker.size() == 0) return null; StateTransitionTracker tracker = this.stateTransitionMultiTracker.getTracker(tabIndex); if (tracker == null) { return null; } else { return tracker.getModelStateInfo(); } } /** * Returns the current state for the specified tab. * * @param tabIndex * Tab index. * @return The current state for the specified tab. */ protected ComponentState getTabState(int tabIndex, boolean toAllowIgnoringSelectedState) { boolean isEnabled = this.tabPane.isEnabledAt(tabIndex); StateTransitionTracker tracker = this.stateTransitionMultiTracker.getTracker(tabIndex); boolean ignoreSelectedState = toAllowIgnoringSelectedState && (SubstanceCoreUtilities.getSkin(tabPane).getTabFadeEnd() <= 0.5); if (tracker == null) { boolean isRollover = this.getRolloverTabIndex() == tabIndex; boolean isSelected = ignoreSelectedState ? false : this.tabPane.getSelectedIndex() == tabIndex; return ComponentState.getState(isEnabled, isRollover, isSelected); } else { ComponentState fromTracker = ignoreSelectedState ? tracker.getModelStateInfo().getCurrModelStateNoSelection() : tracker.getModelStateInfo().getCurrModelState(); return fromTracker; // return ComponentState.getState(isEnabled, // fromTracker.isFacetActive(ComponentStateFacet.ROLLOVER), // ignoreSelectedState ? false : // fromTracker.isFacetActive(ComponentStateFacet.SELECTION)); } } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTabbedPaneUI#paintText(java.awt.Graphics, * int, java.awt.Font, java.awt.FontMetrics, int, java.lang.String, * java.awt.Rectangle, boolean) */ @Override protected void paintText(Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected) { g.setFont(font); View v = this.getTextViewForTab(tabIndex); if (v != null) { // html v.paint(g, textRect); } else { // plain text int mnemIndex = this.tabPane.getDisplayedMnemonicIndexAt(tabIndex); // Special handling of tabs under skins that show partial visuals boolean isTextOnParentBackground = (SubstanceCoreUtilities.getSkin(tabPane).getTabFadeEnd() <= 0.5); ComponentState currState = this.getTabState(tabIndex, true); if (isTextOnParentBackground) { // Ignore all other "aspects" of tab's state currState = this.tabPane.isEnabledAt(tabIndex) ? ComponentState.ENABLED : ComponentState.DISABLED_UNSELECTED; } StateTransitionTracker.ModelStateInfo modelStateInfo = isTextOnParentBackground ? null : this.getModelStateInfo(tabIndex); //System.out.println("Tab " + title + ":" + currState); Color fg = null; if (modelStateInfo != null) { Map<ComponentState, StateContributionInfo> activeStates = modelStateInfo .getStateContributionMap(); SubstanceColorScheme colorScheme = SubstanceColorSchemeUtilities .getColorScheme(tabPane, tabIndex, ColorSchemeAssociationKind.TAB, currState); if (currState.isDisabled() || (activeStates == null) || (activeStates.size() == 1)) { fg = colorScheme.getForegroundColor(); } else { float aggrRed = 0; float aggrGreen = 0; float aggrBlue = 0; for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : activeStates .entrySet()) { ComponentState activeState = activeEntry.getKey(); SubstanceColorScheme scheme = SubstanceColorSchemeUtilities .getColorScheme(tabPane, tabIndex, ColorSchemeAssociationKind.TAB, activeState); Color schemeFg = scheme.getForegroundColor(); float contribution = activeEntry.getValue() .getContribution(); // System.out.println("\t" + activeState + ":" // + contribution + ":" + scheme.getDisplayName() // + ":" + schemeFg); aggrRed += schemeFg.getRed() * contribution; aggrGreen += schemeFg.getGreen() * contribution; aggrBlue += schemeFg.getBlue() * contribution; } // System.out.println(""); fg = new Color((int) aggrRed, (int) aggrGreen, (int) aggrBlue); } } else { SubstanceColorScheme scheme = SubstanceColorSchemeUtilities .getColorScheme(tabPane, tabIndex, ColorSchemeAssociationKind.TAB, currState); fg = scheme.getForegroundColor(); } Graphics2D graphics = (Graphics2D) g.create(); if (currState.isDisabled()) { Color bgFillColor = SubstanceColorUtilities .getBackgroundFillColor(this.tabPane); fg = SubstanceColorUtilities.getInterpolatedColor(fg, bgFillColor, SubstanceColorSchemeUtilities.getAlpha( this.tabPane.getComponentAt(tabIndex), currState)); } graphics.clip(getTabRectangle(tabIndex)); SubstanceTextUtilities.paintText(graphics, this.tabPane, textRect, title, mnemIndex, graphics.getFont(), fg, null); graphics.dispose(); } } @Override protected void paintIcon(Graphics g, int tabPlacement, int tabIndex, Icon icon, Rectangle iconRect, boolean isSelected) { if (icon == null) return; Graphics2D g2d = (Graphics2D) g.create(); g2d.translate(iconRect.x, iconRect.y); if (SubstanceCoreUtilities.useThemedDefaultIcon(this.tabPane)) { ComponentState currState = this.getTabState(tabIndex, true); StateTransitionTracker tabTracker = stateTransitionMultiTracker .getTracker(tabIndex); if (tabTracker == null) { if (currState.isFacetActive(ComponentStateFacet.ROLLOVER) || currState.isFacetActive(ComponentStateFacet.SELECTION) || currState.isDisabled()) { // use the original (full color or disabled) icon icon.paintIcon(this.tabPane, g2d, 0, 0); return; } } Icon themed = SubstanceCoreUtilities.getThemedIcon(this.tabPane, tabIndex, icon); if (tabTracker == null) { themed.paintIcon(this.tabPane, g2d, 0, 0); } else { icon.paintIcon(this.tabPane, g2d, 0, 0); g2d.setComposite(LafWidgetUtilities.getAlphaComposite( this.tabPane, 1.0f - tabTracker.getFacetStrength(ComponentStateFacet.ROLLOVER), g2d)); themed.paintIcon(this.tabPane, g2d, 0, 0); } } else { icon.paintIcon(this.tabPane, g2d, 0, 0); } g2d.dispose(); } @Override protected MouseListener createMouseListener() { return null; } /** * Extension point to allow horizontal orientation of left / right placed * tabs. * * @param tabPlacement * Tab placement. * @return Indication whether the tabs in the specified placement should be * rotated. */ protected boolean toRotateTabsOnPlacement(int tabPlacement) { return (tabPlacement == SwingConstants.LEFT) || (tabPlacement == SwingConstants.RIGHT); } private StateTransitionTracker getTracker(final int tabIndex, boolean initialRollover, boolean initialSelected) { StateTransitionTracker tracker = stateTransitionMultiTracker .getTracker(tabIndex); if (tracker == null) { ButtonModel model = new DefaultButtonModel(); model.setSelected(initialSelected); model.setRollover(initialRollover); tracker = new StateTransitionTracker(tabPane, model); tracker.registerModelListeners(); tracker.setRepaintCallback(() -> new TabRepaintCallback(tabPane, tabIndex)); stateTransitionMultiTracker.addTracker(tabIndex, tracker); } return tracker; } private void trackTabModification(int tabIndex, Component tabComponent) { Timeline modifiedTimeline = new Timeline(tabPane); AnimationConfigurationManager.getInstance().configureModifiedTimeline( modifiedTimeline); modifiedTimeline.addCallback(new TabRepaintCallback(tabPane, tabIndex)); modifiedTimeline.playLoop(RepeatBehavior.REVERSE); modifiedTimelines.put(tabComponent, modifiedTimeline); } @Override public void update(Graphics g, JComponent c) { Graphics2D g2d = (Graphics2D) g.create(); RenderingUtils.installDesktopHints(g2d, c); super.update(g2d, c); g2d.dispose(); } }