com.jgoodies.forms.builder.ListViewBuilder Maven / Gradle / Ivy
/*
* Copyright (c) 2002-2015 JGoodies Software GmbH. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Software GmbH nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jgoodies.forms.builder;
import static com.jgoodies.common.base.Preconditions.checkNotNull;
import static com.jgoodies.common.base.Preconditions.checkState;
import static com.jgoodies.common.internal.Messages.MUST_NOT_BE_BLANK;
import static com.jgoodies.common.internal.Messages.MUST_NOT_BE_NULL;
import java.awt.Component;
import java.awt.FocusTraversalPolicy;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import com.jgoodies.common.base.Strings;
import com.jgoodies.forms.FormsSetup;
import com.jgoodies.forms.factories.ComponentFactory;
import com.jgoodies.forms.factories.Forms;
import com.jgoodies.forms.factories.Paddings;
import com.jgoodies.forms.internal.InternalFocusSetupUtils;
import com.jgoodies.forms.util.FocusTraversalType;
/**
* Builds list/table views from a set of mandatory and optional components:
* label, filter/search,
* list (table),
* list buttons, list extras,
* details view (or preview).
*
* Examples:
* return ListViewBuilder.create()
* .labelText("&Contacts:")
* .listView(contactsTable)
* .listBar(newButton, editButton, deleteButton)
* .build();
*
* return ListViewBuilder.create()
* .padding(Paddings.DLU14)
* .label(contactsLabel)
* .filterView(contactsSearchField)
* .listView(contactsTable)
* .listBar(newButton, editButton, deleteButton, null, printButton)
* .detailsView(contactDetailsView)
* .build();
*
* For more examples see the JGoodies Showcase application.
*
* @author Karsten Lentzsch
*
* @since 1.9
*/
public final class ListViewBuilder {
private ComponentFactory factory;
private JComponent label;
private JComponent filterView;
private JComponent listView;
private JComponent listBarView;
private JComponent listExtrasView;
private JComponent detailsView;
private JComponent listStackView;
private Border border;
private boolean honorsVisibility = true;
private Component initialComponent;
private FocusTraversalType focusTraversalType;
private FocusTraversalPolicy focusTraversalPolicy;
private String namePrefix = "ListView";
private String filterViewColSpec = "[100dlu, p]";
private String listViewRowSpec = "fill:[100dlu, d]:grow";
/**
* Holds the panel that has been lazily built in {@code #buildPanel}.
*/
private JComponent panel;
// Instance Creation ******************************************************
/**
* Constructs a ListViewBuilder using the global default component factory.
* The factory is required by {@link #labelText(String, Object...)} and
* {@link #headerText(String, Object...)}.
*/
private ListViewBuilder() {
// Do nothing.
}
/**
* Creates and returns a ListViewBuilder using the global default
* component factory.
* The factory is required by {@link #labelText(String, Object...)} and
* {@link #headerText(String, Object...)}.
*
* @return the ListViewBuilder
*/
public static ListViewBuilder create() {
return new ListViewBuilder();
}
// API ********************************************************************
/**
* Sets an optional border that surrounds the list view including
* the label and details.
*
* @param border the border to set
*/
public ListViewBuilder border(Border border) {
this.border = border;
invalidatePanel();
return this;
}
/**
* Sets an optional padding (an empty border) that surrounds the list view
* including the label and details.
*
* @param padding the white space to use around the list view panel
*
* @since 1.9
*/
public ListViewBuilder padding(EmptyBorder padding) {
border(padding);
return this;
}
/**
* Sets the panel's padding as an EmptyBorder using the given specification
* for the top, left, bottom, right margins in DLU. For example
* "1dlu, 2dlu, 3dlu, 4dlu" sets an empty border with 1dlu in the top,
* 2dlu in the left side, 3dlu at the bottom, and 4dlu in the right hand
* side.
*
* Equivalent to {@code padding(Paddings.createPadding(paddingSpec, args))}.
*
* @param paddingSpec describes the top, left, bottom, right margins
* of the padding (an EmptyBorder) to use
* @param args optional format arguments,
* used if {@code paddingSpec} is a format string
* @return a reference to this builder
*
* @see #padding(EmptyBorder)
* @see Paddings#createPadding(String, Object...)
*
* @since 1.9
*/
public ListViewBuilder padding(String paddingSpec, Object... args) {
padding(Paddings.createPadding(paddingSpec, args));
return this;
}
/**
* Sets the component that shall receive the focus if this panel's
* parent is made visible the first time.
*
* @param initialComponent the component that shall receive the focus
* if the panel is made visible the first time
* @return a reference to this builder
*
* @see #focusTraversalType(FocusTraversalType)
*/
public ListViewBuilder initialComponent(JComponent initialComponent) {
checkNotNull(initialComponent, MUST_NOT_BE_NULL, "initial component");
checkState(this.initialComponent == null,
"The initial component must be set once only.");
checkValidFocusTraversalSetup();
this.initialComponent = initialComponent;
return this;
}
/**
*
* @param focusTraversalType either: layout or container order
* @return a reference to this builder
*
* @see #initialComponent(JComponent)
*/
public ListViewBuilder focusTraversalType(FocusTraversalType focusTraversalType) {
checkNotNull(focusTraversalType, MUST_NOT_BE_NULL, "focus traversal type");
checkState(this.focusTraversalType == null,
"The focus traversal type must be set once only.");
checkValidFocusTraversalSetup();
this.focusTraversalType = focusTraversalType;
return this;
}
/**
* Sets the panel's focus traversal policy and sets the panel
* as focus traversal policy provider. Hence, this call is equivalent to:
*
* builder.getPanel().setFocusTraversalPolicy(policy);
* builder.getPanel().setFocusTraversalPolicyProvider(true);
*
*
* @param policy the focus traversal policy that will manage
* keyboard traversal of the children in this builder's panel
* @return a reference to this builder
*
* @see JComponent#setFocusTraversalPolicy(FocusTraversalPolicy)
* @see JComponent#setFocusTraversalPolicyProvider(boolean)
*/
public ListViewBuilder focusTraversalPolicy(FocusTraversalPolicy policy) {
checkNotNull(policy, MUST_NOT_BE_NULL, "focus traversal policy");
checkState(this.focusTraversalPolicy == null,
"The focus traversal policy must be set once only.");
checkValidFocusTraversalSetup();
this.focusTraversalPolicy = policy;
return this;
}
/**
* Specifies whether invisible components shall be taken into account by
* this builder for computing the layout size and setting component bounds.
* If set to {@code true} invisible components will be ignored by
* the layout. If set to {@code false} components will be taken into
* account regardless of their visibility. Visible components are always
* used for sizing and positioning.
*
* The default value for this setting is {@code true}.
* It is useful to set the value to {@code false} (in other words
* to ignore the visibility) if you switch the component visibility
* dynamically and want the container to retain the size and
* component positions.
*
* A typical use case for ignoring the visibility is here:
* if the list selection is empty, the details view is made invisible
* to hide the then obsolete read-only labels. If visibility is honored,
* the list view would grow and shrink on list selection. If ignored,
* the layout remains stable.
*
* @param b {@code true} to honor the visibility, i.e. to exclude
* invisible components from the sizing and positioning,
* {@code false} to ignore the visibility, in other words to
* layout visible and invisible components
*/
public ListViewBuilder honorVisibility(boolean b) {
this.honorsVisibility = b;
invalidatePanel();
return this;
}
/**
* Sets the prefix that is prepended to the component name of components
* that have no name set or that are are implicitly created by this builder,
* e.g. the (header) label. The default name prefix is "ListView".
* @param namePrefix the prefix to be used
* @return a reference to this builder
*/
public ListViewBuilder namePrefix(String namePrefix) {
this.namePrefix = namePrefix;
return this;
}
/**
* Sets {@code factory} as this builder's new component factory
* that is used to create the label or header components.
* If not called, the default factory will be used
* that can be configured via
* {@link FormsSetup#setComponentFactoryDefault(ComponentFactory)}.
*
* @param factory the factory to be used to create the header or label
* @return a reference to this builder
*/
public ListViewBuilder factory(ComponentFactory factory) {
this.factory = factory;
return this;
}
/**
* Sets the mandatory label view. Useful to set a bound label that updates
* its text when the list content changes, for example to provide the
* number of list elements.
*
* @param labelView the component that shall label the list view,
* often a bound label
* @return a reference to this builder
*/
public ListViewBuilder label(JComponent labelView) {
this.label = labelView;
overrideNameIfBlank(labelView, "label");
invalidatePanel();
return this;
}
/**
* Creates a plain label for the given marked text and sets it as label view.
* If no arguments are provided, the plain String is used.
* Otherwise the string will be formatted using {@code String.format}
* with the given arguments.
* Equivalent to:
*
* label(aComponentFactory.createLabel(Strings.get(markedText, args)));
*
*
* @param markedText the label's text, may contain a mnemonic marker
* @param args optional format arguments forwarded to {@code String#format}
* @return a reference to this builder
*
* @see String#format(String, Object...)
*/
public ListViewBuilder labelText(String markedText, Object... args) {
label(getFactory().createLabel(Strings.get(markedText, args)));
return this;
}
/**
* Creates a header label for the given marked text and sets it as label view.
* If no arguments are provided, the plain String is used.
* Otherwise the string will be formatted using {@code String.format}
* with the given arguments.
* Equivalent to:
*
* labelView(aComponentFactory.createHeaderLabel(Strings.get(markedText, args)));
*
*
* @param markedText the label's text, may contain a mnemonic marker
* @param args optional format arguments forwarded to {@code String#format}
* @return a reference to this builder
*
* @see String#format(String, Object...)
*/
public ListViewBuilder headerText(String markedText, Object... args) {
label(getFactory().createHeaderLabel(Strings.get(markedText, args)));
return this;
}
/**
* Sets an optional view that will be placed in the upper right corner
* of the built list view panel. This can be a search field, a panel
* with filtering check boxes ("Only valid items"), etc.
*
* @param filterView the view to be added.
* @return a reference to this builder
*/
public ListViewBuilder filterView(JComponent filterView) {
this.filterView = filterView;
overrideNameIfBlank(filterView, "filter");
invalidatePanel();
return this;
}
/**
* Changes the FormLayout column specification used to lay out
* the filter view.
* The default value is {@code "[100dlu, p]"}, which is a column where
* the width is determined by the filter view's preferred width,
* but a minimum width of 100dlu is ensured. The filter view won't grow
* horizontally, if the container gets more space.
*
* @param colSpec specifies the horizontal layout of the filter view
* @param args optional {@code colSpec} format arguments
* forwarded to {@code String#format}
* @return a reference to this builder
*
* @throws NullPointerException if {@code colSpec} is {@code null}
*/
public ListViewBuilder filterViewColumn(String colSpec, Object... args) {
checkNotNull(colSpec, MUST_NOT_BE_BLANK, "filter view column specification");
this.filterViewColSpec = Strings.get(colSpec, args);
invalidatePanel();
return this;
}
/**
* Sets the given component as the the mandatory list view.
* If {@code listView} is a JTable, JList, or JTree, it is
* automatically wrapped with a JScrollPane, before the scroll pane
* is set as list view.
*
* @param listView the component to be used as scrollable list view
* @return a reference to this builder
*
* @throws NullPointerException if {@code listView} is {@code null}
*/
public ListViewBuilder listView(JComponent listView) {
checkNotNull(listView, MUST_NOT_BE_BLANK, "list view");
if (listView instanceof JTable || listView instanceof JList || listView instanceof JTree) {
this.listView = new JScrollPane(listView);
} else {
this.listView = listView;
}
overrideNameIfBlank(listView, "listView");
invalidatePanel();
return this;
}
/**
* Changes the FormLayout row specification used to lay out the list view.
* The default value is {@code "fill:[100dlu, pref]:grow"}, which is a row
* that is filled by the list view; the height is determined
* by the list view's preferred height, but a minimum of 100dlu is ensured.
* The list view grows vertically, if the container gets more vertical
* space.
*
* Examples:
*
* .listViewRow("fill:100dlu"); // fixed height
* .listViewRow("f:100dlu:g"); // fixed start height, grows
* .listViewRow("f:p"); // no minimum height
*
*
* @param rowSpec specifies the vertical layout of the list view
* @param args optional {@code rowSpec} format arguments
* forwarded to {@code String#format}
* @return a reference to this builder
*
* @throws NullPointerException if {@code rowSpec} is {@code null}
*/
public ListViewBuilder listViewRow(String rowSpec, Object... args) {
checkNotNull(rowSpec, MUST_NOT_BE_BLANK, "list view row specification");
this.listViewRowSpec = Strings.get(rowSpec, args);
invalidatePanel();
return this;
}
/**
* Sets an optional list bar - often a button bar -
* that will be located in the lower left corner of the list view.
* If the list bar view consists only of buttons,
* use {@link #listBar(JComponent...)} instead.
*
* @param listBarView the component to set
* @return a reference to this builder
*/
public ListViewBuilder listBarView(JComponent listBarView) {
this.listBarView = listBarView;
overrideNameIfBlank(listBarView, "listBarView");
invalidatePanel();
return this;
}
/**
* Builds a button bar using the given buttons and sets it as list bar.
* Although JButtons are expected, any JComponent is accepted
* to allow custom button component types.
*
* Equivalent to {@code listBarView(Forms.buttonBar(buttons))}.
*
* @param buttons the buttons in the list bar
* @return a reference to this builder
*
* @throws NullPointerException if {@code buttons} is {@code null}
* @throws IllegalArgumentException if no buttons are provided
*
* @see Forms#buttonBar(JComponent...)
*/
public ListViewBuilder listBar(JComponent... buttons) {
listBarView(Forms.buttonBar(buttons));
return this;
}
/**
* Sets an optional list stack - often a stack of buttons -
* that will be located on the right-hand side of the list view.
* If the list stack view consists only of buttons,
* use {@link #listStack(JComponent...)} instead.
*
* @param listStackView the component to set
* @return a reference to this builder
*/
public ListViewBuilder listStackView(JComponent listStackView) {
this.listStackView = listStackView;
overrideNameIfBlank(listStackView, "listStackView");
invalidatePanel();
return this;
}
/**
* Builds a button stack using the given buttons and sets it as list stack.
* Although JButtons are expected, any JComponent is accepted
* to allow custom button component types.
*
* Equivalent to {@code listStackView(Forms.buttonStack(buttons))}.
*
* @param buttons the buttons in the list stack
* @return a reference to this builder
*
* @throws NullPointerException if {@code buttons} is {@code null}
* @throws IllegalArgumentException if no buttons are provided
*
* @see Forms#buttonStack(JComponent...)
*/
public ListViewBuilder listStack(JComponent... buttons) {
listStackView(Forms.buttonStack(buttons));
return this;
}
/**
* Sets an optional view that is located in the lower right corner
* of the list view, aligned with the list bar.
*
* @param listExtrasView the component to set
* @return a reference to this builder
*/
public ListViewBuilder listExtrasView(JComponent listExtrasView) {
this.listExtrasView = listExtrasView;
overrideNameIfBlank(listExtrasView, "listExtrasView");
invalidatePanel();
return this;
}
/**
* Sets an optional details view that is located under the list view.
* Often this is the details view or preview of a master-details view.
*
* @param detailsView the component to set
* @return a reference to this builder
*/
public ListViewBuilder detailsView(JComponent detailsView) {
this.detailsView = detailsView;
overrideNameIfBlank(detailsView, "detailsView");
invalidatePanel();
return this;
}
/**
* Lazily builds and returns the list view panel.
*
* @return the built panel
*/
public JComponent build() {
if (panel == null) {
panel = buildPanel();
}
return panel;
}
// Implementation *********************************************************
private ComponentFactory getFactory() {
if (factory == null) {
factory = FormsSetup.getComponentFactoryDefault();
}
return factory;
}
private void invalidatePanel() {
panel = null;
}
private JComponent buildPanel() {
checkNotNull(listView, "The list view must be set before #build is invoked.");
String stackGap = hasStack() ? "$rg" : "0";
String detailsGap = hasDetails() ? "14dlu" : "0";
FormBuilder builder = FormBuilder.create()
.columns("fill:default:grow, %s, p", stackGap)
.rows("p, %1$s, p, %2$s, p", listViewRowSpec, detailsGap)
.honorsVisibility(honorsVisibility)
.border(border)
.add(hasHeader(), buildHeader()) .xy(1, 1)
.add(true, listView) .xy(1, 2)
.add(hasOperations(), buildOperations()).xy(1, 3)
.add(hasStack(), listStackView) .xy(3, 2)
.add(hasDetails(), detailsView) .xy(1, 5);
// Set up the label-for relation - if not already set.
if (label instanceof JLabel) {
JLabel theLabel = (JLabel) label;
if (theLabel.getLabelFor() == null) {
theLabel.setLabelFor(listView);
}
}
InternalFocusSetupUtils.setupFocusTraversalPolicyAndProvider(
builder.getPanel(),
focusTraversalPolicy,
focusTraversalType,
initialComponent);
return builder.build();
}
private JComponent buildHeader() {
if (!hasHeader()) {
return null;
}
String columnSpec = hasFilter()
? "default:grow, 9dlu, %s"
: "default:grow, 0, 0";
return FormBuilder.create()
.columns(columnSpec, filterViewColSpec)
.rows("[14dlu, p], $lcg")
.labelForFeatureEnabled(false)
.add(hasLabel(), label) .xy(1, 1)
.add(hasFilter(), filterView).xy(3, 1)
.build();
}
private JComponent buildOperations() {
if (!hasOperations()) {
return null;
}
String gap = hasListExtras() ? "9dlu" : "0";
return FormBuilder.create()
.columns("left:default, %s:grow, right:pref", gap)
.rows("$rgap, p")
.honorsVisibility(honorsVisibility)
.add(hasListBar(), listBarView) .xy(1, 2)
.add(hasListExtras(), listExtrasView).xy(3, 2)
.build();
}
// Helper Code ************************************************************
private boolean hasLabel() {
return label != null;
}
private boolean hasFilter() {
return filterView != null;
}
private boolean hasHeader() {
return hasLabel() || hasFilter();
}
private boolean hasListBar() {
return listBarView != null;
}
private boolean hasListExtras() {
return listExtrasView != null;
}
private boolean hasOperations() {
return hasListBar() || hasListExtras();
}
private boolean hasStack() {
return listStackView != null;
}
private boolean hasDetails() {
return detailsView != null;
}
private void overrideNameIfBlank(JComponent component, String suffix) {
if (component != null && Strings.isBlank(component.getName())) {
component.setName(namePrefix + '.' + suffix);
}
}
/**
* Checks that if the API user has set a focus traversal policy,
* no focus traversal type and no initial component has been set.
*/
private void checkValidFocusTraversalSetup() {
InternalFocusSetupUtils.checkValidFocusTraversalSetup(
focusTraversalPolicy, focusTraversalType, initialComponent);
}
}