3030
3131package org .scijava .ui .swing .widget ;
3232
33- import java .awt .Adjustable ;
3433import java .awt .Component ;
3534import java .awt .Dimension ;
3635import java .awt .event .AdjustmentEvent ;
4342import java .math .BigInteger ;
4443import java .text .DecimalFormat ;
4544import java .text .ParsePosition ;
45+ import java .util .Hashtable ;
4646
4747import javax .swing .JComponent ;
48+ import javax .swing .JLabel ;
4849import javax .swing .JPanel ;
4950import javax .swing .JScrollBar ;
5051import 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