/*******************************************************************************
* 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 org.eclipse.core.runtime.Assert;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
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.Text;
import org.eclipse.riena.internal.ui.swt.MultilineButton;
import org.eclipse.riena.ui.swt.layout.DpiGridLayout;
import org.eclipse.riena.ui.swt.utils.SwtUtilities;
/**
* This composite presents a list of single or multiple choices. It is mapped to a {@link org.eclipse.riena.ui.ridgets.ISingleChoiceRidget} or
* {@link org.eclipse.riena.ui.ridgets.IMultipleChoiceRidget}.
*/
public class ChoiceComposite extends Composite implements SelectionListener {
private final boolean isMulti;
private int marginHeight;
private int marginWidth;
private int vSpacing;
private int hSpacing = 3;
private int orientation;
private boolean isEditable;
private Color bgColor;
private boolean wrapOptionsText;
private final Composite content;
/**
* Create a new ChoiceComposite instance given its parent and style value. The default orientation is <tt>SWT.VERTICAL</tt> (see also
* {@link #setOrientation(int)}). Multiple selection is allowed (=check boxes).
*
* @param parent
* the parent Composite (non-null)
* @param style
* the SWT style of the Composite
*/
public ChoiceComposite(final Composite parent, final int style) {
this(parent, style, true);
}
/**
* Create a new ChoiceComposite instance given its parent and style value. The default orientation is <tt>SWT.VERTICAL</tt> (see also
* {@link #setOrientation(int)}).
*
* @param parent
* the parent Composite (non-null)
* @param style
* the SWT style of the Composite
* @param multipleSelection
* true to allow multiple selection (=check boxes), false for single selection (=radio buttons)
* @see #createChild(String)
* @see #setOrientation(int)
*/
public ChoiceComposite(final Composite parent, final int style, final boolean multipleSelection) {
super(parent, style);
this.isMulti = multipleSelection;
this.orientation = SWT.VERTICAL;
isEditable = true;
final DpiGridLayout layout = new DpiGridLayout(1, false);
layout.marginWidth = 0;
layout.marginHeight = 0;
layout.verticalSpacing = 0;
setLayout(layout);
// an invisible text field to prevent unwanted radio button selection
// while org.eclipse.riena.ui.ridgets.swt.FocusManager handles focus events
// on neighbor widgets with readOnly=true
new Text(this, SWT.NONE).setLayoutData(new GridData(1, 0));
content = new Composite(this, SWT.NONE);
content.setLayoutData(new GridData(GridData.FILL_BOTH));
applyLayout();
}
/**
* Creates an appropriate button (check or radio) within this composite.
*
* @param caption
* the String on the Button; never null
* @param value
* the selection value for the button
*
* @since 3.0
*/
public Button createChild(final String caption) {
int style = isMulti ? SWT.CHECK : SWT.RADIO;
Button result;
if (wrapOptionsText) {
style |= SWT.WRAP;
result = new MultilineButton(content, style);
} else {
result = new Button(content, style);
}
result.setText(caption);
result.setForeground(getForeground());
result.setBackground(getBackground());
result.addSelectionListener(this);
new EventForwarder(result, this);
updateEnabled(result, isEnabled());
return result;
}
/**
* Gets the editable state.
*
* @return whether or not the receiver is editable
*
* @exception SWTException
* <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @since 3.0
*/
public final boolean getEditable() {
checkWidget();
return isEditable;
}
/**
* 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 3.0
*/
public final Point getMargins() {
return new Point(marginHeight, marginWidth);
}
/**
* Returns the orientation of this ChoiceComposite.
*
* @return one of <tt>SWT.VERTICAL</tt> or <tt>SWT.HORIZONTAL</tt>.
*/
@Override
public int getOrientation() {
return orientation;
}
/**
* 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 3.0
*/
public final Point getSpacing() {
return new Point(hSpacing, vSpacing);
}
/**
* Returns true if this instance allows multiple selection, false otherwise.
*/
public final boolean isMultipleSelection() {
return isMulti;
}
/**
* {@inheritDoc}
* <p>
* The value will be propagated to children of this composite.
*/
@Override
public final void setBackground(final Color color) {
setRedraw(false);
try {
this.bgColor = color;
updateBgColor();
} finally {
setRedraw(true);
}
}
/**
* Sets the editable state.
* <p>
* Implementation notes:
* <p>
* (a) {@code setEnabled(false)} takes precedence over {@code setEditable(boolean)}
* <p>
* (b) on {@code setEditable(false)} the widget will automatically disable all buttons which are not selected. It will also block / revert selection events
* from the user. However, if the selection of the buttons is changed programatically, you have to invoke {@code choice.setEditable(false)} afterwards to
* update the state of the contained buttons.
*
* @param editable
* the new editable state
*
* @exception SWTException
* <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
*
* @since 3.0
*/
public final void setEditable(final boolean editable) {
this.isEditable = editable;
updateEnabled(getEnabled());
}
/**
* {@inheritDoc}
* <p>
* This will also update the enabled state all buttons contained in this widget.
* <p>
* Implementation note: {@code setEnabled(false)} takes precedence over {@code setEditable(boolean)}.
*/
@Override
public final void setEnabled(final boolean enabled) {
setRedraw(false);
try {
super.setEnabled(enabled);
updateEnabled(enabled);
updateBgColor();
} finally {
setRedraw(true);
}
}
/**
* {@inheritDoc}
* <p>
* The value will be propagated to children of this composite.
*/
@Override
public final void setForeground(final Color color) {
setRedraw(false);
try {
super.setForeground(color);
for (final Control child : getChildren()) {
child.setForeground(color);
}
} finally {
setRedraw(true);
}
}
/**
* 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 3.0
*/
public final void setMargins(final int marginHeight, final int marginWidth) {
Assert.isLegal(marginHeight >= 0, "marginHeight must be greater or equal to zero: " + marginHeight); //$NON-NLS-1$
Assert.isLegal(marginWidth >= 0, "marginWidth must be greater or equal to zero: " + marginWidth); //$NON-NLS-1$
this.marginHeight = marginHeight;
this.marginWidth = marginWidth;
applyLayout();
}
/**
* Sets the orientation (vertical or horizontal) of the choices in this composite.
*
* @param orientation
* <tt>SWT.VERTICAL</tt> for vertical orientation or <tt>SWT.HORIZONTAL</tt> for horizontal orientation
* @throws RuntimeException
* if orientation has an unsupported value
*/
@Override
public final void setOrientation(final int orientation) {
Assert.isLegal(orientation == SWT.VERTICAL || orientation == SWT.HORIZONTAL);
if (this.orientation != orientation) {
this.orientation = orientation;
applyLayout();
}
}
/**
* Sets the spacing for the right/left and top/bottom edges of the cells within the widget.
* <p>
* Implementation note: only one of the two values will be used depending on the orientation (horizontal or vertical – see
* {@link #setOrientation(int)}).
*
* @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 3.
* @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 0.
*
* @since 3.0
*/
public final void setSpacing(final int hSpacing, final int vSpacing) {
Assert.isLegal(hSpacing >= 0, "hSpacing must be greater or equal to zero: " + hSpacing); //$NON-NLS-1$
Assert.isLegal(vSpacing >= 0, "vSpacing must be greater or equal to zero: " + vSpacing); //$NON-NLS-1$
this.hSpacing = hSpacing;
this.vSpacing = vSpacing;
applyLayout();
}
/**
* @noreference This method is not intended to be referenced by clients.
*/
public void widgetSelected(final SelectionEvent e) {
if (!isEditable) {
final Button button = (Button) e.widget;
button.setSelection(button.isEnabled());
}
}
/**
* @noreference This method is not intended to be referenced by clients.
*/
public void widgetDefaultSelected(final SelectionEvent e) {
// unused
}
/**
* Set this flag to enable multi line presentation of the options text. When wrapOptionsText is <code>true</code> the character '\n' will be interpreted as
* a line break. Default is <code>false</code>.
*
* @param wrapOptionsText
* <code>true</code> to enable word wrap for the options text
* @since 5.0
*/
public void setWrapOptionsText(final boolean wrapOptionsText) {
this.wrapOptionsText = wrapOptionsText;
}
/**
* Returns the "children" buttons in this choice composite (one button for each choice option). Due to {@link ChoiceComposite} internals, these children
* must not be the same as the controls returned by {@link #getChildren()}. All elements of the returned array will be of type {@link Button}.
*
* @return a {@link Button}s array
* @since 5.0
*/
public Control[] getChildrenButtons() {
return content.getChildren();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Composite#setFocus()
*/
@Override
public boolean setFocus() {
return isMulti ? content.setFocus() : super.setFocus();
}
// helping methods
// ////////////////
/**
* This method is not API.
* <p>
* visible for testing
*
* @return the content composite holding the buttons
* @since 5.0
*/
public Composite getContentComposite() {
return content;
}
private void applyLayout() {
if (orientation == SWT.VERTICAL) {
final FillLayout layout = new FillLayout(SWT.VERTICAL);
layout.marginHeight = SwtUtilities.convertYToDpiTruncate(marginHeight);
layout.marginWidth = SwtUtilities.convertXToDpiTruncate(marginWidth);
layout.spacing = SwtUtilities.convertYToDpiTruncate(vSpacing);
content.setLayout(layout);
} else {
final RowLayout layout = new RowLayout(SWT.HORIZONTAL);
layout.center = true;
layout.marginHeight = SwtUtilities.convertYToDpiTruncate(marginHeight);
layout.marginWidth = SwtUtilities.convertXToDpiTruncate(marginWidth);
layout.marginLeft = 0;
layout.marginRight = 0;
layout.marginTop = 0;
layout.marginBottom = 0;
layout.spacing = SwtUtilities.convertXToDpiTruncate(hSpacing);
layout.wrap = false;
content.setLayout(layout);
}
}
private void updateBgColor() {
updateBgColor(isEnabled() && bgColor != null ? bgColor : getParent().getBackground());
}
private void updateBgColor(final Color color) {
super.setBackground(color);
content.setBackground(color);
for (final Control child : getChildren()) {
child.setBackground(color);
}
for (final Control child : getChildrenButtons()) {
child.setBackground(color);
}
}
private void updateEnabled(final boolean isEnabled) {
for (final Control child : getChildrenButtons()) {
updateEnabled(child, isEnabled);
}
}
private void updateEnabled(final Control child, final boolean isEnabled) {
if (!isEditable) {
child.setEnabled(false);
return;
}
child.setEnabled(isEnabled);
}
/**
* Updates the children buttons read only state.
*
* @param readonly
* @since 6.2
*/
public void updateReadOnly(final boolean readonly) {
this.isEditable = !readonly;
final Control[] childrenButtons = getChildrenButtons();
for (final Control control : childrenButtons) {
control.setEnabled(!readonly);
}
}
}