blob: ebbd1eed4c68b89f8332575b540464773dd88461 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2010 Oracle. 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:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.common.ui.internal.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jpt.common.utility.internal.StringTools;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
/**
* This class is responsible to set a preferred width on the registered widgets
* (either <code>Control</code> or <code>ControlAligner</code>) based on the
* widest widget.
* <p>
* Important: The layout data has to be a <code>GridData</code>. If none is set,
* then a new <code>GridData</code> is automatically created.
* <p>
* Here an example of the result if this aligner is used to align controls
* within either one or two group boxes, the controls added are the labels in
* this case. It is also possible to align controls on the right side of the
* main component, a spacer can be used for extra space.
* <p>
* Here's an example:
* <pre>
* - Group Box 1 --------------------------------------------------------------
* | -------------------------------------- ------------- |
* | Name: | I | | Browse... | |
* | -------------------------------------- ------------- |
* | --------- |
* | Preallocation Size: | |I| |
* | --------- |
* | -------------------------------------- |
* | Descriptor: | |v| |
* | -------------------------------------- |
* ----------------------------------------------------------------------------
*
* - Group Box 2 --------------------------------------------------------------
* | -------------------------------------- |
* | Mapping Type: | |V| |
* | -------------------------------------- |
* | -------------------------------------- |
* | Check in Script: | I | |
* | -------------------------------------- |
* ----------------------------------------------------------------------------</pre>
*
* @version 2.0
* @since 2.0
*/
@SuppressWarnings("nls")
public final class ControlAligner
{
/**
* Flag used to prevent a validation so it can be done after an operation
* completed.
*/
private boolean autoValidate;
/**
* The utility class used to support bound properties.
*/
private Collection<Listener> changeSupport;
/**
* The listener added to each of the controls that listens only to a text
* change.
*/
private Listener listener;
/**
* Prevents infinite recursion when recalculating the preferred width.
* This happens in an hierarchy of <code>ControlAligner</code>s. The lock
* has to be placed here and not in the {@link ControlAlignerWrapper}.
*/
private boolean locked;
/**
* The length of the widest control. If the length was not calculated, then
* this value is 0.
*/
private int maximumWidth;
/**
* The collection of {@link Wrapper}s encapsulating either <code>Control</code>s
* or {@link ControlAligner}s.
*/
private Collection<Wrapper> wrappers;
/**
* A null-<code>Point</code> object used to clear the preferred size.
*/
private static final Point DEFAULT_SIZE = new Point(SWT.DEFAULT, SWT.DEFAULT);
/**
* The types of events to listen in order to properly adjust the size of all
* the widgets.
*/
private static final int[] EVENT_TYPES = {
SWT.Dispose,
SWT.Hide,
SWT.Resize,
SWT.Show
};
/**
* Creates a new <code>ControlAligner</code>.
*/
public ControlAligner() {
super();
initialize();
}
/**
* Creates a new <code>ControlAligner</code>.
*
* @param controls The collection of <code>Control</code>s
*/
public ControlAligner(Collection<? extends Control> controls) {
this();
addAllControls(controls);
}
/**
* Adds the given control. Its width will be used along with the width of all
* the other registered controls in order to get the greater witdh and use
* it as the width for all the controls.
*
* @param control The <code>Control</code> to be added
*/
public void add(Control control) {
Assert.isNotNull(control, "Can't add null to this ControlAligner");
Wrapper wrapper = buildWrapper(control);
wrapper.addListener(listener);
wrappers.add(wrapper);
revalidate(false);
}
/**
* Adds the given control. Its width will be used along with the width of all
* the other registered controls in order to get the greater witdh and use
* it as the width for all the controls.
*
* @param controlAligner The <code>ControlAligner</code> to be added
* @exception IllegalArgumentException Can't add the <code>ControlAligner</code>
* to itself
*/
public void add(ControlAligner controlAligner) {
Assert.isNotNull(controlAligner, "Can't add null to this ControlAligner");
Assert.isLegal(controlAligner != this, "Can't add the ControlAligner to itself");
Wrapper wrapper = buildWrapper(controlAligner);
wrapper.addListener(listener);
wrappers.add(wrapper);
if (!controlAligner.wrappers.isEmpty()) {
revalidate(false);
}
}
/**
* Adds the items contained in the given collection into this
* <code>ControlAligner</code>. The preferred width of each item will be
* used along with the width of all the other items in order to get the
* widest control and use its width as the width for all the controls.
*
* @param aligners The collection of <code>ControlAligner</code>s
*/
public void addAllControlAligners(Collection<ControlAligner> aligners) {
// Deactivate the auto validation while adding all the Controls and/or
// ControlAligners in order to improve performance
boolean oldAutoValidate = autoValidate;
autoValidate = false;
for (ControlAligner aligner : aligners) {
add(aligner);
}
autoValidate = oldAutoValidate;
revalidate(false);
}
/**
* Adds the items contained in the given collection into this
* <code>ControlAligner</code>. The preferred width of each item will be
* used along with the width of all the other items in order to get the
* widest control and use its width as the width for all the controls.
*
* @param controls The collection of <code>Control</code>s
*/
public void addAllControls(Collection<? extends Control> controls) {
// Deactivate the auto validation while adding all the Controls and/or
// ControlAligners in order to improve performance
boolean oldAutoValidate = autoValidate;
autoValidate = false;
for (Control control : controls) {
add(control);
}
autoValidate = oldAutoValidate;
revalidate(false);
}
/**
* Adds the given <code>ControListener</code>.
*
* @param listener The <code>Listener</code> to be added
*/
private void addListener(Listener listener) {
if (changeSupport == null) {
changeSupport = new ArrayList<Listener>();
}
changeSupport.add(listener);
}
/**
* Creates a new <code>Wrapper</code> that encapsulates the given source.
*
* @param control The control to be wrapped
* @return A new {@link Wrapper}
*/
private Wrapper buildWrapper(Control control) {
return new ControlWrapper(control);
}
/**
* Creates a new <code>Wrapper</code> that encapsulates the given source.
*
* @param ControlAligner The <code>ControlAligner</code> to be wrapped
* @return A new {@link ControlAlignerWrapper}
*/
private Wrapper buildWrapper(ControlAligner ControlAligner) {
return new ControlAlignerWrapper(ControlAligner);
}
/**
* Calculates the width taken by the widgets and returns the maximum width.
*
* @param recalculateSize <code>true</code> to recalculate the preferred size
* of all the wrappers contained within them rather than using the cached
* size; <code>false</code> to use the cached size
*/
private int calculateWidth(boolean recalculateSize) {
int width = 0;
for (Wrapper wrapper : wrappers) {
Point size = wrapper.cachedSize();
// The size has not been calculated yet
if (recalculateSize || (size.x == 0)) {
size = wrapper.calculateSize();
}
// Only keep the greatest width
width = Math.max(size.x, width);
}
return width;
}
/**
* Reports a bound property change.
*
* @param oldValue the old value of the property (as an int)
* @param newValue the new value of the property (as an int)
*/
private void controlResized(int oldValue, int newValue) {
if ((changeSupport != null) && (oldValue != newValue)) {
Event event = new Event();
event.widget = SWTUtil.getShell();
event.data = this;
for (Listener listener : changeSupport) {
listener.handleEvent(event);
}
}
}
/**
* Disposes this <code>ControlAligner</code>, this can improve the speed of
* disposing a pane. When a pane is disposed, this aligner doesn't need to
* revalidate its size upon dispose of its widgets.
*/
public void dispose() {
for (Iterator<Wrapper> iter = wrappers.iterator(); iter.hasNext(); ) {
Wrapper wrapper = iter.next();
wrapper.removeListener(listener);
iter.remove();
}
this.wrappers.clear();
}
/**
* Returns the length of the widest control. If the length was not
* calculated, then this value is 0.
*
* @return The width of the widest control or 0 if the length has not been
* calculated yet
*/
public int getMaximumWidth() {
return maximumWidth;
}
/**
* Initializes this <code>ControlAligner</code>.
*/
private void initialize() {
this.autoValidate = true;
this.maximumWidth = 0;
this.listener = new ListenerHandler();
this.wrappers = new ArrayList<Wrapper>();
}
/**
* Invalidates the size of the given object.
*
* @param source The source object to be invalidated
*/
private void invalidate(Object source) {
Wrapper wrapper = retrieveWrapper(source);
if (!wrapper.locked()) {
Point size = wrapper.cachedSize();
size.x = size.y = 0;
wrapper.setSize(DEFAULT_SIZE);
}
}
/**
* Updates the maximum length based on the widest control. This methods
* does not update the width of the controls.
*
* @param recalculateSize <code>true</code> to recalculate the preferred size
* of all the wrappers contained within them rather than using the cached
* size; <code>false</code> to use the cached size
*/
private void recalculateWidth(boolean recalculateSize) {
int width = calculateWidth(recalculateSize);
try {
locked = true;
setMaximumWidth(width);
}
finally {
locked = false;
}
}
/**
* Removes the given control. Its preferred width will not be used when
* calculating the preferred width.
*
* @param control The control to be removed
* @exception AssertionFailedException If the given <code>Control</code> is
* <code>null</code>
*/
public void remove(Control control) {
Assert.isNotNull(control, "The Control to remove cannot be null");
Wrapper wrapper = retrieveWrapper(control);
wrapper.removeListener(listener);
wrappers.remove(wrapper);
revalidate(true);
}
/**
* Removes the given <code>ControlAligner</code>. Its preferred width
* will not be used when calculating the preferred witdh.
*
* @param controlAligner The <code>ControlAligner</code> to be removed
* @exception AssertionFailedException If the given <code>ControlAligner</code>
* is <code>null</code>
*/
public void remove(ControlAligner controlAligner) {
Assert.isNotNull(controlAligner, "The ControlAligner to remove cannot be null");
Wrapper wrapper = retrieveWrapper(controlAligner);
wrapper.removeListener(listener);
wrappers.remove(wrapper);
revalidate(true);
}
/**
* Removes the given <code>Listener</code>.
*
* @param listener The <code>Listener</code> to be removed
*/
private void removeListener(Listener listener) {
changeSupport.remove(listener);
if (changeSupport.isEmpty()) {
changeSupport = null;
}
}
/**
* Retrieves the <code>Wrapper</code> that is encapsulating the given object.
*
* @param source Either a <code>Control</code> or a <code>ControlAligner</code>
* @return Its <code>Wrapper</code>
*/
private Wrapper retrieveWrapper(Object source) {
for (Wrapper wrapper : wrappers) {
if (wrapper.source() == source) {
return wrapper;
}
}
throw new IllegalArgumentException("Can't retrieve the Wrapper for " + source);
}
/**
* If the count of control is greater than one and {@link #isAutoValidate()}
* returns <code>true</code>, then the size of all the registered
* <code>Control</code>s will be udpated.
*
* @param recalculateSize <code>true</code> to recalculate the preferred size
* of all the wrappers contained within them rather than using the cached
* size; <code>false</code> to use the cached size
*/
private void revalidate(boolean recalculateSize) {
if (autoValidate) {
recalculateWidth(recalculateSize);
updateWrapperSize(recalculateSize);
}
}
/**
* Bases on the information contained in the given <code>Event</code>,
* resize the controls.
*
* @param event The <code>Event</code> sent by the UI thread when the state
* of a widget changed
*/
private void revalidate(Event event) {
// We don't need to revalidate during a revalidation process
if (locked) {
return;
}
Object source;
if (event.widget != SWTUtil.getShell()) {
source = event.widget;
Control control = (Control) source;
// When a dialog is opened, we need to actually force a layout of
// the controls, this is required because the control is actually
// not visible when the preferred width is caculated
if (control == control.getShell()) {
if (event.type == SWT.Dispose) {
return;
}
source = null;
}
}
else {
source = event.data;
}
// Either remove the ControlWrapper if the widget was disposed or
// invalidate the widget in order to recalculate the preferred size
if (source != null) {
if (event.type == SWT.Dispose) {
Wrapper wrapper = retrieveWrapper(source);
wrappers.remove(wrapper);
}
else {
invalidate(source);
}
}
// Now revalidate all the Controls and ControlAligners
revalidate(true);
}
/**
* Sets the length of the widest control. If the length was not calulcated,
* then this value is 0.
*
* @param maximumWidth The width of the widest control
*/
private void setMaximumWidth(int maximumWidth) {
int oldMaximumWidth = this.maximumWidth;
this.maximumWidth = maximumWidth;
controlResized(oldMaximumWidth, maximumWidth);
}
/**
* Returns a string representation of this <code>ControlAligner</code>.
*
* @return Information about this object
*/
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("maximumWidth=");
sb.append(maximumWidth);
sb.append(", wrappers=");
sb.append(wrappers);
return StringTools.buildToStringFor(this, sb);
}
/**
* Updates the size of every <code>Wrapper</code> based on the maximum width.
*
* @param forceRevalidate <code>true</code> to revalidate the wrapper's size
* even though its current size might be the same as the maximum width;
* <code>false</code> to only revalidate the wrappers with a different width
*/
private void updateWrapperSize(boolean forceRevalidate) {
for (Wrapper wrapper : wrappers) {
Point cachedSize = wrapper.cachedSize();
// No need to change the size of the wrapper since it's always using
// the maximum width
if (forceRevalidate || (cachedSize.x != maximumWidth)) {
Point size = new Point(maximumWidth, cachedSize.y);
wrapper.setSize(size);
}
}
}
/**
* This <code>Wrapper</code> encapsulates a {@link ControlAligner}.
*/
private class ControlAlignerWrapper implements Wrapper {
/**
* The cached size, which is {@link ControlAligner#maximumWidth}.
*/
private final Point cachedSize;
/**
* The <code>ControlAligner</code> encapsulated by this
* <code>Wrapper</code>.
*/
private final ControlAligner controlAligner;
/**
* Creates a new <code>ControlAlignerWrapper</code> that encapsulates
* the given <code>ControlAligner</code>.
*
* @param controlAligner The <code>ControlAligner</code> to be
* encapsulated by this <code>Wrapper</code>
*/
private ControlAlignerWrapper(ControlAligner controlAligner) {
super();
this.controlAligner = controlAligner;
this.cachedSize = new Point(controlAligner.maximumWidth, 0);
}
/*
* (non-Javadoc)
*/
public void addListener(Listener listener) {
controlAligner.addListener(listener);
}
/*
* (non-Javadoc)
*/
public Point cachedSize() {
cachedSize.x = controlAligner.maximumWidth;
return cachedSize;
}
/*
* (non-Javadoc)
*/
public Point calculateSize() {
Point size = new Point(controlAligner.calculateWidth(false), 0);
if (size.x != SWT.DEFAULT) {
cachedSize.x = size.x;
}
else {
cachedSize.x = 0;
}
if (size.y != SWT.DEFAULT) {
cachedSize.y = size.y;
}
else {
cachedSize.y = 0;
}
return size;
}
/*
* (non-Javadoc)
*/
public boolean locked() {
return controlAligner.locked;
}
/*
* (non-Javadoc)
*/
public void removeListener(Listener listener) {
controlAligner.removeListener(listener);
}
/*
* (non-Javadoc)
*/
public void setSize(Point size) {
if (size == DEFAULT_SIZE) {
controlAligner.maximumWidth = 0;
}
else if (controlAligner.maximumWidth != size.x) {
controlAligner.maximumWidth = size.x;
controlAligner.updateWrapperSize(true);
}
}
/*
* (non-Javadoc)
*/
public Object source() {
return controlAligner;
}
/*
* (non-Javadoc)
*/
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("Cached size=");
sb.append(cachedSize);
sb.append(", ControlAligner=");
sb.append(controlAligner);
return StringTools.buildToStringFor(this, sb);
}
}
/**
* This <code>Wrapper</code> encapsulates a {@link Control}.
*/
private class ControlWrapper implements Wrapper {
/**
* The cached size, which is control's size.
*/
private Point cachedSize;
/**
* The control to be encapsulated by this <code>Wrapper</code>.
*/
private final Control control;
/**
* Creates a new <code>controlWrapper</code> that encapsulates the given
* control.
*
* @param control The control to be encapsulated by this <code>Wrapper</code>
*/
private ControlWrapper(Control control) {
super();
this.control = control;
this.cachedSize = new Point(0, 0);
}
/*
* (non-Javadoc)
*/
public void addListener(Listener listener) {
for (int eventType : EVENT_TYPES) {
control.addListener(eventType, listener);
}
}
/*
* (non-Javadoc)
*/
public Point cachedSize() {
return cachedSize;
}
/*
* (non-Javadoc)
*/
public Point calculateSize() {
cachedSize = control.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
// Update right away the control's GridData
GridData gridData = (GridData) control.getLayoutData();
if (gridData == null) {
gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
control.setLayoutData(gridData);
}
gridData.widthHint = cachedSize.x;
gridData.heightHint = cachedSize.y;
// Make sure the size is not -1
if (cachedSize.x == SWT.DEFAULT) {
cachedSize.x = 0;
}
if (cachedSize.y == SWT.DEFAULT) {
cachedSize.y = 0;
}
return cachedSize;
}
/*
* (non-Javadoc)
*/
public boolean locked() {
return false;
}
/*
* (non-Javadoc)
*/
public void removeListener(Listener listener) {
for (int eventType : EVENT_TYPES) {
control.removeListener(eventType, listener);
}
}
/*
* (non-Javadoc)
*/
public void setSize(Point size) {
if (control.isDisposed()) {
return;
}
// Update the GridData with the new size
GridData gridData = (GridData) control.getLayoutData();
if (gridData == null) {
gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
control.setLayoutData(gridData);
}
gridData.widthHint = size.x;
gridData.heightHint = size.y;
// Force the control to be resized, and tell its parent to layout
// its widgets
if (size.x > 0) {
// locked = true;
// try {
//// control.getParent().layout(new Control[] { control });
// control.getParent().layout(true);
// }
// finally {
// locked = false;
// }
Rectangle bounds = control.getBounds();
// Only update the control's width if it's
// different from the current size
if (bounds.width != size.x) {
locked = true;
try {
// control.setBounds(bounds.x, bounds.y, size.x, size.y);
control.getParent().layout(true);
}
finally
{
locked = false;
}
}
}
}
/*
* (non-Javadoc)
*/
public Control source() {
return control;
}
/*
* (non-Javadoc)
*/
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("Cached size=");
sb.append(cachedSize);
sb.append(", Control=");
sb.append(control);
return StringTools.buildToStringFor(this, sb);
}
}
/**
* The listener added to each of the control that is notified in order to
* revalidate the preferred size.
*/
private class ListenerHandler implements Listener {
public void handleEvent(Event event) {
ControlAligner.this.revalidate(event);
}
}
/**
* This <code>Wrapper</code> helps to encapsulate heterogeneous objects and
* apply the same behavior on them.
*/
private interface Wrapper {
/**
* Adds the given <code>Listener</code> to wrapped object in order to
* receive notification when its property changed.
*
* @param listener The <code>Listener</code> to be added
*/
void addListener(Listener listener);
/**
* Returns the cached size of the encapsulated source.
*
* @return A non-<code>null</code> <code>Point</code> where the x is the
* width and the y is the height of the widget
*/
Point cachedSize();
/**
* Calculates the preferred size the wrapped object would take by itself.
*
* @return The calculated size
*/
Point calculateSize();
/**
* Prevents infinite recursion when recalculating the preferred width.
* This happens in an hierarchy of <code>ControlAligner</code>s.
*
* @return <code>true</code> to prevent this <code>Wrapper</code> from
* being invalidated; otherwise <code>false</code>
*/
boolean locked();
/**
* Removes the given <code>Listener</code>.
*
* @param listener The <code>Listener</code> to be removed
*/
void removeListener(Listener listener);
/**
* Sets the size on the encapsulated source.
*
* @param size The new size
*/
void setSize(Point size);
/**
* Returns the encapsulated object.
*
* @return The object that is been wrapped
*/
Object source();
}
}