content: /*
   * Copyright 2019 FormDev Software GmbH
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  /*
   * Smooth scrolling code partly based on code from IntelliJ IDEA Community Edition,
   * which is licensed under the Apache 2.0 license. Copyright 2000-2016 JetBrains s.r.o.
   * See: https://github.com/JetBrains/intellij-community/blob/31e1b5a8e43219b9571951bab6457cfb3012e3ef/platform/platform-api/src/com/intellij/ui/components/SmoothScrollPane.java#L141-L185
   *
   */
  package themeengine.include.com.formdev.flatlaf.ui;
  
  import java.awt.Component;
  import java.awt.Graphics;
  import java.awt.Insets;
  import java.awt.Rectangle;
  import java.awt.event.ContainerEvent;
  import java.awt.event.ContainerListener;
  import java.awt.event.FocusEvent;
  import java.awt.event.FocusListener;
  import java.awt.event.MouseWheelEvent;
  import java.awt.event.MouseWheelListener;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
  import javax.swing.BorderFactory;
  import javax.swing.JButton;
  import javax.swing.JComponent;
  import javax.swing.JScrollBar;
  import javax.swing.JScrollPane;
  import javax.swing.JTable;
  import javax.swing.JViewport;
  import javax.swing.LookAndFeel;
  import javax.swing.ScrollPaneConstants;
  import javax.swing.Scrollable;
  import javax.swing.SwingConstants;
  import javax.swing.UIManager;
  import javax.swing.plaf.ComponentUI;
  import javax.swing.plaf.basic.BasicScrollPaneUI;
  import themeengine.include.com.formdev.flatlaf.FlatClientProperties;
  
  /**
   * Provides the Flat LaF UI delegate for {@link javax.swing.JScrollPane}.
   *
   * <!-- BasicScrollPaneUI -->
   *
   * @uiDefault ScrollPane.font				Font	unused
   * @uiDefault ScrollPane.background			Color
   * @uiDefault ScrollPane.foreground			Color	unused
   * @uiDefault ScrollPane.border				Border
   * @uiDefault ScrollPane.viewportBorder		Border
   *
   * <!-- FlatScrollPaneUI -->
   *
   * @uiDefault ScrollPane.smoothScrolling		boolean
   *
   * @author Karl Tauber
   */
  public class FlatScrollPaneUI
  	extends BasicScrollPaneUI
  {
  	private Handler handler;
  
  	public static ComponentUI createUI( JComponent c ) {
  		return new FlatScrollPaneUI();
  	}
  
  	@Override
  	public void installUI( JComponent c ) {
  		super.installUI( c );
  
  		int focusWidth = UIManager.getInt( "Component.focusWidth" );
  		LookAndFeel.installProperty( c, "opaque", focusWidth == 0 );
  
  		MigLayoutVisualPadding.install( scrollpane );
  	}
  
  	@Override
  	public void uninstallUI( JComponent c ) {
  		MigLayoutVisualPadding.uninstall( scrollpane );
  
  		super.uninstallUI( c );
  	}
  
  	@Override
  	protected void installListeners( JScrollPane c ) {
  		super.installListeners( c );
  
  		addViewportListeners( scrollpane.getViewport() );
  	}
  
  	@Override
  	protected void uninstallListeners( JComponent c ) {
  		super.uninstallListeners( c );
  
  		removeViewportListeners( scrollpane.getViewport() );
  
  		handler = null;
  	}
  
  	@Override
  	protected MouseWheelListener createMouseWheelListener() {
  		return new BasicScrollPaneUI.MouseWheelHandler() {
  			@Override
  			public void mouseWheelMoved( MouseWheelEvent e ) {
  				if( isSmoothScrollingEnabled() &&
  					scrollpane.isWheelScrollingEnabled() &&
  					e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL &&
  					e.getPreciseWheelRotation() != 0 &&
  					e.getPreciseWheelRotation() != e.getWheelRotation() )
  				{
  					mouseWheelMovedSmooth( e );
  				} else
  					super.mouseWheelMoved( e );
  			}
  		};
  	}
  
  	protected boolean isSmoothScrollingEnabled() {
  		Object smoothScrolling = scrollpane.getClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING );
  		if( smoothScrolling instanceof Boolean )
  			return (Boolean) smoothScrolling;
  
  		// Note: Getting UI value "ScrollPane.smoothScrolling" here to allow
  		// applications to turn smooth scrolling on or off at any time
  		// (e.g. in application options dialog).
  		return UIManager.getBoolean( "ScrollPane.smoothScrolling" );
  	}
  
  	private static final double EPSILON = 1e-5d;
  
  	private void mouseWheelMovedSmooth( MouseWheelEvent e ) {
  		// return if there is no viewport
  		JViewport viewport = scrollpane.getViewport();
  		if( viewport == null )
  			return;
  
  		// find scrollbar to scroll
  		JScrollBar scrollbar = scrollpane.getVerticalScrollBar();
  		if( scrollbar == null || !scrollbar.isVisible() || e.isShiftDown() ) {
  			scrollbar = scrollpane.getHorizontalScrollBar();
  			if( scrollbar == null || !scrollbar.isVisible() )
  				return;
  		}
  
  		// consume event
  		e.consume();
  
  		// get precise wheel rotation
  		double rotation = e.getPreciseWheelRotation();
  
  		// get unit and block increment
  		int unitIncrement;
  		int blockIncrement;
  		int orientation = scrollbar.getOrientation();
  		Component view = viewport.getView();
  		if( view instanceof Scrollable ) {
  			Scrollable scrollable = (Scrollable) view;
  
  			// Use (0, 0) view position to obtain constant unit increment of first item
  			// (which might otherwise be variable on smaller-than-unit scrolling).
  			Rectangle visibleRect = new Rectangle( viewport.getViewSize() );
  			unitIncrement = scrollable.getScrollableUnitIncrement( visibleRect, orientation, 1 );
  			blockIncrement = scrollable.getScrollableBlockIncrement( visibleRect, orientation, 1 );
  
  			if( unitIncrement > 0 ) {
  				// For the case that the first item (e.g. in a list) is larger
  				// than the other items, get the unit increment of the second item
  				// and use the smaller one.
  				if( orientation == SwingConstants.VERTICAL ) {
  					visibleRect.y += unitIncrement;
  					visibleRect.height -= unitIncrement;
  				} else {
  					visibleRect.x += unitIncrement;
  					visibleRect.width -= unitIncrement;
  				}
  				int unitIncrement2 = scrollable.getScrollableUnitIncrement( visibleRect, orientation, 1 );
  				if( unitIncrement2 > 0 )
  					unitIncrement = Math.min( unitIncrement, unitIncrement2 );
  			}
  		} else {
  			int direction = rotation < 0 ? -1 : 1;
  			unitIncrement = scrollbar.getUnitIncrement( direction );
  			blockIncrement = scrollbar.getBlockIncrement( direction );
  		}
  
  		// limit scroll amount (number of units to scroll) for small viewports
  		// (e.g. vertical scrolling in file chooser)
  		int scrollAmount = e.getScrollAmount();
  		int viewportWH = (orientation == SwingConstants.VERTICAL)
  			? viewport.getHeight()
  			: viewport.getWidth();
  		if( unitIncrement * scrollAmount > viewportWH )
  			scrollAmount = Math.max( viewportWH / unitIncrement, 1 );
  
  		// compute relative delta
  		double delta = rotation * scrollAmount * unitIncrement;
  		boolean adjustDelta = Math.abs( rotation ) < (1.0 + EPSILON);
  		double adjustedDelta = adjustDelta
  			? Math.max( -blockIncrement, Math.min( delta, blockIncrement ) )
  			: delta;
  
  		// compute new value
  		int value = scrollbar.getValue();
  		double minDelta = scrollbar.getMinimum() - value;
  		double maxDelta = scrollbar.getMaximum() - scrollbar.getModel().getExtent() - value;
  		double boundedDelta = Math.max( minDelta, Math.min( adjustedDelta, maxDelta ) );
  		int newValue = value + (int) Math.round( boundedDelta );
  
  		// set new value
  		if( newValue != value )
  			scrollbar.setValue( newValue );
  
  /*debug
  		System.out.println( String.format( "%4d  %9f / %4d %4d / %12f %5s %12f / %4d %4d %4d / %12f %12f %12f / %4d",
  			e.getWheelRotation(),
  			e.getPreciseWheelRotation(),
  			unitIncrement,
  			blockIncrement,
  			delta,
  			adjustDelta,
  			adjustedDelta,
  			value,
  			scrollbar.getMinimum(),
  			scrollbar.getMaximum(),
  			minDelta,
  			maxDelta,
  			boundedDelta,
  			newValue ) );
  */
  	}
  
  	@Override
  	protected PropertyChangeListener createPropertyChangeListener() {
  		return new BasicScrollPaneUI.PropertyChangeHandler() {
  			@Override
  			public void propertyChange( PropertyChangeEvent e ) {
  				super.propertyChange( e );
  
  				switch( e.getPropertyName() ) {
  					case FlatClientProperties.SCROLL_BAR_SHOW_BUTTONS:
  						JScrollBar vsb = scrollpane.getVerticalScrollBar();
  						JScrollBar hsb = scrollpane.getHorizontalScrollBar();
  						if( vsb != null ) {
  							vsb.revalidate();
  							vsb.repaint();
  						}
  						if( hsb != null ) {
  							hsb.revalidate();
  							hsb.repaint();
  						}
  						break;
  
  					case ScrollPaneConstants.LOWER_LEFT_CORNER:
  					case ScrollPaneConstants.LOWER_RIGHT_CORNER:
  					case ScrollPaneConstants.UPPER_LEFT_CORNER:
  					case ScrollPaneConstants.UPPER_RIGHT_CORNER:
  						// remove border from buttons added to corners
  						Object corner = e.getNewValue();
  						if( corner instanceof JButton &&
  							((JButton)corner).getBorder() instanceof FlatButtonBorder &&
  							scrollpane.getViewport() != null &&
  							scrollpane.getViewport().getView() instanceof JTable )
  						{
  							((JButton)corner).setBorder( BorderFactory.createEmptyBorder() );
  							((JButton)corner).setFocusable( false );
  						}
  					break;
  				}
  			}
  		};
  	}
  
  	private Handler getHandler() {
  		if( handler == null )
  			handler = new Handler();
  		return handler;
  	}
  
  	@Override
  	protected void updateViewport( PropertyChangeEvent e ) {
  		super.updateViewport( e );
  
  		JViewport oldViewport = (JViewport) (e.getOldValue());
  		JViewport newViewport = (JViewport) (e.getNewValue());
  
  		removeViewportListeners( oldViewport );
  		addViewportListeners( newViewport );
  	}
  
  	private void addViewportListeners( JViewport viewport ) {
  		if( viewport == null )
  			return;
  
  		viewport.addContainerListener( getHandler() );
  
  		Component view = viewport.getView();
  		if( view != null )
  			view.addFocusListener( getHandler() );
  	}
  
  	private void removeViewportListeners( JViewport viewport ) {
  		if( viewport == null )
  			return;
  
  		viewport.removeContainerListener( getHandler() );
  
  		Component view = viewport.getView();
  		if( view != null )
  			view.removeFocusListener( getHandler() );
  	}
  
  	@Override
  	public void update( Graphics g, JComponent c ) {
  		if( c.isOpaque() ) {
  			FlatUIUtils.paintParentBackground( g, c );
  
  			// paint background so that corners have same color as scroll bars
  			Insets insets = c.getInsets();
  			g.setColor( c.getBackground() );
  			g.fillRect( insets.left, insets.top,
  				c.getWidth() - insets.left - insets.right,
  				c.getHeight() - insets.top - insets.bottom );
  		}
  
  		paint( g, c );
  	}
  
  	//---- class Handler ------------------------------------------------------
  
  	/**
  	 * ContainerListener is added to JViewport to keep focus listener on view up-to-date.
  	 * FocusListener is added to view for repainting when view gets focused.
  	 */
  	private class Handler
  		implements ContainerListener, FocusListener
  	{
  		@Override
  		public void componentAdded( ContainerEvent e ) {
  			e.getChild().addFocusListener( this );
  		}
  
  		@Override
  		public void componentRemoved( ContainerEvent e ) {
  			e.getChild().removeFocusListener( this );
  		}
  
  		@Override
  		public void focusGained( FocusEvent e ) {
  			scrollpane.repaint();
  		}
  
  		@Override
  		public void focusLost( FocusEvent e ) {
  			scrollpane.repaint();
  		}
  	}
  }
  