com.actelion.research.gui.JPruningBar Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openchemlib Show documentation
Show all versions of openchemlib Show documentation
Open Source Chemistry Library
/*
* 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 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 not 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