Skip to content

Commit 96b4a3b

Browse files
authoredJun 5, 2018
Merge pull request #35 from scijava/calibrated-sliders
Let sliders in number widget support floating point numbers
2 parents b6b7924 + 3a91c1a commit 96b4a3b

File tree

1 file changed

+177
-40
lines changed

1 file changed

+177
-40
lines changed
 

‎src/main/java/org/scijava/ui/swing/widget/SwingNumberWidget.java

+177-40
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030

3131
package org.scijava.ui.swing.widget;
3232

33-
import java.awt.Adjustable;
3433
import java.awt.Component;
3534
import java.awt.Dimension;
3635
import java.awt.event.AdjustmentEvent;
@@ -43,8 +42,10 @@
4342
import java.math.BigInteger;
4443
import java.text.DecimalFormat;
4544
import java.text.ParsePosition;
45+
import java.util.Hashtable;
4646

4747
import javax.swing.JComponent;
48+
import javax.swing.JLabel;
4849
import javax.swing.JPanel;
4950
import javax.swing.JScrollBar;
5051
import javax.swing.JSlider;
@@ -82,8 +83,8 @@ public class SwingNumberWidget extends SwingInputWidget<Number> implements
8283
@Parameter
8384
private LogService log;
8485

85-
private JScrollBar scrollBar;
86-
private JSlider slider;
86+
private CalibratedScrollBar scrollBar;
87+
private CalibratedSlider slider;
8788
private JSpinner spinner;
8889

8990
// -- InputWidget methods --
@@ -119,6 +120,9 @@ else if (model.isStyle(NumberWidget.SLIDER_STYLE)) {
119120
final SpinnerNumberModel spinnerModel =
120121
new SpinnerNumberModelFactory().createModel(value, min, max, stepSize);
121122
spinner = new JSpinner(spinnerModel);
123+
Dimension spinnerSize = spinner.getSize();
124+
spinnerSize.width = 50;
125+
spinner.setPreferredSize(spinnerSize);
122126
fixSpinner(type);
123127
setToolTip(spinner);
124128
getComponent().add(spinner);
@@ -141,7 +145,7 @@ public boolean supports(final WidgetModel model) {
141145
@Override
142146
public void adjustmentValueChanged(final AdjustmentEvent e) {
143147
// sync spinner with scroll bar value
144-
final int value = scrollBar.getValue();
148+
final Number value = scrollBar.getCalibratedValue();
145149
spinner.setValue(value);
146150
}
147151

@@ -152,7 +156,7 @@ public void stateChanged(final ChangeEvent e) {
152156
final Object source = e.getSource();
153157
if (source == slider) {
154158
// sync spinner with slider value
155-
final int value = slider.getValue();
159+
final Number value = slider.getCalibratedValue();
156160
spinner.setValue(value);
157161
}
158162
else if (source == spinner) {
@@ -166,9 +170,9 @@ else if (source == spinner) {
166170

167171
@Override
168172
public void mouseWheelMoved(final MouseWheelEvent e) {
169-
int value = getValue().intValue() + e.getWheelRotation();
170-
value = Math.min(value, this.get().getMax().intValue());
171-
value = Math.max(value, this.get().getMin().intValue());
173+
Number value = getValue().doubleValue() + e.getWheelRotation() * get().getStepSize().doubleValue();
174+
value = Math.min(value.doubleValue(), this.get().getMax().doubleValue());
175+
value = Math.max(value.doubleValue(), this.get().getMin().doubleValue());
172176
spinner.setValue(value);
173177
syncSliders();
174178
}
@@ -182,14 +186,17 @@ private void addScrollBar(final Number min, final Number max,
182186
log.warn("Invalid min/max/step; cannot render scroll bar");
183187
return;
184188
}
185-
int mn = min.intValue();
186-
if (mn == Integer.MIN_VALUE) mn = Integer.MIN_VALUE + 1;
187-
int mx = max.intValue();
188-
if (mx < Integer.MAX_VALUE) mx++;
189-
final int st = step.intValue();
190-
191-
scrollBar = new JScrollBar(Adjustable.HORIZONTAL, mn, 1, mn, mx);
192-
scrollBar.setUnitIncrement(st);
189+
190+
// TODO Integer cases can possibly be handled in a simpler way
191+
int sMin = 0;
192+
int sMax = (int) ((max.doubleValue() - min.doubleValue()) / step.doubleValue());
193+
long range = sMax - sMin;
194+
if (range > Integer.MAX_VALUE) {
195+
log.warn("Scrollbar span too large; max - min < 2^31 required.");
196+
return;
197+
}
198+
199+
scrollBar = new CalibratedScrollBar(min, max, step);
193200
setToolTip(scrollBar);
194201
getComponent().add(scrollBar);
195202
scrollBar.addAdjustmentListener(this);
@@ -203,26 +210,17 @@ private void addSlider(final Number min, final Number max,
203210
log.warn("Invalid min/max/step; cannot render slider");
204211
return;
205212
}
206-
final int mn = min.intValue();
207-
final int mx = max.intValue();
208-
final int st = step.intValue();
209-
if ((long) mx - mn > Integer.MAX_VALUE) {
213+
214+
// TODO Integer cases can possibly be handled in a simpler way
215+
int sMin = 0;
216+
int sMax = (int) ((max.doubleValue() - min.doubleValue()) / step.doubleValue());
217+
long range = sMax - sMin;
218+
if (range > Integer.MAX_VALUE) {
210219
log.warn("Slider span too large; max - min < 2^31 required.");
211220
return;
212221
}
213-
final int span = mx - mn;
214222

215-
slider = new JSlider(mn, mx, mn);
216-
217-
// Compute optimal major ticks and labels.
218-
final int labelWidth = Math.max(("" + mn).length(), ("" + mx).length());
219-
slider.setMajorTickSpacing(labelWidth < 5 ? span / 4 : span);
220-
slider.setPaintLabels(labelWidth < 10);
221-
222-
// Compute optimal minor ticks.
223-
final int stepCount = span / st + 1;
224-
slider.setMinorTickSpacing(st);
225-
slider.setPaintTicks(stepCount < 100);
223+
slider = new CalibratedSlider(min, max, step);
226224

227225
setToolTip(slider);
228226
getComponent().add(slider);
@@ -314,20 +312,20 @@ public void run() {
314312
private void syncSliders() {
315313
if (slider != null) {
316314
// clamp value within slider bounds
317-
int value = getValue().intValue();
318-
if (value < slider.getMinimum()) value = slider.getMinimum();
319-
else if (value > slider.getMaximum()) value = slider.getMaximum();
315+
Number value = getValue();
316+
if (value.doubleValue() < slider.getCalibratedMinimum().doubleValue()) value = slider.getCalibratedMinimum();
317+
else if (value.doubleValue() > slider.getCalibratedMaximum().doubleValue()) value = slider.getCalibratedMaximum();
320318
slider.removeChangeListener(this);
321-
slider.setValue(value);
319+
slider.setCalibratedValue(value);
322320
slider.addChangeListener(this);
323321
}
324322
if (scrollBar != null) {
325323
// clamp value within scroll bar bounds
326-
int value = getValue().intValue();
327-
if (value < scrollBar.getMinimum()) value = scrollBar.getMinimum();
328-
else if (value > scrollBar.getMaximum()) value = scrollBar.getMaximum();
324+
Number value = getValue();
325+
if (value.doubleValue() < scrollBar.getCalibratedMinimum().doubleValue()) value = scrollBar.getCalibratedMinimum();
326+
else if (value.doubleValue() > scrollBar.getCalibratedMaximum().doubleValue()) value = scrollBar.getCalibratedMaximum();
329327
scrollBar.removeAdjustmentListener(this);
330-
scrollBar.setValue(getValue().intValue());
328+
scrollBar.setCalibratedValue(value);
331329
scrollBar.addAdjustmentListener(this);
332330
}
333331
}
@@ -340,4 +338,143 @@ public void doRefresh() {
340338
if (spinner.getValue().equals(value)) return; // no change
341339
spinner.setValue(value);
342340
}
341+
342+
private class CalibratedSlider extends JSlider {
343+
344+
private Number min;
345+
private Number max;
346+
private Number stepSize;
347+
348+
private CalibratedSlider(final Number min, final Number max, final Number stepSize) {
349+
super();
350+
351+
this.min = min;
352+
this.max = max;
353+
this.stepSize = stepSize;
354+
355+
int sMin = 0;
356+
int sMax = (int) ((max.doubleValue() - min.doubleValue()) / stepSize.doubleValue());
357+
358+
// Adjust max to be an integer multiple of stepSize
359+
this.max = min.doubleValue() + (sMax-sMin) * stepSize.doubleValue();
360+
361+
setMinimum(sMin);
362+
setMaximum(sMax);
363+
setValue(sMin);
364+
365+
// Compute label width to determine number of labels
366+
int scale = Math.max(0, new BigDecimal(stepSize.toString()).stripTrailingZeros().scale());
367+
JLabel minLabel = makeLabel(min, scale);
368+
JLabel maxLabel = makeLabel(max, scale);
369+
final int labelWidth = Math.max(minLabel.getText().length(), maxLabel.getText().length());
370+
371+
// Add labels
372+
Hashtable<Integer, JLabel> labelTable = new Hashtable<>(2);
373+
labelTable.put(sMin, minLabel);
374+
labelTable.put(sMax, maxLabel);
375+
if (labelWidth < 5 && sMax % 5 == 0) {
376+
// Put four intermediate labels
377+
labelTable.put(1 * sMax / 5,
378+
makeLabel(toCalibrated(1 * sMax / 5), scale));
379+
labelTable.put(2 * sMax / 5,
380+
makeLabel(toCalibrated(2 * sMax / 5), scale));
381+
labelTable.put(3 * sMax / 5,
382+
makeLabel(toCalibrated(3 * sMax / 5), scale));
383+
labelTable.put(4 * sMax / 5,
384+
makeLabel(toCalibrated(4 * sMax / 5), scale));
385+
} else if (labelWidth < 6) {
386+
// Put three intermediate labels
387+
labelTable.put(1 * sMax / 4,
388+
makeLabel(toCalibrated(1 * sMax / 4), scale));
389+
labelTable.put(2 * sMax / 4,
390+
makeLabel(toCalibrated(2 * sMax / 4), scale));
391+
labelTable.put(3 * sMax / 4,
392+
makeLabel(toCalibrated(3 * sMax / 4), scale));
393+
}
394+
setLabelTable(labelTable);
395+
setPaintLabels(true);
396+
setMinorTickSpacing(1);
397+
setPaintTicks(sMax < 100);
398+
}
399+
400+
private void setCalibratedValue(Number value) {
401+
setValue(fromCalibrated(value));
402+
}
403+
404+
private Number getCalibratedValue() {
405+
return toCalibrated(getValue());
406+
}
407+
408+
private Number getCalibratedMinimum() {
409+
return min;
410+
}
411+
412+
private Number getCalibratedMaximum() {
413+
return max;
414+
}
415+
416+
private int fromCalibrated(Number n) {
417+
return (int) Math.round((n.doubleValue() - min.doubleValue()) / stepSize.doubleValue());
418+
}
419+
420+
private Number toCalibrated(int n) {
421+
return n * stepSize.doubleValue() + min.doubleValue();
422+
}
423+
424+
private JLabel makeLabel(Number n, int scale) {
425+
return new JLabel(String.format("%." + scale + "f", n.doubleValue()));
426+
}
427+
428+
}
429+
430+
private class CalibratedScrollBar extends JScrollBar {
431+
432+
private Number min;
433+
private Number max;
434+
private Number stepSize;
435+
436+
private CalibratedScrollBar(final Number min, final Number max, final Number stepSize) {
437+
// set extent to 1 to make sure the scroll bar is visible
438+
super(HORIZONTAL, 0, 1, 0, 1);
439+
440+
this.min = min;
441+
this.max = max;
442+
this.stepSize = stepSize;
443+
444+
int sMin = 0;
445+
int sMax = (int) ((max.doubleValue() - min.doubleValue()) / stepSize.doubleValue()) + 1;
446+
447+
// Adjust max to be an integer multiple of stepSize
448+
this.max = min.doubleValue() + (sMax-sMin) * stepSize.doubleValue();
449+
450+
setMinimum(sMin);
451+
setMaximum(sMax);
452+
setValue(sMin);
453+
}
454+
455+
private void setCalibratedValue(Number value) {
456+
setValue(fromCalibrated(value));
457+
}
458+
459+
private Number getCalibratedValue() {
460+
return toCalibrated(getValue());
461+
}
462+
463+
private Number getCalibratedMinimum() {
464+
return min;
465+
}
466+
467+
private Number getCalibratedMaximum() {
468+
return max;
469+
}
470+
471+
private int fromCalibrated(Number n) {
472+
return (int) Math.round((n.doubleValue() - min.doubleValue()) / stepSize.doubleValue());
473+
}
474+
475+
private Number toCalibrated(int n) {
476+
return n * stepSize.doubleValue() + min.doubleValue();
477+
}
478+
479+
}
343480
}

0 commit comments

Comments
 (0)
Please sign in to comment.