/* * 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.AlphaComposite; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.image.BufferedImage; import java.util.Map; import java.util.Set; import javax.swing.AbstractButton; import javax.swing.ButtonModel; import javax.swing.JButton; import org.pushingpixels.lafwidget.LafWidgetUtilities; import org.pushingpixels.lafwidget.contrib.intellij.UIUtil; import org.pushingpixels.substance.api.ColorSchemeAssociationKind; import org.pushingpixels.substance.api.ComponentState; import org.pushingpixels.substance.api.SubstanceColorScheme; import org.pushingpixels.substance.api.SubstanceConstants; import org.pushingpixels.substance.api.SubstanceConstants.Side; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.painter.border.SubstanceBorderPainter; import org.pushingpixels.substance.api.painter.fill.SubstanceFillPainter; import org.pushingpixels.substance.api.shaper.RectangularButtonShaper; import org.pushingpixels.substance.api.shaper.StandardButtonShaper; import org.pushingpixels.substance.api.shaper.SubstanceButtonShaper; import org.pushingpixels.substance.internal.animation.ModificationAwareUI; import org.pushingpixels.substance.internal.animation.RootPaneDefaultButtonTracker; import org.pushingpixels.substance.internal.animation.StateTransitionTracker; import org.pushingpixels.substance.internal.animation.TransitionAwareUI; import org.pushingpixels.trident.Timeline; import org.pushingpixels.trident.Timeline.TimelineState; /** * Delegate class for painting backgrounds of buttons in <b>Substance </b> look * and feel. This class is <b>for internal use only</b>. * * @author Kirill Grouchnikov */ public class ButtonBackgroundDelegate { /** * Cache for background images. Each time * {@link #getBackground(AbstractButton, SubstanceButtonShaper, SubstanceFillPainter, int, int)} * is called, it checks <code>this</code> map to see if it already contains * such background. If so, the background from the map is returned. */ private static LazyResettableHashMap<BufferedImage> regularBackgrounds = new LazyResettableHashMap<BufferedImage>( "ButtonBackgroundDelegate"); /** * Retrieves the background for the specified button. * * @param button * Button. * @param model * Button model. * @param shaper * Button shaper. * @param fillPainter * Button fill painter. * @param borderPainter * Button border painter. * @param width * Button width. * @param height * Button height. * @return Button background. */ public static BufferedImage getFullAlphaBackground(AbstractButton button, ButtonModel model, SubstanceButtonShaper shaper, SubstanceFillPainter fillPainter, SubstanceBorderPainter borderPainter, int width, int height) { TransitionAwareUI transitionAwareUI = (TransitionAwareUI) button.getUI(); StateTransitionTracker.ModelStateInfo modelStateInfo = transitionAwareUI .getTransitionTracker().getModelStateInfo(); ComponentState currState = modelStateInfo.getCurrModelState(); // ComponentState prevState = stateTransitionModel.getPrevModelState(); // System.out.println(button.getText() + ": " + prevState.name() + // " -> " // + state.name() + " at " // + stateTransitionModel.getTransitionPosition()); // compute cycle count (for animation) float cyclePos = 0.0f;// currState.getCyclePosition(); // boolean isPulsating = false; if (button instanceof JButton) { JButton jb = (JButton) button; if (RootPaneDefaultButtonTracker.isPulsating(jb) && (currState != ComponentState.PRESSED_SELECTED) && (currState != ComponentState.PRESSED_UNSELECTED)) { // isPulsating = true; cyclePos = RootPaneDefaultButtonTracker.getTimelinePosition(jb); } } // compute the straight sides Set<SubstanceConstants.Side> straightSides = SubstanceCoreUtilities.getSides(button, SubstanceLookAndFeel.BUTTON_SIDE_PROPERTY); boolean isRoundButton = StandardButtonShaper.isRoundButton(button); float radius = 0.0f; if (shaper instanceof RectangularButtonShaper) { radius = ((RectangularButtonShaper) shaper).getCornerRadius(button, 0.0f); } Set<Side> openSides = SubstanceCoreUtilities.getSides(button, SubstanceLookAndFeel.BUTTON_OPEN_SIDE_PROPERTY); // String openKey = ""; // for (Side oSide : openSides) { // openKey += oSide.name() + "-"; // } // String extraModelKey = ""; // for (String modelKey : extraModelKeys) { // extraModelKey += (modelKey + "-"); // } boolean isContentAreaFilled = button.isContentAreaFilled(); boolean isBorderPainted = button.isBorderPainted(); int factor = UIUtil.getScaleFactor(); // compute color scheme SubstanceColorScheme baseBorderScheme = SubstanceColorSchemeUtilities.getColorScheme(button, ColorSchemeAssociationKind.BORDER, currState); // see if need to use attention-drawing animation // boolean isWindowModified = false; if (button.getUI() instanceof ModificationAwareUI) { ModificationAwareUI modificationAwareUI = (ModificationAwareUI) button.getUI(); Timeline modificationTimeline = modificationAwareUI.getModificationTimeline(); if (modificationTimeline != null) { if (modificationTimeline.getState() != TimelineState.IDLE) { // isWindowModified = true; SubstanceColorScheme colorScheme2 = SubstanceColorSchemeUtilities.YELLOW; SubstanceColorScheme colorScheme = SubstanceColorSchemeUtilities.ORANGE; cyclePos = modificationTimeline.getTimelinePosition(); HashMapKey key1 = SubstanceCoreUtilities.getHashKey(width, height, colorScheme.getDisplayName(), baseBorderScheme.getDisplayName(), shaper.getDisplayName(), fillPainter.getDisplayName(), borderPainter.getDisplayName(), straightSides, openSides, button.getClass().getName(), isRoundButton, radius, isContentAreaFilled, isBorderPainted, SubstanceSizeUtils.getComponentFontSize(button)); BufferedImage layer1 = regularBackgrounds.get(key1); if (layer1 == null) { layer1 = createBackgroundImage(button, shaper, fillPainter, borderPainter, width, height, colorScheme, baseBorderScheme, openSides, isContentAreaFilled, isBorderPainted); regularBackgrounds.put(key1, layer1); } HashMapKey key2 = SubstanceCoreUtilities.getHashKey(width, height, colorScheme2.getDisplayName(), baseBorderScheme.getDisplayName(), shaper.getDisplayName(), fillPainter.getDisplayName(), borderPainter.getDisplayName(), straightSides, openSides, button.getClass().getName(), isRoundButton, radius, isContentAreaFilled, isBorderPainted, SubstanceSizeUtils.getComponentFontSize(button)); BufferedImage layer2 = regularBackgrounds.get(key2); if (layer2 == null) { layer2 = createBackgroundImage(button, shaper, fillPainter, borderPainter, width, height, colorScheme2, baseBorderScheme, openSides, isContentAreaFilled, isBorderPainted); regularBackgrounds.put(key2, layer2); } BufferedImage result = SubstanceCoreUtilities.getBlankImage(width, height); Graphics2D g2d = result.createGraphics(); if (cyclePos < 1.0f) g2d.drawImage(layer1, 0, 0, layer1.getWidth() / factor, layer1.getHeight() / factor, null); if (cyclePos > 0.0f) { g2d.setComposite(AlphaComposite.SrcOver.derive(cyclePos)); g2d.drawImage(layer2, 0, 0, layer2.getWidth() / factor, layer2.getHeight() / factor, null); } g2d.dispose(); return result; } } } // see if need to use transition animation. Important - don't do it // on pulsating buttons (such as default or close buttons // of modified frames). Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = modelStateInfo .getStateContributionMap(); SubstanceColorScheme baseFillScheme = SubstanceColorSchemeUtilities.getColorScheme(button, currState); HashMapKey keyBase = SubstanceCoreUtilities.getHashKey(width, height, baseFillScheme.getDisplayName(), baseBorderScheme.getDisplayName(), shaper.getDisplayName(), fillPainter.getDisplayName(), borderPainter.getDisplayName(), straightSides, openSides, button.getClass().getName(), isRoundButton, (int) (1000 * radius), isContentAreaFilled, isBorderPainted, SubstanceSizeUtils.getComponentFontSize(button)); BufferedImage layerBase = regularBackgrounds.get(keyBase); if (layerBase == null) { layerBase = createBackgroundImage(button, shaper, fillPainter, borderPainter, width, height, baseFillScheme, baseBorderScheme, openSides, isContentAreaFilled, isBorderPainted); regularBackgrounds.put(keyBase, layerBase); } if (currState.isDisabled() || (activeStates.size() == 1)) { return layerBase; } BufferedImage result = SubstanceCoreUtilities.getBlankImage(width, height); Graphics2D g2d = result.createGraphics(); // draw the base layer g2d.drawImage(layerBase, 0, 0, layerBase.getWidth() / factor, layerBase.getHeight() / factor, null); // System.out.println("\nPainting base state " + currState); // draw the other active layers for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : activeStates .entrySet()) { ComponentState activeState = activeEntry.getKey(); // System.out.println("Painting state " + activeState + "[curr is " // + currState + "] with " + activeEntry.getValue()); if (activeState == currState) continue; float stateContribution = activeEntry.getValue().getContribution(); if (stateContribution > 0.0f) { g2d.setComposite(AlphaComposite.SrcOver.derive(stateContribution)); SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities .getColorScheme(button, activeState); SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(button, ColorSchemeAssociationKind.BORDER, activeState); HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height, fillScheme.getDisplayName(), borderScheme.getDisplayName(), shaper.getDisplayName(), fillPainter.getDisplayName(), borderPainter.getDisplayName(), straightSides, openSides, button.getClass().getName(), isRoundButton, (int) (1000 * radius), isContentAreaFilled, isBorderPainted, SubstanceSizeUtils.getComponentFontSize(button)); BufferedImage layer = regularBackgrounds.get(key); if (layer == null) { layer = createBackgroundImage(button, shaper, fillPainter, borderPainter, width, height, fillScheme, borderScheme, openSides, isContentAreaFilled, isBorderPainted); regularBackgrounds.put(key, layer); } g2d.drawImage(layer, 0, 0, layer.getWidth() / factor, layer.getHeight() / factor, null); } } g2d.dispose(); return result; } private static BufferedImage createBackgroundImage(AbstractButton button, SubstanceButtonShaper shaper, SubstanceFillPainter fillPainter, SubstanceBorderPainter borderPainter, int width, int height, SubstanceColorScheme colorScheme, SubstanceColorScheme borderScheme, Set<Side> openSides, boolean isContentAreaFilled, boolean isBorderPainted) { int openDelta = (int) (Math.ceil(3.0 * SubstanceSizeUtils.getBorderStrokeWidth())); openDelta *= UIUtil.getScaleFactor(); int deltaLeft = ((openSides != null) && openSides.contains(Side.LEFT)) ? openDelta : 0; int deltaRight = ((openSides != null) && openSides.contains(Side.RIGHT)) ? openDelta : 0; int deltaTop = ((openSides != null) && openSides.contains(Side.TOP)) ? openDelta : 0; int deltaBottom = ((openSides != null) && openSides.contains(Side.BOTTOM)) ? openDelta : 0; // System.err.println(key); float borderDelta = SubstanceSizeUtils.getBorderStrokeWidth() / 2.0f; Shape contour = shaper.getButtonOutline(button, borderDelta, width + deltaLeft + deltaRight, height + deltaTop + deltaBottom, false); BufferedImage newBackground = SubstanceCoreUtilities.getBlankImage(width, height); Graphics2D finalGraphics = (Graphics2D) newBackground.getGraphics(); finalGraphics.translate(-deltaLeft, -deltaTop); if (isContentAreaFilled) { fillPainter.paintContourBackground(finalGraphics, button, width + deltaLeft + deltaRight, height + deltaTop + deltaBottom, contour, false, colorScheme, true); } if (isBorderPainted) { float borderThickness = SubstanceSizeUtils.getBorderStrokeWidth(); Shape contourInner = borderPainter.isPaintingInnerContour() ? shaper.getButtonOutline(button, borderDelta + borderThickness, width + deltaLeft + deltaRight, height + deltaTop + deltaBottom, true) : null; borderPainter.paintBorder(finalGraphics, button, width + deltaLeft + deltaRight, height + deltaTop + deltaBottom, contour, contourInner, borderScheme); } return newBackground; } /** * Simple constructor. */ public ButtonBackgroundDelegate() { super(); } /** * Updates background of the specified button. * * @param g * Graphic context. * @param button * Button to update. */ public void updateBackground(Graphics g, AbstractButton button) { // failsafe for LAF change if (!SubstanceLookAndFeel.isCurrentLookAndFeel()) return; if (SubstanceCoreUtilities.isButtonNeverPainted(button)) return; int width = button.getWidth(); int height = button.getHeight(); int y = 0; if (SubstanceCoreUtilities.isScrollButton(button) || SubstanceCoreUtilities.isSpinnerButton(button)) { PairwiseButtonBackgroundDelegate.updatePairwiseBackground(g, button, width, height, false); return; } SubstanceFillPainter fillPainter = SubstanceCoreUtilities.getFillPainter(button); SubstanceButtonShaper shaper = SubstanceCoreUtilities.getButtonShaper(button); SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(button); BufferedImage bgImage = getFullAlphaBackground(button, button.getModel(), shaper, fillPainter, borderPainter, width, height); TransitionAwareUI transitionAwareUI = (TransitionAwareUI) button.getUI(); StateTransitionTracker stateTransitionTracker = transitionAwareUI.getTransitionTracker(); StateTransitionTracker.ModelStateInfo modelStateInfo = stateTransitionTracker .getModelStateInfo(); Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = modelStateInfo .getStateContributionMap(); // Two special cases here: // 1. Button has flat appearance. // 2. Button is disabled. // For both cases, we need to set custom translucency. boolean isFlat = SubstanceCoreUtilities.hasFlatAppearance(button); boolean isSpecial = isFlat || !button.isEnabled(); float extraAlpha = 1.0f; if (isSpecial) { if (isFlat) { // Special handling of flat buttons extraAlpha = 0.0f; for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : activeStates .entrySet()) { ComponentState activeState = activeEntry.getKey(); if (activeState.isDisabled()) continue; if (activeState == ComponentState.ENABLED) continue; extraAlpha += activeEntry.getValue().getContribution(); } } else { if (!button.isEnabled()) { extraAlpha = SubstanceColorSchemeUtilities.getAlpha(button, modelStateInfo.getCurrModelState()); } } } if (extraAlpha > 0.0f) { Graphics2D graphics = (Graphics2D) g.create(); graphics.setComposite(LafWidgetUtilities.getAlphaComposite(button, extraAlpha, g)); int factor = UIUtil.getScaleFactor(); graphics.drawImage(bgImage, 0, y, bgImage.getWidth() / factor, bgImage.getHeight() / factor, null); graphics.dispose(); } } /** * Checks whether the specified button has round corners. * * @param button * Button to check. * @return <code>true</code> if the specified button has round corners, * <code>false</code> otherwise. */ public static boolean isRoundButton(AbstractButton button) { return (!SubstanceCoreUtilities.isComboBoxButton(button)) && (!SubstanceCoreUtilities.isScrollButton(button)) && SubstanceCoreUtilities.hasText(button); } /** * Returns <code>true</code> if the specified <i>x,y </i> location is * contained within the look and feel's defined shape of the specified * component. <code>x</code> and <code>y</code> are defined to be relative * to the coordinate system of the specified component. * * @param button * the component where the <i>x,y </i> location is being queried; * @param x * the <i>x </i> coordinate of the point * @param y * the <i>y </i> coordinate of the point * @return <code>true</code> if the specified <i>x,y </i> location is * contained within the look and feel's defined shape of the * specified component, <code>false</code> otherwise. */ public static boolean contains(AbstractButton button, int x, int y) { // failsafe for LAF change if (!SubstanceLookAndFeel.isCurrentLookAndFeel()) { return false; } SubstanceButtonShaper shaper = SubstanceCoreUtilities.getButtonShaper(button); if (shaper == null) return false; Shape contour = shaper.getButtonOutline(button, 0.0f, button.getWidth(), button.getHeight(), false); return contour.contains(x, y); } /** * Returns the memory usage string. * * @return Memory usage string. */ static String getMemoryUsage() { StringBuffer sb = new StringBuffer(); sb.append("SubstanceBackgroundDelegate: \n"); sb.append("\t" + regularBackgrounds.size() + " regular"); // + pairwiseBackgrounds.size() + " pairwise"); return sb.toString(); } }