The DND viewer basically displays a set of View objects, where each view is render its assigned Content object. A content is something like a message, a service object, a domain object or a field within a domain object. If a view is to show more than one thing (eg the contents of a list, or the properties of an object) then the view must provide a set of subviews and arrange those views is some orderly fashion. The viewer provides AbstractCompositeView for this purpose. A further subclass, CompositeViewBuilder, provides an even better mechanism by allowing a ViewBuilder to be assigned to create and add the subviews, while an AbstractBuilderDecorator can be assigned to layout the subviews. Many builders and layouts are provided that can be combined to create specific views.
In addtion to this creation process the views themselves can be decorated to provide specific behaviour and looks.
Each available view is created via a ViewSpecification instance. An instance of each specification is registered with the view factory, either directly (via the ViewFactory instance) or indirectly via the configuration file. To register a view specification at runtime add something like the following to the nakedobjects.properties, or other properties, file.
nakedobjects.viewer.dnd.specification.view=org.nakedobjects.viewer.dnd.view.calendar.CalendarSpecification, \
org.nakedobjects.viewer.dnd.view.form.WindowExpandableFormSpecification
Each specification determines if it can display a particular object, by returning true/false from the canDisplay(Content) method.
The name of the specification, provided via the getName method, is used to create a list of available views that the user can select from.
An open view (isOpen returning true) indicates that the view shows content of the object, as opposed to just the object itself.
A subview (isSubview returning true) indicates that the view can be used as a child part of another view. Otherwise the view is a root view, standing by itself.
A view that can be replaced by another view within the screen area is indicated by isReplaceable returning true.
TODO what is isAligned?
Displayed objects are rendered using an instance of View. View classes are typically created by subclassing AbstractView.
Views are designed so that the can be decorated to add looks and behaviour. Such decorators are typically created by subclassing AbstractViewDecorator.
Views can request a specific area to render themselves within. The parent view, starting from the top-level workspace, asks each component view by calling the view's getRequiredSize method. The parent passes in the maximum size so that component can make the most of the available space.
Each view provides a layout mechanism (via the layout(Size) method) that should size and position each of its subviews. This is not applicable to node views, which by definition have no children. The size passed in the amount of space available to the view. Before the layout method is called the getRequiredSize method is called to determine the optimum size to provide for the view. The laying out of a view should only occur when the contents have changed so a flag should be maintained to track this. The invalidateLayout method should set this flag so the the viewer can indicate when a view needs to be re-laid out. It should also pass up the request to ensure that any work done by the superclass will also be completed. The example below shows a typical layout scenario.
public void invalidateLayout() {
super.invalidateLayout();
invalidLayout = true;
}
public void layout(final Size maximumSize) {
if (invalidLayout) {
Size formSize = form.getRequiredSize(maximumSize);
form.setSize(formSize);
form.layout(maximumSize);
form.setLocation(new Location(0,0));
Size collectionSize = collection.getRequiredSize(maximumSize);
collection.setSize(collectionSize);
collection.layout(maximumSize);
collection.setLocation(new Location(0, formSize.getHeight()));
invalidLayout = false;
}
}
The parent asks each component to draw itself by calling the view's draw method. A Canvas object is passed in that is then used to do the actual drawing.
A view can also provide a print method for drawing to a printing surface. In the AbstractView class this simply delegates to the draw method to allow the one method to render both to screen and paper.
A view can be given a border by chaining a WindowBorder decorator to a view as follows, indicating if the window is scrollable with the second parameter.
new WindowBorder(new SimpleView(content, axis), false);
The style of the border can be changed by providing a BorderDrawing class that is used for all instances.
Although you can create your own subclass of View that contain other views, it can be a lot simpler to extend the AbstractCompositeView class or to use the CompositeViewBuilder class in conjunction with some ViewBuilder objects to build and layout a view. The bulk of the work in a composite view, however, is in passing the events etc on to the right subview, and this is something the AbstractCompositeView view does for you.
For a view that you want to construct and layout extend the AbstractCompositeView class and add methods to build the view, determine its required size and layout its components. The following example shows a view for summarising an object. The buildView method calls addView for each of the component view, which in this example are the default views for each field object. The getMaximumSize method add the heights of all the component views togther and finds the maximum width. The doLayout method simply sets the size of each component to its required size and sets the vertical position so they are stacked vertically.
public class SummaryView extends AbstractCompositeView {
public SummaryView(Content content, ViewSpecification specification, ViewAxis viewAxis) {
super(content, specification, viewAxis);
}
protected void buildView() {
NakedObjectSpecification spec = getContent().getSpecification();
AuthenticationSession session = NakedObjectsContext.getAuthenticationSession();
NakedObject target = getContent().getNaked();
NakedObjectAssociation[] fields = spec.getAssociations(NakedObjectAssociationFilters.dynamicallyVisible(session, target));
for (int i = 0; i < fields.length; i++) {
Content fieldContent = Toolkit.getContentFactory().createFieldContent(fields[i], target, fields[i].get(target));
if (fieldContent instanceof TextParseableContent) {
View view = Toolkit.getViewFactory().createFieldView((TextParseableField) fieldContent, null);
addView(view);
}
if (fieldContent instanceof ObjectContent) {
View view = Toolkit.getViewFactory().createView(new ViewRequirement(fieldContent, ViewRequirement.CLOSED));
addView(view);
}
}
}
public Size getMaximumSize() {
Size size = new Size(0, 0);
for (View view : getSubviews()) {
Size viewSize = view.getMaximumSize();
size.extendHeight(viewSize.getHeight());
size.ensureWidth(viewSize.getWidth());
}
return size;
}
protected void doLayout(Size maximumSize) {
int y = 0;
for (View view : getSubviews()) {
Size viewSize = view.getMaximumSize();
view.setSize(viewSize);
view.layout(maximumSize);
view.setLocation(new Location(0, y));
y += viewSize.getHeight();
}
}
public String toString() {
return "Form" + getId();
}
}
The following example shows this
public class MyListSpecification extends AbstractCompositeViewSpecification {
public MyListSpecification() {
SubviewSpec subviewSpec = new SubviewSpec() {
public View createSubview(final Content content, final ViewAxis axis) {
return new InternalFormSpecification().createView(content, axis);
}
public View decorateSubview(final View subview) {
return new EmptyBorder(3, new BackgroundBorder(new LineBorder(1, 8, new IconBorder(subview))));
}
};
builder = new StackLayout(new CollectionElementBuilder(subviewSpec));
}
public String getName() {
return "My List";
}
public boolean canDisplay(final Content content) {
return content.isCollection();
}
}
A panel with a set of closed objects on one side and an opened object on the other side can be created using the MasterDetailPanel class. When the master view is created a SelectableViewAxis is created and passed to each item so that it can indicate that is selected and tell the main view to show that object on the detail side. For this to work the item view must detect the SelectableViewAxis and use it to set and show that item's selected state. This is currently provided TreeNodeBorder and by the SubviewIconSpecification, which adds a SelectedBorder decorator to the icon.
The detail side is initially created with BlankView object. This is then replaced by a view created for the object that is selected via the SelectableViewAxis.
If separate views need to be related, for example for their sizes to be made consistent or find out a maximum value, then an ViewAxis object should be created and passed to each subview.
Borders decorate views by adding behaviour and content to their edges. Border are used at all levels to distinguish and control windows, objects and fields. Borders are decorators, so are added like this, which gives 3 pixels of empty space, a default white background, a rounded rectangle and an object icon :
new EmptyBorder(3, new BackgroundBorder(new LineBorder(1, 8, new IconBorder(subview))));
Windows can be created by decorating a view with a WindowBorder or DialogBorder.
Control buttons can be added to views (as done for dialogs) using a ButtonBorder.
Simple decorative borders include: BackgroundBorder, LineBorder, EmptyBorder and LabelBorder.
Views are typically fixed size by design so that a view always, where possible, shows it entire content. A view can be made resizable by adding a ViewResizeBorder decorator, which will provide a drag handle to resize the view with.
view = new ViewResizeBorder(view);
Menu items are added by creating UserAction objects and UserActionSet for submenus. The UserActionAbstract class provides the basic behaviour for menu items.
All user events - keyboard and mouse events - are routed through InteractionHandler.
If a view, and particularly a border, has it contents changed so it is wrapping a different object, then it must reset the view property via a call to the setView method as shown below. This example is from a border that replaces an icon with a form so initially we
ExpanderBorder --> ObjectBorder --> Icon
and after the first click we get
ExpanderBorder --> IconBorder --> CompositeView
where the border and composite are newly created. Because the form was created without reference to the ExpanderBorder it has the view property set to reference the IconBorder. Call setView as below corrects this so that view refers to the ExpanderBorder instead.
View parent = wrappedView.getParent();
getViewManager().removeFromNotificationList(wrappedView);
if (isOpen) {
wrappedView = new InternalFormSpecification().createView(getContent(), null);
} else {
ViewRequirement requirement = new ViewRequirement(getContent(), ViewRequirement.CLOSED);
wrappedView = Toolkit.getViewFactory().createView(requirement );
}
setView(this);
setParent(parent);
Image busyImage = ImageFactory.getInstance().loadIcon("busy", 16, null);