JavaFX Calendar App

It's not possible to talk about technology and not to try writing something on it. So, because I have something to compare to (Aqris AMS, which was written using GWT http://ams.aqris.com) I wanted to make a similar calendar, but on javafx.

Firstly, it was really hard to get used to new syntax. Secondly, components base is a little poor and I'll show you why.

My first thought was to do calendar view using simple table, that has 96 rows and 7 columns. For that purpose I took swing component, that is called JList and after some time playing with layouts and other stuff I finally draw my first calendar. By default it has single cell selection. "Nice.." was my first reaction, when without additional effort I got cell selection. I thought, that if there is a single cell selection, then there should be also multiselection. Ctrl + space for searching appropriate argument in this class did not give anything interesting, neither java-doc for JList. I've started researc in internet and found, that it does not support multiple selection in [JavaFX]...."weird isn't it"((c) Homer Simpson).

OK, but one guy showed in some blog how it's possible to add this kind of support. Basically, all you need to do is create your own swing component and put there JList as a field. Then add mouse dragging event and for every new cell, mouse comes in, add it into your list selection:

onMouseDragged: function( e: MouseEvent ): Void {
  swingList.list.addSelectionInteraval(e.x, e.y);//instead of e.x & e.y thare were other variables, that were calculated based on e.x and e.y
}

public class SwingList extends SwingComponent {

  public var list: JList;
  ........
  public function addSelectionInterval(anchor : Integer, lead : Integer) : Void {
    list.addSelectionInterval(anchor, lead);
  }
  ......
}

However, I faced other problems here, concerning selection and after that I stopped using JList.

Another way to draw list was to draw 7 * 96 cells, which are basically rectangles. Why i did not choose drawing borders as lines (it would be 8 * 97 lines) is because of showing selected area. As far as I see it, it is not possible to fill some part of background (for instance two cells containing part 11 * 40 px) with other color, you still need to have some component (shape) to be filled on this part.

It isn't rocket science for javafx to draw a table (basically, some layout info can be placed in CSS file. However to write CSS for javafx, you have to follow some additional rules http://forums.sun.com/thread.jspa?threadID=5357325):

var cells: CalendarCell[] = {
  for (i in [0..6]) {
  for (j in [0..95]) {
   CalendarCell {
    x: bind (cellWidth * i) + TIME_COLUMN_WIDHT as Integer
    y: bind cellHeight * j as Integer
    width: bind cellWidth as Integer
    height: bind cellHeight as Integer
    day: i
    timeOfADay: j
    stroke: Color.web("#DDDDDD");
    fill: {if (isCurrentDay(i)) Color.web("#FFFFCC") else Color.WHITE}
    cellWidth: bind cellWidth
    cellHeight: bind cellHeight
    isCurrentDayCell: isCurrentDay(i)
   }
  }
 }
}

Calendar cell itself is a simple rectangle with additional info:

public class CalendarCell extends Rectangle {
  public var day: Integer = 0;
  public var timeOfADay: Integer = 0;
  public var cellHeight: Integer;
  public var cellWidth: Integer;
  public var isCurrentDayCell: Boolean;
}

Now, I have calendar view, that is fitted into user's window, depending on window size. That means, if user will re-size the window, then the calendar will be also re-sized (works only if it is desktop application, because in case of embedding it into browser it will have fixed height and width parameters). This is done by using really powerful javafx feature binding. In two words: it means that if value of bound variable will change, then the value of variable to which it is bound to will be changed automatically.

Basically, for GWT it goes also quite easily:

private void drawGrid() {
  drawRows();
  drawColumns();
}

private void drawRows() {
  for (int row = 1; row < model.getRowCount(); row++) {
    addRowSeparator(new RowSeparator(row, model.getCellHeight(), model
        .getTimeGranularityModel().getCellDuration()));
  }
}

private void drawColumns() {
  for (int col = 0; col < model.getColumnCount(); col++) {
    final ColumnSeparator separator = new ColumnSeparator(col, model
        .getCellWidthPerc());
    addColumnSeparator(separator);
  }
}

However, here you have to play with CSS (you know what it means? yep! IE..) to draw cell borders ([ColumnSeparator] and [RowSeparator]).

public class ColumnSeparator extends Widget {

  public ColumnSeparator(final int column, final float cellWidthInPercentage) {
    setElement(DOM.createDiv());
    setStyleName("line vertical");
    DOM.setStyleAttribute(getElement(), "left", column * cellWidthInPercentage + "%");
  }
}

Unfortunately, here the feeling that javafx has lack of components comes again - there is no support for scrolling, yet (despite the fact, there is a [ScrollBar] component). To create content, which can be scrolled in GWT you would probably do something like this:

scrollPanel = new ScrollPanel(mainContentGrid); //mainContentGrid contains created above calendar

But for javafx you need to be creative again (google, google, google....). Nice blogpost about how to create scrollable content: http://blog.alutam.com/2009/08/30/implementing-a-scroll-view-in-javafx/. All I had to do is just change some parameters to fit into my layout. In the end you will get something like this:

ScrollView {
  width: bind sceneWidth - 120 as Integer
  height: bind sceneHeight - 120
  translateX: 50
  translateY: 50
  node:
    Group {
      content: bind [ //content that can be scrolled
        timeColumn,
        cells,
        sceneActivities = Group {
          content: bind activities
        }
     ]
   }
}

OK, works nicely...lets try to add some cells selection. In javafx every component (or almoust every?) has support for mouse events like onMousePressed, onMouseReleased, onMouseDragged, onMouseWheelMoved etc. So, all you need to do is just implement appropriate method, and voilà:

ScrollView {
....
onMouseDragged: function(e: MouseEvent): Void {
  var selectedTime = e.y / cellHeight as Integer;
  var selectedCell = getSelectedCell(selectedDayCol, selectedTime);
  if (e.y > prevEY) {//mouse moves down
    if (selectedTime < firstSelectedCell) { //mouse moves down but time is smaller than strat time
      if (selectedCell.isCurrentDayCell) {
        selectedCell.fill = Color.web("#FFFFCC")
      } else {
        selectedCell.fill = Color.WHITE;
      }
    } else {
      selectedCell.fill = Color.web("#CCCCE0");
    }
  } else {
    if (selectedTime > firstSelectedCell) { //mouse  moves up but time is grater than strat time
      if (selectedCell.isCurrentDayCell) {
         selectedCell.fill = Color.web("#FFFFCC")
      } else {
         selectedCell.fill = Color.WHITE;
      }
    } else {
      selectedCell.fill = Color.web("#CCCCE0");
    }
  }
  prevEY = e.y;
}
...
}

In case of GWT there is no rectangles or something that represents cells on the page and it much more painful to achieve the same effect.

Now it would be great to have popup window to create activity.
Damn, but there is no support for popup windows in javafx For sure, it is not hard to create one: just draw another rectangle with some fields inside it:

public class PopupWindow extends CustomNode {
  ...
 override public function create(): Node {
   return Group {
     content: [
       Rectangle {
         x: bind popupPosX, y: bind popupPosY
         width: 280, height: 180
         fill: Color.web("#E8EEF7")
         blocksMouse: true
       },

       SwingTextField {
         layoutX: bind popupPosX + 10
         layoutY: bind popupPosY + 10
         columns: 10
         text: bind activityText with inverse
         editable: true
         blocksMouse: true

       },
       fromHoursComboBox, fromMinutesComboBox,
       toHoursComboBox, toMinutesComboBox,

       Button {
         text: "Add"
         layoutX: bind popupPosX + 10
         layoutY: bind popupPosY + 100

         action: function() {
           this.visible = false;

           def activity: Activity = Activity {
             activityId: sizeof activities
             visible: true;
             activityStartPosX: activityStartPosX;
             activityStartPosY: (fromHoursComboBox.selectedIndex * 4 + fromMinutesComboBox.selectedIndex) * cellHeight
             activityWidth: bind activityWidth
             activityHeight: ((toHoursComboBox.selectedIndex * 4 + toMinutesComboBox.selectedIndex) - (fromHoursComboBox.selectedIndex * 4 + fromMinutesComboBox.selectedIndex) ) * cellHeight;
             fromTime: fromHoursComboBox.selectedIndex * 4 + fromMinutesComboBox.selectedIndex
             toTime: toHoursComboBox.selectedIndex * 4 + toMinutesComboBox.selectedIndex
             activityDay: selectedDayCol
             cellHeight: bind cellHeight
           }
           insert activity into activities;
           clearSelection();
         }
       },

       CloseButton {
         blocksMouse: true
         windowToClose: this
         translateX: bind popupPosX + 258
         translateY: bind popupPosY + 2
         layoutInfo: LayoutInfo { hpos: HPos.RIGHT, vpos: VPos.TOP }
         selectedCellsIndxs: bind selectedCellsIndxs
         cells: cells
       }
     ]
   }
 }

Nothing complicated here, except that it's needed to implement close button for the popup (basically, it can be a simple rectangle or two cross-lines which react to mousePress and release events like that:

onMouseReleased: function(me:MouseEvent):Void {
 windowToClose.visible = false;
 windowToClose.clearSelection();
}

However, concerning popup window and it's communication with activities and with calendar, here starts the hardest part. First of all it has to appear on position, that depends on mouse. That means that position has to be changed all the time. So, that means we have to bind parameters, that can change, so that they will change automatically:

popup = PopupWindow {
  visible: bind showPopup with inverse
  popupPosX: bind popupPosX
  popupPosY: bind popupPosY
  activities: bind activities with inverse
  activityStartPosX: bind activityStartPosX;
  activityStartPosY: bind activityStartPosY;
  activityDuration: bind cellHeight * numOfCellsSelected
  activityWidth: bind cellWidth
  fromTime: bind firstSelectedCell
  toTime: bind lastSelectedCell + 1
  selectedCellsIndxs: bind selectedCellsIndxs
  cells: cells
  selectedDayCol: bind selectedDayCol
  cellHeight: bind cellHeight
}

But! The position of activity (activityStartPosX and activityStartPosY) is also bound, but I wanted to do it so that it would be possible to change activity start time and end time by using comboboxes (it means, that in calendar you can select one interval, but through comboboxes you will be able to change it). This means, that activity position is also depending on values, that user select in appropriate comboboxes. What is the solution here? Binding! I've bound selected values from comboboxes to activity positions, done some calculations and as a result got exception: "Exception in thread "main" com.sun.javafx.runtime.[BindingException]: Both components of bijective bind must be mutable" o_O wtf? Solution for this problem would be using so called "Bridge pattern", described here: http://blogs.sun.com/clarkeman/entry/bounding_bridge. Done! I have implemented bridge variables, compile project, run...."com.sun.javafx.runtime.[AssignToBoundException]" is what I see instead of calendar. So, javafx cannot bind variable if it is already bound. As a solution you should use temporary variable http://forums.sun.com/thread.jspa?messageID=10747205#10747205. After all these manipulations I have got piece of quite nice code:

public var fromTime: Integer on replace {
  fromTimeMinutes = fromTime mod 4;
  fromTimeHours = Math.floor(fromTime / 4) as Integer;
  newFromTime = fromTimeHours + fromTimeMinutes;
};

//bridge variable
var fromTimeMinutes: Integer on replace {
  newFromTime = fromTimeMinutes + fromTimeHours * 4; //recalculate time
};

//bridge variable
var fromTimeHours: Integer on replace {
  newFromTime = fromTimeMinutes + fromTimeHours * 4; //recalculate time
};

//temp variable to elimineate AssignToBoundException
var newFromTime: Integer on replace {
  tempActivityStartPosY = newFromTime * cellHeight; //recalculate start position for drawing
  //newToTime = newStartTime + activityDuration / cellHeight as Integer;
};

//to time bridge
public var toTime: Integer on replace {
  toTimeMinutes = toTime mod 4;
  toTimeHours = Math.floor(toTime / 4) as Integer;
  newToTime = toTimeHours + toTimeMinutes;
};

//bridge variable
var toTimeMinutes: Integer on replace {
  newToTime = toTimeMinutes + toTimeHours * 4; //recalculate time
};

//bridge variable
var toTimeHours: Integer on replace {
  newToTime = toTimeMinutes + toTimeHours * 4; //recalculate time
};

//temp variable to elimineate AssignToBoundException
var newToTime: Integer on replace {
  newActivityDuration = (newToTime - newFromTime) * cellHeight;
};

var tempActivityStartPosY: Number;
  var newActivityDuration: Number;

Fortunately, a piece of jelly in my head told me, that it is better to take values, directly from comboboxes. In this case there is no need to overcome this exceptions.

At least! the final part is to draw activity: nothing unusual here, again rectangles and some other shapes do the stuff. And also, to add activity drag effect and re-sizing effect, you need to implement appropriate methods in the appropriate way. For instance, activity dragging effect would look like this:

Rectangle {//activity header
  x: bind activityStartPosX
  y: bind activityStartPosY
  width: bind activityWidth
  height: 10
  fill: Color.web("#1E4420")
  blocksMouse: true
  cursor: Cursor.HAND

  onMouseDragged: function(e: MouseEvent): Void {
    //don't allow to drag outside scrollable area
    if (e.x >= 60 and e.x < scene.width - 135 and e.y > 0 //scroll width = 120 + slider width 15
      and e.y + activityHeight < 2 * scene.height - 40) { //-40 due to bottom margin
      var selectedDay = (e.x - 60) / (activityWidth) as Integer;//60 = TIME_COLUMN_WIDHT
      activityStartPosX = (selectedDay * activityWidth) + 60;//60 = TIME_COLUMN_WIDHT
      var selectedTime = e.y / cellHeight as Integer;
      activityStartPosY = (selectedTime * cellHeight);//e.y;
      fromTime = selectedTime;
      toTime = fromTime + (activityHeight / cellHeight) as Integer;
    }
  }
}

That's it!

Author note

This is post by: Alexander Gavrilov

Labels

javafx javafx Delete
gwt gwt Delete
ams ams Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Nov 09, 2009

    Anonymous says:

    You can download full application code from here or you can try it out. Any feed...

    You can download full application code from here or you can try it out. Any feedback is appreciated.

    Alexander Gavrilov.

  2. Jan 19, 2010

    Anonymous says:

    Hello from Luxembourg, It is quite only a demo of a calendar app but it looks ...

    Hello from Luxembourg,

    It is quite only a demo of a calendar app but it looks good...

    What do you think of javafx ?

    Pretty easy to do some custom stuff, isn't it ?

    Regards,

    Guido Amabili

    1. Feb 22

      Anonymous says:

      Hello from Tallinn:) Thanks for feedback! It is quite easy to start writi...

      Hello from Tallinn:)

      Thanks for feedback!

      It is quite easy to start writing on javafx. However, I don't like that there is no separation for UI (as they have it in Flex). I think that in this case the problem of bad code is critical (especially for beginners). And also it is not that mature as I would expect. Please, read second blog (in case you have not read it yet) about my first steps in javafx http://www.aqris.com/display/DEV/2009/11/02/JavaFX+first+experience).

      Regards,

      Alexander Gavrilov