/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * 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. * * * Neither the name of Business Objects 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. */ /* * SliderNumberValueEntryPanel.java * Creation date: Sept 30, 2004 * By: Richard Webster */ package org.openquark.gems.client.valueentry; import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.text.NumberFormat; import java.util.Hashtable; import javax.swing.JLabel; import javax.swing.JSlider; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.openquark.cal.compiler.TypeExpr; import org.openquark.cal.valuenode.LiteralValueNode; import org.openquark.cal.valuenode.ValueNode; import org.openquark.util.OrientationType; import org.openquark.util.ui.NumericTextField; /** * A value entry panel displaying a slider for setting and displaying the value. * @author Richard Webster */ public class SliderNumberValueEntryPanel extends ValueEntryPanel { private static final long serialVersionUID = 6895591427785034118L; /** * When the state of the slider changes, we have to update the ValueNode and * the label's text. * It also notifies the ValueEditorListeners of the value change. */ private class SliderChangeListener implements ChangeListener { public void stateChanged(ChangeEvent evt) { if (!ignoreChangeEvents) { double newValue = sliderValueToRealValue(slider.getValue()); changeValue(newValue); // Update the label. ignoreChangeEvents = true; label.setText(getCurrentValueString()); ignoreChangeEvents = false; // Commit the value when the user lets go of the slider. if (!slider.getValueIsAdjusting()) { handleCommitGesture(); } } } } /** The number of slider positions to use (when using fractional values). */ private static final int N_SLIDER_POSITIONS = 100; /** The slider control. */ private final JSlider slider; /** A text entry field for showing the current value. */ private JTextField label; /** Whether the values must be integers or whether the values can be fractional. */ private final boolean integerValues; /** The scaling factor between the real values and the slider integer values. */ private final double scalingFactor; /** The current value displayed in the controls. */ private double currentValue; /** This flag will be true when changing the value of the slider or label * programatically in order to avoid extra events from being fired. */ private boolean ignoreChangeEvents = false; /** * SliderNumberValueEntryPanel constructor. * @param valueEditorHierarchyManager the value editor hierarchy manager * @param valueNode the initial value for the VEP * @param realLowerBound the lower bound value for the slider * @param realUpperBound the upper bound value for the slider * @param orientation the orientation for the slider * @param integerValues True if working with integral values */ public SliderNumberValueEntryPanel(ValueEditorHierarchyManager valueEditorHierarchyManager, ValueNode valueNode, double realLowerBound, double realUpperBound, OrientationType orientation, boolean integerValues) { super(valueEditorHierarchyManager, valueNode); this.integerValues = integerValues; // Make sure that the upper bound is higher than the lower bound. realUpperBound = (realUpperBound > realLowerBound) ? realUpperBound : (realLowerBound + 1.0); // Update the displayed values. currentValue = 0; Object value = getValueNode().getValue(); if (value instanceof Number) { currentValue = ((Number) value).doubleValue(); } // Make sure that the current value is within the slider bounds. realLowerBound = Math.min(realLowerBound, currentValue); realUpperBound = Math.max(realUpperBound, currentValue); // Determine the scaling factor between the real values and the slider integer values. this.scalingFactor = integerValues ? 1.0 : (realUpperBound - realLowerBound) / N_SLIDER_POSITIONS; this.slider = new JSlider(orientation.isHorizontal() ? SwingConstants.HORIZONTAL : SwingConstants.VERTICAL, realValueToSliderValue(realLowerBound), realValueToSliderValue(realUpperBound), realValueToSliderValue(currentValue)); initializeUI(realLowerBound, realUpperBound, orientation); } /** * Initialize the UI components for this Value Entry Panel */ private void initializeUI(final double lowerBound, final double upperBound, OrientationType orientation) { // We don't need the launch editor button, nor the value text field. this.remove(getLaunchEditorButton()); this.remove(this.getValueField()); // Configure the slider. slider.addChangeListener(new SliderChangeListener()); // Show tick marks and labels in the slider. final int nMajorTicks = 5; int majorTickSpacing; if (integerValues) { majorTickSpacing = ((int) upperBound - (int) lowerBound + 2) / nMajorTicks; } else { majorTickSpacing = N_SLIDER_POSITIONS / nMajorTicks; } int minorTickSpacing = majorTickSpacing / 2; slider.setMajorTickSpacing(majorTickSpacing); slider.setMinorTickSpacing(minorTickSpacing); slider.setPaintTicks(true); slider.setPaintLabels(true); slider.setBackground(Color.white); slider.setInverted(orientation.isReversed()); // Create the label table. // Note that a Hashtable is required by slider.setLabelTable(). Hashtable<Integer, JLabel> labelTable = new Hashtable<Integer, JLabel>(); for (int i = 0; i <= nMajorTicks; ++i) { double dblVal = lowerBound + sliderValueToRealValue(i * majorTickSpacing); String labelVal = formatDouble(dblVal); JLabel label = new JLabel(labelVal); // TODO: can the formatting of the labels be set to match the rest of the component? labelTable.put(new Integer(realValueToSliderValue(lowerBound) + i * majorTickSpacing), label); } slider.setLabelTable(labelTable); // Cancel the sliding action if the Esc key is pressed. slider.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { handleCancelGesture(); currentValue = 0; Object value = getValueNode().getValue(); if (value instanceof Number) { currentValue = ((Number) value).doubleValue(); } ignoreChangeEvents = true; slider.setValue(realValueToSliderValue(currentValue)); label.setText(getCurrentValueString()); ignoreChangeEvents = false; // TODO: find a proper way to reset the UI... slider.updateUI(); } } }); this.add(slider, BorderLayout.CENTER); // Create the value label. label = integerValues ? NumericTextField.createIntegerTextField() : NumericTextField.createDecimalTextField(); label.setText(getCurrentValueString()); label.setBackground(Color.white); label.setBorder(null); label.setHorizontalAlignment(orientation.isHorizontal() ? SwingConstants.RIGHT : SwingConstants.CENTER); // TODO: do this properly for the fractional values case... int nColumns = integerValues ? Math.max(nCharsForValue((int) lowerBound), nCharsForValue((int) upperBound)) : 4; label.setColumns(nColumns); label.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (!ignoreChangeEvents) { double newValue = Double.parseDouble(label.getText()); if (newValue >= lowerBound && newValue <= upperBound) { changeValue(newValue); // Update the slider. ignoreChangeEvents = true; slider.setValue(realValueToSliderValue(currentValue)); ignoreChangeEvents = false; // Commit the change. handleCommitGesture(); } } } }); this.add(label, orientation.isHorizontal() ? orientation.isReversed() ? BorderLayout.WEST : BorderLayout.EAST : orientation.isReversed() ? BorderLayout.NORTH : BorderLayout.SOUTH); } /** * Returns the slider value corresponding to a real value. * @param realValue a real value * @return the slider value corresponding to a real value */ private int realValueToSliderValue(double realValue) { return (int) Math.round(realValue / scalingFactor); } /** * Returns the real value corresponding to a slider value. * @param sliderValue a slider value * @return the real value corresponding to a slider value */ private double sliderValueToRealValue(int sliderValue) { return sliderValue * scalingFactor; } /** * Sets the current value to the specified value. * @param newValue the new current value */ private void changeValue(double newValue) { currentValue = newValue; ValueNode oldValue = getValueNode(); Object newValueObj = integerValues ? (Object) new Integer((int) newValue) : (Object) new Double(newValue); TypeExpr typeExpr = getValueNode().getTypeExpr(); replaceValueNode(new LiteralValueNode(newValueObj, typeExpr), true); notifyValueChanged(oldValue); } /** * Returns a string representation of the current value. */ private String getCurrentValueString() { if (integerValues) { return String.valueOf((int) currentValue); } else { return formatDouble(currentValue); } } /** * Formats a double value as a string. * @param dblVal a double value * @return the formatted double value */ private String formatDouble(double dblVal) { // TODO: figure this out automatically... final int nDecimalPlaces = 2; // TODO: perhaps this should use a NumberFormatter... NumberFormat numberFormat = NumberFormat.getInstance (); numberFormat.setMinimumFractionDigits (0); numberFormat.setMaximumFractionDigits (nDecimalPlaces); return numberFormat.format(dblVal); } /** * Returns the number of chars needed to represent the specified integer value. * @param value the integer value to be represented. * @return the number of chars needed to represent the integer value */ private int nCharsForValue(int value) { int nDigits = (value == 0) ? 1 : (int) (Math.log(Math.abs(value)) / Math.log(10.0)) + 1; return (value >= 0) ? nDigits : nDigits + 1; } /** * @see org.openquark.gems.client.valueentry.ValueEditor#setEditable(boolean) */ @Override public void setEditable(boolean isEditable) { super.setEditable(isEditable); slider.setEnabled(isEditable); label.setEditable(isEditable); } }