1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package io.github.tonywasher.joceanus.tethys.swing.button;
18
19 import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
20 import io.github.tonywasher.joceanus.oceanus.date.OceanusDateConfig;
21 import io.github.tonywasher.joceanus.oceanus.event.OceanusEventManager;
22 import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar;
23 import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar.OceanusEventProvider;
24 import io.github.tonywasher.joceanus.tethys.api.base.TethysUIEvent;
25 import io.github.tonywasher.joceanus.tethys.core.base.TethysUIResource;
26 import io.github.tonywasher.joceanus.tethys.swing.base.TethysUISwingArrowIcon;
27
28 import javax.swing.AbstractAction;
29 import javax.swing.ActionMap;
30 import javax.swing.BorderFactory;
31 import javax.swing.Box;
32 import javax.swing.BoxLayout;
33 import javax.swing.InputMap;
34 import javax.swing.JButton;
35 import javax.swing.JComponent;
36 import javax.swing.JDialog;
37 import javax.swing.JLabel;
38 import javax.swing.JPanel;
39 import javax.swing.KeyStroke;
40 import javax.swing.SwingConstants;
41 import javax.swing.border.Border;
42 import java.awt.BorderLayout;
43 import java.awt.Color;
44 import java.awt.Font;
45 import java.awt.GridLayout;
46 import java.awt.Insets;
47 import java.awt.Point;
48 import java.awt.Rectangle;
49 import java.awt.Window;
50 import java.awt.event.ActionEvent;
51 import java.awt.event.KeyEvent;
52 import java.awt.event.MouseAdapter;
53 import java.awt.event.MouseEvent;
54 import java.awt.event.WindowAdapter;
55 import java.awt.event.WindowEvent;
56 import java.io.Serial;
57 import java.time.DayOfWeek;
58 import java.time.format.TextStyle;
59 import java.util.Calendar;
60 import java.util.Locale;
61
62
63
64
65 public final class TethysUISwingDateDialog
66 implements OceanusEventProvider<TethysUIEvent> {
67
68
69
70 private static final String NLS_NULLSELECT = TethysUIResource.DIALOG_NULL.getValue();
71
72
73
74
75 private static final String ACTION_ESCAPE = "Escape";
76
77
78
79
80 private final JDialog theDialog;
81
82
83
84
85 private final OceanusEventManager<TethysUIEvent> theEventManager;
86
87
88
89
90 private final PanelMonth theDaysPanel;
91
92
93
94
95 private final PanelNavigation theNavigation;
96
97
98
99
100 private final JButton theNullButton;
101
102
103
104
105 private boolean isNullActive;
106
107
108
109
110 private final OceanusDateConfig theConfig;
111
112
113
114
115 private boolean doBuildNames = true;
116
117
118
119
120 private final JPanel theContainer;
121
122
123
124
125 private boolean haveSelected;
126
127
128
129
130
131
132 TethysUISwingDateDialog(final OceanusDateConfig pConfig) {
133
134 theDialog = new JDialog();
135
136
137 theDialog.setUndecorated(true);
138
139
140 theConfig = pConfig;
141
142
143 theEventManager = new OceanusEventManager<>();
144
145
146 theDaysPanel = new PanelMonth(this);
147 theNavigation = new PanelNavigation(this);
148
149
150 theNullButton = new JButton(NLS_NULLSELECT);
151 theNullButton.addActionListener(e -> setSelected(-1));
152
153
154 theContainer = new JPanel(new BorderLayout());
155 theContainer.setBorder(BorderFactory.createLineBorder(Color.black));
156 theContainer.add(theNavigation.getPanel(), BorderLayout.NORTH);
157 theContainer.add(theDaysPanel.getPanel(), BorderLayout.CENTER);
158 theDialog.setContentPane(theContainer);
159 theDialog.pack();
160
161
162 initialiseMonth();
163
164
165 handleEscapeKey(theContainer);
166
167
168 theDialog.addWindowFocusListener(new CalendarFocus());
169
170
171 theConfig.getEventRegistrar().addEventListener(e -> doBuildNames());
172 }
173
174 @Override
175 public OceanusEventRegistrar<TethysUIEvent> getEventRegistrar() {
176 return theEventManager.getEventRegistrar();
177 }
178
179
180
181
182 private void doBuildNames() {
183 doBuildNames = true;
184 }
185
186
187
188
189
190
191 public boolean haveSelected() {
192 return haveSelected;
193 }
194
195
196
197
198
199
200 public OceanusDateConfig getConfig() {
201 return theConfig;
202 }
203
204
205
206
207 void buildMonth() {
208
209 theNavigation.buildMonth();
210 theDaysPanel.buildMonth();
211 }
212
213
214
215
216
217
218 void setSelected(final int pDay) {
219
220 theConfig.setSelectedDay(pDay);
221
222
223 haveSelected = true;
224
225
226 theDialog.setVisible(false);
227
228
229 theEventManager.fireEvent(TethysUIEvent.NEWVALUE, theConfig.getSelectedDate());
230 }
231
232
233
234
235 void reSizeDialog() {
236 theDialog.pack();
237 }
238
239
240
241
242 private void initialiseMonth() {
243
244 theConfig.initialiseCurrent();
245
246
247 if (doBuildNames) {
248 theDaysPanel.buildDayNames();
249 }
250 doBuildNames = false;
251
252
253 buildMonth();
254
255
256 if (isNullActive != theConfig.allowNullDateSelection()) {
257
258 if (!isNullActive) {
259 theContainer.add(theNullButton, BorderLayout.SOUTH);
260 } else {
261 theContainer.remove(theNullButton);
262 }
263
264
265 isNullActive = theConfig.allowNullDateSelection();
266 reSizeDialog();
267 }
268 }
269
270
271
272
273
274
275 public void showDialogUnderNode(final JComponent pNode) {
276
277 final Point myLoc = pNode.getLocationOnScreen();
278 theDialog.setLocation(myLoc.x, myLoc.y
279 + pNode.getHeight());
280
281
282 showDialog();
283 }
284
285
286
287
288
289
290 public void showDialogUnderRectangle(final Rectangle pRect) {
291
292 theDialog.setLocation(pRect.x, pRect.y + pRect.height);
293
294
295 showDialog();
296 }
297
298
299
300
301 private void showDialog() {
302
303 theEventManager.fireEvent(TethysUIEvent.PREPAREDIALOG, theConfig);
304
305
306 haveSelected = false;
307
308
309 initialiseMonth();
310 theDialog.setVisible(true);
311 theDialog.requestFocus();
312 }
313
314
315
316
317
318
319 private void handleEscapeKey(final JPanel pPane) {
320
321 final ActionMap myAction = pPane.getActionMap();
322 final InputMap myInput = pPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
323
324
325 myInput.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), ACTION_ESCAPE);
326 myAction.put(ACTION_ESCAPE, new CalendarAction());
327 }
328
329
330
331
332 void closeNonModal() {
333
334 theDialog.setVisible(false);
335
336
337 theEventManager.fireEvent(TethysUIEvent.WINDOWCLOSED);
338 }
339
340
341
342
343 private final class CalendarAction
344 extends AbstractAction {
345
346
347
348 @Serial
349 private static final long serialVersionUID = 5464442251457102478L;
350
351 @Override
352 public void actionPerformed(final ActionEvent e) {
353 closeNonModal();
354 }
355 }
356
357
358
359
360 private final class CalendarFocus
361 extends WindowAdapter {
362 @Override
363 public void windowLostFocus(final WindowEvent e) {
364 final Window myOppo = e.getOppositeWindow();
365
366
367
368
369 if (myOppo != null) {
370 closeNonModal();
371 }
372 }
373 }
374
375
376
377
378 private static final class PanelNavigation {
379
380
381
382 private static final String NLS_NEXTMONTH = TethysUIResource.DIALOG_NEXTMONTH.getValue();
383
384
385
386
387 private static final String NLS_PREVMONTH = TethysUIResource.DIALOG_PREVMONTH.getValue();
388
389
390
391
392 private static final String NLS_NEXTYEAR = TethysUIResource.DIALOG_NEXTYEAR.getValue();
393
394
395
396
397 private static final String NLS_PREVYEAR = TethysUIResource.DIALOG_PREVYEAR.getValue();
398
399
400
401
402 private final JPanel thePanel;
403
404
405
406
407 private final TethysUISwingDateDialog theDialog;
408
409
410
411
412 private final OceanusDateConfig theConfig;
413
414
415
416
417 private final JLabel theDateLabel;
418
419
420
421
422 private final JButton thePrevMonthButton;
423
424
425
426
427 private final JButton theNextMonthButton;
428
429
430
431
432 private final JButton thePrevYearButton;
433
434
435
436
437 private final JButton theNextYearButton;
438
439
440
441
442
443
444 PanelNavigation(final TethysUISwingDateDialog pDialog) {
445
446 theDialog = pDialog;
447
448
449 thePanel = new JPanel();
450
451
452 theConfig = pDialog.getConfig();
453
454
455 theDateLabel = new JLabel();
456
457
458 thePrevMonthButton = new JButton(TethysUISwingArrowIcon.LEFT);
459 theNextMonthButton = new JButton(TethysUISwingArrowIcon.RIGHT);
460 thePrevYearButton = new JButton(TethysUISwingArrowIcon.DOUBLELEFT);
461 theNextYearButton = new JButton(TethysUISwingArrowIcon.DOUBLERIGHT);
462
463
464 theNextMonthButton.setToolTipText(NLS_NEXTMONTH);
465 thePrevMonthButton.setToolTipText(NLS_PREVMONTH);
466 theNextYearButton.setToolTipText(NLS_NEXTYEAR);
467 thePrevYearButton.setToolTipText(NLS_PREVYEAR);
468
469
470 thePrevMonthButton.addActionListener(e -> {
471 theConfig.previousMonth();
472 theDialog.buildMonth();
473 });
474 theNextMonthButton.addActionListener(e -> {
475 theConfig.nextMonth();
476 theDialog.buildMonth();
477 });
478 thePrevYearButton.addActionListener(e -> {
479 theConfig.previousYear();
480 theDialog.buildMonth();
481 });
482 theNextYearButton.addActionListener(e -> {
483 theConfig.nextYear();
484 theDialog.buildMonth();
485 });
486
487
488 thePrevMonthButton.setMargin(new Insets(1, 1, 1, 1));
489 theNextMonthButton.setMargin(new Insets(1, 1, 1, 1));
490 thePrevYearButton.setMargin(new Insets(1, 1, 1, 1));
491 theNextYearButton.setMargin(new Insets(1, 1, 1, 1));
492
493
494 thePanel.setLayout(new BoxLayout(thePanel, BoxLayout.X_AXIS));
495 thePanel.add(thePrevYearButton);
496 thePanel.add(Box.createHorizontalStrut(1));
497 thePanel.add(thePrevMonthButton);
498 thePanel.add(Box.createHorizontalGlue());
499 thePanel.add(theDateLabel);
500 thePanel.add(Box.createHorizontalGlue());
501 thePanel.add(theNextMonthButton);
502 thePanel.add(Box.createHorizontalStrut(1));
503 thePanel.add(theNextYearButton);
504 }
505
506
507
508
509
510
511 JPanel getPanel() {
512 return thePanel;
513 }
514
515
516
517
518 private void buildMonth() {
519
520 final OceanusDate myBase = theConfig.getCurrentMonth();
521 final Locale myLocale = theConfig.getLocale();
522
523
524 final String myMonth = myBase.getMonthValue().getDisplayName(TextStyle.FULL, myLocale);
525 final String myYear = Integer.toString(myBase.getYear());
526
527
528 theDateLabel.setText(myMonth
529 + ", "
530 + myYear);
531
532
533 final OceanusDate myEarliest = theConfig.getEarliestDate();
534 final OceanusDate myLatest = theConfig.getLatestDate();
535
536
537 thePrevMonthButton.setEnabled(!OceanusDateConfig.isSameMonth(myEarliest, myBase));
538 thePrevYearButton.setEnabled(!OceanusDateConfig.isSameYear(myEarliest, myBase));
539 theNextMonthButton.setEnabled(!OceanusDateConfig.isSameMonth(myLatest, myBase));
540 theNextYearButton.setEnabled(!OceanusDateConfig.isSameYear(myLatest, myBase));
541 }
542 }
543
544
545
546
547 private static final class PanelMonth {
548
549
550
551 private static final int DAYS_IN_WEEK = 7;
552
553
554
555
556 private static final int MAX_WEEKS_IN_MONTH = 6;
557
558
559
560
561 private final DayOfWeek[] theDaysOfWk = new DayOfWeek[DAYS_IN_WEEK];
562
563
564
565
566 private final JLabel[] theHdrs = new JLabel[DAYS_IN_WEEK];
567
568
569
570
571 private final PanelDay[][] theDays = new PanelDay[MAX_WEEKS_IN_MONTH][DAYS_IN_WEEK];
572
573
574
575
576 private final JPanel thePanel;
577
578
579
580
581 private final TethysUISwingDateDialog theDialog;
582
583
584
585
586 private final OceanusDateConfig theConfig;
587
588
589
590
591 private int theNumRows = MAX_WEEKS_IN_MONTH;
592
593
594
595
596
597
598 PanelMonth(final TethysUISwingDateDialog pDialog) {
599
600 thePanel = new JPanel();
601
602
603 theDialog = pDialog;
604
605
606 theConfig = pDialog.getConfig();
607
608
609 final GridLayout myLayout = new GridLayout();
610 myLayout.setColumns(DAYS_IN_WEEK);
611 myLayout.setRows(0);
612 thePanel.setLayout(myLayout);
613
614
615 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
616
617 final JLabel myLabel = new JLabel();
618 theHdrs[iCol] = myLabel;
619
620
621 myLabel.setHorizontalAlignment(SwingConstants.CENTER);
622 myLabel.setBackground(Color.lightGray);
623 myLabel.setOpaque(true);
624
625
626 thePanel.add(myLabel);
627 }
628
629
630 for (int iRow = 0; iRow < MAX_WEEKS_IN_MONTH; iRow++) {
631 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
632 final PanelDay myDay = new PanelDay(pDialog);
633 theDays[iRow][iCol] = myDay;
634 thePanel.add(myDay.getLabel());
635 }
636 }
637 }
638
639
640
641
642
643
644 JPanel getPanel() {
645 return thePanel;
646 }
647
648
649
650
651
652
653 private void reSizeRows(final int iNumRows) {
654
655 while (iNumRows < theNumRows) {
656
657 theNumRows--;
658
659
660 for (final PanelDay day : theDays[theNumRows]) {
661
662 thePanel.remove(day.getLabel());
663 }
664 }
665
666
667 while (iNumRows > theNumRows) {
668
669 for (final PanelDay day : theDays[theNumRows]) {
670
671 thePanel.add(day.getLabel());
672 }
673
674
675 theNumRows++;
676 }
677
678
679 theDialog.reSizeDialog();
680 }
681
682
683
684
685
686
687
688 private static boolean isWeekend(final DayOfWeek pDoW) {
689 switch (pDoW) {
690 case SATURDAY:
691 case SUNDAY:
692 return true;
693 default:
694 return false;
695 }
696 }
697
698
699
700
701
702
703
704 private int getDayColumn(final DayOfWeek pDoW) {
705 for (int i = 0; i < DAYS_IN_WEEK; i++) {
706 if (theDaysOfWk[i] == pDoW) {
707 return i;
708 }
709 }
710 return -1;
711 }
712
713
714
715
716 private void buildMonth() {
717 int iRow = 0;
718 int iCol = 0;
719
720
721 final OceanusDate myCurr = new OceanusDate(theConfig.getCurrentMonth());
722 final int iMonth = myCurr.getMonth();
723
724
725 final DayOfWeek myWeekDay = myCurr.getDayOfWeek();
726 final int iFirstCol = getDayColumn(myWeekDay);
727
728
729 final int iCurrent = theConfig.getCurrentDay();
730 final int iSelected = theConfig.getSelectedDay();
731 final int iEarliest = theConfig.getEarliestDay();
732 final int iLatest = theConfig.getLatestDay();
733
734
735 if (iFirstCol > 0) {
736 myCurr.adjustDay(-iFirstCol);
737 }
738
739
740 for (int iDay = myCurr.getDay(); iCol < iFirstCol; iCol++, iDay++, myCurr.adjustDay(1)) {
741
742 final PanelDay myLabel = theDays[0][iCol];
743
744
745 myLabel.resetDay(false);
746 myLabel.setDay(iDay, false);
747 }
748
749
750 for (int iDay = 1; iMonth == myCurr.getMonth(); iCol++, iDay++, myCurr.adjustDay(1)) {
751
752 if (iCol > MAX_WEEKS_IN_MONTH) {
753 iRow++;
754 iCol = 0;
755 }
756
757
758 final PanelDay myLabel = theDays[iRow][iCol];
759
760
761 myLabel.resetDay(true);
762 if (isWeekend(myCurr.getDayOfWeek())) {
763 myLabel.setWeekend();
764 }
765 if (iCurrent == iDay) {
766 myLabel.setCurrent();
767 }
768 if (iSelected == iDay) {
769 myLabel.setSelected();
770 }
771
772
773 boolean isSelectable = true;
774 if (iEarliest > 0) {
775 isSelectable &= iDay >= iEarliest;
776 }
777 if (iLatest > 0) {
778 isSelectable &= iDay <= iLatest;
779 }
780
781
782 isSelectable &= theConfig.isAllowed(iDay);
783
784
785 myLabel.setDay(iDay, isSelectable);
786 }
787
788
789 for (int iDay = 1; iCol < DAYS_IN_WEEK; iCol++, iDay++) {
790
791 final PanelDay myLabel = theDays[iRow][iCol];
792
793
794 myLabel.resetDay(false);
795 myLabel.setDay(iDay, false);
796 }
797
798
799 reSizeRows(iRow + 1);
800 }
801
802
803
804
805 void buildDayNames() {
806
807 final Locale myLocale = theConfig.getLocale();
808 final Calendar myDate = Calendar.getInstance(myLocale);
809 int myStart = myDate.getFirstDayOfWeek();
810 if (myStart == Calendar.SUNDAY) {
811 myStart += DAYS_IN_WEEK;
812 }
813
814
815 DayOfWeek myDoW = DayOfWeek.of(myStart - 1);
816 for (int iDay = 0; iDay < DAYS_IN_WEEK; iDay++, myDoW = myDoW.plus(1)) {
817
818 theDaysOfWk[iDay] = myDoW;
819 }
820
821
822 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
823
824 final JLabel myLabel = theHdrs[iCol];
825
826
827 myDoW = theDaysOfWk[iCol];
828 final TextStyle myStyle = theConfig.showNarrowDays()
829 ? TextStyle.NARROW
830 : TextStyle.SHORT;
831 final String myName = myDoW.getDisplayName(myStyle, myLocale);
832
833
834 myLabel.setText(myName);
835
836
837 myLabel.setForeground(isWeekend(myDoW)
838 ? Color.red
839 : Color.black);
840 }
841 }
842 }
843
844
845
846
847 private static final class PanelDay {
848
849
850
851 private static final Border BORDER_STD = BorderFactory.createEmptyBorder();
852
853
854
855
856 private static final Border BORDER_SEL = BorderFactory.createLineBorder(Color.green.darker());
857
858
859
860
861 private static final Border BORDER_HLT = BorderFactory.createLineBorder(Color.orange);
862
863
864
865
866 private static final String FONT_NAME = "Courier";
867
868
869
870
871 private static final int FONT_SIZE = 10;
872
873
874
875
876 private static final Font FONT_STANDARD = new Font(FONT_NAME, Font.PLAIN, FONT_SIZE);
877
878
879
880
881 private static final Font FONT_INACTIVE = new Font(FONT_NAME, Font.ITALIC, FONT_SIZE);
882
883
884
885
886 private static final Font FONT_SELECTED = new Font(FONT_NAME, Font.BOLD, FONT_SIZE);
887
888
889
890
891 private static final String NLS_CURRENTDAY = TethysUIResource.DIALOG_CURRENT.getValue();
892
893
894
895
896 private static final String NLS_SELECTEDDAY = TethysUIResource.DIALOG_SELECTED.getValue();
897
898
899
900
901 private final JLabel theLabel;
902
903
904
905
906 private final TethysUISwingDateDialog theDialog;
907
908
909
910
911 private int theDay = -1;
912
913
914
915
916 private boolean isSelectable;
917
918
919
920
921 private Font theFont;
922
923
924
925
926 private Color theForeGround;
927
928
929
930
931 private Color theBackGround;
932
933
934
935
936 private Border theBorder;
937
938
939
940
941 private String theToolTip;
942
943
944
945
946
947
948 PanelDay(final TethysUISwingDateDialog pDialog) {
949
950 theDialog = pDialog;
951
952
953 theLabel = new JLabel();
954
955
956 theLabel.setHorizontalAlignment(SwingConstants.CENTER);
957 theLabel.setOpaque(true);
958 theLabel.addMouseListener(new CalendarMouse());
959 }
960
961
962
963
964
965
966 JLabel getLabel() {
967 return theLabel;
968 }
969
970
971
972
973
974
975
976 void setDay(final int pDay,
977 final boolean pSelectable) {
978
979 theDay = pDay;
980 isSelectable = pSelectable;
981
982
983 if (pDay > 0) {
984 theLabel.setText(Integer.toString(theDay));
985 } else {
986 theLabel.setText("");
987 }
988
989
990 theLabel.setFont(theFont);
991 theLabel.setForeground(theForeGround);
992 theLabel.setBackground(theBackGround);
993 theLabel.setBorder(theBorder);
994 theLabel.setToolTipText(theToolTip);
995
996
997 theLabel.setEnabled(isSelectable);
998 }
999
1000
1001
1002
1003
1004
1005 void resetDay(final boolean isActive) {
1006
1007 theFont = isActive
1008 ? FONT_STANDARD
1009 : FONT_INACTIVE;
1010 theForeGround = Color.black;
1011 theBackGround = Color.white;
1012 theBorder = BORDER_STD;
1013 theToolTip = null;
1014 isSelectable = isActive;
1015 }
1016
1017
1018
1019
1020 void setWeekend() {
1021
1022 theForeGround = Color.red;
1023 }
1024
1025
1026
1027
1028 void setCurrent() {
1029
1030 theForeGround = Color.blue;
1031 theBackGround = Color.gray;
1032 theToolTip = NLS_CURRENTDAY;
1033 }
1034
1035
1036
1037
1038 void setSelected() {
1039
1040 theFont = FONT_SELECTED;
1041 theForeGround = Color.green.darker();
1042 theBackGround = Color.green.brighter();
1043 theBorder = BORDER_SEL;
1044 theToolTip = NLS_SELECTEDDAY;
1045 }
1046
1047
1048
1049
1050 void handleMouseClicked() {
1051
1052 if (isSelectable) {
1053 theDialog.setSelected(theDay);
1054 }
1055 }
1056
1057
1058
1059
1060 void handleMouseEntered() {
1061
1062 if (isSelectable) {
1063 theLabel.setBorder(BORDER_HLT);
1064 }
1065 }
1066
1067
1068
1069
1070 void handleMouseExited() {
1071
1072 if (isSelectable) {
1073 theLabel.setBorder(theBorder);
1074 }
1075 }
1076
1077
1078
1079
1080 private final class CalendarMouse
1081 extends MouseAdapter {
1082 @Override
1083 public void mouseClicked(final MouseEvent e) {
1084 handleMouseClicked();
1085 }
1086
1087 @Override
1088 public void mouseEntered(final MouseEvent e) {
1089 handleMouseEntered();
1090 }
1091
1092 @Override
1093 public void mouseExited(final MouseEvent e) {
1094 handleMouseExited();
1095 }
1096 }
1097 }
1098 }