Eclipse Forms Programming GuideInitial creation: 21 February 2004Added table wrap sample: 22 February 2004Introduction
This document has been written to help you use the new Eclipse 3.0 feature called 'Eclipse Forms'. The content will eventually move to Eclipse help. In the mean time, you can start experimenting using examples shown below and code in Eclipse 3.0 integration builds.
Eclipse Forms is a plug-in that exposes a set of customs widgets and other supporting classes that were before used as internal to PDE and Update components. They provide for creating polished, 'Web-like' UIs by modestly extending SWT and/or manipulating style bits, colors, fonts and other properties in order to get the desired behavior. This technology was used throughout PDE multi-page editors and will now be available as public API in Eclipse 3.0.
The document represents the state of API at the time of writing. It is possible that some aspects of the API will slightly change until final release. We anticipate only mechanical name changes rather than more fundamental changes.
Problem definition
When Eclipse platform was designed, there were two distinct context in which SWT widgets could appear:
In traditional dialogs (message boxes, dialogs, wizards, preference pages, property pages) In content areas (views and editors)
Controls in dialogs typically used default colors and fonts as provided by the operating system and were layed out using GridLayout in 9 out of 10 cases. The goal was to look like a dialog and fill the dialog space in both directions.
Controls in views and editors were 'stretchy' i.e. they filled the entire content area. Stretchy controls can scroll their content (trees, tables, text areas, lists etc.). The colors and fonts used here were those provided by the system for use in content area (these colors can be obtained from the Display using SWT.LIST_BACKGROUND or SWT.LIST_FOREGROUND keys).
What was missing was the third option where widgets you would normally see in the dialog context are created in views or editors (where you would expect 'stretchy' controls). Several problems needed to be solved:
SWT controls like buttons, labels, composites etc. look strange in the content areas as-is (with the default dialog background) Composites with GridLayout in the content area are problematic because they are not scrolled. Therefore, it is easy to get the controls to clip or start riding one another. Controls in the content area are normally wrapped to fill the provided width. Wrapping is very hard to accomplish with the existing SWT layouts. When problems 1-3 are fixed, the result looks very much like forms in HTML browsers, and a natural leap is to add hyperlinks and mix them with other controls.
Eclipse Forms were designed to solve these problems. They provide:
A concept of a 'form' that is suitable for inclusion into the content area (editor or view) A toolkit that manages colors, hyperlink groups and other aspects of the form, as well as serve as a factory for a number of SWT controls (these controls are created and configured to fit into the form context) A new layout manager that lays out controls using the algorithm similar to HTML tables Custom controls designed to fit into the form (hyperlink, image hyperlink, scrollable composite, section). Multi-page editor where each page is a form (as in PDE). Setup
In order to be able to use Eclipse Forms, all you need to do is add 'org.eclipse.ui.forms' plug-in to your list of required plug-ins. This plug-ins is RCP-friendly in that it does not have dependency on IDE plug-ins (its only dependency is org.eclipse.ui).
Examples
Source code for most of the examples shown in this document can be found in Eclipse repository (HEAD) as org.eclipse.ui.forms.examples. Check them out into the workspace and look inside the 'src' directory in a Java or PDE perspective.
API packages
Eclipse Forms has the following public (API) packages:
org.eclipse.ui.forms - the main package org.eclipse.ui.forms.editor - classes that employ Eclipse Forms on top of the MultiPageEditorPart to create multi-page editors as seen in PDE org.eclipse.ui.forms.events - new events created to support custom widgets org.eclipse.ui.forms.widgets - a set of custom widgets and layouts created specifically for Eclipse Forms Getting Started
We will start playing with Eclipse Forms by creating an empty form in a view. As we said above, views are content areas that require flexible content. Here is the code sample:
package org.eclipse.ui.forms.examples.views;import org.eclipse.swt.widgets.Composite;import org.eclipse.ui.forms.widgets.*;import org.eclipse.ui.part.ViewPart;
public class FormView extends ViewPart { private FormToolkit toolkit; private ScrolledForm form;
/** * This is a callback that will allow us * to create the viewer and initialize it. */ public void createPartControl(Composite parent) { toolkit = new FormToolkit(parent.getDisplay()); form = toolkit.createForm(parent); form.setText("Hello, Eclipse Forms"); }
/** * Passing the focus request to the form. */ public void setFocus() { form.setFocus(); }
/** * This is a callback that will allow us * to create the viewer and initialize it. */ public void createPartControl(Composite parent) { toolkit = new FormToolkit(parent.getDisplay()); form = toolkit.createForm(parent); form.setText("Hello, Eclipse Forms"); }
/** * Passing the focus request to the form. */ public void setFocus() { form.setFocus(); }
/** * Passing the focus request to the form. */ public void setFocus() { form.setFocus(); }
/** * Disposes the toolkit. */ public void dispose() { toolkit.dispose(); super.dispose(); }}
/** * Disposes the toolkit. */ public void dispose() { toolkit.dispose(); super.dispose(); }} org.eclipse.ui.forms.examples.views;import org.eclipse.swt.widgets.Composite;import org.eclipse.ui.forms.widgets.*;import org.eclipse.ui.part.ViewPart;
public class FormView extends ViewPart { private FormToolkit toolkit; private ScrolledForm form;
/** * This is a callback that will allow us * to create the viewer and initialize it. */ public void createPartControl(Composite parent) { toolkit = new FormToolkit(parent.getDisplay()); form = toolkit.createForm(parent); form.setText("Hello, Eclipse Forms"); }
/** * Passing the focus request to the form. */ public void setFocus() { form.setFocus(); }
/** * This is a callback that will allow us * to create the viewer and initialize it. */ public void createPartControl(Composite parent) { toolkit = new FormToolkit(parent.getDisplay()); form = toolkit.createForm(parent); form.setText("Hello, Eclipse Forms"); }
/** * Passing the focus request to the form. */ public void setFocus() { form.setFocus(); }
/** * Passing the focus request to the form. */ public void setFocus() { form.setFocus(); }
/** * Disposes the toolkit. */ public void dispose() { toolkit.dispose(); super.dispose(); }}
/** * Disposes the toolkit. */ public void dispose() { toolkit.dispose(); super.dispose(); }}
In the snippet above, we have created a view by creating an instance of a toolkit first. We then used the toolkit to create a form. We set the title of the form. It is important not to forget that toolkit manages resources that need to be disposed of (hence the dispose method).
When we register this view with the workbench and run, we get the following:
The form we just created does the following things:
It renders the title we set using setText
It can render a background image behind the title
The title will wrap if there is no space to show it on one line
The form will introduce scroll bars if the content cannot be shown in the provided space
The last item in the list needs some clarification. In some instances, you will want to make the form be a child of a more complex layout that is itself capable of scrolling. In that case, you may want to get all the capabilities of a form, but without scrolling. In that case you would use the class Form instead of ScrolledForm (in fact, ScrolledForm has-a Form as an internal object and delegates most of the methods like setText to it).
Adding content
Now that we have the form view running, we can start adding some content to it. Eclipse forms have a body and all the content should be created there:
public void createPartControl(Composite parent) { toolkit = new FormToolkit(parent.getDisplay()); form = toolkit.createForm(parent); form.setText("Hello, Eclipse Forms"); GridLayout layout = new GridLayout(); form.getBody().setLayout(layout); Hyperlink link = toolkit.createHyperlink(form.getBody(), "Click here.", SWT.WRAP); link.addHyperlinkListener(new HyperlinkAdapter() { public void linkActivated(HyperlinkEvent e) { System.out.println("Link activated!"); } }); }
Notice that we first set layout on the body, then used the body as a parent to create a hyperlink. Hyperlink control is one of the custom widgets in Eclipse Forms and acts like a label that is clickable. A new type of listener (HyperlinkListener), its default implementation (HyperlinkAdapter) and the even type are all defined in Eclipse Forms to support the hyperlink. Handling events of the hyperlink are very similar to other SWT widgets, as shown in the code above. Our view now looks like this:
Note how hyperlink has focus rectangle painted around it. When the view is activated, focus is transferred to the form, which passes it to the first control capable of accepting focus, our link in this case.
Hyperlink Groups
Form toolkit has hyperlink group that each hyperlink created by it is added to. Hyperlink groups serve several roles:
They capture colors (both normal, hover and active) for all the links in the group
They change colors of the managed links based on their state
They change underlining style of the managed links based on their state
They manage cursors and show busy cursor before link listeners process the link activation, and change it after.
You can change the default settings of the hyperlink group by obtaining the object from the toolkit (getHyperlinkGroup()).
Creating common controls
One of the design goals of Eclipse Forms was to allow common SWT controls to be created in the editor/view content space. Since form body is a normal composite, you can use any layout and control you want inside it. However, remember that 'raw' SWT controls are designed to fit inside a dialog. We will now create some using their constructors by continuing the method above:
layout.numColumns = 2; GridData gd = new GridData(); gd.horizontalSpan = 2; link.setLayoutData(gd); Label label = new Label(form.getBody(), SWT.NULL); label.setText("Text field label:"); Text text = new Text(form.getBody(), SWT.BORDER); text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Button button = new Button(form.getBody(), SWT.CHECK); button.setText("An example of a checkbox in a form"); gd = new GridData(); gd.horizontalSpan = 2; button.setLayoutData(gd);
We are now using two columns and creating a label, a text field and a checkbox. The result is below:
It is easy to spot the problem - controls created directly look out of place because they think they are in a dialog. This is the reason there are factory methods on the toolkit to create some of the commonly used controls. Factory methods set the correct colors and also hook certain listeners that ensure controls behave correctly when in a form. If we use the corresponding factory methods instead, the code would look like this:
layout.numColumns = 2; GridData gd = new GridData(); gd.horizontalSpan = 2; link.setLayoutData(gd); Label label = toolkit.createLabel(form.getBody(), "Text field label:"); Text text = toolkit.createText(form.getBody(), ""); text.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Button button = toolkit.createButton(form.getBody(), "An example of a checkbox in a form", SWT.CHECK); gd = new GridData(); gd.horizontalSpan = 2; button.setLayoutData(gd);
The view will now look better:
In general, you should use toolkit methods to create all the controls that you can. In case of a control that is not available from the toolkit, you can use the method adapt. This method takes an SWT controls and adapts it for use in the form. Most of the toolkit factory methods call adapt themselves.
Achieving the 'flat' look
One of the recongizable attributes of Eclipse Forms used in PDE editors was the elegant 'flat' look of the controls. All the controls used there were without 3D borders that looks fine in dialogs but out of place in the content space of editors or views. This support is built into the FormToolkit class. However, on some platform it comes at a price of some custom rendering. For example, look at the following screen capture from PDE editor (version 2.1):
Controls like tables, text entries, combo box etc. are rendered with flat one-pixel borders. These borders do not come from controls themselves (SWT.BORDER style is not used). Instead, if the toolkit is instructed, it will add itself as a paint listener to their parent, and draw borders around controls during paint events. In order to make this happen, you need to call toolkit method 'paintBordersFor(parent)' for each composite where you created controls like text, tree, table etc.
Form toolkit knows which controls require a custom border. However, you may create a new one that also need the border that is not on its list. You can give a hint to the toolkit by making the following call:
Control myControl = new MyControl(parent);myControl.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER);
or
myControl.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
toolkit.paintBordersFor(parent);
As you can see from the picture above, 'structural' controls like trees, tables etc. have a different border from text areas and you can choose which one to render for your control. Note that this is not needed for control created using toolkit's factory methods.
In Eclipse 3.0, border rendering is not done on Windows XP. Since Eclipse on Windows XP can use skinned widgets, controls like text, table, tree etc. are already flat and further 'fixing' is not needed. In order to be portable, you should call 'paintBordersFor' anyway, and let the toolkit choose what to do depending on the windowing system.
Custom Layouts
Eclipse Forms offer two new layouts over and above those provided by SWT.
TableWrapLayout
Now that we know how to populate the form, lets throw it a curve: change the hyperlink text to be much longer:
link.setText("This is an example of a form that is much longer and will need to wrap.");
Let's see the result on screen:
What happened? Remember that we are using GridLayout. When it asked the link control to compute its size, it gave it the size needed to render the text in one long line. Although we instructed the control to wrap, it didn't matter because GridLayout requires that a control returns its size in isolation. The link itself (as well as other SWT controls like Label) is capable of computing the height given its width (if you pass the width instead of SWT.DEFAULT in computeSize), but GridLayout is not passing it.
What we really need is a different layout algorithm that works more like HTML tables. We want the content to try to fit in the provided client area, and grow vertically to compensate. Eclipse Forms provide an alternative layout for just such purpose called TableWrapLayout. There are many similarities between GridLayout and TableWrapLayout. Both organize parent's children in grids. Both have layout data that instructs the layout how to treat each control. Both can accept hints on which control should grab excess space etc.
However, they fundamentally differ in the approach to layout. TableWrapLayout starts with columns. It computes minimal, preferred and maximum widths of each column and uses this information to assign excess space. It also tries to be fair when dividing space across columns so that there is no excess wrapping for some controls.
It is possible to mix GridLayout and TableWrapLayout but the branch where you use GridLayout is the one where wrapping stops. This is quite acceptable if you don't want it to wrap (if the composite contains controls that cannot wrap anyway, like text, buttons, trees etc.). However, you should have an unbroken path from the form body to each text control that needs to wrap.
Lets rework our example to use TableWrapLayout (changes are highlighted);
public void createPartControl(Composite parent) { toolkit = new FormToolkit(parent.getDisplay()); form = toolkit.createForm(parent); form.setText("Hello, Eclipse Forms"); TableWrapLayout layout = new TableWrapLayout(); form.getBody().setLayout(layout); Hyperlink link = toolkit.createHyperlink(form.getBody(), "Click here.", SWT.WRAP); link.addHyperlinkListener(new HyperlinkAdapter() { public void linkActivated(HyperlinkEvent e) { System.out.println("Link activated!"); } }); link.setText("This is an example of a form that is much longer and will need to wrap."); layout.numColumns = 2; TableWrapData td = new TableWrapData(); td.colspan = 2; link.setLayoutData(td); Label label = toolkit.createLabel(form.getBody(), "Text field label:"); Text text = toolkit.createText(form.getBody(), ""); td = new TableWrapData(TableWrapData.FILL_GRAB); text.setLayoutData(td); Button button = toolkit.createButton(form.getBody(), "An example of a checkbox in a form", SWT.CHECK); td = new TableWrapData(); td.colspan = 2; button.setLayoutData(td); }
Obviously, we used more-less the same concepts. Some of the variables have different names (for example, colspan and rowspan, align and valign are taken from HTML TABLE attributes), but you are doing the same thing - creating a grid with two columns where link and button span two columns. Since margins are the same as in GridLayout, the result will look similar except link will be wrapped as expected:
This is more like it. The way we configured the layout, text control will grab the excess space in its column, and both the checkbox and the link will just spread across both columns. In addition, link will not try to render in one long line - it will take the view width and push the rest of the content down in order to wrap.
One of the main differences between TableWrapLayout and GridLayout is that with the former you should stop thinking about the vertical dimension. In GridLayout, you would typically let the 'rigid' controls assume their natural positions and let 'flexible' controls (text, tree, table) grab horizontal and/or vertical excess space. In contrast, TableWrapLayout works top-down and when it places all the controls, it is done. The concept of grabbing excess space still exists in horizontal dimension (as shown above). However, vertically you can only choose to FILL the cell in case it is taller than the control, or pick TOP, MIDDLE or BOTTOM vertical alignment.
You may notice one thing that may seem in contradiction with the previous claim: TableWrapData still has grabVertical variable. However, the variable is there for a distinct purpose: when a control with a fixed height spans multiple rows, its height will create a local case where vertical dimension is known, and controls in the spanned cells need to divide the extra space between themselves.
In order to have good results with TableWrapLayout, ensure that controls that need to wrap have the appropriate style (SWT.WRAP). Composite custom controls provided by Eclipse Forms are wrap-enabled out of the box. This is achieved by using internal layouts that implement ILayoutExtension interface:
public interface ILayoutExtension {/** * Computes the minimum width of the parent. All widgets * capable of word wrapping should return the width * of the longest word that cannot be wrapped. * @param parent * @param changed * @return */ public int computeMinimumWidth(Composite parent, boolean changed);/** * Computes the maximum width of the parent. All widgets * capable of word wrapping should return the length * of the entire text without wrapping. * @param parent * @param changed * @return */ public int computeMaximumWidth(Composite parent, boolean changed); }
TableWrapLayout implements this interface itself, which allows it to handle cases where composites with this layout are children of the composite that is laid out. The additional two methods allow it to compute the two extreme cases - the absolute minimum width and the maximum width if all the controls are spread as wide as possible. The difference between the two allow the algorithm to distribute extra space fairly between columns to minimize excess wrapping.
Let us take a closer look at the space distribution. We will comment the code we wrote so far in the view and replace it with the following:
layout.numColumns = 3; Label label; TableWrapData td; label = toolkit.createLabel(form.getBody(), "Some text to put in the first column", SWT.WRAP); label = toolkit.createLabel(form.getBody() ,"Some text to put in the second column and make it a bit longer so that we can see what happens with column distribution. This text must be the longest so that it can get more space allocated to the columns it belongs to.", SWT.WRAP); td = new TableWrapData(); td.colspan = 2; label.setLayoutData(td); label = toolkit.createLabel(form.getBody(), "This text will span two rows and should not grow the column.", SWT.WRAP); td = new TableWrapData(); td.rowspan = 2; label.setLayoutData(td); label = toolkit.createLabel(form.getBody(), "This text goes into column 2 and consumes only one cell", SWT.WRAP); label.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB)); label = toolkit.createLabel(form.getBody(), "This text goes into column 3 and consumes only one cell too", SWT.WRAP); label.setLayoutData(new TableWrapData(TableWrapData.FILL)); label = toolkit.createLabel(form.getBody(), "This text goes into column 2 and consumes only one cell", SWT.WRAP); label.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB)); label = toolkit.createLabel(form.getBody(), "This text goes into column 3 and consumes only one cell too", SWT.WRAP); label.setLayoutData(new TableWrapData(TableWrapData.FILL)); form.getBody().setBackground(form.getBody().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
The code above creates a number of wrapping labels with text of variable length. Some labels span columns, some span rows. To make the test easier, we will set the form background to widget background so that cells will be easy to spot. When we run the example, we will get the following:
The key in space distribution is that difference between control minimum and maximum width is compared. If this difference is greater, the control will cause the column width to receive greater allocation of excess width (excess width is any width greater than the width needed to fit all the controls with their minimum widths taken into account). Notice how column 3 is slightly wider than column 2 because text in column 2 is somewhat longer. The overall goal is to avoid too much empty space in cells. If you want to read more about the theory behind the algorithm, go to W3C recommendations for HTML table auto-layout.
Column Layout
Another custom layout in Eclipse Forms is a variation of the RowLayout. If you configure RowLayout to place children vertically (in columns), and to make all controls the same with within the column, we would get several columns (depending on the width of controls), but the last column would typically not be completely filled (depends on the number of controls). Again, if placed in a form, we would get all the controls in one column because RowLayout cannot do 'vertical' wrapping. If we use GridLayout, we must choose the number of columns upfront and live with the choice.
There are situations in more complex forms where we want the number of columns to be adaptive. In other words, we would like it to change depending on the width of the form - use more when possible, drop the number down if the width decreases. We would also like to fill the form area more-less equally (with all the columns roughly the same height). All this can be achieved with ColumnLayout.
Compared to TableWrapLayout, ColumnLayout is much simpler. Hardly any configuration is needed. The only choice you need to make is the range of columns you want to have (default is 1 to 3). The following example shows a form with a large number of sections (we will talk about sections later) using ColumnLayout. Since there is enough space, three columns are used
The sample exact example will drop the number of columns to two when the form is narrowed:
Controls are laid out top-down, left to right. One column is filled, then then the next one etc. However, the space computation first ensures that each column is roughly the same height so that space is used rationally.
You can still tweak the sizing of the individual controls when using ColumnLayout using ColumnLayoutData. For example, if the control is 'stretchy', you may want to limit its width or height by setting width or height hint. By default, controls 'fill' the width of the column, but you can change that by choosing LEFT, CENTER or RIGHT.
Complex controlsExpandable composite
A very common theme in sleek web pages is the ability to collapse a portion of page content using a toggle control. Eclipse Form offers such a control: ExpandableComposite.
ExpandableComposite ec = toolkit.createExpandableComposite(form.getBody(), ExpandableComposite.TREE_NODE); ec.setText("Expandable Composite title"); String ctext = "We will now create a somewhat long text so that "+ "we can use it as content for the expandable composite. "+ "Expandable composite is used to hide or show the text using the "+ "toggle control"; Label client = toolkit.createLabel(ec, ctext, SWT.WRAP); ec.setClient(client); td = new TableWrapData(); td.colspan = 2; ec.setLayoutData(td); ec.addExpansionListener(new ExpansionAdapter() { public void expansionStateChanged(ExpansionEvent e) { form.reflow(true); } });
The composite accepts a number of styles that control its appearance and behavior. Style TREE_NODE will create the toggle control used in a tree widget for expanding and collapsing nodes, while TWISTIE will create a triangle-style toggle. Using EXPANDED will create the control in the initial expanded state. If style COMPACT, control will report width in the collapsed state enough to fit in the title line only (i.e. when collapsed, it will be as compact horizontally as possible).
Control itself is responsible for rendering the toggle control and the title. The control to expand or collapse is set as a client. Note that it is required that the client is a direct child of the expandable composite.
We needed to add expansion listener to the control and reflow the form on the state change. This is because expansion causes changes in expandable composite size, but the change will not take effect until the next time the parent is laid out (hence we need to force it). In general, every time you do something that causes the layout of the form to be invalid, you need to reflow the form. Reflowing the form will position the controls according to the new sizes AND update scroll bars.
The view now looks like this:
When you click on the '+' sign or on the title itself, composite expands to reveal the client:
This is not bad, but we would like to align the client with the title, not the toggle control. If we bitwise-OR CLIENT_INDENT style with TREE_NODE, the client will receive the desired indent:
It goes without saying that expandable composite uses internal layout that implements ILayoutExtension, and can therefore be added to the parent that uses TableWrapLayout.
Section
One of the most versatile custom controls in Eclipse Forms (and seen in all PDE editors) is Section. It extends expandable composite and introduced the following concepts:
Separator - a separator control can be created below the title
Description - an optional description can be added below the title and separator.
The code is not much different than in case of the expandable composite:
Section section = toolkit.createSection(form.getBody(), Section.DESCRIPTION|Section.TWISTIE|Section.EXPANDED); td = new TableWrapData(TableWrapData.FILL); td.colspan = 2; section.setLayoutData(td); section.addExpansionListener(new ExpansionAdapter() { public void expansionStateChanged(ExpansionEvent e) { form.reflow(true); } }); section.setText("Section title"); toolkit.createCompositeSeparator(section); section.setDescription("This is the description that goes below the title"); Composite sectionClient = toolkit.createComposite(section); sectionClient.setLayout(new GridLayout()); button = toolkit.createButton(sectionClient, "Radio 1", SWT.RADIO); button = toolkit.createButton(sectionClient, "Radio 2", SWT.RADIO); section.setClient(sectionClient);
This time, we used TWISTIE toggle style, added the description and also created composite separator. The view now looks like this:
Image hyperlink
Image hyperlink is a subclass of Hyperlink that adds an image before the link. This combination is so common that it just made a lot of sense to make one control out of it and save on widgets. The control can be used as image link only (when no text is set), or as a combination of link and image. Images for normal, hover and active (selected) state can be set.
Form text control
Using the combination of labels, hyperlinks (and image hyperlinks) and TableWrapLayout, it is possible to create rich form content:
However, soon you will notice that there are limits in how close you can get to the 'web look'. If you inspect the picture above closely, you will notice that all elements are clearly separated: images from text, bold labels from normal labels etc. Images are all at the beginning of the text, not the in the middle. Hyperlinks stand alone - on their own.
To remedy the problem, Eclipse Forms plug-in provides a very rudimentary text control that can do the following:
Render plain wrapped text
Render plain text but convert any segment that starts with http:// into a hyperlink on the fly
Render text with XML tags
In all three modes, form text control is capable of rendering either a string or an input stream.
Rendering normal text (label mode)
FormText rtext = toolkit.createFormText(form.getBody(), true); td = new TableWrapData(TableWrapData.FILL); td.colspan = 2; rtext.setLayoutData(td); String data = "Here is some plain text for the text to render."; rtext.setText(data, false, false);
Second argument set to false means that we will not bother to parse tags, and the third that we will not try to expand URLs if found.
Automatic conversion of URLs into hyperlinks
Now we will add a hyperlink in the text and turn the third argument into true:
FormText rtext = toolkit.createFormText(form.getBody(), true); td = new TableWrapData(TableWrapData.FILL); td.colspan = 2; rtext.setLayoutData(td); String data = "Here is some plain text for the text to render; this text is at http://www.eclipse.org web site."; rtext.setText(data, false, true);
When we look at our view, it now looks like this:
The URL has been converted into a link. The link is part of the wrapped text - we didn't have to create a separate Hyperlink control and try to sandwich it between two labels.
Since rich text control can render hyperlinks, it accepts hyperlink listeners. When created by the toolkit, it assumes hyperlink settings from the hyperlink group that belongs to the toolkit.
Parsing formatting markup
The most powerful use of the form text control is when formatting tags are added to the text. The expected root tag is 'form'. It can one or more children that can either be <p> or <li>. Either of these can have normal text, text between <b> or <span> tags, images and links. Images are declared using <img href="image key"/> (no content), while links are expressed using <a href="href">text</a>.
Of the tags shown above, some have additional attributes. Tag <a> can accept 'nowrap='true'' to block the link from being wrapped into the new line. Tag <p> can have attribute 'vspace="false"' (true by default). Tag <li> has more attributes:
style - can be "text", "bullet" and "image" (default is "bullet")
value - not used for "bullet"; if style is "text", the value will be rendered instead in place of a bullet; if style is "image", value represents a key in the image table of an image to be rendered in place of a bullet
vspace - the same as for the 'p' tag.
indent - the number of pixels to indent text
bindent - the number of pixels to indent the bullet (this number is independent from 'indent' - be careful not to overlap them)
Tags that affect appearance of the normal text are <b> (works as expected), and <span>. The later allows you to change font and/or color of the text within the tag.
How does all this work in practice? Lets make data text that will use all of these tags together:
StringBuffer buf = new StringBuffer(); buf.append("<form>"); buf.append("<p>"); buf.append("Here is some plain text for the text to render; "); buf.append("this text is at <a href=\"http://www.eclipse.org\" nowrap=\"true\">http://www.eclipse.org</a> web site."); buf.append("</p>"); buf.append("<p>"); buf.append("<span color=\"header\" font=\"header\">This text is in header font and color.</span>"); buf.append("</p>"); buf.append("<p>This line will contain some <b>bold</b> and some <span font=\"text\">source</span> text. "); buf.append("We can also add <img href=\"image\"/> an image. "); buf.append("</p>"); buf.append("<li>A default (bulleted) list item.</li>"); buf.append("<li>Another bullet list item.</li>"); buf.append("<li style=\"text\" value=\"1.\">A list item with text.</li>"); buf.append("<li style=\"text\" value=\"2.\">Another list item with text</li>"); buf.append("<li style=\"image\" value=\"image\">List item with an image bullet</li>"); buf.append("<li style=\"text\" bindent=\"20\" indent=\"40\" value=\"3.\">A list item with text.</li>"); buf.append("<li style=\"text\" bindent=\"20\" indent=\"40\" value=\"4.\">A list item with text.</li>"); buf.append("</form>"); FormText rtext = toolkit.createFormText(form.getBody(), true); td = new TableWrapData(TableWrapData.FILL); td.colspan = 2; rtext.setLayoutData(td); rtext.setImage("image", ExamplesPlugin.getDefault().getImageRegistry().get(ExamplesPlugin.IMG_SAMPLE)); rtext.setColor("header", toolkit.getColors().getColor(FormColors.TITLE)); rtext.setFont("header", JFaceResources.getHeaderFont()); rtext.setText(buf.toString(), true, false); rtext.addHyperlinkListener(new HyperlinkAdapter() { public void linkActivated(HyperlinkEvent e) { System.out.println("Link active: "+e.getHref()); } });
One common theme that can be observed is that the widget itself is not responsible for loading images, fonts, resolving links or colors. This is not a browser and it is much better to separate concerns and simply assign images and colors managed elsewhere. Both links and images simply have 'href' attribute to reference them. For links, the value of this attribute will be provided in the hyperlink event when listeners are notified. Images need to be registered with the text control using the matching 'href' key. This way, the control does not need to worry about loading the images - it has them in the hash table and can render them immediately.
Similar approach has been used for colors. Colors are already handled by the toolkit, so you can allocate as many as you want using a unique key and RGB values by calling toolkit.getColors().createColor(). What is left is to set all the colors referenced in the 'span' tag so that the control will be able to use them during rendering.
When the code above is executed, the view now looks like this (section has been collapsed to make room for text):
If we expand the section and the expandable composite, and make the window narrower, the form still holds and everything wraps perfectly:
Appropriate usage and limitations
Although the image above is very promising, it is very important to contain excitement. A 'prime directive' of the rich text control is:
"Form text control is not, nor will it ever be, a web browser"
It is very easy to spot limitations of the control. Tags within list items or paragraphs cannot be nested. Bold is supported, but not italic (although any font can be set and associated via 'span' tag). Attributes for vertical alignment of text with respect to images are missing. List support is decidedly low level. Although the cursor turns into the I-beam when over text, the text cannot be selected. Color of the text segments cannot be changed etc etc. There is no BIDI support.
Another important limitation to consider is performance. Form text is neither a performance nor memory hog, but it was written to be small and lean, not powerful. For that reason, it does not have sophisticated painting routines, dirty region management etc. These routines are cost-effective when the control owns the entire window and must be scalable. In contrast, form text is best suited for short snippets of tagged text that are mixed with other controls on a form. This way, the control is one of several in the parent, allowing native dirty region management from the OS toolkit to handle repaints. The best results in Eclipse forms are achieved with the mixture that is similar to our example above.
If you have a problem with the form text limitations, you should consider alternatives:
If you need editing and styled text (with different fonts and colors) will fit your bill, use StyledText (used by styled text editors like Java editor in Eclipse) If you need read-only support but have complex text formatting needs, use org.eclipse.swt.browser.Browser. Embedded web browser works on most platforms and will give you full HTML support.
What this widget is for is to provide for modest text formatting in combination with TableWrapLayout and other controls in a form. The power lies in the fact that you have full access to every control on your form - a kind of direct access that you can only achieve in a browser if you use proprietary DOM access mechanisms. In contrast, Eclipse Forms are portable - run everywhere where SWT runs.
Color And Toolkit Management
When using forms in a non-trivial way, it is important to share as much as possible to conserve resources. For this reason, color management should be separated from the toolkit when there are more than one form to handle.
Of course, it is possible to create one toolkit per form, but that is too wasteful if there are many forms. Instead:
Create one toolkit for all the forms that have the same life cycle. For example, if creating a multi-page editor, create one toolkit per editor and dispose it when editor is disposed. All the pages in the editor should share this toolkit.
Create one color manager (FormColors) per plug-in. When creating the toolkit, pass the color manager to the toolkit. The toolkit will know that the colors are shared and will not dispose them.
Use platform support for fonts and if possible, use JFaceResources predefined fonts. Between default, 'banner' and 'header' fonts, you can accomplish a lot. Using many fonts is very confusing for the user, and if you do manage your own, you must ensure alternatives across platforms. The mentioned JFace forms are guaranteed to work on all the platforms Eclipse ships on.
Dispose the color manager on plug-in shutdown (don't assume that plug-ins shutdown is platform shutdown - in Eclipse 3.0 OSGi can uninstall your plug-in dynamically).
Use form color manager to allocate all the colors needed by the forms.
Managed forms
Managed forms are wrappers that add life cycle management and notification to form members. Managed form is not a form by itself. It has a form and accepts registration of IFormPart element. For each IFormPart, it manages events like dirty state, saving, commit, focus, selection changes etc. In order to reach to the WrappedForm widget, you call 'getfForm()' method.
You should view managed forms as 'viewers' - the relationship between a form and a managed form is similar to the one between a Table widget and TableViewer in JFace, for example.
Not every control on the form needs to be a form part. It is better to group a number of controls and implement IFormPart interface for the group. Section is a natural group and Eclipse Form provides SectionPart implementation. It implements the interface and contains a Section instance (either created outside and passed into constructor, or created in the part itself).
Master/Details block
Master/Details is a pattern used throughout the UI world. It consists of a list or a tree ('master') and a set of properties ('details') driven by the selection in the master. Eclipse Forms provide the implementation of the pattern as a useful building block with the following properties:
While details part is created, master part factory method is abstract and must be implemented by the subclass
Master and details parts are children of the sash form and the ratio of the form space allocated for each can be changed by moving the sash.
Through the nature of the sash form, master and details parts can be organized horizontally or vertically in the form.
The idea of master/details block is to create a tree or a table section that fires the selection notification via the managed form. If the details part can handle the selected object, it should switch to the page for it and display properties. When building on top of the provided master/details block, subclasses should:
Create the master part (the one that drives the details)
Contribute actions to the form tool bar (consumes upper-right portion of the form in the title area)
Register details pages, one for each distinct input that can arrive from the master part
We will show how this looks in practice using the example from org.eclipse.ui.forms.examples plug-in (available in Eclipse repository). It creates a table for the master part and hooks a few dummy objects of two distinct types. Two details pages are registered to handle selections of these types. Pages can contain any number of controls - they are created on demand in the provided parent. The details area is individually scrolled.
public class ScrolledPropertiesBlock extends MasterDetailsBlock { private FormPage page; public ScrolledPropertiesBlock(FormPage page) { this.page = page; } /** * @param id * @param title */ class MasterContentProvider implements IStructuredContentProvider { public Object[] getElements(Object inputElement) { if (inputElement instanceof SimpleFormEditorInput) { SimpleFormEditorInput input = (SimpleFormEditorInput) page.getEditor().getEditorInput(); return input.getModel().getContents(); } return new Object[0]; } public void dispose() { } public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } } class MasterLabelProvider extends LabelProvider implements ITableLabelProvider { public String getColumnText(Object obj, int index) { return obj.toString(); } public Image getColumnImage(Object obj, int index) { if (obj instanceof TypeOne) { return PlatformUI.getWorkbench().getSharedImages().getImage( ISharedImages.IMG_OBJ_ELEMENT); } if (obj instanceof TypeTwo) { return PlatformUI.getWorkbench().getSharedImages().getImage( ISharedImages.IMG_OBJ_FILE); } return null; } } protected void createMasterPart(final ManagedForm managedForm, Composite parent) { final ScrolledForm form = managedForm.getForm(); FormToolkit toolkit = managedForm.getToolkit(); Section section = toolkit.createSection(parent, Section.DESCRIPTION); section.setText("Model Objects"); section.setDescription("The list contains objects from the model whose details are editable on the right"); section.marginWidth = 10; section.marginHeight = 5; toolkit.createCompositeSeparator(section); Composite client = toolkit.createComposite(section, SWT.WRAP); GridLayout layout = new GridLayout(); layout.numColumns = 2; layout.marginWidth = 0; layout.marginHeight = 0; client.setLayout(layout); Table t = toolkit.createTable(client, SWT.NULL); GridData gd = new GridData(GridData.FILL_BOTH); gd.heightHint = 20; gd.widthHint = 100; t.setLayoutData(gd); toolkit.paintBordersFor(client); Button b = toolkit.createButton(client, "Add...", SWT.PUSH); gd = new GridData(GridData.VERTICAL_ALIGN_BEGINNING); b.setLayoutData(gd); section.setClient(client); final SectionPart spart = new SectionPart(section); managedForm.addPart(spart); TableViewer viewer = new TableViewer(t); viewer.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { managedForm.fireSelectionChanged(spart, event.getSelection()); } }); viewer.setContentProvider(new MasterContentProvider()); viewer.setLabelProvider(new MasterLabelProvider()); viewer.setInput(page.getEditor().getEditorInput()); } protected void createToolBarActions(ManagedForm managedForm) { final ScrolledForm form = managedForm.getForm(); Action haction = new Action("hor", Action.AS_RADIO_BUTTON) { public void run() { sashForm.setOrientation(SWT.HORIZONTAL); form.reflow(true); } }; haction.setChecked(true); haction.setToolTipText("Horizontal orientation"); haction.setImageDescriptor(ExamplesPlugin.getDefault() .getImageRegistry() .getDescriptor(ExamplesPlugin.IMG_HORIZONTAL)); Action vaction = new Action("ver", Action.AS_RADIO_BUTTON) { public void run() { sashForm.setOrientation(SWT.VERTICAL); form.reflow(true); } }; vaction.setChecked(false); vaction.setToolTipText("Vertical orientation"); vaction.setImageDescriptor(ExamplesPlugin.getDefault() .getImageRegistry().getDescriptor(ExamplesPlugin.IMG_VERTICAL)); form.getToolBarManager().add(haction); form.getToolBarManager().add(vaction); } protected void registerPages(DetailsPart detailsPart) { detailsPart.registerPage(TypeOne.class, new TypeOneDetailsPage()); detailsPart.registerPage(TypeTwo.class, new TypeTwoDetailsPage()); }}
This part can now be used anywhere, but the example it was take from was using it inside a multi-page editor. Two distinct object types were used (TypeOne and TypeTwo), and their classes were used directly as keys (other mapping between details pages and objects are possible). Details pages registered with the detials part need to implement the following interface:
public interface IDetailsPage { void initialize(IManagedForm form); void createContents(Composite parent); void inputChanged(IStructuredSelection selection); void commit(); void setFocus(); void dispose(); boolean isDirty(); void refresh();}
The page is first initialized with the managed form to get access to the toolkit and other objects. It is then asked to create contents in the provided parent. It should also set focus on the first control when asked, be able to refresh, commit, dispose and react to the input change. As long as objects in the master part are of the compatible type, the page will stay visible and 'inputChanged' will be called.
Let's see how all this looks in practice (you can read the complete example code in the mentioned example plug-in):
An instance of TypeOne object is selected in the master part. The appropriate page for the type is loaded in the details part. Obviously the page can contain any number of controls introduced in the preceding text. The ratio of parts on the page can be changed by scrolling the sash (shown in the picture on mouse down). Also note two actions in the upper-right corner of the form - this is the form tool bar. In this case, two radio buttons were contributed to control orientation of the part (horizontal or vertical).
If we change the orientation and also select one of the TypeTwo instances, we get into the following state:
Orientation is now vertical, and a different details page is shown for the type.
It is possible to register detail page provider instead of registering pages upfront. This allows dynamic resolution of the object that has been selected.
Multi-page form editors
It was a long way to this topic, yet PDE multi-page editors were the first to spark interest in Eclipse Forms. Today, multi-page editors still represent the widest users of the technology, even though it now starts to appear in other places.
Eclipse Forms are designed to work in a widest variety of settings. For this reason, the plug-in cannot make assumptions of the types of editor inputs your editor can have. A number of concepts employed in PDE UI cannot be used here because without knowing about the editor input in more detail, it is hard to provide reusable set of classes. Nevertheless, Eclipse Forms provide the basic support for multi-page editors you can build on.
You should start building an Eclipse Forms multi-page editor by extending FormEditor:
public class SimpleFormEditor extends FormEditor { /** * */ public SimpleFormEditor() { } /* * (non-Javadoc) * @see org.eclipse.ui.forms.editor.FormEditor#createToolkit(org.eclipse.swt.widgets.Display) */ protected FormToolkit createToolkit(Display display) { // Create a toolkit that shares colors between editors. return new FormToolkit(ExamplesPlugin.getDefault().getFormColors( display)); } /* * (non-Javadoc) * * @see org.eclipse.ui.forms.editor.FormEditor#addPages() */ protected void addPages() { try { addPage(new FreeFormPage(this)); addPage(new SecondPage(this)); addPage(new ThirdPage(this)); addPage(new ScrolledPropertiesPage(this)); addPage(new PageWithSubPages(this)); } catch (PartInitException e) { // } } /* * (non-Javadoc) * * @see org.eclipse.ui.ISaveablePart#doSave(org.eclipse.core.runtime.IProgressMonitor) */ public void doSave(IProgressMonitor monitor) { } /* * (non-Javadoc) * * @see org.eclipse.ui.ISaveablePart#doSaveAs() */ public void doSaveAs() { } /* * (non-Javadoc) * * @see org.eclipse.ui.ISaveablePart#isSaveAsAllowed() */ public boolean isSaveAsAllowed() { return false; }
A very simple way to get started is to create pages and add them as above. Each page need to implement FormPage and override createFormContent(ManagedForm managedForm) method. Obviously there is a managed form already created in the page, and you should create contents in the enclosed form, and also register any form part that needs to be part of the managed life cycle.
In addition to form pages, you can add one or more text editors as a raw source alternative to the GUI pages. For this, you should call 'addPage(IEditorPart, IEditorInput input)' method in the superclass.
Multi-page editor example
Project org.eclipse.ui.forms.examples contains an example of the multi-page editor suitable for inclusion in RCP application because it does not have any dependencies on IDE plug-ins.
To run the example, customize the perspective by checking the following checkbox:
Window>Customize Perspective...>Commands>Eclipse Forms Examples
This will add a menu to the menu bar called 'Form Editors'. The menu contains one action - Simple Form Editor. The editor will open. It has the following pages:
The first page contains a page with TableWrapLayout and some sample content including FormText widget. The page content is free-form in that it will wrap and flow top to bottom.
In contrast, the second page contains 'jelly' controls (two table views) that consume all the excess space on the page. Since the controls are capable of scrolling themselves, GridLayout is used. Several PDE editor pages are constructed like this.
The third page (Flow Page) has already been show before in the section about ColumnLayout - it demonstrates how to use this layout to accommodate to the available space. Master/Details page has also been shown above.
The last page shows how to embed CTabFolder in order to add another dimension. In this particular example, the actual content is not switched - the text control stays, but its content is loaded from different objects depending on the selected tab.
Recommended practices for Eclipse Forms multi-page editors
There are many ways you can go about writing a form-based multi-page editor. It mostly depend on the type of content you are editing and proficiency of your users. There are two ways you can approach it:
If the typical users are using the editor infrequently, raw source is hard to edit by hand or complex, your users are not very technical etc., you should make COMPLETE pages that are fully capable of editing every aspect of the content without the need to turn to the raw source. In this approach, source page is there only for occasional validation, rather than for regular work. In that respect, you can get away with a basic text editor.
If your users are more technical, have no problem editing the file by hand but would appreciate some help from time to time, consider providing a mixed experience - make a good source editor with all the add-ons like incremental outline, context assist, syntax highlighting etc. In turn, add complex value-add functionality in the form pages that are hard to achieve from source. We have learned from experience that it is very hard to convince seasoned users to switch from source editing if the value-add is marginal or debatable. However, function that was only available in GUI pages and was very high-quality was used readily.
Accepting that users will switch pages frequently requires a good model of the underlying content. Model should be directly tied to the underlying document(s) so that it is in sync both when users type in text directly and when change it structurally through the GUI pages.