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.
   */
  
  package themeengine.include.com.formdev.flatlaf.ui;
  
  import static themeengine.include.com.formdev.flatlaf.util.UIScale.scale;
  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.Insets;
  import java.awt.LayoutManager;
  import java.awt.Rectangle;
  import java.awt.Shape;
  import java.awt.event.FocusEvent;
  import java.awt.event.FocusListener;
  import java.awt.geom.Rectangle2D;
  import java.beans.PropertyChangeEvent;
  import java.beans.PropertyChangeListener;
  import javax.swing.JComponent;
  import javax.swing.JSpinner;
  import javax.swing.JTextField;
  import javax.swing.LookAndFeel;
  import javax.swing.SwingConstants;
  import javax.swing.UIManager;
  import javax.swing.plaf.ComponentUI;
  import javax.swing.plaf.basic.BasicSpinnerUI;
  import themeengine.include.com.formdev.flatlaf.FlatClientProperties;
  
  /**
   * Provides the Flat LaF UI delegate for {@link javax.swing.JSpinner}.
   *
   * <!-- BasicSpinnerUI -->
   *
   * @uiDefault Spinner.font						Font
   * @uiDefault Spinner.background				Color
   * @uiDefault Spinner.foreground				Color
   * @uiDefault Spinner.border					Border
   * @uiDefault Spinner.disableOnBoundaryValues	boolean	default is false
   * @uiDefault Spinner.editorAlignment			int		0=center, 2=left, 4=right, 10=leading, 11=trailing
   * @uiDefault Spinner.editorBorderPainted		boolean	paint inner editor border; defaults to false
   *
   * <!-- FlatSpinnerUI -->
   *
   * @uiDefault Component.minimumWidth			int
   * @uiDefault Spinner.buttonStyle				String	button (default) or none
   * @uiDefault Component.arrowType				String	triangle (default) or chevron
   * @uiDefault Component.isIntelliJTheme			boolean
   * @uiDefault Component.borderColor				Color
   * @uiDefault Component.disabledBorderColor		Color
   * @uiDefault Spinner.disabledBackground		Color
   * @uiDefault Spinner.disabledForeground		Color
   * @uiDefault Spinner.buttonBackground			Color
   * @uiDefault Spinner.buttonArrowColor			Color
   * @uiDefault Spinner.buttonDisabledArrowColor	Color
   * @uiDefault Spinner.buttonHoverArrowColor		Color
   * @uiDefault Spinner.padding					Insets
   *
   * @author Karl Tauber
   */
  public class FlatSpinnerUI
  	extends BasicSpinnerUI
  {
  	private Handler handler;
  
  	protected int minimumWidth;
  	protected String buttonStyle;
  	protected String arrowType;
  	protected boolean isIntelliJTheme;
  	protected Color borderColor;
  	protected Color disabledBorderColor;
  	protected Color disabledBackground;
  	protected Color disabledForeground;
  	protected Color buttonBackground;
  	protected Color buttonArrowColor;
  	protected Color buttonDisabledArrowColor;
  	protected Color buttonHoverArrowColor;
  	protected Insets padding;
  
  	public static ComponentUI createUI( JComponent c ) {
  		return new FlatSpinnerUI();
  	}
  
  	@Override
  	protected void installDefaults() {
  		super.installDefaults();
  
  		LookAndFeel.installProperty( spinner, "opaque", false );
  
  		minimumWidth = UIManager.getInt( "Component.minimumWidth" );
  		buttonStyle = UIManager.getString( "Spinner.buttonStyle" );
  		arrowType = UIManager.getString( "Component.arrowType" );
  		isIntelliJTheme = UIManager.getBoolean( "Component.isIntelliJTheme" );
  		borderColor = UIManager.getColor( "Component.borderColor" );
  		disabledBorderColor = UIManager.getColor( "Component.disabledBorderColor" );
  		disabledBackground = UIManager.getColor( "Spinner.disabledBackground" );
  		disabledForeground = UIManager.getColor( "Spinner.disabledForeground" );
  		buttonBackground = UIManager.getColor( "Spinner.buttonBackground" );
  		buttonArrowColor = UIManager.getColor( "Spinner.buttonArrowColor" );
  		buttonDisabledArrowColor = UIManager.getColor( "Spinner.buttonDisabledArrowColor" );
  		buttonHoverArrowColor = UIManager.getColor( "Spinner.buttonHoverArrowColor" );
  		padding = UIManager.getInsets( "Spinner.padding" );
  
  		// scale
  		padding = scale( padding );
  
  		MigLayoutVisualPadding.install( spinner );
  	}
  
  	@Override
  	protected void uninstallDefaults() {
  		super.uninstallDefaults();
  
  		borderColor = null;
  		disabledBorderColor = null;
  		disabledBackground = null;
  		disabledForeground = null;
  		buttonBackground = null;
  		buttonArrowColor = null;
  		buttonDisabledArrowColor = null;
  		buttonHoverArrowColor = null;
  		padding = null;
  
  		MigLayoutVisualPadding.uninstall( spinner );
  	}
  
  	@Override
  	protected void installListeners() {
  		super.installListeners();
  
  		addEditorFocusListener( spinner.getEditor() );
  		spinner.addFocusListener( getHandler() );
  		spinner.addPropertyChangeListener( getHandler() );
  	}
  
  	@Override
  	protected void uninstallListeners() {
  		super.uninstallListeners();
  
  		removeEditorFocusListener( spinner.getEditor() );
  		spinner.removeFocusListener( getHandler() );
  		spinner.removePropertyChangeListener( getHandler() );
  
  		handler = null;
  	}
  
  	private Handler getHandler() {
  		if( handler == null )
  			handler = new Handler();
  		return handler;
  	}
  
  	@Override
  	protected JComponent createEditor() {
  		JComponent editor = super.createEditor();
  
  		// explicitly make non-opaque
  		editor.setOpaque( false );
  		JTextField textField = getEditorTextField( editor );
  		if( textField != null )
  			textField.setOpaque( false );
  
  		updateEditorColors();
  		return editor;
  	}
  
  	@Override
  	protected void replaceEditor( JComponent oldEditor, JComponent newEditor ) {
  		super.replaceEditor( oldEditor, newEditor );
  
  		removeEditorFocusListener( oldEditor );
  		addEditorFocusListener( newEditor );
  		updateEditorColors();
  	}
  
  	private void addEditorFocusListener( JComponent editor ) {
  		JTextField textField = getEditorTextField( editor );
  		if( textField != null )
  			textField.addFocusListener( getHandler() );
  	}
  
  	private void removeEditorFocusListener( JComponent editor ) {
  		JTextField textField = getEditorTextField( editor );
  		if( textField != null )
  			textField.removeFocusListener( getHandler() );
  	}
  
  	private void updateEditorColors() {
  		JTextField textField = getEditorTextField( spinner.getEditor() );
  		if( textField != null ) {
  			// use non-UIResource colors because when SwingUtilities.updateComponentTreeUI()
  			// is used, then the text field is updated after the spinner and the
  			// colors are again replaced with default colors
  			textField.setForeground( FlatUIUtils.nonUIResource( getForeground( true ) ) );
  			textField.setDisabledTextColor( FlatUIUtils.nonUIResource( getForeground( false ) ) );
  		}
  	}
  
  	private static JTextField getEditorTextField( JComponent editor ) {
  		return editor instanceof JSpinner.DefaultEditor
  			? ((JSpinner.DefaultEditor)editor).getTextField()
  			: null;
  	}
  
  	protected Color getBackground( boolean enabled ) {
  		return enabled
  			? spinner.getBackground()
  			: (isIntelliJTheme ? FlatUIUtils.getParentBackground( spinner ) : disabledBackground);
  	}
  
  	protected Color getForeground( boolean enabled ) {
  		return enabled ? spinner.getForeground() : disabledForeground;
  	}
  
  	@Override
  	protected LayoutManager createLayout() {
  		return getHandler();
  	}
  
  	@Override
  	protected Component createNextButton() {
  		return createArrowButton( SwingConstants.NORTH, "Spinner.nextButton" );
  	}
  
  	@Override
  	protected Component createPreviousButton() {
  		return createArrowButton( SwingConstants.SOUTH, "Spinner.previousButton" );
  	}
  
  	private Component createArrowButton( int direction, String name ) {
  		FlatArrowButton button = new FlatArrowButton( direction, arrowType, buttonArrowColor,
  			buttonDisabledArrowColor, buttonHoverArrowColor, null );
  		button.setName( name );
  		button.setYOffset( (direction == SwingConstants.NORTH) ? 1 : -1 );
  		if( direction == SwingConstants.NORTH )
  			installNextButtonListeners( button );
  		else
  			installPreviousButtonListeners( button );
  		return button;
  	}
  
  	@Override
  	public void update( Graphics g, JComponent c ) {
  		float focusWidth = FlatUIUtils.getBorderFocusWidth( c );
  		float arc = FlatUIUtils.getBorderArc( c );
  
  		// fill background if opaque to avoid garbage if user sets opaque to true
  		if( c.isOpaque() && (focusWidth > 0 || arc > 0) )
  			FlatUIUtils.paintParentBackground( g, c );
  
  		Graphics2D g2 = (Graphics2D) g;
  		FlatUIUtils.setRenderingHints( g2 );
  
  		int width = c.getWidth();
  		int height = c.getHeight();
  		Component nextButton = getHandler().nextButton;
  		int arrowX = nextButton.getX();
  		int arrowWidth = nextButton.getWidth();
  		boolean paintButton = !"none".equals( buttonStyle );
  		boolean enabled = spinner.isEnabled();
  		boolean isLeftToRight = spinner.getComponentOrientation().isLeftToRight();
  
  		// paint background
  		g2.setColor( getBackground( enabled ) );
  		FlatUIUtils.paintComponentBackground( g2, 0, 0, width, height, focusWidth, arc );
  
  		// paint arrow buttons background
  		if( paintButton && enabled ) {
  			g2.setColor( buttonBackground );
  			Shape oldClip = g2.getClip();
  			if( isLeftToRight )
  				g2.clipRect( arrowX, 0, width - arrowX, height );
  			else
  				g2.clipRect( 0, 0, arrowX + arrowWidth, height );
  			FlatUIUtils.paintComponentBackground( g2, 0, 0, width, height, focusWidth, arc );
  			g2.setClip( oldClip );
  		}
  
  		// paint vertical line between value and arrow buttons
  		if( paintButton ) {
  			g2.setColor( enabled ? borderColor : disabledBorderColor );
  			float lw = scale( 1f );
  			float lx = isLeftToRight ? arrowX : arrowX + arrowWidth - lw;
  			g2.fill( new Rectangle2D.Float( lx, focusWidth, lw, height - 1 - (focusWidth * 2) ) );
  		}
  
  		paint( g, c );
  	}
  
  	//---- class Handler ------------------------------------------------------
  
  	private class Handler
  		implements LayoutManager, FocusListener, PropertyChangeListener
  	{
  		//---- interface LayoutManager ----
  
  		private Component editor = null;
  		private Component nextButton;
  		private Component previousButton;
  
  		@Override
  		public void addLayoutComponent( String name, Component c ) {
  			switch( name ) {
  				case "Editor":	editor = c; break;
  				case "Next":		nextButton = c; break;
  				case "Previous":	previousButton = c; break;
  			}
  		}
  
  		@Override
  		public void removeLayoutComponent( Component c ) {
  			if( c == editor )
  				editor = null;
  			else if( c == nextButton )
  				nextButton = null;
  			else if( c == previousButton )
  				previousButton = null;
  		}
  
  		@Override
  		public Dimension preferredLayoutSize( Container parent ) {
  			Insets insets = parent.getInsets();
  			Dimension editorSize = (editor != null) ? editor.getPreferredSize() : new Dimension( 0, 0 );
  
  			// the arrows width is the same as the inner height so that the arrows area is square
  			int minimumWidth = FlatUIUtils.minimumWidth( spinner, FlatSpinnerUI.this.minimumWidth );
  			int innerHeight = editorSize.height + padding.top + padding.bottom;
  			float focusWidth = FlatUIUtils.getBorderFocusWidth( spinner );
  			return new Dimension(
  				Math.max( insets.left + insets.right + editorSize.width + padding.left + padding.right + innerHeight, scale( minimumWidth ) + Math.round( focusWidth * 2 ) ),
  				insets.top + insets.bottom + innerHeight );
  		}
  
  		@Override
  		public Dimension minimumLayoutSize( Container parent ) {
  			return preferredLayoutSize( parent );
  		}
  
  		@Override
  		public void layoutContainer( Container parent ) {
  			Dimension size = parent.getSize();
  			Insets insets = parent.getInsets();
  			Rectangle r = FlatUIUtils.subtractInsets( new Rectangle( size ), insets );
  
  			if( nextButton == null && previousButton == null ) {
  				if( editor != null )
  					editor.setBounds( r );
  				return;
  			}
  
  			Rectangle editorRect = new Rectangle( r );
  			Rectangle buttonsRect = new Rectangle( r );
  
  			// make button area square
  			int buttonsWidth = r.height;
  			buttonsRect.width = buttonsWidth;
  
  			if( parent.getComponentOrientation().isLeftToRight() ) {
  				editorRect.width -= buttonsWidth;
  				buttonsRect.x += editorRect.width;
  			} else {
  				editorRect.x += buttonsWidth;
  				editorRect.width -= buttonsWidth;
  			}
  
  			if( editor != null )
  				editor.setBounds( FlatUIUtils.subtractInsets( editorRect, padding ) );
  
  			int nextHeight = (buttonsRect.height / 2) + (buttonsRect.height % 2); // round up
  			if( nextButton != null )
  				nextButton.setBounds( buttonsRect.x, buttonsRect.y, buttonsRect.width, nextHeight );
  			if( previousButton != null ) {
  				// for precise layout of arrows, the "previous" button has the same height
  				// as the "next" button and may overlap on uneven buttonsRect.height
  				int previousY = buttonsRect.y + buttonsRect.height - nextHeight;
  				previousButton.setBounds( buttonsRect.x, previousY, buttonsRect.width, nextHeight );
  			}
  		}
  
  		//---- interface FocusListener ----
  
  		@Override
  		public void focusGained( FocusEvent e ) {
  			spinner.repaint();
  
  			// if spinner gained focus, transfer it to the editor text field
  			if( e.getComponent() == spinner ) {
  				JTextField textField = getEditorTextField( spinner.getEditor() );
  				if( textField != null )
  					textField.requestFocusInWindow();
  			}
  		}
  
  		@Override
  		public void focusLost( FocusEvent e ) {
  			spinner.repaint();
  		}
  
  		//---- interface PropertyChangeListener ----
  
  		@Override
  		public void propertyChange( PropertyChangeEvent e ) {
  			switch( e.getPropertyName() ) {
  				case "foreground":
  				case "enabled":
  					updateEditorColors();
  					break;
  
  				case FlatClientProperties.COMPONENT_ROUND_RECT:
  					spinner.repaint();
  					break;
  
  				case FlatClientProperties.MINIMUM_WIDTH:
  					spinner.revalidate();
  					break;
  			}
  		}
  	}
  }
  