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