/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.ui.swt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Table;
import org.eclipse.riena.ui.common.IComplexComponent;
import org.eclipse.riena.ui.ridgets.IMasterDetailsRidget;
import org.eclipse.riena.ui.swt.facades.SWTFacade;
import org.eclipse.riena.ui.swt.layout.DpiGridLayout;
import org.eclipse.riena.ui.swt.layout.DpiGridLayoutFactory;
import org.eclipse.riena.ui.swt.lnf.LnfKeyConstants;
import org.eclipse.riena.ui.swt.lnf.LnfManager;
import org.eclipse.riena.ui.swt.nls.Messages;
import org.eclipse.riena.ui.swt.utils.SWTBindingPropertyLocator;
import org.eclipse.riena.ui.swt.utils.SWTControlFinder;
import org.eclipse.riena.ui.swt.utils.UIControlsFactory;
/**
* This composite contains a table-like widget (the "master") of n columns, as well as new, remove and update buttons. It also contains an arbitrary composite
* (the "details"), which is updated automatically when the selected row in the table changes.
* <p>
* Subclasses must override the {@link #createDetails(Composite)} method, to populate the details composite with additional widgets. Widgets in the details
* composite that should be bound to ridgets, must be registered by invoking the {@link #addUIControl(Object, String)} method.
*
* @see IMasterDetailsRidget
*
* @since 1.2
*/
public abstract class AbstractMasterDetailsComposite extends Composite implements IComplexComponent {
/**
* Binding id of the table control {@value} .
*/
public static final String BIND_ID_TABLE = "mdTable"; //$NON-NLS-1$
/**
* Binding id of the new button {@value} .
*/
public static final String BIND_ID_NEW = "mdNewButton"; //$NON-NLS-1$
/**
* Binding id of the remove button {@value} .
*/
public static final String BIND_ID_REMOVE = "mdRemoveButton"; //$NON-NLS-1$
/**
* Binding id of the apply button {@value} .
*/
public static final String BIND_ID_APPLY = "mdApplyButton"; //$NON-NLS-1$
private final List<Object> controls = new ArrayList<Object>();
private Control table;
private Composite details;
private final Composite master;
private Composite buttonComposite;
/**
* Create an instance of MasterDetailsComposite with the details area at the top or bottom.
*
* @param parent
* the parent Composite; not null
* @param style
* the style bits; values are restricted to those supported by {@link Composite}
* @param orientation
* SWT.TOP or SWT.BOTTOM, to create the details area at the top or bottom part of the composite
*/
public AbstractMasterDetailsComposite(final Composite parent, final int style, final int orientation) {
super(parent, style);
checkOrientation(orientation);
if (LnfManager.getLnf().useDpiGridLayout()) {
setLayout(DpiGridLayoutFactory.swtDefaults().margins(0, 0).spacing(0, 5).create());
} else {
setLayout(GridLayoutFactory.swtDefaults().margins(0, 0).spacing(0, 5).create());
}
if (orientation == SWT.TOP) {
details = createComposite(getDetailsStyle());
createDetails(details);
}
master = createComposite(getMasterStyle());
createMaster(master);
if (orientation == SWT.BOTTOM) {
details = createComposite(getDetailsStyle());
createDetails(details);
}
setBackground(LnfManager.getLnf().getColor(LnfKeyConstants.SUB_MODULE_BACKGROUND));
}
/**
* Add a control to the list of 'bound' controls. These controls will be bound to ridgets by the framework.
*
* @param uiControl
* the UI control to bind; never null
* @param bindingId
* a non-empty non-null bindind id for the control. Must be unique within this composite
* @see #getUIControls()
*/
public final void addUIControl(final Object uiControl, final String bindingId) {
Assert.isNotNull(uiControl);
Assert.isNotNull(bindingId);
controls.add(uiControl);
SWTBindingPropertyLocator.getInstance().setBindingProperty(uiControl, bindingId);
}
/**
* Asks the user to confirm discarding a dirty "details" area.
* <p>
* This implementation will show a blocking dialog. Subclasses may overwrite this method without calling super, to change the standard behavior. Examples:
* (a) opening a customized dialog, (b) returning true without asking the user.
*
* @return true to discard changes, false to keep changes
* @since 1.2
*/
public boolean confirmDiscardChanges() {
final String title = Messages.MasterDetailsComposite_dialogMessage_confirmDiscard;
final String message = Messages.MasterDetailsComposite_dialogTitle_confirmDiscard;
final boolean result = RienaMessageDialog.openQuestion(getShell(), title, message);
return result;
}
/**
* Ask the user to confirm removal of the selected entry.
* <p>
* The default implementation always returns true. Subclasses may override.
*
* @param item
* the item to be removed. Can never be null.
* @return true to remove the selected entry, false otherwise.
* @since 2.0
*/
public boolean confirmRemove(final Object item) {
return true;
}
/**
* Return the 'Apply' control.
*
* @return a Button or ImageButton instance; may be null if 'Apply' is unsupported.
*/
public final Control getButtonApply() {
final Control result = getUIControl(BIND_ID_APPLY);
checkButton(result, false);
return result;
}
/**
* Return the 'New' control.
*
* @return a Button or ImageButton instance; may be null if 'New' is unsupported.
*/
public final Control getButtonNew() {
final Control result = getUIControl(BIND_ID_NEW);
checkButton(result, true);
return result;
}
/**
* Return the 'Remove' control.
*
* @return a Button or ImageButton instance; may be null if 'Remove' is unsupported.
*/
public final Control getButtonRemove() {
final Control result = getUIControl(BIND_ID_REMOVE);
checkButton(result, true);
return result;
}
/**
* Returns the 'details' composite.
*
* @return a Composite; never null
*/
public final Composite getDetails() {
return details;
}
/**
* Return the margins, in pixels, for the top/bottom, left/right edges of the widget.
*
* @return a Point; never null. The x value corresponds to the top/bottom margin. The y value corresponds to the left/right margin.
* @since 2.0
*/
public Point getMargins() {
if (getLayout() instanceof DpiGridLayout) {
final DpiGridLayout layout = (DpiGridLayout) getLayout();
final Point result = new Point(layout.marginHeight, layout.marginWidth);
return result;
} else {
final GridLayout layout = (GridLayout) getLayout();
final Point result = new Point(layout.marginHeight, layout.marginWidth);
return result;
}
}
/**
* Return the spacing, in pixels, for the right/left and top/bottom edgets of the cells within the widget.
*
* @return a Point; never null. The x value corresponds to the right/left spacing. The y value corresponds to the top/bottom spacing.
* @since 2.0
*/
public Point getSpacing() {
if (getLayout() instanceof DpiGridLayout) {
final DpiGridLayout layout = (DpiGridLayout) getLayout();
final DpiGridLayout mLayout = (DpiGridLayout) master.getLayout();
final Point result = new Point(mLayout.horizontalSpacing, layout.verticalSpacing);
return result;
} else {
final GridLayout layout = (GridLayout) getLayout();
final GridLayout mLayout = (GridLayout) master.getLayout();
final Point result = new Point(mLayout.horizontalSpacing, layout.verticalSpacing);
return result;
}
}
/**
* Returns the Table control of the 'master' area.
*
* @return a Table; never null
*/
public Control getTable() {
return table;
}
public final List<Object> getUIControls() {
registerControls(details);
registerControls(buttonComposite);
return Collections.unmodifiableList(controls);
}
private void registerControls(final Composite cmp) {
if (null == cmp) {
return;
}
final SWTControlFinder finder = new SWTControlFinder(cmp) {
@Override
public boolean skip(final Control control) {
return controls.contains(control);
}
@Override
public void handleBoundControl(final Control control, final String bindingProperty) {
controls.add(control);
}
};
finder.run();
}
/**
* @since 1.2
*/
@Override
public void setBackground(final Color color) {
master.setBackground(color);
details.setBackground(color);
super.setBackground(color);
}
/**
* Sets the margin for the top/bottom, left/right edges of the widget.
*
* @param marginHeight
* the margin, in pixels, that will be placed along the top and bottom edges of the widget. The default value is 0.
* @param marginWidth
* the margin, in pixels, that will be placed along the left and right edges of the widget. The default value is 0.
* @since 2.0
*/
public void setMargins(final int marginHeight, final int marginWidth) {
if (getLayout() instanceof DpiGridLayout) {
final DpiGridLayout layout = (DpiGridLayout) getLayout();
layout.marginHeight = marginHeight;
layout.marginWidth = marginWidth;
layout(false);
} else {
final GridLayout layout = (GridLayout) getLayout();
layout.marginHeight = marginHeight;
layout.marginWidth = marginWidth;
layout(false);
}
}
/**
* Sets the spacing for the right/left and top/bottom edges of the cells within the widget.
*
* @param hSpacing
* the space, in pixels, between the right edge of a cell and the left edge of the cell to the left. The default value is 0.
* @param vSpacing
* the space, in pixels, between the bottom edge of a cell and the top edge of the cell underneath. The default value is 5.
* @since 2.0
*/
public void setSpacing(final int hSpacing, final int vSpacing) {
if (getLayout() instanceof DpiGridLayout) {
final DpiGridLayout layout = (DpiGridLayout) getLayout();
layout.verticalSpacing = vSpacing;
final DpiGridLayout mLayout = (DpiGridLayout) master.getLayout();
mLayout.horizontalSpacing = hSpacing;
layout(false);
} else {
final GridLayout layout = (GridLayout) getLayout();
layout.verticalSpacing = vSpacing;
final GridLayout mLayout = (GridLayout) master.getLayout();
mLayout.horizontalSpacing = hSpacing;
layout(false);
}
}
/**
* Informs the user that apply failed for the given {@code reason}.
* <p>
* This implementation will show a blocking dialog. Subclasses may overwrite this method without calling super, to change the standard behavior.
*
* @param reason
* A string describing why apply failed; never null
*
* @since 1.2
*/
public void warnApplyFailed(final String reason) {
Assert.isNotNull(reason);
final String title = Messages.MasterDetailsComposite_dialogTitle_applyFailed;
RienaMessageDialog.openWarning(getShell(), title, reason);
}
/**
* Informs the user that removal failed for the given {@code reason}.
* <p>
* This implementation will show a blocking dialog. Subclasses may overwrite this method without calling super, to change the standard behavior.
*
* @param reason
* A string describing why removal is not possible; never null
* @since 2.0
*/
public void warnRemoveFailed(final String reason) {
Assert.isNotNull(reason);
final String title = Messages.MasterDetailsComposite_dialogTitle_removeFailed;
RienaMessageDialog.openWarning(getShell(), title, reason);
}
// protected methods
////////////////////
/**
* Creates the 'Apply' Control. Subclasses may override.
*
* @param compButton
* the parent composite; never null
*
* @return a Control or null. If this returns null you are responsible for adding a button with the binding id {@link #BIND_ID_APPLY} to this control
* elsewhere.
*/
protected Control createButtonApply(final Composite compButton) {
return UIControlsFactory.createButton(compButton, Messages.MasterDetailsComposite_buttonApply);
}
/**
* Creates the 'New' Control. Subclasses may override.
*
* @param compButton
* the parent composite; never null
*
* @return a Control or null. If this returns null you are responsible for adding a button with the binding id {@link #BIND_ID_NEW} to this composite
* elsewhere – otherwise 'New' will not be available.
*/
protected Control createButtonNew(final Composite compButton) {
return UIControlsFactory.createButton(compButton, Messages.MasterDetailsComposite_buttonNew);
}
/**
* Creates the 'Remove' Control. Subclasses may override.
*
* @param compButton
* the parent composite; never null
*
* @return a Control or null. If this returns null you are responsible for adding a button with the binding id {@link #BIND_ID_REMOVE} to this composite
* elsewhere – otherwise 'Remove' will not be available.
*/
protected Control createButtonRemove(final Composite compButton) {
return UIControlsFactory.createButton(compButton, Messages.MasterDetailsComposite_buttonRemove);
}
/**
* Create the composite containing the buttons. Subclasses may override.
* <p>
* Implementation note: it is appropriate to return null and create the Buttons somewhere else. In that case the methods createButtonNew,
* createButtonRemove, createButtonApply will not be called (unless you call them) and you are responsible to add three buttons widgets with the ids:
* BIND_ID_NEW, BIND_ID_REMOVE, BIND_ID_APPLY, by invoking {@link #addUIControl(Object, String)}.
*
* @param parent
* @return a Composite or null
*/
protected Composite createButtons(final Composite parent) {
final Composite result = UIControlsFactory.createComposite(parent);
final RowLayout buttonLayout = new RowLayout(SWT.VERTICAL);
buttonLayout.marginTop = 0;
buttonLayout.marginLeft = 3;
buttonLayout.marginRight = 2;
buttonLayout.fill = true;
result.setLayout(buttonLayout);
final Control btnNew = createButtonNew(result);
if (btnNew != null) {
addUIControl(btnNew, BIND_ID_NEW);
}
final Control btnRemove = createButtonRemove(result);
if (btnRemove != null) {
addUIControl(btnRemove, BIND_ID_REMOVE);
}
final Control btnApply = createButtonApply(result);
if (btnApply != null) {
addUIControl(btnApply, BIND_ID_APPLY);
}
return result;
}
/**
* Subclasses must override this method to populate the details area.
*
* @param details
* the Composite for the details area; never null.
*/
protected void createDetails(final Composite details) {
// Sublasses should override.
}
/**
* Creates a widget for displaying the available rows. This is a table- / grid- / matrix-like Control.
* <p>
* Subclasses should override and return an appropriate Control.
*
* @param tableComposite
* a parent Composite; never null. It already has a TableColumnLayout, which should be used if you are creating a Table. If you are creating
* another type of widget you should set an appropriate layout to the {@code tableComposite}.
* @param layout
* the TableColumnLayout for the widget; never null. Add information about the columns as necessary. Can be ignored, if you are creating a widget
* of a different type (i.e. other than Table).
* @return a Table-like control; never null; must have the SWT.SINGLE style-bit (i.e. single selection only). If the returned widget is a Table and has too
* few or too many columns, the columns will be re-created. If you care about the specific configuration/layout of the Table columns, make sure the
* Table has as many columns a needed by the ridget.
* @see {@code IMasterDetailsRidget#bindToModel(...)}
*/
abstract protected Control createTable(Composite tableComposite, TableColumnLayout layout);
/**
* Returns the style bits for the 'details' composite. Subclasses may override, but has to return a value that is supported by {@link Composite}.
*
* @return {@code SWT.NONE}
*/
protected int getDetailsStyle() {
return SWT.NONE;
}
/**
* Returns the style bits for the 'master' composite. Subclasses may override, but has to return a value that is supported by {@link Composite}.
*
* @return {@code SWT.BORDER} (since 2.0)
*/
protected int getMasterStyle() {
return SWT.BORDER;
}
// helping methods
//////////////////
private void checkButton(final Control control, final boolean allowNull) {
if (allowNull && control == null) {
return;
}
final boolean ok = control instanceof Button || control instanceof ImageButton;
Assert.isLegal(ok, "Control must be a Button or ImageButton: " + String.valueOf(control)); //$NON-NLS-1$
}
private void checkOrientation(final int orientation) {
final int[] allowedValues = { SWT.TOP, SWT.BOTTOM };
for (final int value : allowedValues) {
if (orientation == value) {
return;
}
}
throw new IllegalArgumentException("unsupported orientation: " + orientation); //$NON-NLS-1$
}
private Composite createComposite(final int style) {
final Composite result = UIControlsFactory.createComposite(this, style);
GridDataFactory.fillDefaults().grab(true, false).applyTo(result);
return result;
}
private void createMaster(final Composite parent) {
if (LnfManager.getLnf().useDpiGridLayout()) {
DpiGridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false).spacing(0, 0).applyTo(parent);
} else {
GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false).spacing(0, 0).applyTo(parent);
}
final Composite compTable = createTableComposite(parent);
buttonComposite = createButtons(parent);
if (buttonComposite != null) {
if (buttonComposite.getLayoutData() == null) {
GridDataFactory.fillDefaults().applyTo(buttonComposite);
}
if ((buttonComposite.getStyle() & SWT.BORDER) == 0) {
SWTFacade.getDefault().addPaintListener(buttonComposite, new LinePaintListener());
}
} else {
((GridData) compTable.getLayoutData()).horizontalSpan = 2;
}
}
/**
* @return the buttonComposite
* @since 4.0
*/
public Composite getButtonComposite() {
return buttonComposite;
}
private Composite createTableComposite(final Composite parent) {
final Composite result = UIControlsFactory.createComposite(parent);
final TableColumnLayout layout = new TableColumnLayout();
table = createTable(result, layout);
if (table instanceof Table && result.getLayout() == null) {
// this is specific to the MasterDetailsComposite, but is done here
// for backwards compatibility
result.setLayout(layout);
final int wHint = 200;
final int hHint = (((Table) table).getItemHeight() * 8) + ((Table) table).getHeaderHeight();
GridDataFactory.fillDefaults().grab(true, false).hint(wHint, hHint).applyTo(result);
}
addUIControl(table, BIND_ID_TABLE);
return result;
}
private Control getUIControl(final String id) {
Control result = null;
final SWTBindingPropertyLocator bpLocator = SWTBindingPropertyLocator.getInstance();
final Iterator<Object> iter = controls.iterator();
while (result == null && iter.hasNext()) {
final Control control = (Control) iter.next();
if (id.equals(bpLocator.locateBindingProperty(control))) {
result = control;
}
}
Assert.isNotNull(result);
return result;
}
// helping classes
//////////////////
private static final class LinePaintListener implements PaintListener {
private Color fgColor;
public void paintControl(final PaintEvent e) {
final GC gc = e.gc;
final Color oldFg = gc.getForeground();
if (fgColor == null) {
fgColor = LnfManager.getLnf().getColor(LnfKeyConstants.MASTER_DETAILS_WIDGET_SEPARATOR_FOREGROUND);
}
gc.setForeground(fgColor);
final Rectangle bounds = ((Control) e.widget).getBounds();
gc.drawLine(0, 0, 0, bounds.height);
gc.setForeground(oldFg);
}
}
}