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.time.temporal.WeekFields;
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 return switch (pDoW) {
690 case SATURDAY, SUNDAY -> true;
691 default -> false;
692 };
693 }
694
695
696
697
698
699
700
701 private int getDayColumn(final DayOfWeek pDoW) {
702 for (int i = 0; i < DAYS_IN_WEEK; i++) {
703 if (theDaysOfWk[i].equals(pDoW)) {
704 return i;
705 }
706 }
707 return -1;
708 }
709
710
711
712
713 private void buildMonth() {
714 int iRow = 0;
715 int iCol = 0;
716
717
718 final OceanusDate myCurr = new OceanusDate(theConfig.getCurrentMonth());
719 final int iMonth = myCurr.getMonth();
720
721
722 final DayOfWeek myWeekDay = myCurr.getDayOfWeek();
723 final int iFirstCol = getDayColumn(myWeekDay);
724
725
726 final int iCurrent = theConfig.getCurrentDay();
727 final int iSelected = theConfig.getSelectedDay();
728 final int iEarliest = theConfig.getEarliestDay();
729 final int iLatest = theConfig.getLatestDay();
730
731
732 if (iFirstCol > 0) {
733 myCurr.adjustDay(-iFirstCol);
734 }
735
736
737 for (int iDay = myCurr.getDay(); iCol < iFirstCol; iCol++, iDay++, myCurr.adjustDay(1)) {
738
739 final PanelDay myLabel = theDays[0][iCol];
740
741
742 myLabel.resetDay(false);
743 myLabel.setDay(iDay, false);
744 }
745
746
747 for (int iDay = 1; iMonth == myCurr.getMonth(); iCol++, iDay++, myCurr.adjustDay(1)) {
748
749 if (iCol > MAX_WEEKS_IN_MONTH) {
750 iRow++;
751 iCol = 0;
752 }
753
754
755 final PanelDay myLabel = theDays[iRow][iCol];
756
757
758 myLabel.resetDay(true);
759 if (isWeekend(myCurr.getDayOfWeek())) {
760 myLabel.setWeekend();
761 }
762 if (iCurrent == iDay) {
763 myLabel.setCurrent();
764 }
765 if (iSelected == iDay) {
766 myLabel.setSelected();
767 }
768
769
770 boolean isSelectable = true;
771 if (iEarliest > 0) {
772 isSelectable &= iDay >= iEarliest;
773 }
774 if (iLatest > 0) {
775 isSelectable &= iDay <= iLatest;
776 }
777
778
779 isSelectable &= theConfig.isAllowed(iDay);
780
781
782 myLabel.setDay(iDay, isSelectable);
783 }
784
785
786 for (int iDay = 1; iCol < DAYS_IN_WEEK; iCol++, iDay++) {
787
788 final PanelDay myLabel = theDays[iRow][iCol];
789
790
791 myLabel.resetDay(false);
792 myLabel.setDay(iDay, false);
793 }
794
795
796 reSizeRows(iRow + 1);
797 }
798
799
800
801
802 void buildDayNames() {
803
804 final Locale myLocale = theConfig.getLocale();
805 final WeekFields myWeekFields = WeekFields.of(myLocale);
806
807
808 DayOfWeek myDoW = myWeekFields.getFirstDayOfWeek();
809 for (int iDay = 0; iDay < DAYS_IN_WEEK; iDay++, myDoW = myDoW.plus(1)) {
810
811 theDaysOfWk[iDay] = myDoW;
812 }
813
814
815 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
816
817 final JLabel myLabel = theHdrs[iCol];
818
819
820 myDoW = theDaysOfWk[iCol];
821 final TextStyle myStyle = theConfig.showNarrowDays()
822 ? TextStyle.NARROW
823 : TextStyle.SHORT;
824 final String myName = myDoW.getDisplayName(myStyle, myLocale);
825
826
827 myLabel.setText(myName);
828
829
830 myLabel.setForeground(isWeekend(myDoW)
831 ? Color.red
832 : Color.black);
833 }
834 }
835 }
836
837
838
839
840 private static final class PanelDay {
841
842
843
844 private static final Border BORDER_STD = BorderFactory.createEmptyBorder();
845
846
847
848
849 private static final Border BORDER_SEL = BorderFactory.createLineBorder(Color.green.darker());
850
851
852
853
854 private static final Border BORDER_HLT = BorderFactory.createLineBorder(Color.orange);
855
856
857
858
859 private static final String FONT_NAME = "Courier";
860
861
862
863
864 private static final int FONT_SIZE = 10;
865
866
867
868
869 private static final Font FONT_STANDARD = new Font(FONT_NAME, Font.PLAIN, FONT_SIZE);
870
871
872
873
874 private static final Font FONT_INACTIVE = new Font(FONT_NAME, Font.ITALIC, FONT_SIZE);
875
876
877
878
879 private static final Font FONT_SELECTED = new Font(FONT_NAME, Font.BOLD, FONT_SIZE);
880
881
882
883
884 private static final String NLS_CURRENTDAY = TethysUIResource.DIALOG_CURRENT.getValue();
885
886
887
888
889 private static final String NLS_SELECTEDDAY = TethysUIResource.DIALOG_SELECTED.getValue();
890
891
892
893
894 private final JLabel theLabel;
895
896
897
898
899 private final TethysUISwingDateDialog theDialog;
900
901
902
903
904 private int theDay = -1;
905
906
907
908
909 private boolean isSelectable;
910
911
912
913
914 private Font theFont;
915
916
917
918
919 private Color theForeGround;
920
921
922
923
924 private Color theBackGround;
925
926
927
928
929 private Border theBorder;
930
931
932
933
934 private String theToolTip;
935
936
937
938
939
940
941 PanelDay(final TethysUISwingDateDialog pDialog) {
942
943 theDialog = pDialog;
944
945
946 theLabel = new JLabel();
947
948
949 theLabel.setHorizontalAlignment(SwingConstants.CENTER);
950 theLabel.setOpaque(true);
951 theLabel.addMouseListener(new CalendarMouse());
952 }
953
954
955
956
957
958
959 JLabel getLabel() {
960 return theLabel;
961 }
962
963
964
965
966
967
968
969 void setDay(final int pDay,
970 final boolean pSelectable) {
971
972 theDay = pDay;
973 isSelectable = pSelectable;
974
975
976 if (pDay > 0) {
977 theLabel.setText(Integer.toString(theDay));
978 } else {
979 theLabel.setText("");
980 }
981
982
983 theLabel.setFont(theFont);
984 theLabel.setForeground(theForeGround);
985 theLabel.setBackground(theBackGround);
986 theLabel.setBorder(theBorder);
987 theLabel.setToolTipText(theToolTip);
988
989
990 theLabel.setEnabled(isSelectable);
991 }
992
993
994
995
996
997
998 void resetDay(final boolean isActive) {
999
1000 theFont = isActive
1001 ? FONT_STANDARD
1002 : FONT_INACTIVE;
1003 theForeGround = Color.black;
1004 theBackGround = Color.white;
1005 theBorder = BORDER_STD;
1006 theToolTip = null;
1007 isSelectable = isActive;
1008 }
1009
1010
1011
1012
1013 void setWeekend() {
1014
1015 theForeGround = Color.red;
1016 }
1017
1018
1019
1020
1021 void setCurrent() {
1022
1023 theForeGround = Color.blue;
1024 theBackGround = Color.gray;
1025 theToolTip = NLS_CURRENTDAY;
1026 }
1027
1028
1029
1030
1031 void setSelected() {
1032
1033 theFont = FONT_SELECTED;
1034 theForeGround = Color.green.darker();
1035 theBackGround = Color.green.brighter();
1036 theBorder = BORDER_SEL;
1037 theToolTip = NLS_SELECTEDDAY;
1038 }
1039
1040
1041
1042
1043 void handleMouseClicked() {
1044
1045 if (isSelectable) {
1046 theDialog.setSelected(theDay);
1047 }
1048 }
1049
1050
1051
1052
1053 void handleMouseEntered() {
1054
1055 if (isSelectable) {
1056 theLabel.setBorder(BORDER_HLT);
1057 }
1058 }
1059
1060
1061
1062
1063 void handleMouseExited() {
1064
1065 if (isSelectable) {
1066 theLabel.setBorder(theBorder);
1067 }
1068 }
1069
1070
1071
1072
1073 private final class CalendarMouse
1074 extends MouseAdapter {
1075 @Override
1076 public void mouseClicked(final MouseEvent e) {
1077 handleMouseClicked();
1078 }
1079
1080 @Override
1081 public void mouseEntered(final MouseEvent e) {
1082 handleMouseEntered();
1083 }
1084
1085 @Override
1086 public void mouseExited(final MouseEvent e) {
1087 handleMouseExited();
1088 }
1089 }
1090 }
1091 }