1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package io.github.tonywasher.joceanus.tethys.javafx.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.javafx.base.TethysUIFXArrowIcon;
27 import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXUtils;
28 import javafx.collections.ObservableList;
29 import javafx.event.ActionEvent;
30 import javafx.geometry.Bounds;
31 import javafx.scene.Node;
32 import javafx.scene.Scene;
33 import javafx.scene.control.Button;
34 import javafx.scene.control.Label;
35 import javafx.scene.control.Tooltip;
36 import javafx.scene.input.KeyCode;
37 import javafx.scene.input.MouseEvent;
38 import javafx.scene.layout.BorderPane;
39 import javafx.scene.layout.GridPane;
40 import javafx.scene.layout.HBox;
41 import javafx.scene.layout.Priority;
42 import javafx.scene.layout.Region;
43 import javafx.stage.Modality;
44 import javafx.stage.Stage;
45 import javafx.stage.StageStyle;
46
47 import java.time.DayOfWeek;
48 import java.time.format.TextStyle;
49 import java.time.temporal.WeekFields;
50 import java.util.Locale;
51
52
53
54
55 public final class TethysUIFXDateDialog
56 implements OceanusEventProvider<TethysUIEvent> {
57
58
59
60 private static final String CSS_STYLE_NAME = "jtethys-javafx-datedialog.css";
61
62
63
64
65 private static final String CSS_STYLE = TethysUIFXDateDialog.class.getResource(CSS_STYLE_NAME).toExternalForm();
66
67
68
69
70 static final String STYLE_DIALOG = TethysUIFXUtils.CSS_STYLE_BASE + "-datedialog";
71
72
73
74
75 private static final String NLS_NULLSELECT = TethysUIResource.DIALOG_NULL.getValue();
76
77
78
79
80 private final Stage theStage;
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 Button 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 BorderPane theContainer;
121
122
123
124
125 private boolean haveSelected;
126
127
128
129
130
131
132 public TethysUIFXDateDialog(final OceanusDateConfig pConfig) {
133
134 theStage = new Stage(StageStyle.UNDECORATED);
135 theStage.initModality(Modality.NONE);
136
137
138 theConfig = pConfig;
139
140
141 theEventManager = new OceanusEventManager<>();
142
143
144 theDaysPanel = new PanelMonth(this);
145 theNavigation = new PanelNavigation(this);
146
147
148 theNullButton = new Button(NLS_NULLSELECT);
149 theNullButton.setMaxWidth(Double.MAX_VALUE);
150 theNullButton.addEventHandler(ActionEvent.ACTION, e -> setSelected(-1));
151
152
153 theStage.focusedProperty().addListener((v, o, n) -> {
154 if (Boolean.FALSE.equals(n)) {
155 closeNonModal();
156 }
157 });
158
159
160 theContainer = new BorderPane();
161 theContainer.setTop(theNavigation.getHBox());
162 theContainer.setCenter(theDaysPanel.getGridPane());
163 theContainer.getStyleClass().add(STYLE_DIALOG);
164 final Scene myScene = new Scene(theContainer);
165 final ObservableList<String> mySheets = myScene.getStylesheets();
166 mySheets.add(CSS_STYLE);
167 theStage.setScene(myScene);
168
169
170 initialiseMonth();
171
172
173 theContainer.setOnKeyPressed(e -> {
174 if (e.getCode() == KeyCode.ESCAPE) {
175 closeNonModal();
176 }
177 });
178
179
180 theConfig.getEventRegistrar().addEventListener(e -> doBuildNames());
181 }
182
183 @Override
184 public OceanusEventRegistrar<TethysUIEvent> getEventRegistrar() {
185 return theEventManager.getEventRegistrar();
186 }
187
188
189
190
191 private void doBuildNames() {
192 doBuildNames = true;
193 }
194
195
196
197
198
199
200 public boolean haveSelected() {
201 return haveSelected;
202 }
203
204
205
206
207
208
209 public OceanusDateConfig getDateConfig() {
210 return theConfig;
211 }
212
213
214
215
216 void buildMonth() {
217
218 theNavigation.buildMonth();
219 theDaysPanel.buildMonth();
220 }
221
222
223
224
225
226
227 void setSelected(final int pDay) {
228
229 theConfig.setSelectedDay(pDay);
230
231
232 haveSelected = true;
233
234
235 theStage.close();
236
237
238 theEventManager.fireEvent(TethysUIEvent.NEWVALUE, theConfig.getSelectedDate());
239 }
240
241
242
243
244 void reSizeDialog() {
245 theStage.sizeToScene();
246 }
247
248
249
250
251 private void initialiseMonth() {
252
253 theConfig.initialiseCurrent();
254
255
256 if (doBuildNames) {
257 theDaysPanel.buildDayNames();
258 }
259 doBuildNames = false;
260
261
262 buildMonth();
263
264
265 if (isNullActive != theConfig.allowNullDateSelection()) {
266
267 if (!isNullActive) {
268 theContainer.setBottom(theNullButton);
269 } else {
270 theContainer.setBottom(null);
271 }
272
273
274 isNullActive = theConfig.allowNullDateSelection();
275 reSizeDialog();
276 }
277 }
278
279
280
281
282
283
284 public void showDialogUnderNode(final Node pNode) {
285
286 theEventManager.fireEvent(TethysUIEvent.PREPAREDIALOG, theConfig);
287
288
289 final Bounds myBounds = pNode.localToScreen(pNode.getLayoutBounds());
290
291
292 theStage.setX(myBounds.getMinX());
293 theStage.setY(myBounds.getMaxY());
294
295
296 haveSelected = false;
297
298
299 initialiseMonth();
300 theStage.show();
301 }
302
303
304
305
306 private void closeNonModal() {
307
308 theStage.close();
309
310
311 theEventManager.fireEvent(TethysUIEvent.WINDOWCLOSED);
312 }
313
314
315
316
317 private static final class PanelNavigation {
318
319
320
321 private static final String NLS_NEXTMONTH = TethysUIResource.DIALOG_NEXTMONTH.getValue();
322
323
324
325
326 private static final String NLS_PREVMONTH = TethysUIResource.DIALOG_PREVMONTH.getValue();
327
328
329
330
331 private static final String NLS_NEXTYEAR = TethysUIResource.DIALOG_NEXTYEAR.getValue();
332
333
334
335
336 private static final String NLS_PREVYEAR = TethysUIResource.DIALOG_PREVYEAR.getValue();
337
338
339
340
341 private static final String STYLE_BUTTON = STYLE_DIALOG + "-button";
342
343
344
345
346 private static final String STYLE_TITLE = STYLE_DIALOG + "-title";
347
348
349
350
351 private final TethysUIFXDateDialog theDialog;
352
353
354
355
356 private final OceanusDateConfig theConfig;
357
358
359
360
361 private final HBox theHBox;
362
363
364
365
366 private final Label theDateLabel;
367
368
369
370
371 private final Button thePrevMonthButton;
372
373
374
375
376 private final Button theNextMonthButton;
377
378
379
380
381 private final Button thePrevYearButton;
382
383
384
385
386 private final Button theNextYearButton;
387
388
389
390
391
392
393 PanelNavigation(final TethysUIFXDateDialog pDialog) {
394
395 theHBox = new HBox(1.0);
396
397
398 theDialog = pDialog;
399
400
401 theConfig = pDialog.getDateConfig();
402
403
404 theDateLabel = new Label();
405 theDateLabel.getStyleClass().add(STYLE_TITLE);
406
407
408 thePrevMonthButton = new Button();
409 theNextMonthButton = new Button();
410 thePrevYearButton = new Button();
411 theNextYearButton = new Button();
412
413
414 thePrevMonthButton.setGraphic(TethysUIFXArrowIcon.LEFT.getArrow());
415 theNextMonthButton.setGraphic(TethysUIFXArrowIcon.RIGHT.getArrow());
416 thePrevYearButton.setGraphic(TethysUIFXArrowIcon.DOUBLELEFT.getArrow());
417 theNextYearButton.setGraphic(TethysUIFXArrowIcon.DOUBLERIGHT.getArrow());
418
419
420 theNextMonthButton.setTooltip(new Tooltip(NLS_NEXTMONTH));
421 thePrevMonthButton.setTooltip(new Tooltip(NLS_PREVMONTH));
422 theNextYearButton.setTooltip(new Tooltip(NLS_NEXTYEAR));
423 thePrevYearButton.setTooltip(new Tooltip(NLS_PREVYEAR));
424
425
426 thePrevMonthButton.addEventHandler(ActionEvent.ACTION, e -> {
427 theConfig.previousMonth();
428 theDialog.buildMonth();
429 });
430 theNextMonthButton.addEventHandler(ActionEvent.ACTION, e -> {
431 theConfig.nextMonth();
432 theDialog.buildMonth();
433 });
434 thePrevYearButton.addEventHandler(ActionEvent.ACTION, e -> {
435 theConfig.previousYear();
436 theDialog.buildMonth();
437 });
438 theNextYearButton.addEventFilter(ActionEvent.ACTION, e -> {
439 theConfig.nextYear();
440 theDialog.buildMonth();
441 });
442
443
444 thePrevMonthButton.getStyleClass().add(STYLE_BUTTON);
445 theNextMonthButton.getStyleClass().add(STYLE_BUTTON);
446 thePrevYearButton.getStyleClass().add(STYLE_BUTTON);
447 theNextYearButton.getStyleClass().add(STYLE_BUTTON);
448
449
450 final Region myStrut1 = new Region();
451 final Region myStrut2 = new Region();
452 HBox.setHgrow(myStrut1, Priority.ALWAYS);
453 HBox.setHgrow(myStrut2, Priority.ALWAYS);
454
455
456 final ObservableList<Node> myChildren = theHBox.getChildren();
457 myChildren.add(thePrevYearButton);
458 myChildren.add(thePrevMonthButton);
459 myChildren.add(myStrut1);
460 myChildren.add(theDateLabel);
461 myChildren.add(myStrut2);
462 myChildren.add(theNextMonthButton);
463 myChildren.add(theNextYearButton);
464 }
465
466
467
468
469
470
471 HBox getHBox() {
472 return theHBox;
473 }
474
475
476
477
478 private void buildMonth() {
479
480 final OceanusDate myBase = theConfig.getCurrentMonth();
481 final Locale myLocale = theConfig.getLocale();
482
483
484 final String myMonth = myBase.getMonthValue().getDisplayName(TextStyle.FULL, myLocale);
485 final String myYear = Integer.toString(myBase.getYear());
486
487
488 theDateLabel.setText(myMonth
489 + ", "
490 + myYear);
491
492
493 final OceanusDate myEarliest = theConfig.getEarliestDate();
494 final OceanusDate myLatest = theConfig.getLatestDate();
495
496
497 thePrevMonthButton.setDisable(OceanusDateConfig.isSameMonth(myEarliest, myBase));
498 thePrevYearButton.setDisable(OceanusDateConfig.isSameYear(myEarliest, myBase));
499 theNextMonthButton.setDisable(OceanusDateConfig.isSameMonth(myLatest, myBase));
500 theNextYearButton.setDisable(OceanusDateConfig.isSameYear(myLatest, myBase));
501 }
502 }
503
504
505
506
507 private static final class PanelMonth {
508
509
510
511 private static final String STYLE_PANEL = STYLE_DIALOG + "-month";
512
513
514
515
516 private static final String STYLE_HEADER = STYLE_DIALOG + "-hdr";
517
518
519
520
521 private static final int DAYS_IN_WEEK = 7;
522
523
524
525
526 private static final int MAX_WEEKS_IN_MONTH = 6;
527
528
529
530
531 private static final int WIDTH_TILE = 22;
532
533
534
535
536 private final TethysUIFXDateDialog theDialog;
537
538
539
540
541 private final OceanusDateConfig theConfig;
542
543
544
545
546 private final GridPane theGridPane;
547
548
549
550
551 private final DayOfWeek[] theDaysOfWk = new DayOfWeek[DAYS_IN_WEEK];
552
553
554
555
556 private final Label[] theHdrs = new Label[DAYS_IN_WEEK];
557
558
559
560
561 private final PanelDay[][] theDays = new PanelDay[MAX_WEEKS_IN_MONTH][DAYS_IN_WEEK];
562
563
564
565
566 private int theNumRows = MAX_WEEKS_IN_MONTH;
567
568
569
570
571
572
573 PanelMonth(final TethysUIFXDateDialog pDialog) {
574
575 theDialog = pDialog;
576
577
578 theGridPane = new GridPane();
579
580
581 theConfig = pDialog.getDateConfig();
582
583
584 theGridPane.getStyleClass().add(STYLE_PANEL);
585
586
587 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
588 final Label myDay = new Label();
589 myDay.setMaxWidth(Double.MAX_VALUE);
590 theHdrs[iCol] = myDay;
591 theGridPane.add(myDay, iCol, 0);
592 }
593
594
595 for (int iRow = 0; iRow < MAX_WEEKS_IN_MONTH; iRow++) {
596 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
597 final PanelDay myDay = new PanelDay(theDialog);
598 theDays[iRow][iCol] = myDay;
599 theGridPane.add(myDay.getLabel(), iCol, iRow + 1);
600 }
601 }
602
603
604 buildDayNames();
605 }
606
607
608
609
610
611
612 GridPane getGridPane() {
613 return theGridPane;
614 }
615
616
617
618
619
620
621
622 private static boolean isWeekend(final DayOfWeek pDoW) {
623 return switch (pDoW) {
624 case SATURDAY, SUNDAY -> true;
625 default -> false;
626 };
627 }
628
629
630
631
632
633
634
635 private int getDayColumn(final DayOfWeek pDoW) {
636 for (int i = 0; i < DAYS_IN_WEEK; i++) {
637 if (theDaysOfWk[i].equals(pDoW)) {
638 return i;
639 }
640 }
641 return -1;
642 }
643
644
645
646
647 void buildMonth() {
648
649 final OceanusDate myCurr = new OceanusDate(theConfig.getCurrentMonth());
650
651
652 final int iCurrent = theConfig.getCurrentDay();
653 final int iSelected = theConfig.getSelectedDay();
654 final int iEarliest = theConfig.getEarliestDay();
655 final int iLatest = theConfig.getLatestDay();
656
657
658 final DayOfWeek myWeekDay = myCurr.getDayOfWeek();
659 final int iStart = getDayColumn(myWeekDay);
660 if (iStart > 0) {
661 myCurr.adjustDay(-iStart);
662 }
663
664
665 int iCol = 0;
666 for (int iDay = myCurr.getDay(); iCol < iStart; iCol++, iDay++, myCurr.adjustDay(1)) {
667
668 final PanelDay myLabel = theDays[0][iCol];
669
670
671 myLabel.resetDay(false);
672 myLabel.setDay(iDay, false);
673 }
674
675
676 int iRow = 0;
677 final int iMonth = myCurr.getMonth();
678 for (int iDay = 1; iMonth == myCurr.getMonth(); iCol++, iDay++, myCurr.adjustDay(1)) {
679
680 if (iCol >= DAYS_IN_WEEK) {
681 iRow++;
682 iCol = 0;
683 }
684
685
686 final PanelDay myLabel = theDays[iRow][iCol];
687
688
689 myLabel.resetDay(true);
690
691 if (iSelected == iDay) {
692 myLabel.setSelected();
693 } else if (iCurrent == iDay) {
694 myLabel.setCurrent();
695 } else if (isWeekend(myCurr.getDayOfWeek())) {
696 myLabel.setWeekend();
697 }
698
699
700 boolean isSelectable = true;
701 if (iEarliest > 0) {
702 isSelectable &= iDay >= iEarliest;
703 }
704 if (iLatest > 0) {
705 isSelectable &= iDay <= iLatest;
706 }
707
708
709 isSelectable &= theConfig.isAllowed(iDay);
710
711
712 myLabel.setDay(iDay, isSelectable);
713 }
714
715
716 for (int iDay = 1; iCol < DAYS_IN_WEEK; iCol++, iDay++) {
717
718 final PanelDay myLabel = theDays[iRow][iCol];
719
720
721 myLabel.resetDay(false);
722 myLabel.setDay(iDay, false);
723 }
724
725
726 reSizeRows(iRow + 1);
727 }
728
729
730
731
732 void buildDayNames() {
733
734 final Locale myLocale = theConfig.getLocale();
735 final WeekFields myWeekFields = WeekFields.of(myLocale);
736
737
738 DayOfWeek myDoW = myWeekFields.getFirstDayOfWeek();
739 for (int iDay = 0; iDay < DAYS_IN_WEEK; iDay++, myDoW = myDoW.plus(1)) {
740
741 theDaysOfWk[iDay] = myDoW;
742 }
743
744
745 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
746
747 final Label myLabel = theHdrs[iCol];
748
749
750 final ObservableList<String> myStyles = myLabel.getStyleClass();
751 myStyles.clear();
752 myStyles.add(STYLE_HEADER);
753
754
755 myDoW = theDaysOfWk[iCol];
756 final TextStyle myStyle = theConfig.showNarrowDays()
757 ? TextStyle.NARROW
758 : TextStyle.SHORT;
759 final String myName = myDoW.getDisplayName(myStyle, myLocale);
760
761
762 myLabel.setText(myName);
763
764
765 if (isWeekend(myDoW)) {
766 myStyles.add(PanelDay.STYLE_WEEKEND);
767 }
768 }
769 }
770
771
772
773
774
775
776 private void reSizeRows(final int iNumRows) {
777
778 final ObservableList<Node> myNodes = theGridPane.getChildren();
779
780
781 while (iNumRows < theNumRows) {
782
783 theNumRows--;
784
785
786 for (final PanelDay myDay : theDays[theNumRows]) {
787
788 myNodes.remove(myDay.getLabel());
789 }
790 }
791
792
793 while (iNumRows > theNumRows) {
794
795 theNumRows++;
796
797
798 int iCol = 0;
799 for (final PanelDay myDay : theDays[theNumRows - 1]) {
800
801 theGridPane.add(myDay.getLabel(), iCol++, theNumRows);
802 }
803 }
804
805
806 theDialog.reSizeDialog();
807 }
808 }
809
810
811
812
813 private static final class PanelDay {
814
815
816
817 private static final String NLS_CURRENTDAY = TethysUIResource.DIALOG_CURRENT.getValue();
818
819
820
821
822 private static final String NLS_SELECTEDDAY = TethysUIResource.DIALOG_SELECTED.getValue();
823
824
825
826
827 private static final String STYLE_PANEL = STYLE_DIALOG + "-day";
828
829
830
831
832 private static final String STYLE_INACTIVE = STYLE_DIALOG + "-inactive";
833
834
835
836
837 private static final String STYLE_SELECTED = STYLE_DIALOG + "-selected";
838
839
840
841
842 private static final String STYLE_CURRENT = STYLE_DIALOG + "-current";
843
844
845
846
847 private static final String STYLE_WEEKEND = STYLE_DIALOG + "-weekend";
848
849
850
851
852 private final TethysUIFXDateDialog theDialog;
853
854
855
856
857 private final Label theLabel;
858
859
860
861
862 private int theDay = -1;
863
864
865
866
867
868
869 PanelDay(final TethysUIFXDateDialog pDialog) {
870
871 theDialog = pDialog;
872
873
874 theLabel = new Label();
875
876
877 theLabel.setPrefWidth(PanelMonth.WIDTH_TILE);
878 theLabel.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> theDialog.setSelected(theDay));
879 }
880
881
882
883
884
885
886 Label getLabel() {
887 return theLabel;
888 }
889
890
891
892
893
894
895 void resetDay(final boolean isActive) {
896 theLabel.setTooltip(null);
897 theLabel.getStyleClass().clear();
898 theLabel.getStyleClass().add(STYLE_PANEL);
899 if (!isActive) {
900 theLabel.getStyleClass().add(STYLE_INACTIVE);
901 }
902 }
903
904
905
906
907
908
909
910 void setDay(final int pDay,
911 final boolean pSelectable) {
912
913 theDay = pDay;
914
915
916 if (pDay > 0) {
917 theLabel.setText(Integer.toString(theDay));
918 } else {
919 theLabel.setText("");
920 }
921
922
923 theLabel.setDisable(!pSelectable);
924 }
925
926
927
928
929 void setWeekend() {
930 theLabel.getStyleClass().add(STYLE_WEEKEND);
931 }
932
933
934
935
936 void setSelected() {
937 theLabel.getStyleClass().add(STYLE_SELECTED);
938 theLabel.setTooltip(new Tooltip(NLS_SELECTEDDAY));
939 }
940
941
942
943
944 void setCurrent() {
945 theLabel.getStyleClass().add(STYLE_CURRENT);
946 theLabel.setTooltip(new Tooltip(NLS_CURRENTDAY));
947 }
948 }
949 }