/* * org.openmicroscopy.shoola.util.ui.slider.TextualTwoKnobsSlider * *------------------------------------------------------------------------------ * Copyright (C) 2006-2014 University of Dundee. All rights reserved. * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * *------------------------------------------------------------------------------ */ package org.openmicroscopy.shoola.util.ui.slider; //Java imports import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.DecimalFormat; import java.text.NumberFormat; import javax.swing.BoxLayout; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.Document; //Third-party libraries //Application-internal dependencies import org.openmicroscopy.shoola.util.ui.NumericalTextField; import org.openmicroscopy.shoola.util.ui.UIUtilities; /** * Component hosting a two knobs slider and the text fields displaying the * values. Synchronizes the various components. * * @author Jean-Marie Burel      * <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a> * @author Donald MacDonald      * <a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a> * @version 3.0 * <small> * (<b>Internal version:</b> $Revision: $Date: $) * </small> * @since 3.0-Beta3 */ public class TextualTwoKnobsSlider extends JPanel implements ActionListener, DocumentListener, FocusListener, PropertyChangeListener, KeyListener { /** Indicates to layout all the components. */ public static final int LAYOUT_ALL = 0; /** Indicates to layout all the fields only. */ public static final int LAYOUT_FIELDS = 1; /** Indicates to layout the slider only. */ public static final int LAYOUT_SLIDER = 2; /** Indicates to layout the slider and label only. */ public static final int LAYOUT_SLIDER_AND_LABEL = 3; /** Indicates to layout the slider and label only. */ public static final int LAYOUT_SLIDER_FIELDS_X_AXIS = 4; /** Indicates to layout the slider and label only. */ public static final int LAYOUT_SLIDER_FIELDS_LABELS_X_AXIS = 5; /** The id of the action linked to the {@link #startField}. */ public static final int START = 0; /** The id of the action linked to the {@link #startField}. */ public static final int END = 1; /** The name of the property used to identify the text field. */ private static final String NAME_DOC = "name"; /** The precision used for formatting floating point numbers */ private static final int DECIMAL_PRECISION = 3; /** Format used for printing floating point numbers */ private static final NumberFormat NUMBER_FORMAT; static { String s = "0."; for (int i = 0; i < DECIMAL_PRECISION; i++) s += "0"; NUMBER_FORMAT = new DecimalFormat(s); } /** The slider. */ private TwoKnobsSlider slider; /** The field hosting the start value. */ private NumericalTextField startField; /** The field hosting the end value. */ private NumericalTextField endField; /** Limit the textfield length (columns) to the given size */ private static final int MAX_TEXTFIELD_LENGTH = 10; /** The label displayed in front of the {@link #startField}. */ private JLabel startLabel; /** The label displayed in front of the {@link #endField}. */ private JLabel endLabel; /** The label displayed in front of the {@link #slider}. */ private JLabel sliderLabel; /** The start value. */ private double start; /** The end value. */ private double end; /** Flag to indicate if numbers shall be treated as integers */ private boolean intMode = false; /** * Formats the passed value. * * @param value The value to handle. * @return See above. */ private String formatValue(double value) { String s; if (intMode) { s = ""+((int) value); } else { s = NUMBER_FORMAT.format(value); } return s; } /** Attaches the listeners to the components. */ private void attachListeners() { attachSliderListeners(); installFieldListeners(startField, START); installFieldListeners(endField, END); } /** Removes the listeners from the components. */ private void removeListeners() { removeSliderListeners(); uninstallFieldListeners(startField); uninstallFieldListeners(endField); } /** Attaches the listeners to the slider. */ private void attachSliderListeners() { slider.addPropertyChangeListener(this); } /** Removes the listener attached to the slider. */ private void removeSliderListeners() { slider.removePropertyChangeListener(this); } /** * Installs the various listeners for the passed field. * * @param field The text field to handle. * @param id The id of the action command. */ private void installFieldListeners(JTextField field, int id) { field.setActionCommand(""+id); field.addActionListener(this); field.addFocusListener(this); field.addKeyListener(this); Document doc = field.getDocument(); doc.addDocumentListener(this); doc.putProperty(NAME_DOC, ""+id); } /** * Removes the listeners to the passed component. * * @param field The component to handle. */ private void uninstallFieldListeners(JTextField field) { //field.removeActionListener(this); field.removeFocusListener(this); //field.getDocument().removeDocumentListener(this); } /** * Initializes the components. * * @param absMin The absolute minimum value. * @param absMax The absolute maximum value. * @param min The minimum value. * @param max The maximum value. * @param start The start value. * @param end The end value. */ private void initComponents(double absMin, double absMax, double min, double max, double start, double end) { sliderLabel = new JLabel(); startLabel = new JLabel("Start"); endLabel = new JLabel("End"); slider = new TwoKnobsSlider(absMin, absMax, min, max, start, end); setSliderPaintingDefault(false); Class type = intMode ? Integer.class : Double.class; int textFieldLength = calculateRequiredTextfieldLength(absMin, absMax); startField = new NumericalTextField(absMin, absMax, type, NumericalTextField.VALIDATION_MODE_CORRECT); startField.setColumns(textFieldLength); startField.setShowWarning(true); endField = new NumericalTextField(absMin, absMax, type, NumericalTextField.VALIDATION_MODE_CORRECT); endField.setColumns(textFieldLength); endField.setShowWarning(true); endField.setText(formatValue(end)); startField.setText(formatValue(start)); //No need to check values b/c already done by the slider. this.start = start; this.end = end; } /** * Determines the length (columns) needed for the text fields * * @param min * The minimum value which can be entered * @param max * The maximum value which can be entered * @return The required length for the text fields */ private int calculateRequiredTextfieldLength(double min, double max) { double maxValue = Math.max(Math.abs(min), Math.abs(max)); if (min < 0) maxValue *= -1; int result = formatValue(maxValue).length(); if (result > MAX_TEXTFIELD_LENGTH) result = MAX_TEXTFIELD_LENGTH; return result; } /** Sets the start value. */ private void setStartValue() { boolean valid = false; double val = 0; try { val = Double.parseDouble(startField.getText()); // if (slider.getPartialMinimum() <= val && val < end) valid = true; if (startField.getMinimum() <= val && val <= end) valid = true; } catch (NumberFormatException nfe) { } if (!valid) { startField.selectAll(); return; } start = val; // endField.setMinimum(start); if (start < slider.getPartialMinimum()) { val = slider.getPartialMinimum(); } removeSliderListeners(); slider.setStartValue(val); firePropertyChange(TwoKnobsSlider.KNOB_RELEASED_PROPERTY, null, val); attachSliderListeners(); } /** Sets the end value. */ private void setEndValue() { boolean valid = false; double val = 0; try { val = Double.parseDouble(endField.getText()); // if (start < val && val <= slider.getPartialMaximum()) valid = // true; if (start <= val && val <= endField.getMaximum()) valid = true; } catch (NumberFormatException nfe) { } if (!valid) { endField.selectAll(); return; } end = val; if (end > slider.getPartialMaximum()) { val = slider.getPartialMaximum(); } removeSliderListeners(); slider.setEndValue(val); firePropertyChange(TwoKnobsSlider.KNOB_RELEASED_PROPERTY, null, val); attachSliderListeners(); } /** * Synchronizes the slider and the {@link #startField} when the * left knob is moved. * * @param value The value to set. */ private void synchStartValue(double value) { start = value; uninstallFieldListeners(startField); startField.setText(formatValue(value)); //endField.setMinimum(start); installFieldListeners(startField, START); } /** * Synchronizes the slider and the {@link #endField} when the * right knob is moved. * * @param value The value to set. */ private void synchEndValue(double value) { end = value; uninstallFieldListeners(endField); endField.setText(formatValue(value)); //startField.setMaximum(end); installFieldListeners(endField, END); } /** * Handles the lost of focus on text fields. * * @param field The text field that lost the focus. */ private void handleFocusLost(Object field) { if (field == null) return; if (startField == field) setStartValue(); else if (endField == field) setEndValue(); } /** * Builds and lays out the UI component hosting the text fields. * * @return See above. */ private JPanel buildFieldsPane() { JPanel p = new JPanel(); int charWidth = getFontMetrics(getFont()).charWidth('m'); Insets insets = endField.getInsets(); int length = endField.getColumns(); int x = insets.left+length*charWidth+insets.left; Dimension d = startField.getPreferredSize(); startField.setPreferredSize(new Dimension(x, d.height)); d = endField.getPreferredSize(); endField.setPreferredSize(new Dimension(x, d.height)); GridBagConstraints c = new GridBagConstraints(); p.setLayout(new GridBagLayout()); c.weightx = 0; c.anchor = GridBagConstraints.WEST; p.add(startLabel, c); c.gridx = 1; c.ipadx = x; c.weightx = 0.5; p.add(UIUtilities.buildComponentPanel(startField), c); c.gridx = 2; c.ipadx = 0; c.weightx = 0; p.add(endLabel, c); c.gridx = 3; c.ipadx = x; c.weightx = 0.5; p.add(UIUtilities.buildComponentPanel(endField), c); return p; } /** Creates a default instance. */ public TextualTwoKnobsSlider() { this(TwoKnobsSlider.DEFAULT_MIN, TwoKnobsSlider.DEFAULT_MAX); } /** * Creates a new instance. * * @param min The minimum value. * @param max The maximum value. */ public TextualTwoKnobsSlider(int min, int max) { this(min, max, min, max); } /** * Creates a new instance. * * @param min The minimum value. * @param max The maximum value. */ public TextualTwoKnobsSlider(double min, double max) { this(min, max, min, max); } /** * Creates a new instance. * * @param min The minimum value. * @param max The maximum value. * @param start The start value. * @param end The end value. */ public TextualTwoKnobsSlider(int min, int max, int start, int end) { this(min, max, min, max, start, end); } /** * Creates a new instance. * * @param min The minimum value. * @param max The maximum value. * @param start The start value. * @param end The end value. */ public TextualTwoKnobsSlider(double min, double max, double start, double end) { this(min, max, min, max, start, end); } /** * Creates a new instance. (integer mode) * * @param absMin The absolute minimum value of the slider. * @param absMax The absolute maximum value of the slider. * @param min The minimum value. * @param max The maximum value. * @param start The start value. * @param end The end value. */ public TextualTwoKnobsSlider(int absMin, int absMax, int min, int max, int start, int end) { intMode = true; initComponents(absMin, absMax, min, max, start, end); attachListeners(); } /** * Creates a new instance. (floating point mode) * * @param absMin The absolute minimum value of the slider. * @param absMax The absolute maximum value of the slider. * @param min The minimum value. * @param max The maximum value. * @param start The start value. * @param end The end value. */ public TextualTwoKnobsSlider(double absMin, double absMax, double min, double max, double start, double end) { intMode = false; initComponents(absMin, absMax, min, max, start, end); attachListeners(); } /** * Sets the text of the {@link #startLabel}. * * @param text The value to set. */ public void setStartLabelText(String text) { startLabel.setText(text); } /** * Sets the text of the {@link #endLabel}. * * @param text The value to set. */ public void setEndLabelText(String text) { endLabel.setText(text); } /** * Sets the text of the {@link #sliderLabel}. * * @param text The value to set. */ public void setSliderLabelText(String text) { sliderLabel.setText(text); } /** * Returns the start value. * * @return See above. */ public double getStartValue() { return start; } /** * Returns the end value. * * @return See above. */ public double getEndValue() { return end; } /** Lays out the components. */ public void layoutComponents() { layoutComponents(LAYOUT_ALL); } /** * Lays out the components identified by the layout index. * * @param index The layout index. */ public void layoutComponents(int index) { switch (index) { case LAYOUT_FIELDS: add(buildFieldsPane()); break; case LAYOUT_SLIDER: add(slider); break; case LAYOUT_SLIDER_FIELDS_X_AXIS: setLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.gridx = 0; c.gridy = 0; c.weightx = 0; c.weighty = 0; c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.NONE; add(startField, c); c.gridx++; c.weightx = 1; c.fill = GridBagConstraints.HORIZONTAL; add(slider, c); c.gridx++; c.weightx = 0; c.fill = GridBagConstraints.NONE; add(endField, c); break; case LAYOUT_SLIDER_FIELDS_LABELS_X_AXIS: setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); add(sliderLabel); add(startField); add(slider); add(endField); add(endLabel); break; case LAYOUT_SLIDER_AND_LABEL: JPanel content = new JPanel(); content.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); content.add(sliderLabel); content.add(UIUtilities.buildComponentPanel(slider)); add(content); break; case LAYOUT_ALL: default: JPanel content1 = new JPanel(); content1.setLayout(new BoxLayout(content1, BoxLayout.Y_AXIS)); JPanel row = new JPanel(); row.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); row.add(sliderLabel); row.add(UIUtilities.buildComponentPanel(slider)); content1.add(row); content1.add(buildFieldsPane()); add(content1); } } /** * Sets the painting default of the slider. * * @param paint Pass <code>true</code> to paint labels and ticks, * <code>false</code> otherwise. */ public void setSliderPaintingDefault(boolean paint) { slider.setPaintLabels(paint); slider.setPaintEndLabels(paint); slider.setPaintTicks(paint); } /** * Returns the slider hosted by this component. * * @return See above. */ public TwoKnobsSlider getSlider() { return slider; } /** * Resets the default value of the slider. * * @param absoluteMax The absolute maximum value of the slider. * @param absoluteMin The absolute minimum value of the slider. * @param max The maximum value. * @param min The minimum value. * @param start The value of the start knob. * @param end The value of the end knob. */ public void setValues(int absoluteMax, int absoluteMin, int max, int min, int start, int end) { setValues(absoluteMax, absoluteMin, absoluteMax, absoluteMin, max, min, start, end); } /** * Resets the default value of the slider, using integer mode; * * @param absoluteMaxSlider The absolute maximum value of the slider. * @param absoluteMinSlider The absolute minimum value of the slider. * @param absoluteMaxText The absolute maximum value of the slider. * @param absoluteMinText The absolute minimum value of the slider. * @param max The maximum value. * @param min The minimum value. * @param start The value of the start knob. * @param end The value of the end knob. */ public void setValues(int absoluteMaxSlider, int absoluteMinSlider, int absoluteMaxText, int absoluteMinText, int max, int min, int start, int end) { slider.setValues(absoluteMaxSlider, absoluteMinSlider, max, min, start, end); removeListeners(); intMode = true; int textFieldLength = calculateRequiredTextfieldLength(absoluteMinText, absoluteMaxText); startField.setNumberType(Integer.class); endField.setNumberType(Integer.class); startField.setColumns(textFieldLength); endField.setColumns(textFieldLength); endField.setMaximum(absoluteMaxText); endField.setMinimum(absoluteMinText); startField.setMaximum(absoluteMaxText); startField.setMinimum(absoluteMinText); endField.setText(formatValue(end)); startField.setText(formatValue(start)); StringBuffer buffer = new StringBuffer(); buffer.append("<html><body>"); buffer.append("<b>Min:</b> "+min); buffer.append("<br><b>Pixels Type Min: </b>"+absoluteMinText); buffer.append("</body></html>"); startField.setToolTipText(buffer.toString()); buffer = new StringBuffer(); buffer.append("<html><body>"); buffer.append("<b>Max:</b> "+max); buffer.append("<br><b>Pixels Type Max: </b>"+absoluteMaxText); buffer.append("</body></html>"); endField.setToolTipText(buffer.toString()); this.start = start; this.end = end; attachListeners(); } /** * Resets the default value of the slider, using floating point number mode; * * @param absoluteMaxSlider The absolute maximum value of the slider. * @param absoluteMinSlider The absolute minimum value of the slider. * @param absoluteMaxText The absolute maximum value of the slider. * @param absoluteMinText The absolute minimum value of the slider. * @param max The maximum value. * @param min The minimum value. * @param start The value of the start knob. * @param end The value of the end knob. */ public void setValues(double absoluteMaxSlider, double absoluteMinSlider, double absoluteMaxText, double absoluteMinText, double max, double min, double start, double end) { slider.setValues(absoluteMaxSlider, absoluteMinSlider, max, min, start, end); removeListeners(); intMode = false; int textFieldLength = calculateRequiredTextfieldLength(absoluteMinText, absoluteMinText); startField.setNumberType(Double.class); startField.setNegativeAccepted(true); endField.setNumberType(Double.class); endField.setNegativeAccepted(true); startField.setColumns(textFieldLength); endField.setColumns(textFieldLength); endField.setMaximum(absoluteMaxText); endField.setMinimum(absoluteMinText); startField.setMaximum(absoluteMaxText); startField.setMinimum(absoluteMinText); endField.setText(formatValue(end)); startField.setText(formatValue(start)); StringBuffer buffer = new StringBuffer(); buffer.append("<html><body>"); buffer.append("<b>Min:</b> "+min); buffer.append("<br><b>Pixels Type Min: </b>"+absoluteMinText); buffer.append("</body></html>"); startField.setToolTipText(buffer.toString()); buffer = new StringBuffer(); buffer.append("<html><body>"); buffer.append("<b>Max:</b> "+max); buffer.append("<br><b>Pixels Type Max: </b>"+absoluteMaxText); buffer.append("</body></html>"); endField.setToolTipText(buffer.toString()); this.start = start; this.end = end; attachListeners(); } /** * Returns the number of the columns. * * @return See above. */ public int getColumns() { return startField.getColumns(); } /** * Returns the text field corresponding to the passed index. * * @param index The index identifying the component. * @return See above. */ public JComponent getFieldComponent(int index) { switch (index) { case START: return startField; case END: return endField; } return null; } /** * Sets the value of the slider and the text field. * * @param s The start value to set. * @param e The end value to set. */ public void setInterval(double s, double e) { removeListeners(); endField.setText(formatValue(e)); startField.setText(formatValue(s)); slider.setStartValue(s); slider.setEndValue(e); start = s; end = e; attachListeners(); } /** * Sets the color gradient of the slider. * * @param rgbStart The gradient start. * @param rgbEnd The gradient end. */ public void setColourGradients(Color rgbStart, Color rgbEnd) { slider.setColourGradients(rgbStart, rgbEnd); } /** * Overridden to set the text fields and the slider enabled. * @see JPanel#setEnabled(boolean) */ public void setEnabled(boolean enabled) { super.setEnabled(enabled); slider.setEnabled(enabled); endField.setEnabled(enabled); startField.setEnabled(enabled); } /** * Overridden to set the background color. * @see JPanel#setBackground(Color c) */ public void setBackground(Color c) { super.setBackground(c); if (slider != null) slider.setBackground(c); if (endField != null) endField.setBackground(c); if (startField != null) startField.setBackground(c); } /** * Overridden to set the font of the various components. * @see JPanel#setFont(Font) */ public void setFont(Font font) { super.setFont(font); if (slider != null) slider.setFont(font); if (endField != null) endField.setFont(font); if (startField != null) startField.setFont(font); } /** * Updates the text field related to the knob moved. * @see PropertyChangeListener#propertyChange(PropertyChangeEvent) */ public void propertyChange(PropertyChangeEvent evt) { String name = evt.getPropertyName(); if (TwoKnobsSlider.LEFT_MOVED_PROPERTY.equals(name)) { Double value = (Double) evt.getNewValue(); synchStartValue(value); } else if (TwoKnobsSlider.RIGHT_MOVED_PROPERTY.equals(name)) { Double value = (Double) evt.getNewValue(); synchEndValue(value); } firePropertyChange(name, evt.getOldValue(), evt.getNewValue()); } /** * Sets the start or end value. * @see ActionListener#actionPerformed(ActionEvent) */ public void actionPerformed(ActionEvent e) { int index = Integer.parseInt(e.getActionCommand()); switch (index) { case START: setStartValue(); break; case END: setEndValue(); } } /** * Handles the lost of focus on the various text fields. * If focus is lost while editing, then we don't consider the text * currently displayed in the text field and we reset it to the current * value. * @see FocusListener#focusLost(FocusEvent) */ public void focusLost(FocusEvent e) { handleFocusLost(e.getSource()); } /** * Updates the field whose text is modified. * @see DocumentListener#insertUpdate(DocumentEvent) */ public void insertUpdate(DocumentEvent e) { //updateTextValue(e.getDocument()); } /** * @see DocumentListener#removeUpdate(DocumentEvent) */ public void removeUpdate(DocumentEvent e) { } /** * Sets the start or end value depending on the selected fields * @see KeyListener#keyPressed(KeyEvent) */ public void keyPressed(KeyEvent e) { if (KeyEvent.VK_ENTER == e.getKeyCode()) { Object source = e.getSource(); if (source == startField) setStartValue(); else if (source == endField) setEndValue(); } } /** * Required by the {@link DocumentListener} I/F but not actually needed in * our case, no-operation implementation. * @see DocumentListener#changedUpdate(DocumentEvent) */ public void changedUpdate(DocumentEvent e) {} /** * Required by {@link FocusListener} I/F but not actually needed in * our case, no-operation implementation. * @see FocusListener#focusGained(FocusEvent) */ public void focusGained(FocusEvent e) {} /** * Required by {@link KeyListener} I/F but not actually needed in * our case, no-operation implementation. * @see KeyListener#keyReleased(KeyEvent) */ public void keyReleased(KeyEvent e) {} /** * Required by {@link KeyListener} I/F but not actually needed in * our case, no-operation implementation. * @see KeyListener#keyTyped(KeyEvent) */ public void keyTyped(KeyEvent e) {} }