content: /*
   * Copyright (c) 1997 - 2017
   * Actelion Pharmaceuticals Ltd.
   * Gewerbestrasse 16
   * CH-4123 Allschwil, Switzerland
   *
   * All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions are met:
   *
   * 1. Redistributions of source code must retain the above copyright notice, this
   *    list of conditions and the following disclaimer.
   * 2. 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.
   * 3. Neither the name of the copyright holder 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.
   *
   * @author Thomas Sander
   */
  
  package com.actelion.research.gui;
  
  import com.actelion.research.gui.hidpi.HiDPIHelper;
  import com.actelion.research.util.ColorHelper;
  
  import javax.swing.*;
  import java.awt.*;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
  import java.awt.geom.Ellipse2D;
  import java.awt.geom.Rectangle2D;
  import java.util.ArrayList;
  
  /**
   * Lightweight Slider for maintaining two values within given limits
   */
  
  public class JPruningBar extends JPanel implements MouseListener, MouseMotionListener {
  	private static final long serialVersionUID = 0x20070307;
  
  	private static final int cRedWaterRGB = Color.RED.getRGB();
  	private static final int cBlueWaterRGB = 0x156be4;
  
  	private static final int cThumbSize = 2 * HiDPIHelper.scale(7);	// even integer
  	private static final int cBarWidth = cThumbSize - 2 * HiDPIHelper.scale(3);  // even integer
  	private static final int cBorder = HiDPIHelper.scale(2);
  
  	private final float[] mPosition;
  	private float	mX1,mY1;
  	private double	mValuePerPixel,mLowValue,mMinValue,mHighValue,mMaxValue;
  	private final boolean	mIsHorizontal,mAllowDoubleClickChange;
  	private boolean mUpdateNeeded,mUseRedColor,mWasDragged;
  	private int		mID,mMousePosition,mClickedArea,mActiveThumb;
  	private ArrayList<PruningBarListener> mListener;
  
  	public JPruningBar() {
  		this(0, 100, true, 0, false);
  		}
  
  	public JPruningBar(boolean isHorizontal) {
  		this(0, 100, isHorizontal, 0, false);
  		}
  
  	public JPruningBar(boolean isHorizontal, int id) {
  		this(0, 100, isHorizontal, id, false);
  		}
  
  	public JPruningBar(double min, double max, boolean isHorizontal, int id) {
  		this(min, max, isHorizontal, id, false);
  		}
  
  	public JPruningBar(double min, double max, boolean isHorizontal, int id, boolean allowDoubleClick) {
  		init();
  		mMinValue = min;
  		mLowValue = min;
  		mHighValue = max;
  		mMaxValue = max;
  		mIsHorizontal = isHorizontal;
  		mID = id;
  		mAllowDoubleClickChange = allowDoubleClick;
  		mPosition = new float[2];
  		mActiveThumb = -1;
  		}
  
  	public void paintComponent(Graphics g) {
  		super.paintComponent(g);
  		((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  		Graphics2D g2D = (Graphics2D)g;
  		Dimension theSize = getSize();
  		float lineWidth = 2.5f*HiDPIHelper.getUIScaleFactor();
  		g2D.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_BUTT,BasicStroke.JOIN_MITER));
  		Color shadow = LookAndFeelHelper.isDarkLookAndFeel() ?
  				ColorHelper.brighter(getBackground(), 0.7f) : ColorHelper.darker(getBackground(), 0.85f);
  		Color grayThumb = LookAndFeelHelper.isDarkLookAndFeel() ?
  				ColorHelper.brighter(getBackground(), 0.5f) : ColorHelper.darker(getBackground(), 0.75f);
  
  		if (mUpdateNeeded) {
  			float x2,y2;  // display area without border
  			if (mIsHorizontal) {
  				mX1 = cBorder;
  				x2 = theSize.width - cBorder;
  				mY1 = (theSize.height - cThumbSize) / 2;
  				y2 = mY1 + cThumbSize;
  				}
  			else {
  				mX1 = (theSize.width - cThumbSize) / 2;
  				x2 = mX1 + cThumbSize;
  				mY1 = cBorder;
  				y2 = theSize.height - cBorder;
  				}
  
  			setColor(g, shadow);
  			g2D.fill(new Ellipse2D.Float(mX1, mY1, cThumbSize, cThumbSize));
  			g2D.fill(new Ellipse2D.Float(x2-cThumbSize, y2-cThumbSize, cThumbSize, cThumbSize));
  			if (mIsHorizontal)
  				g2D.fill(new Rectangle2D.Float(mX1+cThumbSize-1, mY1+(cThumbSize-cBarWidth)/2, x2-mX1-2*cThumbSize+2, cBarWidth));
  			else
  				g2D.fill(new Rectangle2D.Float(mX1+(cThumbSize-cBarWidth)/2, mY1+cThumbSize-1, cBarWidth, y2-mY1-2*cThumbSize+2));
  
  			float zoomSpace = (mIsHorizontal ? x2-mX1 : y2-mY1) - 2 * cThumbSize;
  			double valueSpace = mMaxValue - mMinValue;
  			mValuePerPixel = valueSpace / zoomSpace;
  			if (mIsHorizontal) {
  				mPosition[0] = (float)((mLowValue - mMinValue) / mValuePerPixel);
  				mPosition[1] = (float)((mHighValue - mMinValue) / mValuePerPixel);
  				}
  			else {   // tribute to inverted Y-scale in java
  				mPosition[0] = (float)((mMaxValue - mHighValue) / mValuePerPixel);
  				mPosition[1] = (float)((mMaxValue - mLowValue) / mValuePerPixel);
  				}
  
  			int rgb = mUseRedColor ? cRedWaterRGB : HeaderPaintHelper.getThemeColors() == null ? cBlueWaterRGB : HeaderPaintHelper.getThemeColors()[0];
  			Color c1 = new Color(ColorHelper.createColor(rgb, 0.7f));
  			Color c2 = new Color(ColorHelper.createColor(rgb, 0.5f));
  			Color c3 = new Color(ColorHelper.createColor(rgb, 0.3f));
  
  			if (mPosition[1] > mPosition[0]) {
  				float waterStart = (mIsHorizontal ? mX1 : mY1) + mPosition[0] + cThumbSize;
  				float waterStop = (mIsHorizontal ? mX1 : mY1) + mPosition[1] + cThumbSize;
  
  				Paint storedPaint = g2D.getPaint();
  				if (mIsHorizontal) {
  					float yy1 = mY1 + (cThumbSize - cBarWidth) / 2;
  					float yy2 = mY1 + (cThumbSize + cBarWidth) / 2;
  					g2D.setPaint(new GradientPaint(0, yy1, c1, 0, yy2, c3));
  					g2D.fill(new Rectangle2D.Float(waterStart, yy1, waterStop - waterStart, cBarWidth));
  					}
  				else {
  					float xx1 = mX1 + (cThumbSize - cBarWidth) / 2;
  					float xx2 = mY1 + (cThumbSize + cBarWidth) / 2;
  					g2D.setPaint(new GradientPaint(xx1, 0, c1, xx2, 0, c3));
  					g2D.fill(new Rectangle2D.Float(xx1, waterStart, cBarWidth, waterStop - waterStart));
  					}
  
  				g2D.setPaint(storedPaint);
  				}
  
  			for (int i=0; i<2; i++)
  				drawThumb(g2D, i, i == mActiveThumb ? c2 : grayThumb, c3);
  			}
  		}
  
  	private void drawThumb(Graphics2D g, int i, Color color1, Color color2) {
  		Ellipse2D.Float elipse = getThumbElipse(i);
  		setColor(g, color1);
  		g.fill(elipse);
  		setColor(g, color2);
  		g.draw(elipse);
  		}
  
  	private Ellipse2D.Float getThumbElipse(int i) {
  		float shift = mPosition[i] + (i == 1 ? cThumbSize : 0);
  		float x = mX1 + (mIsHorizontal ? shift : 0);
  		float y = mY1 + (mIsHorizontal ? 0 : shift);
  		return new Ellipse2D.Float(x, y, cThumbSize, cThumbSize);
  		}
  
  	private void setColor(Graphics g, Color color) {
  		g.setColor(isEnabled() ? color : ColorHelper.intermediateColor(color, Color.LIGHT_GRAY, 0.7f));
  		}
  
  	public void update(Graphics g) {
  		paint(g);
  		}
  
  	public double getLowValue() {
  		return mLowValue;
  		}
  
  	public double getHighValue() {
  		return mHighValue;
  		}
  
  	public double getMaximumValue() {
  		return mMaxValue;
  		}
  
  	public double getMinimumValue() {
  		return mMinValue;
  		}
  
  	/**
  	 * Sets high and low values to max and min, respectively.
  	 * Sends PruningBarEvents in case of a change.
  	 */
  	public void reset() {
  		boolean highChanged = setHigh(mMaxValue);
  		boolean lowChanged = setLow(mMinValue);
  
  		if (highChanged || lowChanged) {
  			informListeners(false);
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  
  	/**
  	 * Updates the low and high values simultaniously, provided the low <= high,
  	 * low >= current min, and high <= current max.
  	 * @param low
  	 * @param high
  	 * @param silent if true, PruningBarEvents are suppressed
  	 */
  	public void setLowAndHigh(double low, double high, boolean silent) {
  		if (low < mMinValue)
  			low = mMinValue;
  		if (high > mMaxValue)
  			high = mMaxValue;
  		if ((low != mLowValue || high != mHighValue)
  		 && low <= high) {
  			mLowValue = low;
  			mHighValue = high;
  			if (!silent)
  				informListeners(false);
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  
  	/**
  	 * Updates the low value provided the new value is >= current min and <= current high.
  	 * Sends PruningBarEvents in case of a successful change.
  	 * @param low
  	 */
  	public void setLowValue(double low) {
  		if (setLow(low)) {
  			informListeners(false);
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  
  	public void setID(int id) {
  		mID = id;
  		}
  
  	/**
  	 * Updates the high value provided the new value is >= current low and <= current max.
  	 * Sends PruningBarEvents in case of a successful change.
  	 * @param high
  	 */
  	public void setHighValue(double high) {
  		if (setHigh(high)) {
  			informListeners(false);
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  
  	/**
  	 * Updates the maximum value; may update low and high to stay within limits
  	 * Sends PruningBarEvents in case of a successful change.
  	 * @param max
  	 */
  	public void setMaximumValue(double max) {
  		if (max < mMinValue)
  			max = mMinValue;
  
  		if (mMaxValue != max) {
  			mMaxValue = max;
  			if (mHighValue > mMaxValue)
  				mHighValue = mMaxValue;
  			if (mLowValue > mHighValue)
  				mLowValue = mHighValue;
  
  			informListeners(false);
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  
  	/**
  	 * Updates the minimum value; may update low and high to stay within limits
  	 * Sends PruningBarEvents in case of a successful change.
  	 * @param min
  	 */
  	public void setMinimumValue(double min) {
  			// changes the allowed min value; may update low and high to stay within limits
  		if (min > mMaxValue)
  			min = mMaxValue;
  
  		if (mMinValue != min) {
  			mMinValue = min;
  			if (mLowValue < mMinValue)
  				mLowValue = mMinValue;
  			if (mHighValue < mLowValue)
  				mHighValue = mLowValue;
  
  			informListeners(false);
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  
  	/**
  	 * Initializes the bar by setting min and low/max and high to the values given.
  	 * Does not(!) send PruningBarEvents.
  	 * @param min
  	 * @param max
  	 */
  	public void setMinAndMax(double min, double max) {
  		mLowValue = mMinValue = min;
  		mHighValue = mMaxValue = max;
  		mUpdateNeeded = true;
  		repaint();
  		}
  
  	public void setUseRedColor(boolean useRed) {
  		if (mUseRedColor != useRed) {
  			mUseRedColor = useRed;
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  	
  	public Dimension getMinimumSize() {
  		if (mIsHorizontal)
  			return new Dimension(4* cThumbSize, cThumbSize+2*cBorder);
  		else
  			return new Dimension(cThumbSize+2*cBorder, 4* cThumbSize);
  		}
  
  	public Dimension getPreferredSize() {
  		if (mIsHorizontal)
  			return new Dimension(100, cThumbSize+2*cBorder);
  		else
  			return new Dimension(cThumbSize+2*cBorder, 100);
  		}
  
  	public Dimension getMaximumSize() {
  		if (mIsHorizontal)
  			return new Dimension(Short.MAX_VALUE, cThumbSize+2*cBorder);
  		else
  			return new Dimension(cThumbSize+2*cBorder, Short.MAX_VALUE);
  		}
  
  	public void mousePressed(MouseEvent e) {
  		if (!isEnabled())
  			return;
  
  		if (mIsHorizontal)
  			mMousePosition = e.getX();
  		else
  			mMousePosition = e.getY();
  
  		mWasDragged = false;
  
  		mClickedArea = 0;
  		if (mMousePosition < mPosition[0])
  			mClickedArea = 0;
  		else if (mMousePosition <= mPosition[0] + cThumbSize)
  			mClickedArea = 1;
  		else if (mMousePosition <= mPosition[1]+ cThumbSize)
  			mClickedArea = 2;
  		else if (mMousePosition <= mPosition[1] + 2 * cThumbSize)
  			mClickedArea = 3;
  		else
  			mClickedArea = 0;
  
  		if (!mIsHorizontal  // tribute to inverted Y-scale in java
  		 && (mClickedArea == 1 || mClickedArea == 3))
  			mClickedArea = 4 - mClickedArea;
  		}
  
  	public void mouseReleased(MouseEvent e) {
  		if (mWasDragged)
  			informListeners(false);
  		}
  
  	public void mouseEntered(MouseEvent e) {}
  	public void mouseExited(MouseEvent e) {}
  
  	/**
  	 * If this pruning bar allows changes via double click and text input, then such an action will
  	 * cause a PruningBarEvent with type=TYPE_TYPED. Its low or high value will contain the number that
  	 * the user has typed in. The other value will be Float.NaN. The JPruningBar will <b>not</b> update
  	 * internal values, because JPruningBar scale and user visible scale may be different. The receiver
  	 * of the event is responsible to translate the user number into JPuningBar scale and call the
  	 * pruning bar's setLowValue() or setHighValue().
  	 * @param e
  	 */
  	public void mouseClicked(MouseEvent e) {
  		if (mAllowDoubleClickChange && e.getClickCount() == 2 && (mClickedArea == 1 || mClickedArea == 3)) {
  			Component c = this;
  			while (c != null && !(c instanceof Window || c instanceof Frame))
  				c = c.getParent();
  			if (c != null) {
  				try {
  					String s = JOptionPane.showInputDialog(c, "Please type in a value!", "Set Value", JOptionPane.QUESTION_MESSAGE);
  					if (s != null) {
  						float d = Float.parseFloat(s);
  						if (mClickedArea == 1) {
  							informListeners(d, Float.NaN);
  							}
  						else if (mClickedArea == 3) {
  							informListeners(Float.NaN, d);
  							}
  						}
  					}
  				catch (NumberFormatException nfe) {}
  				}
  			}
  		}
  
  	public synchronized void mouseDragged(MouseEvent e) {
  		if (!isEnabled())
  			return;
  
  		int position;
  		if (mIsHorizontal)
  			position = e.getX();
  		else
  			position = e.getY();
  
  		if (position == mMousePosition)
  			return;
  
  		mWasDragged = true;
  
  		double change = mValuePerPixel * (position - mMousePosition);
  		if (!mIsHorizontal) // tribute to inverted Y-scale in java
  			change = -change;
  		if (e.isControlDown())
  			change /= 10.0;
  
  		boolean valuesChanged = false;
  		switch (mClickedArea) {
  		case 1:
  			if ((change < 0.0 && mLowValue > mMinValue)
  			 || (change > 0.0 && mLowValue < mHighValue))
  				valuesChanged = true;
  
  			if (mLowValue + change < mMinValue)
  				mLowValue = mMinValue;
  			else if (mLowValue + change > mHighValue)
  				mLowValue = mHighValue;
  			else
  				mLowValue += change;
  			break;
  		case 2:
  			if ((change < 0.0 && mLowValue > mMinValue)
  			 || (change > 0.0 && mHighValue < mMaxValue))
  				valuesChanged = true;
  
  			if (mLowValue + change < mMinValue) {
  				mHighValue -= mLowValue - mMinValue;
  				mLowValue = mMinValue;
  				}
  			else if (mHighValue + change > mMaxValue) {
  				mLowValue += mMaxValue - mHighValue;
  				mHighValue = mMaxValue;
  				}
  			else {
  				mLowValue += change;
  				mHighValue += change;
  				}
  			break;
  		case 3:
  			if ((change < 0.0 && mHighValue > mLowValue)
  			 || (change > 0.0 && mHighValue < mMaxValue))
  				valuesChanged = true;
  
  			if (mHighValue + change > mMaxValue)
  				mHighValue = mMaxValue;
  			else if (mHighValue + change < mLowValue)
  				mHighValue = mLowValue;
  			else
  				mHighValue += change;
  			break;
  			}
  
  		if (valuesChanged) {
  			informListeners(true);
  			mMousePosition = position;
  			mUpdateNeeded = true;
  			repaint();
  			}
  		}
  
  	public void mouseMoved(MouseEvent e) {
  		int old = mActiveThumb;
  		mActiveThumb = -1;
  		for (int i=0; i<2; i++) {
  			if (getThumbElipse(i).contains(e.getX(), e.getY())) {
  				mActiveThumb = i;
  				break;
  				}
  			}
  		if (old != mActiveThumb)
  			repaint();
  		}
  
  	public void addPruningBarListener(PruningBarListener listener) {
  		mListener.add(listener);
  		}
  
  	public void removePruningBarListener(PruningBarListener listener) {
  		mListener.remove(listener);
  		}
  
  	public void firePruningBarChanged() {
  		informListeners(false);
  		}
  
  	private void init() {
  		this.setOpaque(false);
  		mUpdateNeeded = true;
  		addMouseListener(this);
  		addMouseMotionListener(this);
  		mListener = new ArrayList<>();
  		}
  
  	private boolean setHigh(double value) {
  		if (value < mLowValue)
  			value = mLowValue;
  		else if (value > mMaxValue)
  			value = mMaxValue;
  
  		if (value == mHighValue)
  			return false;
  
  		mHighValue = value;
  		return true;
  		}
  
  	private boolean setLow(double value) {
  		if (value < mMinValue)
  			value = mMinValue;
  		else if (value > mHighValue)
  			value = mHighValue;
  
  		if (value == mLowValue)
  			return false;
  
  		mLowValue = value;
  		return true;
  		}
  
  	private void informListeners(double low, double high) {
  		for (int i=0; i<mListener.size(); i++)
  			mListener.get(i).pruningBarChanged(
  				new PruningBarEvent(this, low, high, false, mID, PruningBarEvent.TYPE_TYPED));
  		}
  
  	private void informListeners(boolean isAdjusting) {
  		for (int i=0; i<mListener.size(); i++)
  			mListener.get(i).pruningBarChanged(
  				new PruningBarEvent(this, mLowValue, mHighValue, isAdjusting, mID, PruningBarEvent.TYPE_DRAGGED));
  		}
  	}
  