views:

78

answers:

3

I have a JSpinner using a SpinnerDateModel which has a start at Jan 1, 2010 00:00:00.000 the end date is Jan 1, 2010 00:12:34.217. I would like my JSpinner.DateEditor to use the format HH:mm:ss.SSS but the spinner doesn't spin with this format. It only spins when "yyyy" is added to the format. How can I get around this?

import java.awt.GridLayout;
import java.util.*;
import javax.swing.*;

public class T extends JPanel {

    public T() {
        super(new GridLayout(2, 2));
        init();
    }

    private void init() {
        Calendar start = GregorianCalendar.getInstance();
        Calendar end = GregorianCalendar.getInstance();
        start.clear();
        end.clear();
        start.set(Calendar.YEAR, 2010);
        end.set(Calendar.YEAR, 2010);
        end.add(Calendar.HOUR_OF_DAY, 12);
        SpinnerDateModel m1 =
                new SpinnerDateModel(start.getTime(), start.getTime(),
                end.getTime(), Calendar.MILLISECOND);
        SpinnerDateModel m2 =
                new SpinnerDateModel(start.getTime(), start.getTime(),
                end.getTime(), Calendar.MILLISECOND);
        JSpinner workingSpinner = new JSpinner(m1);
        workingSpinner.setEditor(
                new JSpinner.DateEditor(workingSpinner,
                "yyyy HH:mm:ss.SSS"));
        JSpinner notWorkingSpinner = new JSpinner(m2);
        notWorkingSpinner.setEditor(
                new JSpinner.DateEditor(notWorkingSpinner,
                "HH:mm:ss.SSS"));
        add(new JLabel("Working"));
        add(workingSpinner);
        add(new JLabel("!Working"));
        add(notWorkingSpinner);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new T());
        frame.pack();
        frame.setVisible(true);
    }
}
A: 

I am not sure why that is not working but if you change the declaration of m2 to:

SpinnerDateModel m2 = new SpinnerDateModel(); m2.setValue(start.getTime());

it works.

Switch Commerce
That's because m2 will no longer have an end time which is required.
initialZero
This comment was wrong so I deleted it.
Switch Commerce
+2  A: 

After a good bit of digging around in the JRE source I discovered the the spinner is backed by a text value instead of a true date. When you hit the up and down spin buttons, the value is parsed and then compared to your min and max values. Because your format does not have a year the dates are parsed with the year always being 1970 which is year offset 0 from the epoch. This causes the spinner to always return an out of range error when you try to spin it.

The quickest solution is to simply use 1970 as your year instead of 2010. However, if your initial date is at the end of 1970 the spinner won't let your users roll over into January of 1971 (instead it may jump back to the beginning of 1970).

The other solution can accomodate dates that span calendar year boundaries. However, it is not as simple (or pretty). In the JRE when the DateFormatter parses the date string, it instantiates a class dynamically using a single String parameter constructor. This string is the date from the spinner. By default, this class is either Date or some subclass of it. We can have the formatter instantiate our own Date class which fixes the year before any date comparisons are performed.


Date class that adds the year:

public static class DateThatAddsYear extends Date {
 public DateThatAddsYear( String time ) {
  super( time );
  Calendar cal = GregorianCalendar.getInstance();
  cal.setTime( this );
  // Jump back to 2010, this needs to be implemented more thoroughly in order 
  // to support dates crossing calendar year boundaries
  cal.set( Calendar.YEAR, 2010 );
  setTime( cal.getTimeInMillis() );
 }
}

Manually set up the spinner, using our date fix:

JSpinner notWorkingSpinner = new JSpinner(m2);
JSpinner.DateEditor dateEditor = new JSpinner.DateEditor(notWorkingSpinner);
DateFormatter formatter = new DateFormatter( format );
notWorkingSpinner.setEditor(dateEditor);
dateEditor.getTextField().setFormatterFactory( new DefaultFormatterFactory( formatter ) );
formatter.setValueClass( DateThatAddsYear.class ); // Tell it to use a different value class!

Ugly, but it works.

Also, If you want to poke around in the JRE source I suggest looking at the public method stringToValue(String text) of InternationalFormatter (superclass of DateFormatter).

Andy
Correct, this fails to stop at 12:00:00.000. Much like not setting end in the model.
initialZero
Also, for some reason your example doesn't spin backwards either.
initialZero
Updated -- the problem was much deeper than I had originally thought.
Andy
Good catch about the 1970 Andy. I got it to work completely with one quick and much cleaner fix.Remove all of your original Calendar code, and use this instead:GregorianCalendar start = new GregorianCalendar(1970, Calendar.JANUARY, 1, 0, 0, 0);GregorianCalendar end = new GregorianCalendar(1970, Calendar.JANUARY, 1, 12, 0, 0);
RD
Yeah, that works as well, but with that solution you are limited to dates that are within a single year, i.e., if Dec. 31st is your starting day.. you will not be able to roll over into Jan 1 of the following year! If you implement your custom class properly you can allow dates that span across years.
Andy
Everything was working before he attempted to remove the year from his SimpleDateFormat string, so I assume you can use his original code for spanning across years. His error case appears to be more along the lines of a special case workaround.
RD
Yep: If you have the year it's fine, but if you don't have the year in your format then your spinner won't be able to span across years. This was a pretty fun challenge =P.
Andy
This is not quite correct. There is some very strange behavior when milliseconds alone are spun. For instance, the milliseconds are allowed to go to 1 and back to 999 but no further.
initialZero
I'll take a look a bit later. Play with the custom class implementation a bit and see what you can come up with.
Andy
Not quite correct. This definitely requires more investigation. I'll post the bounty anyways.
initialZero
A: 

This is kind of ugly but I got it working.

Here is the code. I just take care of the range validation inside the addChangeListener for the JSpinner.

import java.awt.GridLayout;
import java.util.*;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class T extends JPanel {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public T() {
        super(new GridLayout(2, 2));
        init();
    }

    public Calendar end;
    public JSpinner notWorkingSpinner;
    private void init() {
        Calendar start = GregorianCalendar.getInstance();
        end = GregorianCalendar.getInstance();
        start.clear();
        end.clear();

        start.set(Calendar.YEAR, 2010);
        end.set(Calendar.YEAR, 2010);

        end.add(Calendar.HOUR_OF_DAY, 12);
        SpinnerDateModel m1 =
                new SpinnerDateModel(start.getTime(), start.getTime(),
                end.getTime(), Calendar.MILLISECOND);

        SpinnerDateModel m2 = new SpinnerDateModel();
        m2.setValue(start.getTime());

        JSpinner workingSpinner = new JSpinner(m1);
        workingSpinner.setEditor(
                new JSpinner.DateEditor(workingSpinner,
                "yyyy HH:mm:ss.SSS"));
        notWorkingSpinner = new JSpinner(m2);
        notWorkingSpinner.setEditor(
                new JSpinner.DateEditor(notWorkingSpinner,
                "HH:mm:ss.SSS"));

        notWorkingSpinner.addChangeListener(new ChangeListener() {

            @Override
        public void stateChanged(ChangeEvent e) {
            SpinnerModel dateModel = notWorkingSpinner.getModel();
            if(dateModel instanceof SpinnerDateModel){
                Date check = ((SpinnerDateModel)dateModel).getDate();

                Calendar checkCal = GregorianCalendar.getInstance();
                checkCal.setTime(check);
                checkCal.set(Calendar.YEAR, end.get(Calendar.YEAR));
                checkCal.set(Calendar.MONTH, end.get(Calendar.MONTH));
                checkCal.set(Calendar.DAY_OF_MONTH, end.get(Calendar.DAY_OF_MONTH));

                if(checkCal.get(Calendar.HOUR_OF_DAY) == 23){
                    dateModel.setValue(start.getTime());
                } else if(checkCal.getTime().compareTo(end.getTime()) > 0){
                    dateModel.setValue(end.getTime());              
                } 
            }
        }
        });

        add(new JLabel("Working"));
        add(workingSpinner);
        add(new JLabel("!Working"));
        add(notWorkingSpinner);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new T());
        frame.pack();
        frame.setVisible(true);
    }
}
Switch Commerce
When you spin backwards it doesn't stop. I imagine it's just another check to start.
initialZero
Yes, I agree. I was just thinking of the top high level check. We just need to add a check for "start" as well.
Switch Commerce
I fixed the answer to accommodate the low end limit as well.
Switch Commerce