Sunday, 1 January 2012

Calendar Control in JavaFX 2.0


It’s been great fun working with JavaFX 2.0. But before the release of Calendar Component in JavaFX I tried  to create my own custom calendar component and to an extent I am succeeded in creating it. 

Before going in detail about the calendar, I would like to inform you all that I had referred the EXTJS Calendar for the design reference. I tried to impose all the features that are present in that calendar.

The complete source code download you can find here.[ Download ] or at the bottom of the post.

About the Calendar

The calendar component mainly contains two panes.
  •          One for showing the dates of the selected month (Base Pane)
  •          Other for navigating between years and months (Top Pane)

Here's output of the FXCalendar that is shown as demo

On clicking the down arrow in the base pane, the top pane is displayed.

Desired month and year can be selected and upon clicking the "Ok" button, the month is displayed in the base pane.








 



Let’s dig deep into the component source code..
It all starts with FXCalendar class which implements a simple HBox,  that consists of two children, a
  •  TextField - to display the selected date or modify the date, and
  •  Button/Image -  to trigger the date picker pop up.
FXCalendar class also contains few setter/getter methods which enable us to customize the calendar to a further extend. I will discuss about those methods later in the post.
As the code for the classes are a bit large, I will just focus on the core part in the classes.
The part of code in FXCalendar class is as below:


public class FXCalendar extends HBox {

    private SimpleIntegerProperty selectedDate = new SimpleIntegerProperty();
    private SimpleIntegerProperty selectedMonth = new SimpleIntegerProperty();
    private SimpleIntegerProperty selectedYear = new SimpleIntegerProperty();
    private SimpleBooleanProperty triggered = new SimpleBooleanProperty();
    private final SimpleObjectProperty<Color> baseColor = new SimpleObjectProperty<Color>();
    private SimpleDoubleProperty dateTextWidth = new SimpleDoubleProperty(74);
    private SimpleObjectProperty<Date> value = new SimpleObjectProperty<Date>();
    private boolean showWeekNumber;
    private FXCalendarUtility fxCalendarUtility;
    private DateTextField dateTxtField;
    private ChangeListener<Boolean> focusOutListener;
    private Popup popup;
    private DatePicker datePicker;
    private final SimpleObjectProperty<Locale> locale = new SimpleObjectProperty<Locale>();
    private final String DEFAULT_STYLE_CLASS = "fx-calendar";

    public FXCalendar() {
        super();
        super.getStyleClass().add(DEFAULT_STYLE_CLASS);
        this.locale.set(Locale.ENGLISH);
        this.baseColor.set(Color.web("#313131"));
        //setSpacing(6);
        setAlignment(Pos.CENTER);
        configureCalendar();
        configureListeners();
    }

    private void configureCalendar() {
        final DateFormatValidator dateFormatValidator = new DateFormatValidator();
        fxCalendarUtility = new FXCalendarUtility();

        popup = new Popup();
        popup.setAutoHide(true);
        popup.setAutoFix(true);
        popup.setHideOnEscape(true);

        addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
            public void handle(KeyEvent event) {
                if (KeyCode.UP.equals(event.getCode()) || KeyCode.DOWN.equals(event.getCode()) || KeyCode.ENTER.equals(event.getCode())) {
                    initiatePopUp();
                    showPopup();
                } else if (KeyCode.TAB.equals(event.getCode())) {
                    hidePopup();
                }
            }
        });

        /* Creating the date text field. */
        dateTxtField = new DateTextField();
        dateTxtField.prefWidthProperty().bind(dateTextWidth);
        this.focusOutListener = new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
                // Handling only when focus is out.
                if (!arg2) {
                    String value = dateTxtField.getText();
                    if(!dateFormatValidator.isValid(value)){
                        clear(); // TODO : Error styling for invalid date format.
                        dateTxtField.setText(value);
                    }else{
                        Date date = fxCalendarUtility.convertStringtoDate(value);
                        if (date != null) {
                            setValue(date);
                        } else {
                            // TODO : Error styling the text field for invalid date
                            // entry.
                            clear();
                        }
                    }
                }
            }
        };
        dateTxtField.focusedProperty().addListener(this.focusOutListener);

        /* Creating the date button. */
        Button popupButton = new Button();
        popupButton.getStyleClass().add("dateButton");
        popupButton.setGraphic(FXCalendarUtility.getDateImage());
        popupButton.setFocusTraversable(false);
        popupButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent paramT) {
                initiatePopUp();
                showPopup();
            }
        });

        getChildren().addAll(dateTxtField, popupButton);
    }

..
..

}


The DatePicker class  is resided in the PopUp control, and is the place where the BasePane and TopPane’s are instantiated. DatePicker is a stackpane which holds the TopPane and BasePane in it and toggles there visibility based on the controls.


The code for the DatePicker is as below.

public class DatePicker extends StackPane {

    private SimpleIntegerProperty selectedDate = new SimpleIntegerProperty();
    private SimpleIntegerProperty selectedMonth = new SimpleIntegerProperty();
    private SimpleIntegerProperty selectedYear = new SimpleIntegerProperty();
    private Rectangle2D calendarBounds = new Rectangle2D(100, 100, 205, 196);
    private FXCalendar fxCalendar;
    private BasePane basePane;
    private TopPane topPane;

    public DatePicker(FXCalendar fxCalendar) {
        super();
        this.fxCalendar = fxCalendar;
        selectedDate.set(fxCalendar.getSelectedDate());
        selectedMonth.set(fxCalendar.getSelectedMonth());
        selectedYear.set(fxCalendar.getSelectedYear());
        fxCalendar.setLocale(Locale.ENGLISH);
        setPrefHeight(calendarBounds.getHeight());
        setPrefWidth(calendarBounds.getWidth());
        setAlignment(Pos.TOP_LEFT);
        FXCalendarUtility.setBaseColorToNode(this, fxCalendar.getBaseColor());
        basePane = new BasePane(this); // Initiating the BasePane
        topPane = new TopPane(this); // Initiating the TopPane
        getChildren().addAll(basePane, topPane);
        showBasePane();
    }

..
..
}

The another important file required for implementing the FXCalendar controll is importing its related stylesheet file. The look and feel of the calendar can further be customized by changing the code of the calendar_styles.css file.

As the source code is already available in the Download link, I am inclining more towards the functionality rather than the code. So I am skipping into the actual usage of this calendar and not showing the code of other important helper classes ( BasePane, TopPane, FXCalendarCell, FXCalendarControls, FXCalendarUtility & DateFormatValidator )

Usage of Calendar

Once the java package of eight files is imported and the style sheet(calendar_styles.css) is loaded to the scene., we are all set to use the calendar and play with it.

The fxcalendar can be initiated as below.

FXCalendar calendar = new FXCalendar();

As the FXCalendar is a customised HBox (as it extends HBox), it can be used just like a normal node and can be placed anywhere in the scene.

The values (java.util.Date) can be set or get using the following methods.

calendar.setValue(date);
calendar.getValue();

Some Additional Features of Calendar

1) Internationalisation
The calendar can be made locale specific or can change its Locale dynamially by setting the corresponding java.util.Locale to the below method.

FXCalendar calendar = FXCalendar();
calendar.setLocale(Locale.FRENCH);

Updates: Based on the comments of PhiLho, the week display implementation for Locale specific is modified. For example, for FRENCH locale the week starts with Monday, likewise for UK, ITALY..etc
The FRENCH locale specific calendar is shown as below.


2) Week Number Display:
The calendar can be set to show/off the display of the week numbers by the following method. By default it is set to false.

FXCalendar calendar = new FXCalendar();
calendar.setShowWeekNumber(true);

The week numbers are displayed in the calendar as below.














3) Customizing Themes:
The calendar theme can be set to suite to your application. The theme can be set by the following method.
Note: Please make a note that it would be better if the base color is dark color, as the skin goes lighter from the base color.

FXCalendar calendar = new FXCalendar();
calendar.setBaseColor(Color.web("#940C02"));

The new theme of the calendar is displayed as below.
















That's it !!!!
I hope this component can be helpful, to fill the gap till the actual JavaFX Calendar component is released.

Download Link : JavaFxCalendar_v0_4.zip
All the above features are demonstrated in the demo file  FXCalendarDemo.java

Happy Coding !! :)

P.S: The code may be improvised a lot. But for conceptual point of view I tried maximum to keep everything in place. Feel free to optimise the code and use ;)


19 comments:

  1. Good job! But FYI, French people start the week on Monday, not on Sunday. I don't know if your internationalisation can go this far. ;-)

    ReplyDelete
  2. Excellent. Will use it and give feedback.

    Do you have any end user license agreement or this? If so let me know the license and text.

    ReplyDelete
  3. PhiLho,
    Thanks for the info :). To be frank I am not aware of that. I have updated the code accordingly and updated the screenshots. You can find the changes in the updated download link.

    ReplyDelete
  4. Subu,
    Thanks, there is no end user license agreement for this :)

    ReplyDelete
  5. Updated the code (JavaFxCalendar_v0_2.zip), to implement locale specific week start.

    ReplyDelete
  6. Hi,

    You should definitely consider joining the JFXtras project - there is already a calendar control there and you could join forces with that developer (Tom Eugelink) to merge the bets parts of both controls. This could then eventually be worked into a future JavaFX release.

    -- Jonathan

    ReplyDelete
  7. Very nice control Sai.

    I created a similar thing (http://jewelsea.wordpress.com/2011/12/05/jqueryui-based-datepicker-for-javafx/) by embedding the jQueryUI datepicker, but the native JavaFX solution you have is preferable.

    You might want to look at the jQueryUI datepicker for further design inspiration (http://jqueryui.com/demos/datepicker/).

    ReplyDelete
  8. @Jonathan,
    Thanks for the suggestion :)
    I will definitely join in JFXtras group and will be aligned with Tom Eugelink for futher steps.

    ReplyDelete
  9. @jewelsea,
    Thanks for the feedback & suggestion :)
    Yeah I looked at your implementation of embedding the jQueryUI datepicker. Very nicely embedded ! :)

    ReplyDelete
  10. Updated the code (JavaFxCalendar_v0_3.zip)
    Added changes to calendar to not to allow selection of dates less than 01/01/01.

    ReplyDelete
  11. Updated the code (JavaFxCalendar_v0_4.zip)
    Fixed the issue of not setting the date in "valueProperty" for January month.

    ReplyDelete
  12. How do I get only the month of DatePicker and pass the name for a label? Example: [dd/MM/yyyy][Btn] Month is: (name of selected month in the date picker);
    I tried to use: label.setText(date.getFXCalendarUtility().getShortMonths(Locale.US).toString());

    but did not work! Can you help me!?

    ReplyDelete
    Replies
    1. Hi Betowebti,
      To get the month of the selected date, I am writing a listener for the valueProperty and getting the month.

      final Label lbl = new Label();
      calendar.valueProperty().addListener(new ChangeListener{
      ...
      ..
      lbl.setText(calendar.getFXCalendarUtility().getShortMonths(calendar.getLocale())[calendar.getSelectedMonth()]);

      });

      Delete
  13. Hi,
    how can I get a value of selected date from a calendar, I tried calendar.getValue() but it return null

    ReplyDelete
  14. Hi,
    I tried to get value of selected date but it return null,
    calendar.valueProperty().addListener(new ChangeListener() {

    @Override
    public void changed(ObservableValue arg0,Object arg1, Object arg2) {
    // TODO Auto-generated method stub

    Date date=new Date();
    date=calendar.getValue();
    System.out.println(date);


    }

    });

    ReplyDelete
  15. There are 2 things I think it can be more better for Internationalisation:

    1. the default Locale:
    maybe use Locale.getDefault() instead of the default Locale.ENGLISH

    2. the button "Today"
    maybe can make a method for setting it's text for self Localization;

    ReplyDelete
  16. Hi, i try to run this program with netbeans 7.2 but always thrwos this erros:
    Exception in Application start method
    Exception in thread "main" java.lang.RuntimeException: Exception in Application start method
    at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:403)
    at com.sun.javafx.application.LauncherImpl.access$000(LauncherImpl.java:47)
    at com.sun.javafx.application.LauncherImpl$1.run(LauncherImpl.java:115)
    at java.lang.Thread.run(Thread.java:662)
    Caused by: java.lang.NullPointerException
    at com.sai.javafx.calendar.demo.FXCalendarDemo.loadStyleSheet(FXCalendarDemo.java:77)
    at com.sai.javafx.calendar.demo.FXCalendarDemo.configureScene(FXCalendarDemo.java:73)
    at com.sai.javafx.calendar.demo.FXCalendarDemo.start(FXCalendarDemo.java:44)
    at com.sun.javafx.application.LauncherImpl$5.run(LauncherImpl.java:319)
    at com.sun.javafx.application.PlatformImpl$5.run(PlatformImpl.java:206)
    at com.sun.javafx.application.PlatformImpl$4.run(PlatformImpl.java:173)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.access$100(WinApplication.java:29)
    at com.sun.glass.ui.win.WinApplication$3$1.run(WinApplication.java:73)
    ... 1 more
    what im doing wrong??? :/

    ReplyDelete
  17. This comment has been removed by the author.

    ReplyDelete