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 javax.swing.event.ChangeEvent;
57  import javax.swing.event.ChangeListener;
58  import javax.swing.event.TableModelEvent;
59  import javax.swing.event.TableModelListener;
60  import javax.swing.table.DefaultTableCellRenderer;
61  import javax.swing.table.JTableHeader;
62  import javax.swing.table.TableColumn;
63  import javax.swing.table.TableModel;
64  import java.awt.BorderLayout;
65  import java.awt.Component;
66  import java.awt.Dimension;
67  import java.awt.GridLayout;
68  import java.awt.event.*;
69  import java.text.DateFormat;
70  import java.time.LocalDate;
71  import java.time.ZoneId;
72  import java.util.*;
73  
74  /**
75   * Created on 26 Mar 2004
76   * Refactored on 21 Jun 2004
77   * Refactored on 8 Jul 2004
78   * Refactored 14 May 2009
79   * Refactored 16 April 2010
80   * Updated 18 April 2010
81   * Updated 26 April 2010
82   * Updated 15 June 2012
83   * Updated 10 August 2012
84   * Updated 6 Jun 2015
85   *
86   * @author Juan Heyns
87   * @author JC Oosthuizen
88   * @author Yue Huang
89   */
90  public class JLocalDatePanel extends JComponent {
91  
92      private static final long serialVersionUID = -2299249311312882915L;
93  
94      /** Constant <code>COMMIT_DATE="commit"</code> */
95      public static final String COMMIT_DATE = "commit";
96  
97      private final Set<ActionListener> actionListeners;
98      private final Set<DateSelectionConstraint> dateConstraints;
99  
100     private boolean showYearButtons;
101     private boolean doubleClickAction;
102 
103     private final InternalCalendarModel internalModel;
104     private final InternalController internalController;
105     private final InternalView internalView;
106 
107     /**
108      * Creates a JDatePanel with a default model.
109      */
110     public JLocalDatePanel() {
111         this(createModel());
112     }
113 
114     /**
115      * Create a JDatePanel with a custom date model.
116      *
117      * @param model a custom date model
118      */
119     public JLocalDatePanel(LocalDateModel model) {
120         actionListeners = new HashSet<>();
121         dateConstraints = new HashSet<>();
122 
123         showYearButtons = false;
124         doubleClickAction = false;
125 
126         internalModel = new InternalCalendarModel(model);
127         internalController = new InternalController();
128         internalView = new InternalView();
129 
130         setLayout(new GridLayout(1, 1));
131         add(internalView);
132     }
133 
134     /**
135      * <p>createModel.</p>
136      *
137      * @return a {@link LocalDateModel} object.
138      */
139     protected static LocalDateModel createModel() {
140         return new LocalDateModel();
141     }
142 
143     /**
144      * <p>addActionListener.</p>
145      *
146      * @param actionListener a {@link ActionListener} object.
147      */
148     public void addActionListener(ActionListener actionListener) {
149         actionListeners.add(actionListener);
150     }
151 
152     /**
153      * <p>removeActionListener.</p>
154      *
155      * @param actionListener a {@link ActionListener} object.
156      */
157     public void removeActionListener(ActionListener actionListener) {
158         actionListeners.remove(actionListener);
159     }
160 
161     /**
162      * Called internally when actionListeners should be notified.
163      */
164     private void fireActionPerformed() {
165         for (ActionListener actionListener : actionListeners) {
166             actionListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, COMMIT_DATE));
167         }
168     }
169 
170     /**
171      * <p>Setter for the field <code>showYearButtons</code>.</p>
172      *
173      * @param showYearButtons a boolean.
174      */
175     public void setShowYearButtons(boolean showYearButtons) {
176         this.showYearButtons = showYearButtons;
177         internalView.updateShowYearButtons();
178     }
179 
180     /**
181      * <p>isShowYearButtons.</p>
182      *
183      * @return a boolean.
184      */
185     public boolean isShowYearButtons() {
186         return this.showYearButtons;
187     }
188 
189     /**
190      * <p>Setter for the field <code>doubleClickAction</code>.</p>
191      *
192      * @param doubleClickAction a boolean.
193      */
194     public void setDoubleClickAction(boolean doubleClickAction) {
195         this.doubleClickAction = doubleClickAction;
196     }
197 
198     /**
199      * <p>isDoubleClickAction.</p>
200      *
201      * @return a boolean.
202      */
203     public boolean isDoubleClickAction() {
204         return doubleClickAction;
205     }
206 
207     /**
208      * <p>getModel.</p>
209      *
210      * @return a {@link LocalDateModel} object.
211      */
212     public LocalDateModel getModel() {
213         return internalModel.getModel();
214     }
215 
216     /**
217      * <p>addDateSelectionConstraint.</p>
218      *
219      * @param constraint a {@link DateSelectionConstraint} object.
220      */
221     public void addDateSelectionConstraint(DateSelectionConstraint constraint) {
222         dateConstraints.add(constraint);
223     }
224 
225     /**
226      * <p>removeDateSelectionConstraint.</p>
227      *
228      * @param constraint a {@link DateSelectionConstraint} object.
229      */
230     public void removeDateSelectionConstraint(DateSelectionConstraint constraint) {
231         dateConstraints.remove(constraint);
232     }
233 
234     /**
235      * <p>removeAllDateSelectionConstraints.</p>
236      */
237     public void removeAllDateSelectionConstraints() {
238         dateConstraints.clear();
239     }
240 
241     /**
242      * <p>getDateSelectionConstraints.</p>
243      *
244      * @return a {@link Set} object.
245      */
246     public Set<DateSelectionConstraint> getDateSelectionConstraints() {
247         return Collections.unmodifiableSet(dateConstraints);
248     }
249 
250     /**
251      * <p>checkConstraints.</p>
252      *
253      * @param model a {@link LocalDateModel} object.
254      * @return a boolean.
255      */
256     protected boolean checkConstraints(LocalDateModel model) {
257         for (DateSelectionConstraint constraint : dateConstraints) {
258             if (!constraint.isValidSelection(new DateModel(Date.from(model.getSafeLocalDate().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())))) {
259                 return false;
260             }
261         }
262         return true;
263     }
264 
265     private static ComponentTextDefaults getTexts() {
266         return ComponentTextDefaults.getInstance();
267     }
268 
269     private static ComponentIconDefaults getIcons() {
270         return ComponentIconDefaults.getInstance();
271     }
272 
273     private AbstractComponentColor colors;
274 
275     /**
276      * <p>Getter for the field <code>colors</code>.</p>
277      *
278      * @return a {@link AbstractComponentColor} object.
279      */
280     public AbstractComponentColor getColors() {
281         if (colors == null) {
282             try {
283                 colors = ComponentColorDefaults.getInstance().clone();
284             } catch (CloneNotSupportedException e) {
285                 colors = ComponentColorDefaults.getInstance();
286             }
287         }
288         return colors;
289     }
290 
291     private static ComponentFormatDefaults getFormats() {
292         return ComponentFormatDefaults.getInstance();
293     }
294 
295     /** {@inheritDoc} */
296     @Override
297     public void setVisible(boolean aFlag) {
298         super.setVisible(aFlag);
299 
300         if (aFlag) {
301             internalView.updateTodayLabel();
302         }
303     }
304 
305     /** {@inheritDoc} */
306     @Override
307     public void setEnabled(boolean enabled) {
308         internalView.setEnabled(enabled);
309 
310         super.setEnabled(enabled);
311     }
312 
313     /**
314      * <p>getLocalDate.</p>
315      *
316      * @return a {@link LocalDate} object.
317      */
318     public LocalDate getLocalDate() {
319         return getModel().getLocalDate();
320     }
321 
322     /**
323      * <p>setLocalDate.</p>
324      *
325      * @param date a {@link LocalDate} object.
326      */
327     public void setLocalDate(LocalDate date) {
328         getModel().setLocalDate(date);
329     }
330 
331     /**
332      * Logically grouping the view controls under this internal class.
333      *
334      * @author Juan Heyns
335      */
336     private class InternalView extends JPanel {
337 
338         private static final long serialVersionUID = -6844493839307157682L;
339 
340         private static final int DEFAULT_WIDTH = 280;
341         private static final int DEFAULT_HEIGHT = 200;
342 
343         private JPanel centerPanel;
344         private JPanel northCenterPanel;
345         private JPanel northPanel;
346         private JPanel southPanel;
347         private JPanel previousButtonPanel;
348         private JPanel nextButtonPanel;
349         private JTable dayTable;
350         private JTableHeader dayTableHeader;
351         private InternalTableCellRenderer dayTableCellRenderer;
352         private JLabel monthLabel;
353         private JLabel todayLabel;
354         private JLabel noneLabel;
355         private JPopupMenu monthPopupMenu;
356         private JMenuItem[] monthPopupMenuItems;
357         private JButton nextMonthButton;
358         private JButton previousMonthButton;
359         private JButton previousYearButton;
360         private JButton nextYearButton;
361         private JSpinner yearSpinner;
362 
363         /**
364          * Update the scroll buttons UI.
365          */
366         private void updateShowYearButtons() {
367             if (showYearButtons) {
368                 getNextButtonPanel().add(getNextYearButton());
369                 getPreviousButtonPanel().removeAll();
370                 getPreviousButtonPanel().add(getPreviousYearButton());
371                 getPreviousButtonPanel().add(getPreviousMonthButton());
372             } else {
373                 getNextButtonPanel().remove(getNextYearButton());
374                 getPreviousButtonPanel().remove(getPreviousYearButton());
375             }
376         }
377 
378         /**
379          * Update the UI of the monthLabel
380          */
381         private void updateMonthLabel() {
382             monthLabel.setText(getTexts().getText(ComponentTextDefaults.Key.getMonthKey(internalModel.getModel().getMonth() - 1)));
383         }
384 
385         InternalView() {
386             initialise();
387         }
388 
389         /**
390          * Initialise the control.
391          */
392         private void initialise() {
393             this.setLayout(new BorderLayout());
394             this.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
395             this.setPreferredSize(getSize());
396             this.setOpaque(false);
397             this.add(getNorthPanel(), BorderLayout.NORTH);
398             this.add(getSouthPanel(), BorderLayout.SOUTH);
399             this.add(getCenterPanel(), BorderLayout.CENTER);
400         }
401 
402         /**
403          * This method initializes northPanel
404          *
405          * @return javax.swing.JPanel The north panel
406          */
407         private JPanel getNorthPanel() {
408             if (northPanel == null) {
409                 northPanel = new JPanel();
410                 northPanel.setLayout(new BorderLayout());
411                 northPanel.setName("");
412                 northPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
413                 northPanel.setBackground(getColors().getColor(AbstractComponentColor.Key.BG_MONTH_SELECTOR));
414                 northPanel.add(getPreviousButtonPanel(), BorderLayout.WEST);
415                 northPanel.add(getNextButtonPanel(), BorderLayout.EAST);
416                 northPanel.add(getNorthCenterPanel(), BorderLayout.CENTER);
417             }
418             return northPanel;
419         }
420 
421         /**
422          * This method initializes northCenterPanel
423          *
424          * @return javax.swing.JPanel
425          */
426         private JPanel getNorthCenterPanel() {
427             if (northCenterPanel == null) {
428                 northCenterPanel = new JPanel();
429                 northCenterPanel.setLayout(new BorderLayout());
430                 northCenterPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
431                 northCenterPanel.setOpaque(false);
432                 northCenterPanel.add(getMonthLabel(), BorderLayout.CENTER);
433                 northCenterPanel.add(getYearSpinner(), BorderLayout.EAST);
434             }
435             return northCenterPanel;
436         }
437 
438         /**
439          * This method initializes monthLabel
440          *
441          * @return javax.swing.JLabel
442          */
443         private JLabel getMonthLabel() {
444             if (monthLabel == null) {
445                 monthLabel = new JLabel();
446                 monthLabel.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_MONTH_SELECTOR));
447                 monthLabel.setHorizontalAlignment(SwingConstants.CENTER);
448                 monthLabel.addMouseListener(internalController);
449                 updateMonthLabel();
450             }
451             return monthLabel;
452         }
453 
454         /**
455          * This method initializes yearSpinner
456          *
457          * @return javax.swing.JSpinner
458          */
459         private JSpinner getYearSpinner() {
460             if (yearSpinner == null) {
461                 yearSpinner = new JSpinner();
462                 yearSpinner.setModel(internalModel);
463             }
464             return yearSpinner;
465         }
466 
467         /**
468          * This method initializes southPanel
469          *
470          * @return javax.swing.JPanel
471          */
472         private JPanel getSouthPanel() {
473             if (southPanel == null) {
474                 southPanel = new JPanel();
475                 southPanel.setLayout(new BorderLayout());
476                 southPanel.setBackground(getColors().getColor(AbstractComponentColor.Key.BG_TODAY_SELECTOR));
477                 southPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
478                 southPanel.add(getTodayLabel(), BorderLayout.WEST);
479                 southPanel.add(getNoneLabel(), BorderLayout.EAST);
480             }
481             return southPanel;
482         }
483 
484         /**
485          * This method initializes todayLabel
486          *
487          * @return javax.swing.JLabel
488          */
489         private JLabel getNoneLabel() {
490             if (noneLabel == null) {
491                 noneLabel = new JLabel();
492                 noneLabel.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_TODAY_SELECTOR_ENABLED));
493                 noneLabel.setHorizontalAlignment(SwingConstants.CENTER);
494                 noneLabel.addMouseListener(internalController);
495                 //TODO get the translations for each language before adding this in
496                 //noneLabel.setToolTipText(getText(ComponentTextDefaults.CLEAR));
497                 noneLabel.setIcon(getIcons().getClearIcon());
498             }
499             return noneLabel;
500         }
501 
502         // TODO use different format with DateTimeFormatter
503         private void updateTodayLabel() {
504             Calendar now = Calendar.getInstance();
505             DateFormat df = getFormats().getFormat(ComponentFormatDefaults.Key.TODAY_SELECTOR);
506             todayLabel.setText(getTexts().getText(
507                     ComponentTextDefaults.Key.TODAY)
508                     + ": "
509                     + df.format(now.getTime()));
510         }
511 
512         /**
513          * This method initializes todayLabel
514          *
515          * @return javax.swing.JLabel
516          */
517         private JLabel getTodayLabel() {
518             if (todayLabel == null) {
519                 todayLabel = new JLabel();
520                 todayLabel.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_TODAY_SELECTOR_ENABLED));
521                 todayLabel.setHorizontalAlignment(SwingConstants.CENTER);
522                 todayLabel.addMouseListener(internalController);
523                 updateTodayLabel();
524             }
525             return todayLabel;
526         }
527 
528         /**
529          * This method initializes centerPanel
530          *
531          * @return javax.swing.JPanel
532          */
533         private JPanel getCenterPanel() {
534             if (centerPanel == null) {
535                 centerPanel = new JPanel();
536                 centerPanel.setLayout(new BorderLayout());
537                 centerPanel.setOpaque(false);
538                 centerPanel.add(getDayTableHeader(), BorderLayout.NORTH);
539                 centerPanel.add(getDayTable(), BorderLayout.CENTER);
540             }
541             return centerPanel;
542         }
543 
544         /**
545          * This method initializes dayTable
546          *
547          * @return javax.swing.JTable
548          */
549         private JTable getDayTable() {
550             if (dayTable == null) {
551                 dayTable = new JTable();
552                 dayTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
553                 dayTable.setModel(internalModel);
554                 dayTable.setShowGrid(true);
555                 dayTable.setGridColor(getColors().getColor(AbstractComponentColor.Key.BG_GRID));
556                 dayTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
557                 dayTable.setCellSelectionEnabled(true);
558                 dayTable.setRowSelectionAllowed(true);
559                 dayTable.setFocusable(false);
560                 dayTable.addMouseListener(internalController);
561                 for (int i = 0; i < 7; i++) {
562                     TableColumn column = dayTable.getColumnModel().getColumn(i);
563                     column.setCellRenderer(getDayTableCellRenderer());
564                 }
565                 dayTable.addComponentListener(new ComponentListener() {
566 
567                     public void componentResized(ComponentEvent e) {
568                         // The new size of the table
569                         final double w = e.getComponent().getSize().getWidth();
570                         final double h = e.getComponent().getSize().getHeight();
571 
572                         // Set the size of the font as a fraction of the width or the height, whichever is smallest
573                         final float sw = (float) (w / 18);
574                         final float sh = (float) (h / 9);
575                         float fs = Math.min(sw, sh);
576                         dayTable.setFont(dayTable.getFont().deriveFont(fs));
577 
578                         // Set the row height as a fraction of the height
579                         final int r = (int) Math.floor(h / 6);
580                         dayTable.setRowHeight(r);
581                     }
582 
583                     public void componentMoved(ComponentEvent e) {
584                         // Do nothing
585                     }
586 
587                     public void componentShown(ComponentEvent e) {
588                         // Do nothing
589                     }
590 
591                     public void componentHidden(ComponentEvent e) {
592                         // Do nothing
593                     }
594 
595                 });
596             }
597             return dayTable;
598         }
599 
600         private InternalTableCellRenderer getDayTableCellRenderer() {
601             if (dayTableCellRenderer == null) {
602                 dayTableCellRenderer = new InternalTableCellRenderer();
603             }
604             return dayTableCellRenderer;
605         }
606 
607         private JTableHeader getDayTableHeader() {
608             if (dayTableHeader == null) {
609                 dayTableHeader = getDayTable().getTableHeader();
610                 dayTableHeader.setResizingAllowed(false);
611                 dayTableHeader.setReorderingAllowed(false);
612                 dayTableHeader.setDefaultRenderer(getDayTableCellRenderer());
613             }
614             return dayTableHeader;
615         }
616 
617         /**
618          * This method initializes previousButtonPanel
619          *
620          * @return javax.swing.JPanel
621          */
622         private JPanel getPreviousButtonPanel() {
623             if (previousButtonPanel == null) {
624                 previousButtonPanel = new JPanel();
625                 GridLayout layout = new GridLayout(1, 2);
626                 layout.setHgap(3);
627                 previousButtonPanel.setLayout(layout);
628                 previousButtonPanel.setName("");
629                 previousButtonPanel.setBackground(getColors().getColor(AbstractComponentColor.Key.BG_MONTH_SELECTOR));
630                 if (isShowYearButtons()) {
631                     previousButtonPanel.add(getPreviousYearButton());
632                 }
633                 previousButtonPanel.add(getPreviousMonthButton());
634             }
635             return previousButtonPanel;
636         }
637 
638         /**
639          * This method initializes nextButtonPanel
640          *
641          * @return javax.swing.JPanel
642          */
643         private JPanel getNextButtonPanel() {
644             if (nextButtonPanel == null) {
645                 nextButtonPanel = new JPanel();
646                 GridLayout layout = new GridLayout(1, 2);
647                 layout.setHgap(3);
648                 nextButtonPanel.setLayout(layout);
649                 nextButtonPanel.setName("");
650                 nextButtonPanel.setBackground(getColors().getColor(AbstractComponentColor.Key.BG_MONTH_SELECTOR));
651                 nextButtonPanel.add(getNextMonthButton());
652                 if (isShowYearButtons()) {
653                     nextButtonPanel.add(getNextYearButton());
654                 }
655             }
656             return nextButtonPanel;
657         }
658 
659         /**
660          * This method initializes nextMonthButton
661          *
662          * @return javax.swing.JButton
663          */
664         private JButton getNextMonthButton() {
665             if (nextMonthButton == null) {
666                 nextMonthButton = new JButton();
667                 nextMonthButton.setIcon(getIcons().getNextMonthIconEnabled());
668                 nextMonthButton.setDisabledIcon(getIcons().getNextMonthIconDisabled());
669                 nextMonthButton.setText("");
670                 nextMonthButton.setPreferredSize(new Dimension(20, 15));
671                 nextMonthButton.setFocusable(false);
672                 nextMonthButton.addActionListener(internalController);
673                 nextMonthButton.setToolTipText(getTexts().getText(ComponentTextDefaults.Key.MONTH));
674             }
675             return nextMonthButton;
676         }
677 
678         /**
679          * This method initializes nextYearButton
680          *
681          * @return javax.swing.JButton
682          */
683         private JButton getNextYearButton() {
684             if (nextYearButton == null) {
685                 nextYearButton = new JButton();
686                 nextYearButton.setIcon(getIcons().getNextYearIconEnabled());
687                 nextYearButton.setDisabledIcon(getIcons().getNextYearIconDisabled());
688                 nextYearButton.setText("");
689                 nextYearButton.setPreferredSize(new Dimension(20, 15));
690                 nextYearButton.setFocusable(false);
691                 nextYearButton.addActionListener(internalController);
692                 nextYearButton.setToolTipText(getTexts().getText(ComponentTextDefaults.Key.YEAR));
693             }
694             return nextYearButton;
695         }
696 
697         /**
698          * This method initializes previousMonthButton
699          *
700          * @return javax.swing.JButton
701          */
702         private JButton getPreviousMonthButton() {
703             if (previousMonthButton == null) {
704                 previousMonthButton = new JButton();
705                 previousMonthButton.setIcon(getIcons().getPreviousMonthIconEnabled());
706                 previousMonthButton.setDisabledIcon(getIcons().getPreviousMonthIconDisabled());
707                 previousMonthButton.setText("");
708                 previousMonthButton.setPreferredSize(new Dimension(20, 15));
709                 previousMonthButton.setFocusable(false);
710                 previousMonthButton.addActionListener(internalController);
711                 previousMonthButton.setToolTipText(getTexts().getText(ComponentTextDefaults.Key.MONTH));
712             }
713             return previousMonthButton;
714         }
715 
716         /**
717          * This method initializes previousMonthButton
718          *
719          * @return javax.swing.JButton
720          */
721         private JButton getPreviousYearButton() {
722             if (previousYearButton == null) {
723                 previousYearButton = new JButton();
724                 previousYearButton.setIcon(getIcons().getPreviousYearIconEnabled());
725                 previousYearButton.setDisabledIcon(getIcons().getPreviousYearIconDisabled());
726                 previousYearButton.setText("");
727                 previousYearButton.setPreferredSize(new Dimension(20, 15));
728                 previousYearButton.setFocusable(false);
729                 previousYearButton.addActionListener(internalController);
730                 previousYearButton.setToolTipText(getTexts().getText(ComponentTextDefaults.Key.YEAR));
731             }
732             return previousYearButton;
733         }
734 
735         /**
736          * This method initializes monthPopupMenu
737          *
738          * @return javax.swing.JPopupMenu
739          */
740         private JPopupMenu getMonthPopupMenu() {
741             if (monthPopupMenu == null) {
742                 monthPopupMenu = new JPopupMenu();
743                 JMenuItem[] menuItems = getMonthPopupMenuItems();
744                 for (JMenuItem menuItem : menuItems) {
745                     monthPopupMenu.add(menuItem);
746                 }
747             }
748             return monthPopupMenu;
749         }
750 
751         private JMenuItem[] getMonthPopupMenuItems() {
752             if (monthPopupMenuItems == null) {
753                 monthPopupMenuItems = new JMenuItem[12];
754                 for (int i = 0; i < 12; i++) {
755                     JMenuItem mi = new JMenuItem(getTexts().getText(ComponentTextDefaults.Key.getMonthKey(i)));
756                     mi.addActionListener(internalController);
757                     monthPopupMenuItems[i] = mi;
758                 }
759             }
760             return monthPopupMenuItems;
761         }
762 
763         @Override
764         public void setEnabled(boolean enabled) {
765             dayTable.setEnabled(enabled);
766             dayTableCellRenderer.setEnabled(enabled);
767             nextMonthButton.setEnabled(enabled);
768             if (nextYearButton != null) {
769                 nextYearButton.setEnabled(enabled);
770             }
771             previousMonthButton.setEnabled(enabled);
772             if (previousYearButton != null) {
773                 previousYearButton.setEnabled(enabled);
774             }
775             yearSpinner.setEnabled(enabled);
776             if (enabled) {
777                 todayLabel.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_TODAY_SELECTOR_ENABLED));
778             } else {
779                 todayLabel.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_TODAY_SELECTOR_DISABLED));
780             }
781 
782             super.setEnabled(enabled);
783         }
784     }
785 
786     /**
787      * This inner class renders the table of the days, setting colors based on
788      * whether it is in the month, if it is today, if it is selected etc.
789      *
790      * @author Juan Heyns
791      */
792     private class InternalTableCellRenderer extends DefaultTableCellRenderer {
793 
794         private static final long serialVersionUID = -2341614459632756921L;
795 
796         public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
797             // Exit this method if the value is null, encountered from JTable#AccessibleJTable
798             if (value == null) {
799                 return super.getTableCellRendererComponent(table, null, isSelected, hasFocus, row, column);
800             }
801 
802             JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
803             label.setHorizontalAlignment(JLabel.CENTER);
804 
805             if (row == -1) {
806                 label.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_GRID_HEADER));
807                 label.setBackground(getColors().getColor(AbstractComponentColor.Key.BG_GRID_HEADER));
808                 label.setHorizontalAlignment(JLabel.CENTER);
809                 return label;
810             }
811 
812             LocalDate today = LocalDate.now();
813             LocalDate date = LocalDate.of(internalModel.getModel().getYear(), internalModel.getModel().getMonth(), internalModel.getModel().getDay());
814 
815             int cellDayValue = (Integer) value;
816             int lastDayOfMonth = date.lengthOfMonth();
817 
818             // Other month
819             if (cellDayValue < 1 || cellDayValue > lastDayOfMonth) {
820                 label.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_GRID_OTHER_MONTH));
821                 LocalDate outDate = date.withDayOfMonth(1).plusDays(cellDayValue);
822                 label.setBackground(checkConstraints(new LocalDateModel(outDate)) ?
823                         getColors().getColor(AbstractComponentColor.Key.BG_GRID) :
824                         getColors().getColor(AbstractComponentColor.Key.BG_GRID_NOT_SELECTABLE)
825                 );
826 
827                 //Past end of month
828                 if (cellDayValue > lastDayOfMonth) {
829                     label.setText(Integer.toString(cellDayValue - lastDayOfMonth));
830                 }
831                 //Before start of month
832                 else {
833                     int lastDayLastMonth = date.withDayOfMonth(1).minusDays(1).lengthOfMonth();
834                     label.setText(Integer.toString(lastDayLastMonth + cellDayValue));
835                 }
836             }
837             //This month
838             else {
839                 label.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_GRID_THIS_MONTH));
840 
841                 LocalDate cellDate = date.withDayOfMonth(cellDayValue);
842                 label.setBackground(checkConstraints(new LocalDateModel(cellDate)) ?
843                         getColors().getColor(AbstractComponentColor.Key.BG_GRID) :
844                         getColors().getColor(AbstractComponentColor.Key.BG_GRID_NOT_SELECTABLE)
845                 );
846 
847                 //Today
848                 if (cellDate.equals(today)) {
849                     label.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_GRID_TODAY));
850                     //Selected
851                     if (internalModel.getModel().isSelected() && cellDate.equals(date)) {
852                         label.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_GRID_TODAY_SELECTED));
853                         label.setBackground(getColors().getColor(AbstractComponentColor.Key.BG_GRID_TODAY_SELECTED));
854                     }
855                 }
856                 //Other day
857                 else {
858                     //Selected
859                     if (internalModel.getModel().isSelected() && cellDate.equals(date)) {
860                         label.setForeground(getColors().getColor(AbstractComponentColor.Key.FG_GRID_SELECTED));
861                         label.setBackground(getColors().getColor(AbstractComponentColor.Key.BG_GRID_SELECTED));
862                     }
863                 }
864             }
865 
866             return label;
867         }
868 
869     }
870 
871     /**
872      * This inner class hides the public view event handling methods from the
873      * outside. This class acts as an internal controller for this component. It
874      * receives events from the view components and updates the model.
875      *
876      * @author Juan Heyns
877      */
878     private class InternalController implements ActionListener, MouseListener {
879 
880         /**
881          * Next, Previous and Month buttons clicked, causes the model to be
882          * updated.
883          */
884         public void actionPerformed(ActionEvent e) {
885             if (!JLocalDatePanel.this.isEnabled()) {
886                 return;
887             }
888 
889             if (e.getSource() == internalView.getNextMonthButton()) {
890                 internalModel.getModel().addMonth(1);
891             } else if (e.getSource() == internalView.getPreviousMonthButton()) {
892                 internalModel.getModel().addMonth(-1);
893             } else if (e.getSource() == internalView.getNextYearButton()) {
894                 internalModel.getModel().addYear(1);
895             } else if (e.getSource() == internalView.getPreviousYearButton()) {
896                 internalModel.getModel().addYear(-1);
897             } else {
898                 for (int month = 0; month < internalView.getMonthPopupMenuItems().length; month++) {
899                     if (e.getSource() == internalView.getMonthPopupMenuItems()[month]) {
900                         internalModel.getModel().setMonth(month + 1);
901                     }
902                 }
903             }
904         }
905 
906         /**
907          * Mouse down on monthLabel pops up a table. Mouse down on todayLabel
908          * sets the value of the internal model to today. Mouse down on day
909          * table will set the day to the value. Mouse down on none label will
910          * clear the date.
911          */
912         public void mouseClicked(MouseEvent e) {
913             if (!JLocalDatePanel.this.isEnabled()) {
914                 return;
915             }
916 
917             if (e.getSource() == internalView.getMonthLabel()) {
918                 internalView.getMonthPopupMenu().setLightWeightPopupEnabled(false);
919                 internalView.getMonthPopupMenu().show((Component) e.getSource(), e.getX(), e.getY());
920             } else if (e.getSource() == internalView.getTodayLabel()) {
921                 internalModel.getModel().setLocalDate(LocalDate.now());
922             } else if (e.getSource() == internalView.getDayTable()) {
923                 int row = internalView.getDayTable().getSelectedRow();
924                 int col = internalView.getDayTable().getSelectedColumn();
925                 if (row >= 0 && row <= 5) {
926                     Integer date = (Integer) internalModel.getValueAt(row, col);
927 
928                     // check constraints
929                     int oldDay = internalModel.getModel().getDay();
930                     internalModel.getModel().setDay(date);
931                     if (!checkConstraints(internalModel.getModel())) {
932                         // rollback
933                         internalModel.getModel().setDay(oldDay);
934                         return;
935                     }
936 
937                     internalModel.getModel().setSelected(true);
938 
939                     if (doubleClickAction && e.getClickCount() == 2) {
940                         fireActionPerformed();
941                     }
942                     if (!doubleClickAction) {
943                         fireActionPerformed();
944                     }
945                 }
946             } else if (e.getSource() == internalView.getNoneLabel()) {
947                 internalModel.getModel().setSelected(false);
948 
949                 if (doubleClickAction && e.getClickCount() == 2) {
950                     fireActionPerformed();
951                 }
952                 if (!doubleClickAction) {
953                     fireActionPerformed();
954                 }
955             }
956 
957             e.consume();
958         }
959 
960         public void mousePressed(MouseEvent e) {
961         }
962 
963         public void mouseEntered(MouseEvent e) {
964         }
965 
966         public void mouseExited(MouseEvent e) {
967         }
968 
969         public void mouseReleased(MouseEvent e) {
970         }
971 
972     }
973 
974     /**
975      * This model represents the selected date. The model implements the
976      * TableModel interface for displaying days, and it implements the
977      * SpinnerModel for the year.
978      *
979      * @author Juan Heyns
980      */
981     protected class InternalCalendarModel implements TableModel, SpinnerModel, ChangeListener {
982 
983         private final LocalDateModel model;
984         private final Set<ChangeListener> spinnerChangeListeners;
985         private final Set<TableModelListener> tableModelListeners;
986 
987         protected InternalCalendarModel(LocalDateModel model) {
988             this.spinnerChangeListeners = new HashSet<>();
989             this.tableModelListeners = new HashSet<>();
990             this.model = model;
991             model.addChangeListener(this);
992         }
993 
994         public LocalDateModel getModel() {
995             return model;
996         }
997 
998         /**
999          * Part of SpinnerModel, year
1000          */
1001         public void addChangeListener(ChangeListener e) {
1002             spinnerChangeListeners.add(e);
1003         }
1004 
1005         /**
1006          * Part of SpinnerModel, year
1007          */
1008         public void removeChangeListener(ChangeListener e) {
1009             spinnerChangeListeners.remove(e);
1010         }
1011 
1012         /**
1013          * Part of SpinnerModel, year
1014          */
1015         public Object getNextValue() {
1016             return Integer.toString(model.getYear() + 1);
1017         }
1018 
1019         /**
1020          * Part of SpinnerModel, year
1021          */
1022         public Object getPreviousValue() {
1023             return Integer.toString(model.getYear() - 1);
1024         }
1025 
1026         /**
1027          * Part of SpinnerModel, year
1028          */
1029         public void setValue(Object text) {
1030             String year = (String) text;
1031             model.setYear(Integer.parseInt(year));
1032         }
1033 
1034         /**
1035          * Part of SpinnerModel, year
1036          */
1037         public Object getValue() {
1038             return Integer.toString(model.getYear());
1039         }
1040 
1041         /**
1042          * Part of TableModel, day
1043          */
1044         public void addTableModelListener(TableModelListener e) {
1045             tableModelListeners.add(e);
1046         }
1047 
1048         /**
1049          * Part of TableModel, day
1050          */
1051         public void removeTableModelListener(TableModelListener e) {
1052             tableModelListeners.remove(e);
1053         }
1054 
1055         /**
1056          * Part of TableModel, day
1057          */
1058         public int getColumnCount() {
1059             return 7;
1060         }
1061 
1062         /**
1063          * Part of TableModel, day
1064          */
1065         public int getRowCount() {
1066             return 6;
1067         }
1068 
1069         /**
1070          * Part of TableModel, day
1071          */
1072         public String getColumnName(int columnIndex) {
1073             ComponentTextDefaults.Key key = ComponentTextDefaults.Key.getDowKey((2 + columnIndex) % 7);
1074             return getTexts().getText(key);
1075         }
1076 
1077         private int[] lookup = null;
1078 
1079         /**
1080          * Results in a mapping which calculates the number of days before the first day of month
1081          * <p/>
1082          * DAY OF WEEK
1083          * M T W T F S S
1084          * 1 2 3 4 5 6 0
1085          * <p/>
1086          * DAYS BEFORE
1087          * 0 1 2 3 4 5 6
1088          *
1089          */
1090         private int[] lookup() {
1091             if (lookup == null) {
1092                 lookup = new int[8];
1093                 lookup[(0) % 7] = 0;
1094                 lookup[(1) % 7] = 1;
1095                 lookup[(2) % 7] = 2;
1096                 lookup[(3) % 7] = 3;
1097                 lookup[(4) % 7] = 4;
1098                 lookup[(5) % 7] = 5;
1099                 lookup[(6) % 7] = 6;
1100             }
1101             return lookup;
1102         }
1103 
1104         /**
1105          * Part of TableModel, day
1106          * <p/>
1107          * previous month (... -1, 0) ->
1108          * current month (1...DAYS_IN_MONTH) ->
1109          * next month (DAYS_IN_MONTH + 1, DAYS_IN_MONTH + 2, ...)
1110          */
1111         public Object getValueAt(int rowIndex, int columnIndex) {
1112             int series = columnIndex + rowIndex * 7 + 1;
1113 
1114             LocalDate startOfMonth = model.getSafeLocalDate().withDayOfMonth(1);
1115             int dowForFirst = startOfMonth.getDayOfWeek().getValue();
1116             int daysBefore = lookup()[dowForFirst - 1];
1117 
1118             return series - daysBefore;
1119         }
1120 
1121         /**
1122          * Part of TableModel, day
1123          */
1124         @SuppressWarnings({"rawtypes"})
1125         public Class getColumnClass(int e) {
1126             return Integer.class;
1127         }
1128 
1129         /**
1130          * Part of TableModel, day
1131          */
1132         public boolean isCellEditable(int e, int arg1) {
1133             return false;
1134         }
1135 
1136         /**
1137          * Part of TableModel, day
1138          */
1139         public void setValueAt(Object e, int arg1, int arg2) {
1140         }
1141 
1142         /**
1143          * Called whenever a change is made to the model value. Notify the
1144          * internal listeners and update the simple controls. Also notifies the
1145          * (external) ChangeListeners of the component, since the internal state
1146          * has changed.
1147          */
1148         private void fireValueChanged() {
1149             //Update year spinner
1150             for (ChangeListener cl : spinnerChangeListeners) {
1151                 cl.stateChanged(new ChangeEvent(this));
1152             }
1153 
1154             //Update month label
1155             internalView.updateMonthLabel();
1156 
1157             //Update day table
1158             for (TableModelListener tl : tableModelListeners) {
1159                 tl.tableChanged(new TableModelEvent(this));
1160             }
1161         }
1162 
1163         /**
1164          * The model has changed and needs to notify the InternalModel.
1165          */
1166         public void stateChanged(ChangeEvent e) {
1167             fireValueChanged();
1168         }
1169 
1170     }
1171 
1172 }