/* * 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.utils; import java.awt.Color; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.util.Map; import javax.swing.AbstractButton; import javax.swing.ButtonModel; import javax.swing.CellRendererPane; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.SwingUtilities; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicGraphicsUtils; import javax.swing.text.JTextComponent; import org.pushingpixels.lafwidget.LafWidgetUtilities; 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.SubstanceLookAndFeel; import org.pushingpixels.substance.api.painter.border.SubstanceBorderPainter; import org.pushingpixels.substance.api.watermark.SubstanceWatermark; import org.pushingpixels.substance.internal.animation.StateTransitionTracker; import org.pushingpixels.substance.internal.animation.TransitionAwareUI; import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils; import org.pushingpixels.substance.internal.utils.border.SubstanceTextComponentBorder; /** * Text-related utilities. This class if for internal use only. * * @author Kirill Grouchnikov */ public class SubstanceTextUtilities { public static final String ENFORCE_FG_COLOR = "substancelaf.internal.textUtilities.enforceFgColor"; /** * Paints text with drop shadow. * * @param c * Component. * @param g * Graphics context. * @param foregroundColor * Foreground color. * @param text * Text to paint. * @param width * Text rectangle width. * @param height * Text rectangle height. * @param xOffset * Text rectangle X offset. * @param yOffset * Text rectangle Y offset. */ public static void paintTextWithDropShadow(JComponent c, Graphics g, Color foregroundColor, Color echoColor, String text, int width, int height, int xOffset, int yOffset) { Graphics2D graphics = (Graphics2D) g.create(); RenderingUtils.installDesktopHints(graphics, c); // blur the text shadow BufferedImage blurred = SubstanceCoreUtilities.getBlankImage(width, height); Graphics2D gBlurred = (Graphics2D) blurred.getGraphics(); gBlurred.setFont(graphics.getFont()); gBlurred.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); float luminFactor = SubstanceColorUtilities.getColorStrength(foregroundColor); gBlurred.setColor(echoColor); ConvolveOp convolve = new ConvolveOp(new Kernel(3, 3, new float[] { .04f, .06f, .04f, .06f, .04f, .06f, .04f, .06f, .04f }), ConvolveOp.EDGE_NO_OP, null); gBlurred.drawString(text, xOffset, yOffset); blurred = convolve.filter(blurred, null); graphics.setComposite(LafWidgetUtilities.getAlphaComposite(c, luminFactor, g)); int scaleFactor = UIUtil.getScaleFactor(); graphics.drawImage(blurred, 0, 0, blurred.getWidth() / scaleFactor, blurred.getHeight() / scaleFactor, null); graphics.setComposite(LafWidgetUtilities.getAlphaComposite(c, g)); FontMetrics fm = graphics.getFontMetrics(); SubstanceTextUtilities.paintText(graphics, c, new Rectangle(xOffset, yOffset - fm.getAscent(), width - xOffset, fm.getHeight()), text, -1, graphics.getFont(), foregroundColor, graphics.getClipBounds()); graphics.dispose(); } /** * Paints the specified text. * * @param g * Graphics context. * @param comp * Component. * @param textRect * Text rectangle. * @param text * Text to paint. * @param mnemonicIndex * Mnemonic index. * @param font * Font to use. * @param color * Color to use. * @param clip * Optional clip. Can be <code>null</code>. * @param transform * Optional transform to apply. Can be <code>null</code>. */ private static void paintText(Graphics g, JComponent comp, Rectangle textRect, String text, int mnemonicIndex, java.awt.Font font, java.awt.Color color, java.awt.Rectangle clip, java.awt.geom.AffineTransform transform) { if ((text == null) || (text.length() == 0)) return; Graphics2D g2d = (Graphics2D) g.create(); g2d.setFont(font); g2d.setColor(color); // fix for issue 420 - call clip() instead of setClip() to // respect the currently set clip shape if (clip != null) g2d.clip(clip); if (transform != null) g2d.transform(transform); BasicGraphicsUtils.drawStringUnderlineCharAt(g2d, text, mnemonicIndex, textRect.x, textRect.y + g2d.getFontMetrics().getAscent()); g2d.dispose(); } /** * Paints the specified text. * * @param g * Graphics context. * @param comp * Component. * @param textRect * Text rectangle. * @param text * Text to paint. * @param mnemonicIndex * Mnemonic index. * @param font * Font to use. * @param color * Color to use. * @param clip * Optional clip. Can be <code>null</code>. */ public static void paintText(Graphics g, JComponent comp, Rectangle textRect, String text, int mnemonicIndex, java.awt.Font font, java.awt.Color color, java.awt.Rectangle clip) { SubstanceTextUtilities.paintText(g, comp, textRect, text, mnemonicIndex, font, color, clip, null); } /** * Paints the specified vertical text. * * @param g * Graphics context. * @param comp * Component. * @param textRect * Text rectangle. * @param text * Text to paint. * @param mnemonicIndex * Mnemonic index. * @param font * Font to use. * @param color * Color to use. * @param clip * Optional clip. Can be <code>null</code>. * @param isFromBottomToTop * If <code>true</code>, the text will be painted from bottom to * top, otherwise the text will be painted from top to bottom. */ public static void paintVerticalText(Graphics g, JComponent comp, Rectangle textRect, String text, int mnemonicIndex, java.awt.Font font, java.awt.Color color, java.awt.Rectangle clip, boolean isFromBottomToTop) { if ((text == null) || (text.length() == 0)) return; AffineTransform at = null; if (!isFromBottomToTop) { at = AffineTransform.getTranslateInstance(textRect.x + textRect.width, textRect.y); at.rotate(Math.PI / 2); } else { at = AffineTransform.getTranslateInstance(textRect.x, textRect.y + textRect.height); at.rotate(-Math.PI / 2); } Rectangle newRect = new Rectangle(0, 0, textRect.width, textRect.height); SubstanceTextUtilities.paintText(g, comp, newRect, text, mnemonicIndex, font, color, clip, at); } /** * Paints the text of the specified button. * * @param g * Graphic context. * @param button * Button * @param textRect * Text rectangle * @param text * Text to paint * @param mnemonicIndex * Mnemonic index. */ public static void paintText(Graphics g, AbstractButton button, Rectangle textRect, String text, int mnemonicIndex) { paintText(g, button, button.getModel(), textRect, text, mnemonicIndex); } /** * Paints the text of the specified button. * * @param g * Graphic context. * @param button * Button * @param model * Button model. * @param textRect * Text rectangle * @param text * Text to paint * @param mnemonicIndex * Mnemonic index. */ public static void paintText(Graphics g, AbstractButton button, ButtonModel model, Rectangle textRect, String text, int mnemonicIndex) { TransitionAwareUI transitionAwareUI = (TransitionAwareUI) button.getUI(); StateTransitionTracker stateTransitionTracker = transitionAwareUI.getTransitionTracker(); if (button instanceof JMenuItem) { // A slightly different path for menu items as we ignore the selection // state for visual consistency in menu content float menuItemAlpha = SubstanceColorSchemeUtilities.getAlpha(button, ComponentState.getState(button.getModel(), button, true)); paintMenuItemText(g, (JMenuItem) button, textRect, text, mnemonicIndex, stateTransitionTracker.getModelStateInfo(), menuItemAlpha); } else { float buttonAlpha = SubstanceColorSchemeUtilities.getAlpha(button, ComponentState.getState(button)); paintText(g, button, textRect, text, mnemonicIndex, stateTransitionTracker.getModelStateInfo(), buttonAlpha); } } /** * Paints the specified text. * * @param g * Graphics context. * @param component * Component. * @param textRect * Text rectangle. * @param text * Text to paint. * @param mnemonicIndex * Mnemonic index. * @param state * Component state. * @param prevState * Component previous state. * @param textAlpha * Alpha channel for painting the text. */ public static void paintText(Graphics g, JComponent component, Rectangle textRect, String text, int mnemonicIndex, ComponentState state, float textAlpha) { Color fgColor = getForegroundColor(component, text, state, textAlpha); SubstanceTextUtilities.paintText(g, component, textRect, text, mnemonicIndex, component.getFont(), fgColor, null); } public static void paintText(Graphics g, JComponent component, Rectangle textRect, String text, int mnemonicIndex, StateTransitionTracker.ModelStateInfo modelStateInfo, float textAlpha) { Color fgColor = getForegroundColor(component, text, modelStateInfo, textAlpha); SubstanceTextUtilities.paintText(g, component, textRect, text, mnemonicIndex, component.getFont(), fgColor, null); } public static void paintMenuItemText(Graphics g, JMenuItem menuItem, Rectangle textRect, String text, int mnemonicIndex, StateTransitionTracker.ModelStateInfo modelStateInfo, float textAlpha) { Color fgColor = getMenuComponentForegroundColor(menuItem, text, modelStateInfo, textAlpha); SubstanceTextUtilities.paintText(g, menuItem, textRect, text, mnemonicIndex, menuItem.getFont(), fgColor, null); } /** * Returns the foreground color for the specified component. * * @param component * Component. * @param text * Text. If empty or <code>null</code>, the result is * <code>null</code>. * @param state * Component state. * @param textAlpha * Alpha channel for painting the text. If value is less than * 1.0, the result is an opaque color which is an interpolation * between the "real" foreground color and the background color * of the component. This is done to ensure that native text * rasterization will be performed on 6u10+ on Windows. * @return The foreground color for the specified component. */ public static Color getForegroundColor(JComponent component, String text, ComponentState state, float textAlpha) { if ((text == null) || (text.length() == 0)) return null; boolean toEnforceFgColor = (SwingUtilities.getAncestorOfClass( CellRendererPane.class, component) != null) || Boolean.TRUE.equals(component .getClientProperty(ENFORCE_FG_COLOR)); Color fgColor = toEnforceFgColor ? component.getForeground() : SubstanceColorSchemeUtilities .getColorScheme(component, state).getForegroundColor(); // System.out.println(text + ":" + prevState.name() + "->" + // state.name() + ":" + fgColor); if (textAlpha < 1.0f) { Color bgFillColor = SubstanceColorUtilities.getBackgroundFillColor(component); fgColor = SubstanceColorUtilities.getInterpolatedColor(fgColor, bgFillColor, textAlpha); } return fgColor; } /** * Returns the foreground color for the specified component. * * @param component * Component. * @param text * Text. If empty or <code>null</code>, the result is * <code>null</code>. * @param textAlpha * Alpha channel for painting the text. If value is less than * 1.0, the result is an opaque color which is an interpolation * between the "real" foreground color and the background color * of the component. This is done to ensure that native text * rasterization will be performed on 6u10 on Windows. * @return The foreground color for the specified component. */ public static Color getForegroundColor(JComponent component, String text, StateTransitionTracker.ModelStateInfo modelStateInfo, float textAlpha) { if ((text == null) || (text.length() == 0)) return null; boolean toEnforceFgColor = (SwingUtilities.getAncestorOfClass( CellRendererPane.class, component) != null) || Boolean.TRUE.equals(component .getClientProperty(ENFORCE_FG_COLOR)); Color fgColor = null; if (toEnforceFgColor) { fgColor = component.getForeground(); } else { fgColor = SubstanceColorUtilities.getForegroundColor(component, modelStateInfo); } // System.out.println(text + ":" + prevState.name() + "->" + // state.name() + ":" + fgColor); if (textAlpha < 1.0f) { Color bgFillColor = SubstanceColorUtilities.getBackgroundFillColor(component); fgColor = SubstanceColorUtilities.getInterpolatedColor(fgColor, bgFillColor, textAlpha); } return fgColor; } /** * Returns the foreground color for the specified menu component. * * @param menuComponent * Menu component. * @param text * Text. If empty or <code>null</code>, the result is * <code>null</code>. * @param modelStateInfo * Model state info for the specified component. * @param textAlpha * Alpha channel for painting the text. If value is less than * 1.0, the result is an opaque color which is an interpolation * between the "real" foreground color and the background color * of the component. This is done to ensure that native text * rasterization will be performed on 6u10 on Windows. * @return The foreground color for the specified component. */ public static Color getMenuComponentForegroundColor(JMenuItem menuComponent, String text, StateTransitionTracker.ModelStateInfo modelStateInfo, float textAlpha) { if ((text == null) || (text.length() == 0)) return null; Color fgColor = SubstanceColorUtilities .getMenuComponentForegroundColor(menuComponent, modelStateInfo); if (textAlpha < 1.0f) { Color bgFillColor = SubstanceColorUtilities .getBackgroundFillColor(menuComponent); fgColor = SubstanceColorUtilities.getInterpolatedColor(fgColor, bgFillColor, textAlpha); } return fgColor; } /** * Paints background of the specified text component. * * @param g * Graphics context. * @param comp * Component. */ public static void paintTextCompBackground(Graphics g, JComponent comp) { Color backgroundFillColor = getTextBackgroundFillColor(comp); boolean toPaintWatermark = (SubstanceLookAndFeel.getCurrentSkin(comp) .getWatermark() != null) && (SubstanceCoreUtilities.toDrawWatermark(comp) || !comp .isOpaque()); paintTextCompBackground(g, comp, backgroundFillColor, toPaintWatermark); } public static Color getTextBackgroundFillColor(JComponent comp) { Color backgroundFillColor = SubstanceColorUtilities.getBackgroundFillColor(comp); JTextComponent componentForTransitions = SubstanceCoreUtilities .getTextComponentForTransitions(comp); if (componentForTransitions != null) { ComponentUI ui = componentForTransitions.getUI(); if (ui instanceof TransitionAwareUI) { TransitionAwareUI trackable = (TransitionAwareUI) ui; StateTransitionTracker stateTransitionTracker = trackable .getTransitionTracker(); Color lighterFill = SubstanceColorUtilities.getLighterColor( backgroundFillColor, 0.4f); lighterFill = SubstanceColorUtilities.getInterpolatedColor(lighterFill, backgroundFillColor, 0.6); float selectionStrength = stateTransitionTracker .getFacetStrength(ComponentStateFacet.SELECTION); float rolloverStrength = stateTransitionTracker .getFacetStrength(ComponentStateFacet.ROLLOVER); backgroundFillColor = SubstanceColorUtilities.getInterpolatedColor(lighterFill, backgroundFillColor, Math.max(selectionStrength, rolloverStrength) / 4.0f); } } return backgroundFillColor; } /** * Paints background of the specified text component. * * @param g * Graphics context. * @param comp * Component. * @param backgr * Background color. * @param toOverlayWatermark * If <code>true</code>, this method will paint the watermark * overlay on top of the background fill. */ private static void paintTextCompBackground(Graphics g, JComponent comp, Color backgr, boolean toOverlayWatermark) { Graphics2D g2d = (Graphics2D) g.create(); BackgroundPaintingUtils.update(g, comp, false); SubstanceWatermark watermark = SubstanceCoreUtilities.getSkin(comp).getWatermark(); if (watermark != null) { watermark.drawWatermarkImage(g2d, comp, 0, 0, comp.getWidth(), comp.getHeight()); } g2d.setColor(backgr); g2d.fillRect(0, 0, comp.getWidth(), comp.getHeight()); if (toOverlayWatermark) { if (watermark != null) { g2d.clipRect(0, 0, comp.getWidth(), comp.getHeight()); watermark.drawWatermarkImage(g2d, comp, 0, 0, comp.getWidth(), comp.getHeight()); } } ComponentState state = comp.isEnabled() ? ComponentState.ENABLED : ComponentState.DISABLED_UNSELECTED; Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = null; JTextComponent componentForTransitions = SubstanceCoreUtilities .getTextComponentForTransitions(comp); if (componentForTransitions != null) { ComponentUI ui = componentForTransitions.getUI(); if (ui instanceof TransitionAwareUI) { TransitionAwareUI trackable = (TransitionAwareUI) ui; StateTransitionTracker stateTransitionTracker = trackable .getTransitionTracker(); StateTransitionTracker.ModelStateInfo modelStateInfo = stateTransitionTracker .getModelStateInfo(); state = modelStateInfo.getCurrModelState(); activeStates = modelStateInfo.getStateContributionMap(); } } if ((componentForTransitions != null) && !componentForTransitions.isEditable()) { // don't paint top shadow on non-editable text fields return; } SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(comp); // Get the base border color SubstanceColorScheme baseBorderScheme = SubstanceColorSchemeUtilities.getColorScheme(comp, ColorSchemeAssociationKind.BORDER, state); Color borderColor = borderPainter.getRepresentativeColor(baseBorderScheme); if (!state.isDisabled() && (activeStates != null) && (activeStates.size() > 1)) { // If we have more than one active state, compute the composite color from all // the contributions for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : activeStates.entrySet()) { ComponentState activeState = activeEntry.getKey(); if (activeState == state) { continue; } float contribution = activeEntry.getValue().getContribution(); if (contribution == 0.0f) { continue; } float alpha = SubstanceColorSchemeUtilities.getAlpha(componentForTransitions, activeState); if (alpha == 0.0f) { continue; } SubstanceColorScheme activeBorderScheme = SubstanceColorSchemeUtilities .getColorScheme(componentForTransitions, ColorSchemeAssociationKind.BORDER, activeState); Color activeBorderColor = borderPainter.getRepresentativeColor(activeBorderScheme); borderColor = SubstanceColorUtilities.getInterpolatedColor(borderColor, activeBorderColor, 1.0f - contribution * alpha); } } // At this point we should have the color that matches the border color. Use that to // paint emulated drop shadow along the top edge of the component. int shadowHeight = 6; g2d.setPaint(new GradientPaint( 0, 0, SubstanceColorUtilities.getAlphaColor(borderColor, 48), 0, shadowHeight, SubstanceColorUtilities.getAlphaColor(borderColor, 0))); float yTop = (comp.getBorder() instanceof SubstanceTextComponentBorder) ? SubstanceSizeUtils.getBorderStrokeWidth() : 0; g2d.fill(new Rectangle2D.Float(0, yTop, comp.getWidth(), shadowHeight)); g2d.dispose(); } }