View Javadoc
1   /**
2    * Copyright 2004 Juan Heyns. All rights reserved.
3    * <p/>
4    * Redistribution and use in source and binary forms, with or without modification, are
5    * permitted provided that the following conditions are met:
6    * <p/>
7    * 1. Redistributions of source code must retain the above copyright notice, this list of
8    * conditions and the following disclaimer.
9    * <p/>
10   * 2. Redistributions in binary form must reproduce the above copyright notice, this list
11   * of conditions and the following disclaimer in the documentation and/or other materials
12   * provided with the distribution.
13   * <p/>
14   * THIS SOFTWARE IS PROVIDED BY JUAN HEYNS ``AS IS'' AND ANY EXPRESS OR IMPLIED
15   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
16   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JUAN HEYNS OR
17   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
21   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
22   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23   * <p/>
24   * The views and conclusions contained in the software and documentation are those of the
25   * authors and should not be interpreted as representing official policies, either expressed
26   * or implied, of Juan Heyns.
27   */
28  package fr.ifremer.quadrige3.ui.swing.component.date;
29  
30  /*
31   * #%L
32   * Reef DB :: UI
33   * $Id:$
34   * $HeadURL:$
35   * %%
36   * Copyright (C) 2014 - 2015 Ifremer
37   * %%
38   * This program is free software: you can redistribute it and/or modify
39   * it under the terms of the GNU Affero General Public License as published by
40   * the Free Software Foundation, either version 3 of the License, or
41   * (at your option) any later version.
42   *
43   * This program is distributed in the hope that it will be useful,
44   * but WITHOUT ANY WARRANTY; without even the implied warranty of
45   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
46   * GNU General Public License for more details.
47   *
48   * You should have received a copy of the GNU Affero General Public License
49   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
50   * #L%
51   */
52  
53  import fr.ifremer.quadrige3.ui.swing.component.date.constraints.DateSelectionConstraint;
54  
55  import javax.swing.*;
56  import java.awt.*;
57  import java.awt.event.*;
58  import java.beans.PropertyChangeEvent;
59  import java.beans.PropertyChangeListener;
60  import java.text.DateFormat;
61  import java.text.SimpleDateFormat;
62  import java.time.LocalDate;
63  import java.time.ZoneId;
64  import java.util.Date;
65  import java.util.HashSet;
66  import java.util.Set;
67  
68  /**
69   * Created on 25 Mar 2004
70   * Refactored 21 Jun 2004
71   * Refactored 14 May 2009
72   * Refactored 16 April 2010
73   * Updated 26 April 2010
74   * Updated 10 August 2012
75   * Updated 6 Jun 2015
76   * Updated 20/11/2015
77   *
78   * @author Juan Heyns
79   * @author JC Oosthuizen
80   * @author Yue Huang
81   * @author Ludovic Pecquot
82   */
83  public class JLocalDatePicker extends JComponent {
84  
85      private static final long serialVersionUID = 2814777654384974503L;
86  
87      /**
88       * Constant <code>COMMIT_DATE="JLocalDatePanel.COMMIT_DATE"</code>
89       */
90      public static final String COMMIT_DATE = JLocalDatePanel.COMMIT_DATE;
91  
92      public static final String POPUP_LOST = "popupLost";
93  
94      private Popup popup;
95      private final FittingPopupFactory popupFactory = new FittingPopupFactory();
96      private final JFormattedTextField formattedTextField;
97      private DateComponentFormatter dateComponentFormatter;
98      private final JButton button;
99  
100     private final JLocalDatePanel datePanel;
101 
102     private final Set<ActionListener> actionListeners;
103     private final AWTEventListener awtEventListener;
104 
105     /**
106      * Create a JDatePicker with a default model.
107      */
108     public JLocalDatePicker() {
109         this(new JLocalDatePanel());
110     }
111 
112     /**
113      * Create a JDatePicker with a custom date model.
114      *
115      * @param model a custom date model
116      */
117     public JLocalDatePicker(LocalDateModel model) {
118         this(new JLocalDatePanel(model));
119     }
120 
121     /**
122      * You are able to set the format of the date being displayed on the label.
123      * Formatting is described at:
124      *
125      * @param datePanel The DatePanel to use
126      */
127     private JLocalDatePicker(JLocalDatePanel datePanel) {
128         this.datePanel = datePanel;
129         datePanel.setBorder(BorderFactory.createLineBorder(datePanel.getColors().getColor(ComponentColorDefaults.Key.POPUP_BORDER)));
130 
131         //Initialise Variables
132         actionListeners = new HashSet<>();
133 
134         //Create Layout
135         setLayout(new BorderLayout());
136 
137         //Add and Configure TextField
138         formattedTextField = createTextField();
139         setTextFieldValue(datePanel.getModel().getLocalDate());
140         add(formattedTextField, BorderLayout.CENTER);
141 
142         //Add and Configure Button
143         button = createButton();
144         add(button, BorderLayout.LINE_END);
145 
146         //Add event listeners
147         addHierarchyBoundsListener(new HierarchyBoundsListener() {
148             @Override
149             public void ancestorMoved(HierarchyEvent e) {
150                 hidePopup();
151             }
152 
153             @Override
154             public void ancestorResized(HierarchyEvent e) {
155                 hidePopup();
156             }
157         });
158 //TODO        addAncestorListener(listener) ?
159 
160         ActionListener actionListener = e -> {
161             if (e.getSource() == button) {
162                 if (popup == null) {
163                     showPopup();
164                 } else {
165                     hidePopup();
166                 }
167             } else if (e.getSource() == datePanel) {
168                 hidePopup();
169             }
170         };
171         button.addActionListener(actionListener);
172         datePanel.addActionListener(actionListener);
173 
174         PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {
175 
176             private boolean modelAdjusting;
177 
178             @Override
179             public void propertyChange(PropertyChangeEvent evt) {
180 
181                 if (modelAdjusting) {
182                     return;
183                 }
184 
185                 try {
186                     modelAdjusting = true;
187 
188                     if (evt.getSource() == formattedTextField) {
189                         // Short circuit if the following cases are found
190                         if (evt.getOldValue() == null && evt.getNewValue() == null) {
191                             return;
192                         }
193                         if (evt.getOldValue() != null && evt.getOldValue().equals(evt.getNewValue())) {
194                             return;
195                         }
196                         if (!formattedTextField.isEditable()) {
197                             return;
198                         }
199 
200                         // If the field is editable and we need to parse the date entered
201                         if (evt.getNewValue() != null) {
202 
203                             LocalDate localDate = null;
204                             if (evt.getNewValue() instanceof Date) {
205 
206                                 Date value = (Date) evt.getNewValue();
207                                 localDate = value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
208 
209                             } else if (evt.getNewValue() instanceof LocalDate) {
210                                 localDate = (LocalDate) evt.getNewValue();
211                             }
212                             LocalDateModel tempModel = new LocalDateModel(localDate);
213 
214                             // check constraints
215                             if (!datePanel.checkConstraints(tempModel)) {
216                                 // rollback
217                                 setTextFieldValue(evt.getOldValue());
218                                 return;
219                             }
220                             getModel().setLocalDate(tempModel.getLocalDate());
221                         } else {
222 
223                             // Clearing textfield will also fire change event
224                             getModel().setLocalDate(null);
225                         }
226 
227                         // fire action
228                         fireActionPerformed();
229 
230                     } else if (evt.getSource() == getModel()) {
231 
232                         if (LocalDateModel.PROPERTY_VALUE.equals(evt.getPropertyName())) {
233                             setTextFieldValue(getModel().getLocalDate());
234                         }
235 
236                     }
237 
238                 } finally {
239                     modelAdjusting = false;
240                 }
241             }
242         };
243         formattedTextField.addPropertyChangeListener("value", propertyChangeListener);
244         datePanel.getModel().addPropertyChangeListener(propertyChangeListener);
245 
246         awtEventListener = event -> {
247             if (MouseEvent.MOUSE_CLICKED == event.getID() && event.getSource() != button) {
248                 Set<Component> components = getAllComponents(datePanel);
249                 boolean clickInPopup = false;
250                 for (Component component : components) {
251                     if (event.getSource() == component) {
252                         clickInPopup = true;
253                     }
254                 }
255                 if (!clickInPopup) {
256                     boolean hidden = hidePopup();
257                     firePropertyChange(POPUP_LOST, false, hidden);
258                 }
259             }
260         };
261     }
262 
263     /**
264      * <p>createTextField.</p>
265      *
266      * @return a {@link JFormattedTextField} object.
267      */
268     protected JFormattedTextField createTextField() {
269         dateComponentFormatter = new DateComponentFormatter();
270         JFormattedTextField textField = new JFormattedTextField(dateComponentFormatter);
271         textField.setEditable(false);
272         return textField;
273     }
274 
275     protected void setTextFieldValue(Object value) {
276         if (value instanceof LocalDate) {
277             formattedTextField.setValue(Date.from(((LocalDate) value).atStartOfDay(ZoneId.systemDefault()).toInstant()));
278         } else if (value instanceof Date) {
279             formattedTextField.setValue(value);
280         } else {
281             formattedTextField.setValue(null);
282         }
283     }
284 
285     /**
286      * <p>createButton.</p>
287      *
288      * @return a {@link JButton} object.
289      */
290     protected JButton createButton() {
291         JButton b = new JButton();
292         b.setRolloverEnabled(true);
293         b.setFocusable(false);
294         Icon icon = ComponentIconDefaults.getInstance().getPopupButtonIcon();
295         b.setIcon(icon);
296         b.setText(icon == null ? "..." : "");
297         return b;
298     }
299 
300     /**
301      * <p>getLocalDate.</p>
302      *
303      * @return a {@link Date} object.
304      */
305     public LocalDate getLocalDate() {
306         return getModel().getLocalDate();
307     }
308 
309     /**
310      * <p>setLocalDate.</p>
311      *
312      * @param date a {@link Date} object.
313      */
314     public void setLocalDate(LocalDate date) {
315         getModel().setLocalDate(date);
316     }
317 
318     /**
319      * <p>addActionListener.</p>
320      *
321      * @param actionListener a {@link ActionListener} object.
322      */
323     public void addActionListener(ActionListener actionListener) {
324         actionListeners.add(actionListener);
325         datePanel.addActionListener(actionListener);
326     }
327 
328     /**
329      * <p>removeActionListener.</p>
330      *
331      * @param actionListener a {@link ActionListener} object.
332      */
333     public void removeActionListener(ActionListener actionListener) {
334         actionListeners.remove(actionListener);
335         datePanel.removeActionListener(actionListener);
336     }
337 
338     /**
339      * <p>getModel.</p>
340      *
341      * @return a {@link LocalDateModel} object.
342      */
343     public LocalDateModel getModel() {
344         return datePanel.getModel();
345     }
346 
347     /**
348      * <p>getEditor.</p>
349      *
350      * @return a {@link JFormattedTextField} object.
351      */
352     public JFormattedTextField getEditor() {
353         return formattedTextField;
354     }
355 
356     /**
357      * <p>setTextEditable.</p>
358      *
359      * @param editable a boolean.
360      */
361     public void setTextEditable(boolean editable) {
362         formattedTextField.setEditable(editable);
363     }
364 
365     /**
366      * <p>isTextEditable.</p>
367      *
368      * @return a boolean.
369      */
370     public boolean isTextEditable() {
371         return formattedTextField.isEditable();
372     }
373 
374     /**
375      * <p>setButtonFocusable.</p>
376      *
377      * @param focusable a boolean.
378      */
379     public void setButtonFocusable(boolean focusable) {
380         button.setFocusable(focusable);
381     }
382 
383     /**
384      * <p>getButtonFocusable.</p>
385      *
386      * @return a boolean.
387      */
388     public boolean getButtonFocusable() {
389         return button.isFocusable();
390     }
391 
392     /**
393      * Called internally to popup the dates.
394      */
395     private void showPopup() {
396         if (popup == null) {
397             datePanel.setVisible(true);
398             Point loc = getLocationOnScreen();
399             popup = popupFactory.getPopup(this, datePanel, loc.x, loc.y + getHeight());
400             SwingUtilities.invokeLater(() -> {
401                 popup.show();
402                 Toolkit.getDefaultToolkit().addAWTEventListener(awtEventListener, MouseEvent.MOUSE_PRESSED);
403             });
404         }
405     }
406 
407     /**
408      * Called internally to hide the popup dates.
409      *
410      * @return true if popup has been hidden
411      */
412     public boolean hidePopup() {
413         if (popup != null) {
414             Toolkit.getDefaultToolkit().removeAWTEventListener(awtEventListener);
415             popup.hide();
416             popup = null;
417             return true;
418         }
419         return false;
420     }
421 
422     private Set<Component> getAllComponents(Component component) {
423         Set<Component> children = new HashSet<>();
424         children.add(component);
425         if (component instanceof Container) {
426             Container container = (Container) component;
427             Component[] components = container.getComponents();
428             for (Component component1 : components) {
429                 children.addAll(getAllComponents(component1));
430             }
431         }
432         return children;
433     }
434 
435     /**
436      * <p>isDoubleClickAction.</p>
437      *
438      * @return a boolean.
439      */
440     public boolean isDoubleClickAction() {
441         return datePanel.isDoubleClickAction();
442     }
443 
444     /**
445      * <p>isShowYearButtons.</p>
446      *
447      * @return a boolean.
448      */
449     public boolean isShowYearButtons() {
450         return datePanel.isShowYearButtons();
451     }
452 
453     /**
454      * <p>setDoubleClickAction.</p>
455      *
456      * @param doubleClickAction a boolean.
457      */
458     public void setDoubleClickAction(boolean doubleClickAction) {
459         datePanel.setDoubleClickAction(doubleClickAction);
460     }
461 
462     /**
463      * <p>setShowYearButtons.</p>
464      *
465      * @param showYearButtons a boolean.
466      */
467     public void setShowYearButtons(boolean showYearButtons) {
468         datePanel.setShowYearButtons(showYearButtons);
469     }
470 
471     /**
472      * <p>addDateSelectionConstraint.</p>
473      *
474      * @param constraint a {@link DateSelectionConstraint} object.
475      */
476     public void addDateSelectionConstraint(DateSelectionConstraint constraint) {
477         datePanel.addDateSelectionConstraint(constraint);
478     }
479 
480     /**
481      * <p>removeDateSelectionConstraint.</p>
482      *
483      * @param constraint a {@link DateSelectionConstraint} object.
484      */
485     public void removeDateSelectionConstraint(DateSelectionConstraint constraint) {
486         datePanel.removeDateSelectionConstraint(constraint);
487     }
488 
489     /**
490      * <p>removeAllDateSelectionConstraints.</p>
491      */
492     public void removeAllDateSelectionConstraints() {
493         datePanel.removeAllDateSelectionConstraints();
494     }
495 
496     /**
497      * <p>getDateSelectionConstraints.</p>
498      *
499      * @return a {@link Set} object.
500      */
501     public Set<DateSelectionConstraint> getDateSelectionConstraints() {
502         return datePanel.getDateSelectionConstraints();
503     }
504 
505     /**
506      * {@inheritDoc}
507      */
508     @Override
509     public void setVisible(boolean aFlag) {
510         if (!aFlag) {
511             hidePopup();
512         }
513         super.setVisible(aFlag);
514     }
515 
516     /**
517      * {@inheritDoc}
518      */
519     @Override
520     public void setEnabled(boolean enabled) {
521         button.setEnabled(enabled);
522         datePanel.setEnabled(enabled);
523         formattedTextField.setEnabled(enabled);
524 
525         super.setEnabled(enabled);
526     }
527 
528     /**
529      * <p>setDateFormat.</p>
530      *
531      * @param datePattern            a {@link String} object.
532      * @param default2DigitYearStart a {@link Date} object.
533      */
534     public void setDateFormat(String datePattern, Date default2DigitYearStart) {
535 
536         SimpleDateFormat outputDateFormat = new SimpleDateFormat(datePattern);
537 
538         SimpleDateFormat inputDateFormat;
539         if (datePattern.contains("yyyy")) {
540             inputDateFormat = new SimpleDateFormat(datePattern.replace("yyyy", "yy"));
541         } else {
542             inputDateFormat = outputDateFormat;
543         }
544         inputDateFormat.set2DigitYearStart(default2DigitYearStart);
545 
546         setFormat(AbstractComponentFormat.Key.OUTPUT_DATE_FIELD, outputDateFormat);
547         setFormat(AbstractComponentFormat.Key.INPUT_DATE_FIELD, inputDateFormat);
548     }
549 
550     /**
551      * <p>setFormat.</p>
552      *
553      * @param formatKey a {@link AbstractComponentFormat.Key} object.
554      * @param format    a {@link DateFormat} object.
555      */
556     public void setFormat(AbstractComponentFormat.Key formatKey, DateFormat format) {
557         dateComponentFormatter.setFormat(formatKey, format);
558     }
559 
560     /**
561      * <p>setColor.</p>
562      *
563      * @param colorKey a {@link AbstractComponentColor.Key} object.
564      * @param color    a {@link Color} object.
565      */
566     public void setColor(AbstractComponentColor.Key colorKey, Color color) {
567         datePanel.getColors().setColor(colorKey, color);
568     }
569 
570     private void fireActionPerformed() {
571         for (ActionListener actionListener : actionListeners) {
572             actionListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, COMMIT_DATE));
573         }
574     }
575 
576 }