/* * [DateSpinner.java] * * Summary: GUI JComponent to enter a yyyy-mm-dd value with a Spinner similar to a JSpinner. * * Copyright: (c) 2011-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2011-02-26 initial version * 1.1 2011-03-22 bugs fixed. */ package com.mindprod.spinner; import com.mindprod.common18.BigDate; import com.mindprod.fastcat.FastCat; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.EventListenerList; import java.awt.FlowLayout; import static java.lang.System.*; /** * GUI JComponent to enter a yyyy-mm-dd value with a Spinner similar to a JSpinner. *

* It is implemented internally as three JSpinners, one each for yyyy, mm, dd that act like one. * It does not use a SpinnerModel. The equivalent SpinnerModel methods work directly on the DataSpinner. * * @author Roedy Green, Canadian Mind Products * @version 1.1 2011-03-22 bugs fixed * @since 2011-02-26 */ public class DateSpinner extends JPanel implements ChangeListener { /** * do you want extra validation */ private static final boolean DEBUGGING = false; /** * latest date we can handle */ private static final BigDate HIGHEST_DISPLAYABLE_DATE = new BigDate( "9999-12-31" ); /** * earliest date we can handle */ private static final BigDate LOWEST_DISPLAYABLE_DATE = new BigDate( "0001-01-01" ); /** * The list of ChangeListeners for this model. Subclasses may * store their own listeners here. */ private final EventListenerList listenerList = new EventListenerList(); /** * internal spinner for day of month */ private final JSpinner ddSpinner; /** * internal spinner for month of year */ private final JSpinner mmSpinner; /** * internal spinner for year */ private final JSpinner yyyySpinner; /** * model to track selected day of month */ private final SpinnerNumberModel ddModel; /** * model to track selected month of year */ private final SpinnerNumberModel mmModel; /** * model to track year */ private final SpinnerNumberModel yyyyModel; /** * biggest allowed date to select */ private BigDate maximum = HIGHEST_DISPLAYABLE_DATE; /** * lowest allowed date to select */ private BigDate minimum = LOWEST_DISPLAYABLE_DATE; /** * current selected date value */ private BigDate value = BigDate.localToday(); /** * Only one ChangeEvent is needed per DateSpinner since the * event's only (read-only) state is the source property. The source * of events generated here is always "this". */ private transient ChangeEvent changeEvent = null; /** * month spinner was showing. */ private int prevMM; /** * year spinner was showing. */ private int prevYYYY; /** * day spinner now showing */ private int selectedDD; /** * month spinner now showing */ private int selectedMM; /** * year spinner now showing. */ private int selectedYYYY; /** * Constructor. */ public DateSpinner() { setLayout( new FlowLayout( FlowLayout.LEFT, 1, 1 ) ); yyyySpinner = new JSpinner(); mmSpinner = new JSpinner(); ddSpinner = new JSpinner(); yyyyModel = new SpinnerNumberModel(); yyyyModel.setStepSize( 1 ); yyyyModel.setValue( this.value.getYYYY() ); yyyySpinner.setModel( yyyyModel ); yyyySpinner.setEditor( new LZNumberEditor( yyyySpinner, 4 ) ); yyyySpinner.setName( "yyyySpinner" ); mmModel = new SpinnerNumberModel(); mmModel.setStepSize( 1 ); mmModel.setValue( this.value.getMM() ); mmSpinner.setModel( mmModel ); mmSpinner.setEditor( new LZNumberEditor( mmSpinner, 2 ) ); mmSpinner.setName( "mmSpinner" ); ddModel = new SpinnerNumberModel(); ddModel.setStepSize( 1 ); ddModel.setValue( this.value.getDD() ); ddSpinner.setModel( ddModel ); ddSpinner.setEditor( new LZNumberEditor( ddSpinner, 2 ) ); ddSpinner.setName( "ddSpinner" ); // get values into the 3 inner spinners this.setValue( this.value ); // pack three spinners left to right add( yyyySpinner ); add( mmSpinner ); add( ddSpinner ); validate(); } /** * one of yyyy mm or dd has changed. Tidy up the date and bounds and notify our clients. */ void commonStateChanged() { // Assume caller has set up selectedYYY selectedMM and selectedDD // Assume caller has done odometer logic to corral date to something legal. if ( DEBUGGING ) { out.println( "commonState change" ); } // stop fibrillation yyyySpinner.removeChangeListener( this ); mmSpinner.removeChangeListener( this ); ddSpinner.removeChangeListener( this ); // corral into app bounds. if ( BigDate.compare( selectedYYYY, selectedMM, selectedDD, minimum.getYYYY(), minimum.getMM(), minimum.getDD() ) < 0 ) { selectedYYYY = minimum.getYYYY(); selectedMM = minimum.getMM(); selectedDD = minimum.getDD(); } if ( BigDate.compare( selectedYYYY, selectedMM, selectedDD, maximum.getYYYY(), maximum.getMM(), maximum.getDD() ) > 0 ) { selectedYYYY = maximum.getYYYY(); selectedMM = maximum.getMM(); selectedDD = maximum.getDD(); } // set bounds on what user can key/spin final int yyyyMinimum = minimum.getYYYY(); final int yyyyMaximum = maximum.getYYYY(); final int mmMinimum = selectedYYYY == minimum.getYYYY() ? minimum.getMM() : 0; final int mmMaximum = selectedYYYY == maximum.getYYYY() ? maximum.getMM() : 13; final int ddMinimum = selectedYYYY == minimum.getYYYY() && selectedMM == minimum.getMM() ? minimum.getDD() : 0; final int ddMaximum = selectedYYYY == maximum.getYYYY() && selectedMM == maximum.getMM() ? maximum.getDD() : BigDate.daysInMonth( selectedMM, selectedYYYY ) + 1; yyyyModel.setMinimum( yyyyMinimum ); yyyyModel.setMaximum( yyyyMaximum ); yyyyModel.setValue( selectedYYYY ); mmModel.setMinimum( mmMinimum ); mmModel.setMaximum( mmMaximum ); mmModel.setValue( selectedMM ); ddModel.setMinimum( ddMinimum ); ddModel.setMaximum( ddMaximum ); ddModel.setValue( selectedDD ); value.set( selectedYYYY, selectedMM, selectedDD, BigDate.CHECK ); prevYYYY = selectedYYYY; prevMM = selectedMM; // hook events from yyyy mm dd components back up again. yyyySpinner.addChangeListener( this ); mmSpinner.addChangeListener( this ); ddSpinner.addChangeListener( this ); // even if no change since selectedYYYY, we still need to fire event fireStateChanged(); // notify our clients of the change. } /** * compose a tool tip based on the minimum and maximum bounds */ void composeToolTip() { final FastCat sb = new FastCat( 5 ); sb.append( "Enter a date between " ); sb.append( minimum.toString() ); sb.append( " and " ); sb.append( maximum.toString() ); sb.append( " by keying the year, then the month, then the date, or by using the spinners." ); final String tip = sb.toString(); yyyySpinner.setToolTipText( tip ); mmSpinner.setToolTipText( tip ); ddSpinner.setToolTipText( tip ); } /** * Invoked when the JSpinner day changes, even one notch in * moving to another value */ void ddStateChanged() { if ( DEBUGGING ) { out.println( "ddSpinner change" ); } selectedYYYY = yyyyModel.getNumber().intValue(); selectedMM = mmModel.getNumber().intValue(); selectedDD = ddModel.getNumber().intValue(); // odometer logic. // adjust if day changed if ( selectedDD <= 0 ) { selectedMM--; if ( selectedMM <= 0 ) { selectedMM = 12; selectedYYYY--; } selectedDD = BigDate.daysInMonth( selectedMM, selectedYYYY ); } else if ( selectedDD > BigDate.daysInMonth( selectedMM, selectedYYYY ) ) { selectedDD = 1; selectedMM++; if ( selectedMM > 12 ) { selectedMM = 1; selectedYYYY++; } } commonStateChanged(); } /** * Run each ChangeListeners stateChanged() method. * * @see EventListenerList */ private void fireStateChanged() { if ( DEBUGGING ) { out.println( "firing" ); } Object[] listeners = listenerList.getListenerList(); for ( int i = listeners.length - 2; i >= 0; i -= 2 ) { if ( listeners[ i ] == ChangeListener.class ) { if ( changeEvent == null ) { changeEvent = new ChangeEvent( this ); } ( ( ChangeListener ) listeners[ i + 1 ] ).stateChanged( changeEvent ); } } } /** * Invoked when the JSpinner month changes, even one notch in * moving to another value */ void mmStateChanged() { if ( DEBUGGING ) { out.println( "mmSpinner change" ); } selectedYYYY = yyyyModel.getNumber().intValue(); selectedMM = mmModel.getNumber().intValue(); selectedDD = ddModel.getNumber().intValue(); // odometer logic. // adjust if month changed if ( selectedMM <= 0 ) { selectedYYYY--; selectedMM = 12; selectedDD = 31; } else if ( selectedMM > 12 ) { selectedYYYY++; selectedMM = 1; selectedDD = 1; } else if ( selectedMM > prevMM ) { selectedDD = 1; } else if ( selectedMM < prevMM ) { selectedDD = BigDate.daysInMonth( selectedMM, selectedYYYY ); } commonStateChanged(); } /** * Invoked when the JSpinner year changes, even one notch in * moving to another value */ void yyyyStateChanged() { if ( DEBUGGING ) { out.println( "yyyySpinner change" ); } selectedYYYY = yyyyModel.getNumber().intValue(); selectedMM = mmModel.getNumber().intValue(); selectedDD = ddModel.getNumber().intValue(); if ( selectedYYYY > prevYYYY ) { selectedMM = 1; selectedDD = 1; } else if ( selectedYYYY < prevYYYY ) { selectedMM = 12; selectedDD = 31; } commonStateChanged(); } /** * Adds a listener to the list that is notified each time a change * to the DateSpinner value occurs. The source of ChangeEvents * delivered to ChangeListeners will be this * DateSpinner. * * @param listener the ChangeListener to add */ public void addChangeListener( ChangeListener listener ) { listenerList.add( ChangeListener.class, listener ); } /** * Get the highest allowable date selection * * @return maximum allowable BigDate */ public BigDate getMaximum() { return ( BigDate ) maximum.clone(); } /** * set the highest allowable date selection. Initially 9999-12-31. * * @param maximum as a BigDate with 4-digit positive year. */ public void setMaximum( BigDate maximum ) { if ( DEBUGGING ) { out.println( "max change" ); } assert LOWEST_DISPLAYABLE_DATE.compareTo( maximum ) <= 0 && maximum.compareTo( HIGHEST_DISPLAYABLE_DATE ) <= 0 : "DateSpinner.maximum out of range"; this.maximum = ( BigDate ) maximum.clone(); // clone in case user later changes. commonStateChanged(); // corral date into new bounds composeToolTip(); } /** * Get the lowest allowable date selection * * @return minimum allowable BigDate */ public BigDate getMinimum() { return ( BigDate ) minimum.clone(); } /** * set the lowest allowable date selection. Initially 0001-01-01. * * @param minimum as a BigDate with 4-digit positive year. */ public void setMinimum( BigDate minimum ) { if ( DEBUGGING ) { out.println( "min change" ); } assert LOWEST_DISPLAYABLE_DATE.compareTo( minimum ) <= 0 && minimum.compareTo( HIGHEST_DISPLAYABLE_DATE ) <= 0 : "DateSpinner.minimum out of range"; this.minimum = ( BigDate ) minimum.clone(); // clone in case user later changes. commonStateChanged(); // corral date into new bounds composeToolTip(); } /** * Get the current date selection * * @return current selected date as a BigDate */ public BigDate getValue() { // value is kept up to data as 3 internal JSpinners change. return ( BigDate ) value.clone(); // don't let caller fiddle with our object. } /** * set the current data selection. Initially today's date. * * @param value as a BigDate with 4-digit positive year. */ @SuppressWarnings( { "WeakerAccess" } ) public void setValue( BigDate value ) { if ( DEBUGGING ) { out.println( "value change" ); } this.value = ( BigDate ) value.clone(); // will be modified, Don't wreck caller's object selectedYYYY = value.getYYYY(); selectedMM = value.getMM(); selectedDD = value.getDD(); commonStateChanged(); } /** * Removes a ChangeListener from the DateSpinners listener list. * * @param listener the ChangeListener to remove * * @see #addChangeListener * @see javax.swing.SpinnerModel#removeChangeListener */ public void removeChangeListener( ChangeListener listener ) { listenerList.remove( ChangeListener.class, listener ); } /** * Enable or disable the spinner * * @param enabled true to enable the DateSpinner */ public void setEnabled( boolean enabled ) { yyyySpinner.setEnabled( enabled ); mmSpinner.setEnabled( enabled ); ddSpinner.setEnabled( enabled ); } /** * fields events from all three yyyy mm dd components * * @param e ChangeEvent, which component triggered event. */ public void stateChanged( ChangeEvent e ) { Object source = e.getSource(); if ( source.equals( yyyySpinner ) ) { yyyyStateChanged(); } else if ( source.equals( mmSpinner ) ) { mmStateChanged(); } else if ( source.equals( ddSpinner ) ) { ddStateChanged(); } else { throw new IllegalArgumentException( "DataSpinner Bug: Change event from unexpected source." + source ); } } }