Friday, October 31, 2008

In search of the perfect API (part 2)

This is the second installment of a sort series about the evolution of the API of DesignGridLayout library.
Although DesignGridLayout is used as an example, this series is more general and its goal is to show how you can improve the API of any library, and what pitfalls you should avoid.

In the first installment of this short series, we have visited the initial API of DesignGridLayout (version 0.1) and seen several drawbacks it exhibited.

Release 0.9 (heavy refactoring of both API and implementation)

When I took over the DesignGridLayout project, a few months ago, I had decided to completely refactor the library source code and make the API has good as possible; in particular, I wanted to remove all drawbacks of version 0.1.

First of all, I have added interfaces in order to isolate API from the implementation details (too many public classes and methods were exposed in version 0.1).

Since, in DesignGridLayout, the accessible features are different if you use:
  • a grid row
  • a non-grid row (center, left or right)
  • an empty row (which sole purpose is to provide some empty vertical space)
I have first decided to make different methods in DesignGridLayout class to create these various kinds of rows; one of my goals was to be able to provide a specific API according to each kind of row; in addition, deciding the type of the row at the time you create it from DesignGridLayout would remove another problem of V0.1 related to multiple changes of the row type (which for the end user is non-sense).
public class DesignGridLayout {
...
public INonGridRow leftRow();
public INonGridRow rightRow();
public INonGridRow centerRow();
public IGridRow row();
public void emptyRow(int height);
}
All names starting with I are interfaces (I borrowed this convention from .NET, although arguable at the beginning, I found it quite interesting in my situation).

Here are the interfaces (simplified, I put only the main methods for clarity):
public interface INonGridRow {
public INonGridRow add(JComponent... children);
}

public interface IGridRow {
public IGridRow label(JLabel label);
public IGridRow add(JComponent... children);
public IGridRow add(JComponent child, int span);
public IGridRow empty();
public IGridRow empty(int span);
}
Now we clearly set the list of methods available to each kind of row, excluding any unsupported method (which we didn't do in V0.1).

We have also replaced the constant EMPTY (previously defined in Row) and replaced it by 2 empty() methods that are more explicit and help the developer (when using IDE code completion facility).

With this API, we have solved most problems existing with V0.1 with one exception: one can still call label() several times in a grid row although it is not supported by the library.

Now the example code shown in the first installment can be rewritten
layout.row().label(lbl1).add(field1).add(field2).add(field3);
layout.row().label(lbl2).add(field4).add(field5);
layout.row() .add(field6).add(field7);
layout.emptyRow(18);
layout.centerRow().add(ok).add(cancel);
to be compared with the previous snippet:
layout.row().label(lbl1).add(field1).add(field2).add(field3);
layout.row().label(lbl2).add(field4).add(field5);
layout.row() .add(field6).add(field7);
layout.row().height(18);
layout.row().center().add(ok).add(cancel);
The difference may not be remarkable here, but believe me you will see the difference when using your preferred IDE (code completion will not show you a plethora of irrelevant methods for the current context).

That's it for the API of V0.9. In next installment, I will describe 2 evolutions (due to adding new features) made to the API in 2 snapshots of the future V1.0.

That's it for this time. Enjoy!

Thursday, October 30, 2008

DesignGridLayout: in search of the perfect API (part 1)

This is the first installment of a short series (4 or 5 posts) telling the story of DesignGridLayout API.

From the first beginning, DesignGridLayout was meant to be easy to use in order to build Swing forms; its API was meant to make the use of any graphical designer totally useless. It was also meant to give code maintainers the possibility to "see" the UI while only browsing the source code.

For that, it has used a fluent API (some may use the term "DSL" or "Domain Specific Language" in this context) so that you could, in one line of code, define one row of your UI, eg:
layout.row().label(lbl1).add(field1).add(field2).add(field3);
layout.row().label(lbl2).add(field4).add(field5);
layout.row() .add(field6).add(field7);
layout.row().height(18);
layout.row().center().add(ok).add(cancel);

The code above would give the following result in a dialog:


As mentioned above, this series is about the evolution of that API. Prior to giving details on API history and changes motivation, I must thank Joshua Bloch for his excellent book "Effective Java, 2nd Edition" (I read it this summer, I had never read the first edition before) which I recommend to every Java programmer and particularly to library developers; this book has helped me a lot in thinking about providing a better API to DesignGridLayout users.

Initial release 0.1

Version 0.1 could be seen as a good prototype of implementing canonical grids in a Swing layout with a rather good API.

The library was then made of 4 classes only (all public)!

public class DesignGridLayout implements LayoutManager {
...
public Row row() {...}
}

public class Row {
public static final JComponent EMPTY = ...;
public Row label(JLabel label) {...}
public Row add(JComponent child) {...}
public Row add(JComponent... children) {...}
public Row add(JComponent child, int span) {...}
public Row left() {...}
public Row center() {...}
public Row right() {...}
public Row grid() {...}
public Row height(int height) {...}
...
}

public enum RowAlign {...}

public class RowItem {...}

Quite straightforward, wasn't it? However, this API suffered from several problems (don't forget that was a kind of prototype at that time):
  1. Some public classes (RowItem, RowAlign) should have been hidden because they were implementation details. By exposing these classes, you make them part of the API, preventing potential future changes in the implementation (don't forget that when designing an API, you should strive to make it as good as can be, but also as narrow as possible, because it will be hard to change afterwards -without breaking compatibility).
  2. Many public methods from DesignGridLayout and Row classes should have been hidden because they served no purpose to the user of the library, once again they were implementation details that were unintentionally added to the API, leading to a crippled API (in the sense that all those useless methods appear and are proposed by IDE code-completion feature)
  3. This API made it possible to change the type of a row as many times as you want (by calling e.g. layout.row().grid().left().right().center().add(...);)
  4. You could add a label to a non-grid row (which was not supported by the implementation) so this could give wrong impressions to users of the library
  5. You could add a component with a column span > 1 in a non-grid row (not supported)
  6. You could specify the height of any row (which is not advised for a proper look). Usage of the height(int) method was intended as in layout.row().height(18) (no component added to the row).
To summarize the main issues with this API:
  • many methods or classes should not be public (they should be either private or package-private)
  • the API allows code that is not supported at runtime and may give the code writer/reader a false idea of how the UI looks like
In the next installment, I will talk about the API changes in version 0.9 (current official version of DesignGridLayout).

That's all for today!

Sunday, October 26, 2008

DesignGridLayout: final 1.0 soon released! Or not?

Today, I have finished fixing the last of the high priority issues of DesignGridLayout.
All fixes are already available on Subversion trunk, and also as snapshots there (note: it is not available through maven).

The past 3 weeks were quite tough in terms of issues fixing, with a lot of refactoring, improvements, and clean-up.

Now I am quite happy with the current standpoint about new features:
  • "smart vertical resize" (already discussed in this post)
  • automatic support of right-to-left orientation Locales (discussed there)
  • smarter -and simpler- API for empty rows (no need to specify a number of pixels: that's one step toward resolution independence)
  • better minimum size management (previously, DesignGridLayout would calculate a minimum size equal to the preferred size of the Container, but during very narrow resize, it would shrink each component to ridiculously small widths)
  • automatic call to setLayout() from DesignGridLayout constructor
  • last, but not least, smart support for forms with multiple label columns (this required some extension of the "spirit" of canonical grids, rising to the concept of "canonical sub-grids")
The concept of "multi-label grids" (or "canonical sub-grids" as I often name them) still needs to be introduced on the project web site (or on this blog) but that should not be a very heavy task.

So I consider it is soon time for an official 1.0 release!

Or is it?

I mean, there are still several open issues (enhancements only, not bugs), some of which might be worth having fixed in the 1.0 version. Of course, willing to fix these issues would then mean postponing the official 1.0 release (might be end November -or later, depending on how many issues we want solved- instead of early November).

So I ask the question here (I have also asked it there actually): what do you, DesignGridLayout users, prefer?
  1. Version 1.0 in about one week, all current issues open solved only in later versions?
  2. Version 1.0 with more issues closed (which ones?), but released later (between mid November and end December, depending on which issues to solve)
If you don't know, then I'll choose on my own (but I have to admit that I am not sure about what I prefer).

For the time being, even if you don't want to answer this question, you can always check the latest snapshot and report problems if you find any. Also, some of the new API method names I have chosen may be argued (I lacked inspiration for some), so don't hesitate to comment also on these (but please suggest a replacement for the new methods which name you don't like).

Enjoy!

Thursday, October 09, 2008

DesignGridLayout: real-time resizing of JScrollPane

Hi,

In my quest to solve the issue #5 of DesignGridLayout (namely: "Layout does not allow additional height usage after resize"), I have created a special dialog for testing my fixes for this issue. You can see a snapshot below (at default -ie preferred- size).


In this sample, I have put several rows, containing various kinds of components, of which some should be given extra height when the user resizes the dialog (eg JTable), and some should never grow taller than their preferred size (eg JTextField).

I could find out that there are only 2 categories of components that want extra height when it becomes available:
  • any Component that is set as the view of a JScrollPane (in particular JTextArea, JTable, JList)
  • any JSlider using JSlider.VERTICAL policy
Besides these, I did not find any component that should grow height when its embedding dialog is resized.

Based on these observations, I have implemented a simple internal mechanism into DesignGridLayout for distinguishing these 2 kinds of components:
interface HeightGrowPolicy
{
/**
* Checks if a {@link Component} can grow in height.
* @param component the component to test
* @return {@code true} if {@code component} has a variable height;
* {@code false} if {@code component} has a fixed height.
*/
public boolean canGrowHeight(Component component);
}
This interface is implemented by several classes:
  • HeightGrowPolicyMapper (allows to map different Component classes to their own specific HeightGrowPolicy),
  • JScrollPaneHeightGrowPolicy (special implementation for JScrollPane),
  • JSliderHeightGrowPolicy (special implementation for JSlider)
The mechanism itself is easily extensible because I can easily add further policies for other kinds of Components.

My first working prototype for issue #5 was roughly that simple (of course I also had to make some little changes in a few existing classes of DesignGridLayout library).

But I was not fully satisfied with it. The main reason for this was that when you extend the height of the dialog, all resizable components get a few pixels more for their height, which is absolutely ugly for a JTable, a JList or a JTextArea, because these components would then start to show, on the bottom side, some "incomplete" row or line of text.

Whenever I see this happening in a GUI application (be it made in Java or any other language) I generally get angry and immediately classify it in the category of "non professional" software.

So I inferred on some way to solve this: that is really the responsibility of a LayoutManager to make sure that whenever you resize a Container, all resized Components keep good-looking.

First I have extended the interface HeightGrowPolicy above:
interface HeightGrowPolicy
{
public boolean canGrowHeight(Component component);

/**
* Computes the maximum amount of extra height that a {@link Component} can
* use.
* @param component the component to test
* @param extraHeight the amount of available extra height
* @return the maximum amount of extra height that {@code component} can use
* without exceeding {@code extraHeight}
*/
public int computeExtraHeight(Component component, int extraHeight);
}
to give a chance to let DesignGridLayout know what amount of extra height a given Component will accept to keep its good look. This amount can be anything between 0 and extraHeight.

Here is the implementation for JScrollPane components:
class JScrollPaneHeightGrowPolicy implements HeightGrowPolicy
{
public boolean canGrowHeight(Component component)
{
return true;
}

public int computeExtraHeight(Component component, int extraHeight)
{
JScrollPane scroller = (JScrollPane) component;
int unit = scroller.getVerticalScrollBar().getUnitIncrement(+1);
if (unit <= 0)
{
return extraHeight;
}
else
{
// Return an integral number of units pixels
return (extraHeight / unit) * unit;
}
}
}

Simple isn't it? It makes use (behind the scenes) of the Scrollable interface that is most often implemented by components that are supposed to be used in JScrollPane (namely JList, JTable, JTextArea and JTree).

Provided that the preferred size of these components of your dialog is already good looking, then DesignGridLayout will ensure that they always stay this way. If you want to have proper preferred size for JTable, JList or JTextArea, you can use specific API of these components in your own code as in the following snippet:
JTextArea area = new JTextArea();
JTable table = new JTable();
JList list = new JList();

// area has preferred height to show exactly 3 lines of text
area.setRows(3);

// table has preferred height to show exactly 4 rows
int height = 4 * table.getRowHeight();
table.setPreferredScrollableViewportSize(new Dimension(PREF_WIDTH, height));

// list has preferred height to show exactly 2 items (ie 2 rows)
list.setVisibleRowCount(2);

You can try this on the Java Web Start enabled example.

Not bad!

But that's not the end of the story yet. If you play a bit with the example above, you'll see that between two actual resize of e.g. the first JTable component, the spacing between that row and the next is increasing, which is in contradiction with DesignGridLayout philosophy which promises to use the "ideal" inter-components spacing (according to the LAF/platform in use).

So I have made a second attempt, that you can experiment through this JWS example.

In this attempt, inter-components spacing is always preserved. However, to my viewpoint, resizing does not have a very good behavior (in real-time I mean):
  • first of all, the user does not "feel" that something is going on during the first pixels of his resizing action: the layout does not change at all! The user may just stop here and think that resize does not work!
  • second, a weird behavior is observed when the exact number of extra pixels is obtained during resize: all components below the first JTable seem to "hop" to a new position several dozens of pixels away from their previous one!
Hence I believe I will stick with the first solution and completely remove the second one. I could make it an option for the library users to choose, but I would not feel satisfied of giving such a possibility to create layouts with such a bad feel during resize.

What do you think?

If you want to look at DesignGridLayout code which snippets have been used in this post, you should check out the latest trunk from subversion.

Have fun!