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 javafx.collections.ObservableList;
25 import javafx.event.ActionEvent;
26 import javafx.geometry.Bounds;
27 import javafx.scene.Node;
28 import javafx.scene.Scene;
29 import javafx.scene.control.Button;
30 import javafx.scene.control.Label;
31 import javafx.scene.control.Tooltip;
32 import javafx.scene.input.KeyCode;
33 import javafx.scene.input.MouseEvent;
34 import javafx.scene.layout.BorderPane;
35 import javafx.scene.layout.GridPane;
36 import javafx.scene.layout.HBox;
37 import javafx.scene.layout.Priority;
38 import javafx.scene.layout.Region;
39 import javafx.stage.Modality;
40 import javafx.stage.Stage;
41 import javafx.stage.StageStyle;
42 import io.github.tonywasher.joceanus.tethys.api.base.TethysUIEvent;
43 import io.github.tonywasher.joceanus.tethys.core.base.TethysUIResource;
44 import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXArrowIcon;
45 import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXUtils;
46
47 import java.time.DayOfWeek;
48 import java.time.format.TextStyle;
49 import java.util.Calendar;
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 switch (pDoW) {
624 case SATURDAY:
625 case SUNDAY:
626 return true;
627 default:
628 return false;
629 }
630 }
631
632
633
634
635
636
637
638 private int getDayColumn(final DayOfWeek pDoW) {
639 for (int i = 0; i < DAYS_IN_WEEK; i++) {
640 if (theDaysOfWk[i] == pDoW) {
641 return i;
642 }
643 }
644 return -1;
645 }
646
647
648
649
650 void buildMonth() {
651
652 final OceanusDate myCurr = new OceanusDate(theConfig.getCurrentMonth());
653
654
655 final int iCurrent = theConfig.getCurrentDay();
656 final int iSelected = theConfig.getSelectedDay();
657 final int iEarliest = theConfig.getEarliestDay();
658 final int iLatest = theConfig.getLatestDay();
659
660
661 final DayOfWeek myWeekDay = myCurr.getDayOfWeek();
662 final int iStart = getDayColumn(myWeekDay);
663 if (iStart > 0) {
664 myCurr.adjustDay(-iStart);
665 }
666
667
668 int iCol = 0;
669 for (int iDay = myCurr.getDay(); iCol < iStart; iCol++, iDay++, myCurr.adjustDay(1)) {
670
671 final PanelDay myLabel = theDays[0][iCol];
672
673
674 myLabel.resetDay(false);
675 myLabel.setDay(iDay, false);
676 }
677
678
679 int iRow = 0;
680 final int iMonth = myCurr.getMonth();
681 for (int iDay = 1; iMonth == myCurr.getMonth(); iCol++, iDay++, myCurr.adjustDay(1)) {
682
683 if (iCol >= DAYS_IN_WEEK) {
684 iRow++;
685 iCol = 0;
686 }
687
688
689 final PanelDay myLabel = theDays[iRow][iCol];
690
691
692 myLabel.resetDay(true);
693
694 if (iSelected == iDay) {
695 myLabel.setSelected();
696 } else if (iCurrent == iDay) {
697 myLabel.setCurrent();
698 } else if (isWeekend(myCurr.getDayOfWeek())) {
699 myLabel.setWeekend();
700 }
701
702
703 boolean isSelectable = true;
704 if (iEarliest > 0) {
705 isSelectable &= iDay >= iEarliest;
706 }
707 if (iLatest > 0) {
708 isSelectable &= iDay <= iLatest;
709 }
710
711
712 isSelectable &= theConfig.isAllowed(iDay);
713
714
715 myLabel.setDay(iDay, isSelectable);
716 }
717
718
719 for (int iDay = 1; iCol < DAYS_IN_WEEK; iCol++, iDay++) {
720
721 final PanelDay myLabel = theDays[iRow][iCol];
722
723
724 myLabel.resetDay(false);
725 myLabel.setDay(iDay, false);
726 }
727
728
729 reSizeRows(iRow + 1);
730 }
731
732
733
734
735 void buildDayNames() {
736
737 final Locale myLocale = theConfig.getLocale();
738 final Calendar myDate = Calendar.getInstance(myLocale);
739 int myStart = myDate.getFirstDayOfWeek();
740 if (myStart == Calendar.SUNDAY) {
741 myStart += DAYS_IN_WEEK;
742 }
743
744
745 DayOfWeek myDoW = DayOfWeek.of(myStart - 1);
746 for (int iDay = 0; iDay < DAYS_IN_WEEK; iDay++, myDoW = myDoW.plus(1)) {
747
748 theDaysOfWk[iDay] = myDoW;
749 }
750
751
752 for (int iCol = 0; iCol < DAYS_IN_WEEK; iCol++) {
753
754 final Label myLabel = theHdrs[iCol];
755
756
757 final ObservableList<String> myStyles = myLabel.getStyleClass();
758 myStyles.clear();
759 myStyles.add(STYLE_HEADER);
760
761
762 myDoW = theDaysOfWk[iCol];
763 final TextStyle myStyle = theConfig.showNarrowDays()
764 ? TextStyle.NARROW
765 : TextStyle.SHORT;
766 final String myName = myDoW.getDisplayName(myStyle, myLocale);
767
768
769 myLabel.setText(myName);
770
771
772 if (isWeekend(myDoW)) {
773 myStyles.add(PanelDay.STYLE_WEEKEND);
774 }
775 }
776 }
777
778
779
780
781
782
783 private void reSizeRows(final int iNumRows) {
784
785 final ObservableList<Node> myNodes = theGridPane.getChildren();
786
787
788 while (iNumRows < theNumRows) {
789
790 theNumRows--;
791
792
793 for (final PanelDay myDay : theDays[theNumRows]) {
794
795 myNodes.remove(myDay.getLabel());
796 }
797 }
798
799
800 while (iNumRows > theNumRows) {
801
802 theNumRows++;
803
804
805 int iCol = 0;
806 for (final PanelDay myDay : theDays[theNumRows - 1]) {
807
808 theGridPane.add(myDay.getLabel(), iCol++, theNumRows);
809 }
810 }
811
812
813 theDialog.reSizeDialog();
814 }
815 }
816
817
818
819
820 private static final class PanelDay {
821
822
823
824 private static final String NLS_CURRENTDAY = TethysUIResource.DIALOG_CURRENT.getValue();
825
826
827
828
829 private static final String NLS_SELECTEDDAY = TethysUIResource.DIALOG_SELECTED.getValue();
830
831
832
833
834 private static final String STYLE_PANEL = STYLE_DIALOG + "-day";
835
836
837
838
839 private static final String STYLE_INACTIVE = STYLE_DIALOG + "-inactive";
840
841
842
843
844 private static final String STYLE_SELECTED = STYLE_DIALOG + "-selected";
845
846
847
848
849 private static final String STYLE_CURRENT = STYLE_DIALOG + "-current";
850
851
852
853
854 private static final String STYLE_WEEKEND = STYLE_DIALOG + "-weekend";
855
856
857
858
859 private final TethysUIFXDateDialog theDialog;
860
861
862
863
864 private final Label theLabel;
865
866
867
868
869 private int theDay = -1;
870
871
872
873
874
875
876 PanelDay(final TethysUIFXDateDialog pDialog) {
877
878 theDialog = pDialog;
879
880
881 theLabel = new Label();
882
883
884 theLabel.setPrefWidth(PanelMonth.WIDTH_TILE);
885 theLabel.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> theDialog.setSelected(theDay));
886 }
887
888
889
890
891
892
893 Label getLabel() {
894 return theLabel;
895 }
896
897
898
899
900
901
902 void resetDay(final boolean isActive) {
903 theLabel.setTooltip(null);
904 theLabel.getStyleClass().clear();
905 theLabel.getStyleClass().add(STYLE_PANEL);
906 if (!isActive) {
907 theLabel.getStyleClass().add(STYLE_INACTIVE);
908 }
909 }
910
911
912
913
914
915
916
917 void setDay(final int pDay,
918 final boolean pSelectable) {
919
920 theDay = pDay;
921
922
923 if (pDay > 0) {
924 theLabel.setText(Integer.toString(theDay));
925 } else {
926 theLabel.setText("");
927 }
928
929
930 theLabel.setDisable(!pSelectable);
931 }
932
933
934
935
936 void setWeekend() {
937 theLabel.getStyleClass().add(STYLE_WEEKEND);
938 }
939
940
941
942
943 void setSelected() {
944 theLabel.getStyleClass().add(STYLE_SELECTED);
945 theLabel.setTooltip(new Tooltip(NLS_SELECTEDDAY));
946 }
947
948
949
950
951 void setCurrent() {
952 theLabel.getStyleClass().add(STYLE_CURRENT);
953 theLabel.setTooltip(new Tooltip(NLS_CURRENTDAY));
954 }
955 }
956 }