// (c) 2003 Allen I Holub. All rights reserved. // Terms of distribution are described below. package com.holub.ui; import com.holub.ui.DateSelectorDialog; import com.holub.ui.DateInput; import com.holub.tools.DateUtil; import javax.swing.*; import java.util.Date; import java.util.Calendar; import java.text.*; import java.awt.*; import java.awt.event.*; // TODO: Button background turns grey when it's pressed! has something // to do with a focus-change notification? /************************************************************************ This class overrides all non-deprecated methods of {@link java.util.Date}, and adds a method that produces a user-interface (a JPanel that combines the {@link DateInput} and {@link DateSelectorDialog} classes into a single control). The control displays itself as a text box (which can be editable or not) and a button. Clicking the button pops up a date-selector dialog. This way you can type in a date or pick one with the mouse. When the user specifies a date through the interface, the value of the current object changes accordingly. If the requested UI isn't editable, the date is dislplayed as a label with a transparent background. <p> The point is that you don't need to worry about updating the date from user input: <pre> Class myClass { InteractiveDate date = new InteractiveDate(); void businessLogic() { // Just use the Interactive Date as if it were a // Date object. } void createUi(Frame parent) { JDialog d = new JDialog( parent, true ); // modal //... d.getContentPane().add( date.getUi() ); //... d.show() // popup the dialog. // At this point, d has been updated automatically // from user input. you don't need to extact a value // from a text field and use it to update d. } } </pre> <p><b>NOTE:</b>The deprecated methods of Date are not overridden, and though you can still call them, you shouldn't. This class will not function properly if you call a deprecated base-class method that modifies the date's value. The only safe way to change the value is with a call to {@link #setTime setTime(...)}. You should use a {@link java.util.Calendar} to get finer control over the date-setting process, then inport the new value like this: <pre> Calendar c = new Calendar(); Date d = new InteractiveDate(); // <-- Base-class reference //... d.setTime( c.getTime().getTime() ); // local convenience overload <pre> or you can use the {@link #setTime(java.util.Calendar)} convenience overload if you're not using a base-class reference: <pre> Calendar c = new Calendar(); InteractiveDate d = new InteractiveDate(); //... d.setTime( c ); // Use local convenience overload. <pre> <!-- ====================== distribution terms ===================== --> <p><blockquote style="border-style: solid; border-width:thin; padding: 1em 1em 1em 1em;"> <center> Copyright © 2003, Allen I. Holub. All rights reserved. </center> <br> <br> This code is distributed under the terms of the <a href="http://www.gnu.org/licenses/gpl.html" >GNU Public License</a> (GPL) with the following ammendment to section 2.c: As a requirement for distributing this code, your splash screen, about box, or equivalent must include an my name, copyright, <em>and URL</em>. An acceptable message would be: <center> This program contains Allen Holub's <em>XXX</em> utility.<br> (c) 2003 Allen I. Holub. All Rights Reserved.<br> http://www.holub.com<br> </center> If your progam does not run interactively, then the foregoing notice must appear in your documentation. </blockquote> <!-- =============================================================== --> @author Allen I. Holub *************************************************************************/ public class InteractiveDate extends java.util.Date { private JComponent ui = null; private DateFormat formatter = DateFormat.getDateInstance( DateFormat.MEDIUM ); private static final String calendarGraphic = "images/10px.calendar.icon.gif"; /** Works like {@link java.util.Date#Date()}. */ public InteractiveDate( ) { super(); } /** Works like {@link java.util.Date#Date(long)}. */ public InteractiveDate(long date) { super(date); } /** Works like {@link java.util.Date#clone}. */ public Object clone () {return super.clone(); } /** Works like {@link java.util.Date#getTime}. */ public long getTime () {return super.getTime(); } /** Works like {@link java.util.Date#before}. */ public boolean before (Date w) {return super.before(w); } /** Works like {@link java.util.Date#after}. */ public boolean after (Date w) {return super.after(w); } /** Works like {@link java.util.Date#equals}. */ public boolean equals (Object o){return super.equals(o); } /** Works like {@link java.util.Date#compareTo(Date)}. */ public int compareTo(Date d) {return super.compareTo(d);} /** Works like {@link java.util.Date#compareTo(Object)}. public int compareTo(Object o){return super.compareTo(o);} */ /** Works like {@link java.util.Date#hashCode}. */ public int hashCode () {return super.hashCode(); } /** Works like {@link java.util.Date#toString}. */ public String toString () {return super.toString(); } /** Works like {@link java.util.Date#setTime}. */ public void setTime( long t ) { super.setTime(t); if( ui != null ) // There's a UI displayed right now. { if( ui instanceof JLabel ) ((JLabel)ui).setText( formatter.format(this) ); else ((Chooser)ui).dateHasChanged(); } } /** A convenience overload extacts the time from a {@link java.util.Calendar} * @param c The calender from which the new date value is extracted */ public void setTime( Calendar c ) { setTime( c.getTime().getTime() ); } /** A convenience overload extacts the time from another {@link java.util.Date}. * @param c The Date from which the new value is extracted */ public void setTime( Date c ) { setTime( c.getTime() ); } /** Gat a user interface for the current date. This UI is "hot:" changes * that the user makes are reflected in the underlying * date object, and changes made to the Date object (via a call * to {@link #setTime}) will change the date displayed in the UI. * Note that user-initiatiated changes are not guaranteed to be * visible until after the user interface shuts down. * <p> * The returned UI is transparent, so the underlying background * color will show through. * <p> * The user interface is disconnected from the Date object that * manufactures it when the Window that holds it shuts down. * Attempts to reuse the UI will fail. It's best to insert * the UI like this: * <pre> * Container c; * //... * c.add( myDate.getUi( isReadOnly ) ); * </pre> * without keeping the returned pointer around anywhere. * <p> * In the current implementation, only one user interface can * be outstanding. Attempts to call getUi() a second time * will fail (with an IllegalStateException) unless the * window that contains the UI returned from the previous * call has shut down. * * @param isReadOnly if true, then a simple label that displays the date * is returned, otherwise, a control initialized with the date, * into which the user can type (or otherwise select) a new date, * is returned. * @return A {@link JComponent} that represents this date. * @throws IllegalStateException if you try to get a UI when a previously * issued UI is still active (the containing window hasn't shut * down). * @see #getUi(boolean,DateFormat) */ public JComponent getUi( boolean isReadOnly ) { if( ui != null ) throw new IllegalStateException(); ui = ( isReadOnly ) ? (JComponent) new JLabel (formatter.format(this)) : (JComponent) new Chooser(formatter.format(this)) ; ui.setOpaque( false ); return ui; } /** Like {@link #getUi(boolean)}, but permanently replaces * the formatter used to create a string representation of the * date with the indicated formatter, then return the UI. * @param isReadOnly true for a read-only representation * @param formatter {@link DateFormat} object to replace the default formatter. * @return A {@link JComponent} that represents this date. * @see #getUi(boolean) */ public JComponent getUi( boolean isReadOnly, DateFormat formatter ) { this.formatter = formatter; return getUi(isReadOnly); } /** Convenience method. Returns a read/write user interface that uses * the current formatter. * @return An editible {@link JComponent} linked to the current object. */ public JComponent getUi() { return getUi( false ); } private class Chooser extends JPanel { private JTextField control; private JButton button; private DateSelectorDialog selector = null; public Chooser( String label ) { setBorder( BorderFactory.createLineBorder(Color.BLACK,1) ); control = (JTextField) new DateInput(label); control.setBorder( BorderFactory.createEmptyBorder(0,2,0,2) ); control.setOpaque( false ); // Catch changes that happen when the user hits Enter or the control // looses focus. Note that listeners are notified only when the contents // are valid. control.addActionListener ( new ActionListener() { public void actionPerformed(ActionEvent event) { setTime( DateUtil.parseDate( control.getText() ).getTime() ); } } ); button = new JButton( new ImageIcon( getClass().getClassLoader().getResource(calendarGraphic))); button.setOpaque ( false ); button.setBorder ( BorderFactory.createEmptyBorder(1,1,1,1) ); button.setFocusPainted ( false ); button.addActionListener ( new ActionListener() { public void actionPerformed(ActionEvent e) { Component current = button; Component parent = null; do { parent = current.getParent(); if( parent == null ) break; if( parent instanceof Dialog ) selector = new DateSelectorDialog((Dialog)parent); else if( parent instanceof Frame) selector = new DateSelectorDialog((Frame)parent); current = parent; } while(selector == null); // assert selector != null; selector.setLocationRelativeTo(button); Date selected = selector.select(); selector = null; if( selected != null ) // not canceled by user setTime( selected.getTime()); // causes dateHasChanged, below, to be called } } ); setLayout( new BorderLayout() ); add( control, BorderLayout.CENTER ); add( button, BorderLayout.EAST ); setOpaque(false); } /** Called by the outer-class setTime() method to indicate that the control * needs to display its value. */ public void dateHasChanged() { if( selector != null ) // kill it { selector.setVisible(false); selector.dispose(); selector=null; } control.setText( formatter.format(InteractiveDate.this) ); } } //---------------------------------------------------------------------- private static class Test { public static void main(String[] args) { JFrame f = new JFrame(); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.getContentPane().add( new JLabel("Parent")); f.pack(); f.show(); JDialog dialog = new JDialog(f,true); Container c = dialog.getContentPane(); c.setLayout( new GridLayout(/*rows*/ 2, /*cols*/ 1) ); c.setBackground( Color.WHITE ); final InteractiveDate d1 = new InteractiveDate(); final InteractiveDate d2 = new InteractiveDate(); JComponent u1 = d1.getUi(true); JComponent u2 = d2.getUi(false); c.add( u1 ); c.add( u2 ); dialog.pack(); dialog.show(); System.out.println("Dialog shut down"); System.out.println("d1: " + d1 ); System.out.println("d2: " + d2 ); } } } // TODO: check that DateInput verifies the date on setText.