/* * 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.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashSet; import java.util.Set; import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.JViewport; import javax.swing.ScrollPaneLayout; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.TableHeaderUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicScrollPaneUI; import javax.swing.table.JTableHeader; import org.pushingpixels.lafwidget.LafWidget; import org.pushingpixels.lafwidget.LafWidgetRepository; import org.pushingpixels.lafwidget.animation.AnimationConfigurationManager; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils; import org.pushingpixels.substance.internal.utils.SubstanceColorUtilities; import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils; import org.pushingpixels.substance.internal.utils.scroll.SubstanceScrollPaneBorder; import org.pushingpixels.trident.Timeline; import org.pushingpixels.trident.callback.UIThreadTimelineCallbackAdapter; import org.pushingpixels.trident.ease.TimelineEase; /** * UI for scroll panes in <b>Substance</b> look and feel. * * @author Kirill Grouchnikov */ public class SubstanceScrollPaneUI extends BasicScrollPaneUI { /** * Property change listener on * {@link SubstanceLookAndFeel#SCROLL_PANE_BUTTONS_POLICY}, * {@link SubstanceLookAndFeel#WATERMARK_TO_BLEED} and * <code>layoutManager</code> properties. */ protected PropertyChangeListener substancePropertyChangeListener; /** * Listener on the vertical scroll bar. Installed for the smart tree scroll * (see {@link SubstanceLookAndFeel#TREE_SMART_SCROLL_ANIMATION_KIND}. */ protected ChangeListener substanceVerticalScrollbarChangeListener; /** * Timeline of the current horizontal scroll under smart tree scroll mode. */ protected Timeline horizontalScrollTimeline; private Set<LafWidget> lafWidgets; /** * Creates new UI delegate. * * @param comp * Component. * @return UI delegate for the component. */ public static ComponentUI createUI(JComponent comp) { SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp); return new SubstanceScrollPaneUI(); } @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.BasicScrollPaneUI#installDefaults(javax.swing. * JScrollPane) */ @Override protected void installDefaults(final JScrollPane scrollpane) { super.installDefaults(scrollpane); if (SubstanceCoreUtilities.toDrawWatermark(scrollpane) && (SubstanceLookAndFeel.getCurrentSkin(scrollpane) .getWatermark() != null)) { scrollpane.setOpaque(false); scrollpane.getViewport().setOpaque(false); } SwingUtilities.invokeLater(() -> installTableHeaderCornerFiller(scrollpane)); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installDefaults(); } } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicScrollPaneUI#uninstallDefaults(javax.swing * .JScrollPane) */ @Override protected void uninstallDefaults(JScrollPane c) { Component upperRight = c.getCorner(JScrollPane.UPPER_RIGHT_CORNER); if (upperRight instanceof UIResource) { c.setCorner(JScrollPane.UPPER_RIGHT_CORNER, null); } Component upperLeft = c.getCorner(JScrollPane.UPPER_LEFT_CORNER); if (upperLeft instanceof UIResource) { c.setCorner(JScrollPane.UPPER_LEFT_CORNER, null); } for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallDefaults(); } super.uninstallDefaults(c); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicScrollPaneUI#installListeners(javax.swing * .JScrollPane) */ @Override protected void installListeners(final JScrollPane c) { super.installListeners(c); this.substancePropertyChangeListener = (PropertyChangeEvent evt) -> { if (SubstanceLookAndFeel.WATERMARK_VISIBLE.equals(evt .getPropertyName())) { boolean toBleed = SubstanceCoreUtilities.toDrawWatermark(c); c.setOpaque(!toBleed); c.getViewport().setOpaque(!toBleed); Component view = c.getViewport().getView(); if (view instanceof JComponent) ((JComponent) view).setOpaque(!toBleed); } if ("background".equals(evt.getPropertyName())) { // propagate application-specific background color to the // scroll bars. Color newBackgr = (Color) evt.getNewValue(); if (!(newBackgr instanceof UIResource)) { JScrollBar vertical = scrollpane.getVerticalScrollBar(); if (vertical != null) { if (vertical.getBackground() instanceof UIResource) { vertical.setBackground(newBackgr); } } JScrollBar horizontal = scrollpane .getHorizontalScrollBar(); if (horizontal != null) { if (horizontal.getBackground() instanceof UIResource) { horizontal.setBackground(newBackgr); } } } } if ("columnHeader".equals(evt.getPropertyName()) || "componentOrientation".equals(evt.getPropertyName()) || "ancestor".equals(evt.getPropertyName())) { SwingUtilities.invokeLater(new Runnable() { public void run() { // need to switch the corner filler based on the // current scroll pane state. if (scrollpane != null) { installTableHeaderCornerFiller(scrollpane); } } }); } }; c.addPropertyChangeListener(this.substancePropertyChangeListener); this.substanceVerticalScrollbarChangeListener = new ChangeListener() { public void stateChanged(ChangeEvent e) { // check if it's a horizontally scrollable tree inside if ((c.getHorizontalScrollBar() != null) && c.getHorizontalScrollBar().isVisible() && (c.getViewport().getView() instanceof JTree)) { JTree tree = (JTree) c.getViewport().getView(); // check if the smart scroll is enabled if (AnimationConfigurationManager.getInstance().isAnimationAllowed( SubstanceLookAndFeel.TREE_SMART_SCROLL_ANIMATION_KIND, tree)) { SubstanceTreeUI treeUI = (SubstanceTreeUI) tree.getUI(); final Rectangle viewportRect = c.getViewport() .getViewRect(); int pivotX = treeUI.getPivotRendererX(viewportRect); int currPivotX = viewportRect.x;// + viewportRect.width // / 2; int delta = pivotX - currPivotX; int finalX = viewportRect.x + delta; if (finalX < 0) { delta -= finalX; } final int finalDelta = delta; if (Math.abs(finalDelta) > viewportRect.width / 6) { if (horizontalScrollTimeline != null) { // abort previous horizontal scroll horizontalScrollTimeline.abort(); } horizontalScrollTimeline = new Timeline(tree); horizontalScrollTimeline .addCallback(new UIThreadTimelineCallbackAdapter() { @Override public void onTimelinePulse( float durationFraction, float timelinePosition) { if (timelinePosition >= 0.5) { int nudge = (int) (finalDelta * (timelinePosition - 0.5)); c.getViewport().setViewPosition( new Point(viewportRect.x + nudge, viewportRect.y)); } } }); horizontalScrollTimeline .setEase(new TimelineEase() { @Override public float map(float durationFraction) { if (durationFraction < 0.5) return 0.5f * durationFraction; return 0.25f + (durationFraction - 0.5f) * 0.75f / 0.5f; } }); AnimationConfigurationManager .getInstance() .configureTimeline(horizontalScrollTimeline); horizontalScrollTimeline .setDuration(2 * horizontalScrollTimeline .getDuration()); horizontalScrollTimeline.play(); } } } } }; c.getVerticalScrollBar().getModel().addChangeListener( this.substanceVerticalScrollbarChangeListener); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installListeners(); } } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicScrollPaneUI#uninstallListeners(javax.swing * .JComponent) */ @Override protected void uninstallListeners(JComponent c) { c.removePropertyChangeListener(this.substancePropertyChangeListener); this.substancePropertyChangeListener = null; JScrollPane jsp = (JScrollPane) c; jsp.getVerticalScrollBar().getModel().removeChangeListener( this.substanceVerticalScrollbarChangeListener); this.substanceVerticalScrollbarChangeListener = null; for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallListeners(); } super.uninstallListeners(c); } /** * Layout manager to adjust the bounds of scrollbars and the viewport when * the default ({@link SubstanceScrollPaneBorder}) border is set on the * relevant {@link JScrollPane}. * * @author Kirill Grouchnikov */ protected static class AdjustedLayout extends ScrollPaneLayout implements UIResource { /** * The delegate layout. */ protected ScrollPaneLayout delegate; /** * Creates a new layout for adjusting the bounds of scrollbars and the * viewport. * * @param delegate * The original (delegate) layout. */ public AdjustedLayout(ScrollPaneLayout delegate) { this.delegate = delegate; } @Override public void addLayoutComponent(String s, Component c) { delegate.addLayoutComponent(s, c); } @Override public boolean equals(Object obj) { return delegate.equals(obj); } @Override public JViewport getColumnHeader() { return delegate.getColumnHeader(); } @Override public Component getCorner(String key) { return delegate.getCorner(key); } @Override public JScrollBar getHorizontalScrollBar() { return delegate.getHorizontalScrollBar(); } @Override public int getHorizontalScrollBarPolicy() { return delegate.getHorizontalScrollBarPolicy(); } @Override public JViewport getRowHeader() { return delegate.getRowHeader(); } @Override public JScrollBar getVerticalScrollBar() { return delegate.getVerticalScrollBar(); } @Override public int getVerticalScrollBarPolicy() { return delegate.getVerticalScrollBarPolicy(); } @Override public JViewport getViewport() { return delegate.getViewport(); } @Override @SuppressWarnings("deprecation") public Rectangle getViewportBorderBounds(JScrollPane scrollpane) { return delegate.getViewportBorderBounds(scrollpane); } @Override public int hashCode() { return delegate.hashCode(); } @Override public Dimension minimumLayoutSize(Container parent) { return delegate.minimumLayoutSize(parent); } @Override public Dimension preferredLayoutSize(Container parent) { return delegate.preferredLayoutSize(parent); } @Override public void removeLayoutComponent(Component c) { delegate.removeLayoutComponent(c); } @Override public void setHorizontalScrollBarPolicy(int x) { delegate.setHorizontalScrollBarPolicy(x); } @Override public void setVerticalScrollBarPolicy(int x) { delegate.setVerticalScrollBarPolicy(x); } @Override public void syncWithScrollPane(JScrollPane sp) { delegate.syncWithScrollPane(sp); } @Override public String toString() { return delegate.toString(); } // ScrollPaneLayout.UIResource { /* * (non-Javadoc) * * @see javax.swing.ScrollPaneLayout#layoutContainer(java.awt.Container) */ @Override public void layoutContainer(Container parent) { delegate.layoutContainer(parent); JScrollPane scrollPane = (JScrollPane) parent; Border border = scrollPane.getBorder(); boolean toAdjust = (border instanceof SubstanceScrollPaneBorder); if (toAdjust) { JScrollBar vertical = scrollPane.getVerticalScrollBar(); JScrollBar horizontal = scrollPane.getHorizontalScrollBar(); int borderDelta = (int) Math.floor(SubstanceSizeUtils.getBorderStrokeWidth() / 2.0); int borderWidth = (int) SubstanceSizeUtils.getBorderStrokeWidth(); int dx = 0, dy = 0, dw = 0, dh = 0; if (scrollPane.getComponentOrientation().isLeftToRight()) { if ((vertical != null) && vertical.isVisible()) { Rectangle vBounds = vertical.getBounds(); dw += (1 + borderDelta); vertical.setBounds(vBounds.x + 1 + borderDelta, vBounds.y + 1 - 2 * borderWidth, vBounds.width, vBounds.height + 2 * borderWidth); } if ((horizontal != null) && horizontal.isVisible()) { dh += (1 + borderDelta); Rectangle hBounds = horizontal.getBounds(); horizontal.setBounds(hBounds.x + ((scrollPane.getRowHeader() == null) ? 1 : 2) - 2 * borderWidth, hBounds.y + 1, hBounds.width + 2 * borderWidth, hBounds.height); } if (delegate.getCorner(ScrollPaneLayout.LOWER_RIGHT_CORNER) != null) { Rectangle lrBounds = delegate.getCorner( ScrollPaneLayout.LOWER_RIGHT_CORNER) .getBounds(); delegate.getCorner(ScrollPaneLayout.LOWER_RIGHT_CORNER) .setBounds(lrBounds.x + 1 + borderDelta, lrBounds.y + 1 + borderDelta, lrBounds.width, lrBounds.height); } if (delegate.getCorner(ScrollPaneLayout.UPPER_RIGHT_CORNER) != null) { Rectangle urBounds = delegate.getCorner( ScrollPaneLayout.UPPER_RIGHT_CORNER) .getBounds(); delegate.getCorner(ScrollPaneLayout.UPPER_RIGHT_CORNER) .setBounds(urBounds.x + 1 + borderDelta, urBounds.y + borderDelta, urBounds.width - 1, urBounds.height); } } else { if ((vertical != null) && vertical.isVisible()) { dx -= (1 + borderDelta); dw += (1 + borderDelta); Rectangle vBounds = vertical.getBounds(); vertical.setBounds(vBounds.x - 1 - borderDelta, vBounds.y - 1 - borderDelta, vBounds.width, vBounds.height + 2 * borderWidth); } if ((horizontal != null) && horizontal.isVisible()) { dh += (1 + borderDelta); Rectangle hBounds = horizontal.getBounds(); horizontal .setBounds( hBounds.x - ((scrollPane.getRowHeader() == null) ? 1 : 2) - borderDelta, hBounds.y + 1 + borderDelta, hBounds.width + 2 * borderWidth, hBounds.height); } if (delegate.getCorner(ScrollPaneLayout.LOWER_LEFT_CORNER) != null) { Rectangle llBounds = delegate.getCorner( ScrollPaneLayout.LOWER_LEFT_CORNER).getBounds(); delegate.getCorner(ScrollPaneLayout.LOWER_LEFT_CORNER) .setBounds(llBounds.x - 1 - borderDelta, llBounds.y - 1 - borderDelta, llBounds.width, llBounds.height); } if (delegate.getCorner(ScrollPaneLayout.UPPER_LEFT_CORNER) != null) { Rectangle ulBounds = delegate.getCorner( ScrollPaneLayout.UPPER_LEFT_CORNER).getBounds(); delegate.getCorner(ScrollPaneLayout.UPPER_LEFT_CORNER) .setBounds(ulBounds.x - borderDelta, ulBounds.y - borderDelta, ulBounds.width - 1, ulBounds.height); } } if (delegate.getViewport() != null) { Rectangle vpBounds = delegate.getViewport().getBounds(); delegate.getViewport().setBounds( new Rectangle(vpBounds.x + dx, vpBounds.y + dy, vpBounds.width + dw, vpBounds.height + dh)); } if (delegate.getColumnHeader() != null) { Rectangle columnHeaderBounds = delegate.getColumnHeader() .getBounds(); delegate.getColumnHeader().setBounds( new Rectangle(columnHeaderBounds.x + dx, columnHeaderBounds.y + dy, columnHeaderBounds.width + dw, columnHeaderBounds.height)); } } } } /* * (non-Javadoc) * * @see javax.swing.plaf.ComponentUI#update(java.awt.Graphics, * javax.swing.JComponent) */ @Override public void update(Graphics g, JComponent c) { BackgroundPaintingUtils.updateIfOpaque(g, c); JScrollPane jsp = (JScrollPane) c; LayoutManager lm = jsp.getLayout(); ScrollPaneLayout scrollLm = null; if (lm instanceof ScrollPaneLayout) { scrollLm = (ScrollPaneLayout) lm; } if (scrollLm != null) { Set<Component> corners = new HashSet<Component>(); if (scrollLm.getCorner(ScrollPaneLayout.LOWER_LEFT_CORNER) != null) { corners.add(scrollLm .getCorner(ScrollPaneLayout.LOWER_LEFT_CORNER)); } if (scrollLm.getCorner(ScrollPaneLayout.LOWER_RIGHT_CORNER) != null) { corners.add(scrollLm .getCorner(ScrollPaneLayout.LOWER_RIGHT_CORNER)); } if (scrollLm.getCorner(ScrollPaneLayout.UPPER_LEFT_CORNER) != null) { corners.add(scrollLm .getCorner(ScrollPaneLayout.UPPER_LEFT_CORNER)); } if (scrollLm.getCorner(ScrollPaneLayout.UPPER_RIGHT_CORNER) != null) { corners.add(scrollLm .getCorner(ScrollPaneLayout.UPPER_RIGHT_CORNER)); } if (SubstanceCoreUtilities.isOpaque(c)) { Graphics2D g2d = (Graphics2D) g.create(); g2d.setColor(SubstanceColorUtilities.getBackgroundFillColorScrollBar( this.scrollpane.getVerticalScrollBar())); for (Component corner : corners) { g2d.fill(corner.getBounds()); // BackgroundPaintingUtils.fillAndWatermark(g, c, c // .getBackground(), corner.getBounds()); } JScrollBar horizontal = this.scrollpane.getHorizontalScrollBar(); JScrollBar vertical = this.scrollpane.getVerticalScrollBar(); if ((horizontal != null) && (vertical != null)) { if (this.scrollpane.getComponentOrientation().isLeftToRight()) { // Bottom right corner if (scrollLm.getCorner(ScrollPaneLayout.LOWER_RIGHT_CORNER) == null) { g2d.fillRect(horizontal.getX() + horizontal.getWidth(), horizontal.getY(), vertical.getWidth(), horizontal.getHeight()); } } else { // Bottom left corner if (scrollLm.getCorner(ScrollPaneLayout.LOWER_LEFT_CORNER) == null) { g2d.fillRect(0, horizontal.getY(), vertical.getWidth(), horizontal.getHeight()); } } } } } super.paint(g, c); } /** * Installs a corner filler that matches the table header. This is done to * provide a continuous appearance for tables with table headers placed in * scroll panes. * * @param scrollpane * Scroll pane. */ protected static void installTableHeaderCornerFiller(JScrollPane scrollpane) { // install custom scroll pane corner filler // for continuous painting of table headers JViewport columnHeader = scrollpane.getColumnHeader(); // System.out.println("Column header " + columnHeader); if (columnHeader == null) return; Component columnHeaderComp = columnHeader.getView(); // System.out.println("Column header comp " + columnHeaderComp); if (!(columnHeaderComp instanceof JTableHeader)) return; JTableHeader tableHeader = (JTableHeader) columnHeaderComp; TableHeaderUI tableHeaderUI = tableHeader.getUI(); if (!(tableHeaderUI instanceof SubstanceTableHeaderUI)) return; SubstanceTableHeaderUI ui = (SubstanceTableHeaderUI) tableHeaderUI; JComponent scrollPaneCornerFiller = ui.getScrollPaneCornerFiller(); String cornerKey = scrollpane.getComponentOrientation().isLeftToRight() ? JScrollPane.UPPER_RIGHT_CORNER : JScrollPane.UPPER_LEFT_CORNER; Component cornerComp = scrollpane.getCorner(cornerKey); // Corner component can be replaced when the current one is null or // UIResource boolean canReplace = (cornerComp == null) || (cornerComp instanceof UIResource); // System.out.println(canReplace + ":" + cornerComp); if (canReplace) { scrollpane.setCorner(cornerKey, scrollPaneCornerFiller); } } }