One common application in the world of computing is scheduling. We’ve covered calendars in the past but never looked at the tricks used in managing appointments. You’ll see this kind of widget on every platform, in one form or another. They typically present the hours in a day and let your add, drag and resize entries visually. It’s the kind of component you can drop into any time-management application, ready for note-taking or other daily scheduling tasks.
JDayTime is designed to manage time ranges in a given day. Figure 1 show what a light afternoon schedule might look like. The entry with the focus has a different border. The left edge of the border can be used to drag the entries around. The top and bottom edges are used to reschedule the start time or the length of the appointment, respectively. This has obvious applications in PIMs (Personal Information Managers) but it could also be used to set up runtime schedules for automated processes, batch runs and the like. Of course, you’re not limited by my lack of imagination, so I’m sure a few of your own uses come to mind.
Whatever the application, this is a component with a lot to demonstrate. To allow entries to move around, we have to use layers so that the entry with the focus stays in front. We have to resolve overlapping entries as well, as you can see in Figure 2. To keep things flexible, we’ll support different time divisions. So long as 60 minutes is equally divisible by your setting, you can have as many minute segments as you like. The entries snap to division boundaries when they are resized, so we’ll need to apply some interesting calculations to make this easy to use. The object behind the entries is a JList, which you can use to recognize events, allowing the user to add new entries. This is a reasonably sophisticated component, from which there is plenty to learn.
As always, we’re limited in the space we can allocate to listings, so you’ll find all the code online at www.java-pro.com. There are 8 classes in this project, a few of which are trivial. The DayTimeGroup class, for example, merely extends ArrayList and adds a getEntry accessor that casts the result for us automatically. The DayTimeBorder class supports two states in which a 4 pixel border is drawn all the way around the top, left and bottom when isSelected is true, and a single pixel line is draw above and below if it is unselected. In either case, the top and bottom of the border border is actually 4 pixels in size but the line drawing is done based on the state. This lets us flip borders when an entry gets and looses the focus, without affecting the position of our objects.
There’s not much to these classes so I’ll leave it to you to peruse the online code. We’ll talk about a few other classes and list the more important ones. Figure 3 shows the class relationships for this component. I mentioned DayTimeBorder and DayTimeGroup, which are pretty straight forward. You can see in the diagram how JDayTime uses a DayTimeLayers class, which contain groups of DayTimeEntry objects, each of which has a pair of associated DayTimeBorder objects.
The DayTimeLayers class inherits from Swing’s LayeredPane. The only tricks it needs to pull off are to put a JList instance on a layer behind all the entries, assign a specialized DayTimeRenderer to the list and respond correctly to getPreferredSize and getMinimumSize method calls by delegating these calls to the JList instance. We intercept the resize method so that the list size stays in sync with the JLayeredPane. In every other way, we are effectively dealing with a standard JLayeredPane when we work with DayTimeLayers.
The DayTimeRenderer is a ListCellRenderer implementation that draws lines to make it easy to see divisions. It recognizes whether it is on the first or last list entry, and draws the top or bottom lines on the edge. All rendered cells add a 4 pixel gap to the left edge and draw a vertical line on either side of the gap. This is the visual cue that helps a user recognize where they can select or drag an entry. We implement the getListCellRendererComponent method to support the ListCellRenderer interface and override the paintComponent method to do the drawing. The constructor lets us pass in the cellHeight so we can set the preferred and minimum size in one sweep.
The DayTimeRuler class is also very simple. It creates 24 instances of the DayTimeLabel class, one for each hour in the day, in a GridLayout. It does a few calculations to let the DayTimeLabel instances know the hour text to use, the number of divisions for each label and whether they are part of the morning or afternoon. It also calculates a preferredSize, based on the number divisions, cellHeight and insets. Finally, it draws a line at the bottom of the ruler so that the labels don’t need to deal with that special case.
You’ll notice that DayTimeRuler uses a 4 pixel empty border at the top and bottom to space things out the same way the DayTimeLayers do. This is necessary because the top and bottom entries can be selected. Since DayTimeBorder paints 4 pixels above and below the selection, we’d see unnecessary clipping if we didn’t do this.
Listing 1 shows the DayTimeLabel class. The constructor is given a string representing the hour, an integer number of divisions and a boolean flag for AM or PM. We check that the division number is equally divisible into 60 and throw an IllegalArgumentException if it isn’t. Otherwise, we set up a BorderLayout manager and call setFont. In effect we are overriding setFont to derive a small and large font from the standard setting. The large font is used for the hour label and the smaller derivative is used for minutes.
The hour label is a JLabel instance aligned at the top and right justified, transparent with the hourFont. We put this label in the CENTER of our border layout and create a minutePanel for the EAST position. To the minutePanel, we add our individual minute labels. The first minute is either "am" or "pm", but we calculate the others based on the divisions requested and add each to a GridLayout with the specified number of vertical cells.
Because we’re using a number of labels with the font set to the minuteFont value, I’ve implemented a protected utility method called createLabel, which reduces what would otherwise be a lot of redundant code. The only other method we need to implement is paintComponent, which lets us draw separator lines between minute labels. The result is a DayTimeLabel class that draws the hour, minute divisions and ruler lines for a given hour.
Listing 2 shows the DayTimeEntry class, which does most of the fancy footwork in our JDayTime implementation. Most of the clever tricks are in the setPosLen, getPos and getLen methods. We also implement several listener interfaces to handle focus, mouse and component events, as well as the Comparable interface, which is used for sorting an intersection list when we adjust for overlapping entries. Lets take a quick look at each of the interface implementations. Then we’ll cover the position and length methods, as well as the overlap collision handling, in that order.
The focusGained and focusLost methods implement the FocusListener interface. We set a flag to keep track of the focus state and bring the entry into the foreground by calling the moveToFront method on the LayeredPane. This is where we flip the border between the 4 pixel top and bottom and the single pixel border, depending on whether we gained or lost the focus. The last thing we do in both methods is call repaint to be sure the new border is visible.
We implement all of the methods in the MouseListener, but only need to handle mousePressed and mouseReleased events. When the mouse is pressed, we remember the current position and get the focus if we’re on the left edge. When the mouse is released we readjust the DayTimeEntry positions by calling the setPosLen method on each entry in the DayTimeGroup known by the JDayTime component we’re in. You’ll see later that this accounts for overlapping adjustments as well as the snap to grid feature when the DayTimeEntry is moved or resized.
The MouseMotionListener interface implements mouseMoved and mouseDragged, both of which are important to us. The mouseDraged event has three possibilities. We are moving because the mouse is on the left edge, we are resizing and moving because the mouse is near the top, or we’re merely resizing because the mouse is near the bottom. In each case we calculate the bounds, based on the context, making use of the convertPoint method in SwingUtilities. After moving, we call revalidate to make sure any changes are reflected visually.
The mouseMoved events let us detect the mouse position and set the cursor based on the context. If the mouse is near the top or bottom, somewhere in the border area, we set the cursor to the predefined N_RESIZE_CURSOR. If we’re on the lest edge, in the border area, we set the cursor to the predefined MOVE_CURSOR. Otherwise, we go back to the DEFAULT_CURSOR.
Because the entry positions may change if the parent component is resized, we register to receive Component events and implement the ComponentListener interface. There are four methods we need to implement, but the only one we’re interested in is componentResized. When componentResized is called, we use setPosLen to make sure any position or size changes are taken into account.
The snap to grid feature, which relates primarily to the top and bottom position, is handled by the setPosLen method. This method calls the getPos and getLen methods, which round the results of a calculation that takes into account the current top or height, the cell height and the number of divisions in each hour to return a double that reflects the position and length of the entry. This way, the rounded number is always on the edge of the visual divisions.
There are three variants on the setPosLen method. The first uses getPos and getLen and requires no arguments. The second allows you to specify the position and length and uses the current width. The third lets you specify all three parameters and is called by each of the first two variants. After applying the math and setting the bound for the entry, we call adjustForOverlaps to adjust for any overlapping entries and then revalidate to reflect the changes visually.
This brings us to the adjustForOverlaps method, which calls the getIntersectionList method to get a DayTimeGroup list of DayTimeEntry objects that intersect. We use the intersects method to compare against any DayTimeEntry other than ourselves. The calculation for intersects is based on the current position and length of the entry being compared.
The adjustForOverlaps method gets the intersection list and sorts the list based on the Collections API, using our implementation of the Comparable interface. Comparable requires the compareTo method, which considers older entries first. We spread the overlapping entries across the width of the display by dividing the space by the number of entries returned in getIntersectionList. You’ll notice we also take 10 pixels off the right to make sure there’s room for JList selections to happen, even if all the entries fill the visible area completely.
That’s basically it for the DayTimeEntry class. We implement the toString method to make debugging easier and the constructor merely stores the passed in values and sets a few state variables, adding listeners we need to receive events from. The child component displayed in the entry is registered to send mouse and focus events our way as well, so we can change to border on focus and deal with mouse clicks effectively. You may remember we didn’t use the hasFocus value in our focus handling, opting instead for our own selected value, and this is why. If we used the hasFocus value, we’d get negative results when the child component had the focus.
With all the work done by DayTimeEntry, JDayTime itself is comparatively simple. Listing 3 shows the code for JDayTime. The constructor sets up the DayTimeRuler and DayTimeLayer subcomponents and passes in values for the number of divisions and the height of each division. We set up a JList with empty strings and change the ListCellRenderer to an instance of DayTimeRenderer, and we set up an instance of DayTimeGroup to keep an eye on our entries.
The getDivisions method is trivial, returning the current number of divisions. The addEntry method merely adds an entry at the given position and length with an arbitrary JComponent. Our examples use a JTextArea but any JComponent is valid.
I haven’t mentioned the values for position and length much, so I’d like to clarify that a little. The numbers are doubles, which identify the hour of the day, from 0 to 23 and the length of an entry in hours. You can use fractions here and the values will be rounded automatically. The granularity depends on the settings you use for divisions. If you use 2 divisions, the rounding will take you up or down a half hour at most.
You can use more divisions to be more precise if you like and you can transfer the model easily enough between displays, though it will be lossy if you go from high to low division counts. The fractions are key to avoiding offsets that differ between display types, depending on the divisions you use. The alternative is to use an integer offset for each division but you’ll get different numbers and can’t change the display between division settings if you do this.
JDayTime is an interesting component to implement, mostly because of the layering required to make it work. In other languages, without Swing to help, this would be more difficult. But the JLayeredPane is there to make all this a lot easier. This implementation could be a little more loosely coupled, with better interfaces and renderers, but the number of classes grows quickly and I thought it was more important to focus on the things that make it interesting than on implementing the ultimate solution. I think you’ll find the code is more than adequate if you want to use it. JDayTime can help you immensely if you’re implementing schedule-related applications.