Writing Google Calendar in JavaFX

  • Post category:Java / javafx / UI

After getting my feet wet writing the ListPicker and CalendarPicker control in JavaFX for the JFXtras project, I felt it was time to put my aim at something bigger; I always wanted to write a control that does a “Google Calendar”. Even though such a control on the surface seems simple, there are many things that make it a real challenge and fun for a software engineer to create. For example the beauty of how overlapping appointments are rendered. So after a few days where the spare time went into drawings and scribbles on paper, fleshing out how things should be set it up, the coding started. There were a few things I would like to do different from Google’s version.

  1. When I plan an appointment, I always forget to look at the whole day events, and often get conflicts because of that. So I wanted a whole day event to have a presence during the whole day.
  2. Google only allows for appointments that span a certain amount time. Tasks (which basically only have a single date & time) are not supported. Google has a task feature, but it is not mature enough IMHO in terms of reminders and visibility.

So before diving into how the control is set up, first a quick peek at the state of affairs when writing this blog (tasks are not implemented yet):

Some points of interest:

  • All whole day appointments are rendered as a ‘flag’, with the ‘pole’ spanning the whole day, so it is clear that this appointment has impact the whole day.
  • Only the ‘day’ part can scroll, the day header containing the whole day appointment’s text is always visible. If the available room is larger than required, the scrollbar will disappear and control will stretch and use that additional space.
  • An appointment can span multiple days, but on itself it is not a whole day appointment.
  • There is a nice red line that denotes ‘now’ (and actually moves in real time).
  • Appointment that have started in the past are white-out.
  • Appointments have different colors. In Google Calendar each color is associated with a separate calendar, but this control has no formal knowledge of where the appointments come from, so it only knows the concept of appointment-groups. Each group has a color.
  • Also note the grey bar at the bottom and top left corner of appointments; these are the duration dragger and a popup menu marker.

The very first version was a finger exercise to see how the different nodes would look. It was implemented in one big rendering method, and the code quickly became totally unmanageable; after only drawing the header, hours labels and lines, and one whole-day plus one regular appointment the code was a mess. Of course I knew that this would happen, but sometimes it is good to experience again how quickly badly structured code becomes a problem. So before I had the quick-n-dirty prototype at the level I wanted, decent software development was forced upon the code.

JavaFX supports styling by CSS and it seemed like a good idea to make use of that to visualize the appointment-groups. CSS styling is applied to any object extending Region. Secondly, the whole control requires some fine rendering, for example the overlapping appointments is not something that regular layout classes would readily support. So the best class to use for this is Pane; it is CSS stylable and lays out its children using their  x, y, width and height values. So the control would consist of a whole bunch of nested Panes, wrapped into a BorderPane to provide the header / detail and a ScrollPane to provide, well, scrolling.

The BorderPane and ScrollPane were easily setup and the first “WeekPane extends Pane” class was quickly created. And then the first issues began. JavaFX tries to minimize the room nodes get, so my custom panes were all 1 pixel in size, stuffed somewhere in the upper left corner. I had initially no idea how to get the panes to match the available size. After peeking around at other people’s code, I found that a lot of solutions used a white or transparent rectangle as the first child, and sized that rectangle to the required dimensions. This would then push out the Pane to the desired size. The code would look something like this:

	class WeekPane extends Pane
	{
		public void layoutChildren()
		{
			getChildren().clear();

			Rectangle lGutsRectangle = new Rectangle(0, 0, myWidth, myHeight);
			getChildren().add(lGutsRectangle);
			...
		}
	}

This worked; the initial version looked quite decent. There was the conceptual issue that a container (for example WeekPane) knew how big the children (DayPanes) should be, but had to push that info into the child itself, so the child could setup its ‘guts’ rectangle to the appropriate size. But that could all be wrapped into some class and be extended away.

Then I started testing with resizing and the set up slowly started to crumble; for some reason layoutChildren() is not called always, and because of that, the contents often simply did not fit; either they were too small leaving unrendered spaces, or too big leaving pieces stuck under other parts of the control. Also I recreated the complete control from scratch on each relayout, which did not improve performance. After trying all kinds of tricks to call layoutChildren() manually, I decided to abandon this approach; overriding layoutChildren apparently was not the way to go.

The next few hours were spent on the couch, staring but not looking at the TV, frustrated and wondering how in the hell JavaFX intended me to do this. Then I realized that in JavaFX everything is build around properties. And that the sizes would all be available as properties, and properties can have listeners. That maybe was a more trustworthy source of information than layoutChildren().

So I rewrote the custom panes to this approach, and everything fell into place. Not only was the layout correct, but also recreating it from scratch was no longer required. Even better; since you can bind stuff, the ‘guts’ rectangle was no longer needed as well! The new basic custom pane looks like this:

	class WeekPane extends Pane
	{
		public WeekPane()
		{
			// ...create and add children ...
			child1 = ...;
			getChildren().add(child1);
			// ...bind child properties to other properties...
			child1.xProperty().bind(...);
			...

			// do the initial layout
			relayout();

			// listen to changes in the size
			widthProperty().addListener(new InvalidationListener()
			{
				@Override
				public void invalidated(Observable arg0)
				{
					relayout();
				}
			});
			heightProperty().addListener(new InvalidationListener()
			{
				@Override
				public void invalidated(Observable arg0)
				{
					relayout();
				}
			});
		}
		Node child1 ...;

		public void relayout()
		{
			// ... position and size the children ...
			child1.setX(...);
			...
		}
	}

Basically there are two ways of laying out the children:

  • When creating the children in the constructor, bind properties to other properties, this will ensure that children automatically follow changes. For example, the X property of the DayHeaderPane is bound to the X property of the DayPane, so the header and the day always align.
  • If binding is not a option, because the values are too complex to calculate, listen to the relevant properties and use the child’s setters.

This concept of working with properties and binding is not one that is used a lot in Java software development, so you initially try to solve problems the old fashion way. Don’t. This new property approach is very powerful and takes away a lot of the hard work from the UI.

Even better, binding supports calculations; you can modify the values while they are bound. For example, the duration dragger at the bottom of an appointment is 50% of the width of its appointment, offset 25% of the width from the left border, always 3 pixels high, and offset 2 pixels from the bottom border:


				durationDragger.xProperty().bind(widthProperty().multiply(0.25)); // 25% offset from the left border
				durationDragger.widthProperty().bind(widthProperty().multiply(0.5)); // 50% of the width of the appointment
				durationDragger.yProperty().bind(heightProperty().subtract(5)); // 5 pixels from the bottom border
				durationDragger.setHeight(3); // 3 pixels high

It takes some getting used to, but this is a killer technology. And to take it even one step further; besides calculations, binding also supports conditions through the “when” construct: …when(cond).then(value1).otherwise(value2).

So, I’m very pleased with having learned this style of coding available in JavaFX. I’ve read about JavaFX’s properties before, I’ve used binding in business models, but never in UI. And it took a good bang against a wall to let it sink in. A very refreshing and inspiring experience.

Oh, do give my new Agenda control in JFXtras a spin, and let me know if you like it, or not.

This Post Has 14 Comments

  1. duBois

    Hi tbeernot,

    You’ve created a very nice control. Congrats for that.
    I have been looking into the internals of the control because I would need different views (1 day, etc..).
    As far as I can see I would need a different kind of AgendaOneDaySkin which does the layout. Correct? On the other hand the current AgendaWeekSkin has several useful inner classes which could maybe be factored out to some sort of AbstractAgendaSkin class to allow reuse in different skin implementations.
    I have not found any other examples of controls with multiple skin implementations so I am still looking for a “best practice guideline” on how to use these skins: create (extend) a new control implementation with the new skin (new stylesheet pointing to the skin) or allow choosing between different skins in the same implementation. Would you have any advise on this?

    Thanks

    1. tbeernot

      I suspect (hope) it will not be that hard to make the week skin only render a single day. At least that is where I am aiming at. But if that turns out to be too much of a hassle, then a separate view AgendaOneDaySkin is needed. And I would indeed refactor the reusable methods into an abstract class.

      CalendarPicker has a skin that extends an abstract class; it used to have a second skin.

      You could create special controls, e.g. AgendaDay, AgendaWeek, this would make the usage in SceneBuilder much more intuitive. But basically these are just extentions to Agenda with a corresponding CSS specifying the appropriate skin. So I intend to develop Agenda with separate skins, and then only define these special controls for SceneBuilder.

      If you were to build additional skins, it would be great if you would contribute them back.

      Tom

  2. Toni

    Hm.. i am actually a little bit confused. Using .WeekPane is not working which i thought is the right approach. Using .Week in css works as you described. Maybe thats the problem.. 😉

    Great that now a callback for editing is available. Thanks!!

    1. tbeernot

      .WeekPane should have worked prior to nov 9th (if you were using 2.2-r5-SNAPSHOT), after that .Week is the correct CSS selector. Good luck with the callback. Send me a screenshot when you’re done.

  3. tbeernot

    Hi Tony,

    Ok, I’ll look into the styling and dialog.

    About the setters; either you provide appointments using the interface, which means you have implemented the setters yourself and can alter the code accordingly. Or you use the provided AppoinmentImpl and then the setters are JavaFX properties and you can add listeners on them.

    Tom

    1. tbeernot

      Ok, I’ve made a small change to the CSS because none of the other pane have “Pane” as post fix. WeekPane is now using “Week”. But that is all that was changed, the background setting is no problem:

      .Week {
      -fx-background-image: url(“Duke_Wave-300.png”);
      }

      You would be using “WeekPane”, but that should work. So I’m betting it’s a problem in your code.

    2. tbeernot

      I’ve added a editCallback which allows to open your own edit dialog, or whatever you like to do with the appointment. Note: after you’re done with editing, you need to call “fresh()”

  4. Toni

    Hello tbeernot. Very nice post and project indeed. Thank you for sharing all of it!! I was playing around with styling the agenda. Unfortunately i wasn’t able to fully style all Controlls and in special the WeekPane. Changing backcolor works but not setting any image for example. Probably cause of the stage of the project…?

    Is it possible to replace the default edit-dialog of an appointment with a custom dialog? And how could i override an actionlistener of an appointment? Any example would be great 🙂

    Anyway, congratulation for this great work!!

    1. tbeernot

      Hello Toni,

      I will look into the styling issue; the WeekPane is a Pane and should support any associated CSS styling. I found that CSS is not always as logical as one would expect; could you first check if your CSS styling works by applying it to a Pane you added to the scene yourself?

      There is no override of the dialog yet, I am considering it but have not worked out how it should be implemented. Maybe a callback and if that is unregistered show the build-in version?

      Concerning the appointments; since Agenda is not responsible for the implementation, you can add any kind of event hooks directly onto the appointment’s setters. If you still require appointment events provided by Agenda, or if they would be very beneficial, I would like to hear the use case.

      1. Toni

        Hello,

        thanks for the fast response. Yes i tried that and it works also, styling other panes.

        Maybe you can provide an abstractdialogprovider with your dialog as default implementation.. but i am sure you know better than me 😉

        I guess drag and drop will be an event of interest. But like you explained, i was more thinking of hooks on appointments. I am stuck with that.. sry .. could you provide a short example how to add a hook to a setter-method of an appointment. Thanks again.

  5. Werner Lehmann

    Insightful post. Thanks for sharing this. BTW, I’d expect that your WeekPane example won’t work like that since you are setting a bound property:

    child1.xProperty().bind(…);

    child1.setX(…);

    Usually you can have only one or the other unless I am missing something. But the idea is clear anyway.

    1. tbeernot

      Ah, you are right, but it was intended as an example; you either bind it here, or set it here.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.