blob: 93b5155f4129ae72fa447e6fb1076bc3b1d2aa35 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 SAP AG, Walldorf.
* 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:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.platform.discovery.ui.internal;
import java.util.Arrays;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.platform.discovery.ui.internal.plugin.DiscoveryUIMessages;
import org.eclipse.platform.discovery.ui.internal.tooltip.ControlTooltipManager;
import org.eclipse.platform.discovery.ui.internal.tooltip.IToolTipConfigurator;
import org.eclipse.platform.discovery.ui.internal.tooltip.ToolTipConfigurator;
import org.eclipse.platform.discovery.ui.internal.tooltip.TooltipCreator;
import org.eclipse.platform.discovery.ui.internal.view.ViewProgressMonitor;
import org.eclipse.platform.discovery.ui.internal.view.impl.SashOffsetCalculator;
import org.eclipse.platform.discovery.util.internal.property.IPropertyAttributeListener;
import org.eclipse.platform.discovery.util.internal.property.Property;
import org.eclipse.platform.discovery.util.internal.property.PropertyAttributeChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
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.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.ToolTip;
import org.eclipse.ui.forms.IFormColors;
import org.eclipse.ui.forms.widgets.FormToolkit;
/**
* A composite which contains of upper and bottom parts. They are separated by a
* sash via which upper and bottom parts can be dynamically resized. The
* composite also provides a button which can hide/show then upper part. The
* sash which separates the upper and bottom parts cannot be dragged below the
* size of the upper part thus not allowing unused space in the upper part<br>
* In addition the sliding composite provides a progress monitor at its bottom
*
* <pre>
* **********************************
* * *
* * *
* * UpperComposite *
* * *
* * *
* * ****************************** *
* ** HideButton **
* * ****************************** *
* *------Sash----------------------*
* * *
* * *
* * *
* * *
* * BottomComposite *
* * *
* * *
* * *
* * *
* *-----Progress monitor-----------*
* **********************************
* </pre>
*
* @author Danail Branekov
*/
public abstract class SlidingComposite
{
private static final int HIDE_CANVAS_HEIGHT = 20;
private static final int RESULT_HEIGHT_TO_SHOW_RESULT_TOOLTIP = 66;
private Composite parentComposite;
private Composite upperComposite;
private Composite bottomComposite;
private Sash sash;
private Canvas hideButton;
private final ViewProgressMonitor viewProgressMonitor;
private FormToolkit formToolkit;
private final Property<Boolean> upperVisible;
private final Property<ORIENTATION> _currentOrientation;
private final Property<Point> upperCompositeSizeProperty;
private final ScrolledComposite scrolledUpperComposite;
private final SlidingCompositeStatePainter _horizontalPainter;
private final SlidingCompositeStatePainter _verticalPainter;
private final BalloonTooltipFactory tooltipFactory = new BalloonTooltipFactory();
/**
* Enumeration which indicates the composite orientation
* @author Danail Branekov
*
*/
public enum ORIENTATION
{
HORIZONTAL, VERTICAL;
}
/**
* Interface for computing the offset of a control relatively to its "zero" position. When the orientation is vertical, the "zero" position is the up side of the parent, otherwise "zero" is considered to be the left parent side
*
* @author Danail Branekov
*
*/
public interface IOffsetCalculator
{
/**
* Determines the default offset of the control under the current circumstances. This method is useful when UI components' size changed and one wants to adjust a control's offset accordingly
*/
public int determineDefaultOffest();
/**
* Calculates the effective offset based on the desired offset specified. The effective offset may differ from the desired one since the calculator will apply additional constraints on the offset value
*
* @param desiredOffset
* the offset which is desired
* @return the effective offset which comforts certain additional restrictions. It is possible to be equal to <code>desiredOffset</code>
*/
public int effectiveOffset(final int desiredOffset);
/**
* Calculates the sash component height/width depending on the orientation
*/
public int sashComponentHeight();
}
/**
* Constructor
*
* @param parent
* the parent pane in which the composite will be created; must
* not be null
*/
public SlidingComposite(final Composite parent, final FormToolkit formToolkit)
{
this.formToolkit = formToolkit;
this._horizontalPainter = new HorizontalStatePainter(getFormToolkit());
this._verticalPainter = new VerticalStatePainter(getFormToolkit());
_currentOrientation = new Property<ORIENTATION>();
// Assume "vertical" by default. If this is not the case, the property value will change as a result of the resize events sent by the framework
_currentOrientation.set(ORIENTATION.VERTICAL);
_currentOrientation.registerValueListener(new OrientationChangedListener(), false);
parentComposite = new Composite(parent, SWT.NONE);
final FormLayout formLayout = new FormLayout();
getParentComposite().setLayout(formLayout);
scrolledUpperComposite = createScrolledComposite(getParentComposite(), this.formToolkit);
upperComposite = formToolkit.createComposite(scrolledUpperComposite, SWT.FLAT);
scrolledUpperComposite.setContent(upperComposite);
configureUpperComposite(upperComposite);
upperComposite.setLayout(new FormLayout());
upperComposite.setBackground(getBackgroundColor());
upperComposite.setForeground(getForegroundColor());
upperComposite.setBackgroundMode(SWT.INHERIT_DEFAULT);
hideButton = new Canvas(getParentComposite(), SWT.NONE);
installHideButtonTooltip(hideButton);
hideButton.addPaintListener(new HideButtonPaintListener());
hideButton.addListener(SWT.MouseUp, new HideButtonSelectionListener());
sash = new Sash(getParentComposite(), SWT.HORIZONTAL);
sash.addListener(SWT.Selection, new SashListener());
bottomComposite = formToolkit.createComposite(getParentComposite(), SWT.FLAT);
bottomComposite.setLayout(new FormLayout());
bottomComposite.setBackground(getBackgroundColor());
bottomComposite.setForeground(getForegroundColor());
bottomComposite.setBackgroundMode(SWT.INHERIT_DEFAULT);
configureBottomComposite(bottomComposite);
viewProgressMonitor = new ViewProgressMonitor(getParentComposite(), Arrays.asList(new Control[] { upperComposite, bottomComposite }));
adjustControlsLayoutData();
this.getParentComposite().layout(true, true);
this.getParentComposite().addListener(SWT.Resize, new ParentCompositeResizeListener());
upperVisible = new Property<Boolean>();
upperVisible.set(true);
upperVisible.registerValueListener(new IPropertyAttributeListener<Boolean>()
{
@Override
public void attributeChanged(PropertyAttributeChangedEvent<Boolean> event)
{
if (event.getNewAttribute())
{
switchUpperVisibilityOn();
} else
{
switchUpperVisibilityOff();
}
}
}, false);
this.upperCompositeSizeProperty = new Property<Point>();
this.upperCompositeSizeProperty.set(upperComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
upperCompositeSizeProperty.registerValueListener(new IPropertyAttributeListener<Point>()
{
@Override
public void attributeChanged(final PropertyAttributeChangedEvent<Point> event)
{
moveSashToPosition(sashOffsetCalculator(getCurrentOrientation()).determineDefaultOffest());
scrolledUpperComposite.setMinHeight(event.getNewAttribute().y);
scrolledUpperComposite.setMinWidth(getCurrentOrientation() == ORIENTATION.HORIZONTAL ? event.getNewAttribute().x : 0);
}
}, true);
}
private void installHideButtonTooltip(final Canvas control)
{
final ControlTooltipManager ttManager = new ControlTooltipManager(control, new TooltipCreator())
{
@Override
protected IToolTipConfigurator createInformation(Control control)
{
return hideParamsButtonTooltipConfig();
}
};
ttManager.install(control);
}
private IToolTipConfigurator hideParamsButtonTooltipConfig()
{
return new ToolTipConfigurator()
{
@Override
protected String textContent()
{
return upperVisible.get() ? DiscoveryUIMessages.SlidingComposite_TooltipMessage_Click_To_Hide + "\n" + DiscoveryUIMessages.SlidingComposite_TooltipMessage_Drag_To_Resize : //$NON-NLS-1$
DiscoveryUIMessages.SlidingComposite_TooltipMessage_Click_To_Restore;
}
};
}
private ORIENTATION getCurrentOrientation()
{
return _currentOrientation.get();
}
private ScrolledComposite createScrolledComposite(final Composite p, final FormToolkit ft) {
final ScrolledComposite sc = new ScrolledComposite(p, SWT.V_SCROLL);
sc.setLayout(new FormLayout());
ft.adapt(sc);
sc.setExpandHorizontal(true);
sc.setExpandVertical(true);
sc.getVerticalBar().setPageIncrement(160);
sc.getVerticalBar().setIncrement(16);
return sc;
}
private Color getForegroundColor() {
return getFormToolkit().getColors().getColor(IFormColors.H_GRADIENT_START);
}
private Color getBackgroundColor() {
return getFormToolkit().getColors().getColor(IFormColors.H_GRADIENT_END);
}
private Composite getParentComposite() {
return parentComposite;
}
/**
* Extenders can override this method in order to configure the upper part.
* The <code>upperComposite</code> layout is set to {@link FormLayout}
*
* @param parent
*/
protected abstract void configureUpperComposite( final Composite upperComposite);
/**
* Extenders can override this method in order to configure the bottom part.
* The <code>bottomComposite</code> layout is set to {@link FormLayout}
*
* @param parent
*/
protected abstract void configureBottomComposite( final Composite bottomComposite);
/**
* Orientation has changed. Extenders should react accordingly
* @param newOrientaion the new orientation
*/
protected abstract void onOrientationChange(final ORIENTATION newOrientation);
/**
* Adjust the UI controls layout data according to the current orientation. This mehtod DOES not call {@link Composite#layout()} method - this is a responsibility of the caller
*/
private void adjustControlsLayoutData()
{
final boolean isCurrentlyVertical = getCurrentOrientation() == ORIENTATION.VERTICAL;
final FormData upperScrolledCompositeData = isCurrentlyVertical ? defaultFormDataVertical(null) : defaultFormDataHorizontal(null);
final FormData upperCompositeData = isCurrentlyVertical ? defaultFormDataVertical(null) : defaultFormDataHorizontal(null);
final FormData hideButtonData = isCurrentlyVertical ? defaultFormDataVertical(scrolledUpperComposite) : defaultFormDataHorizontal(scrolledUpperComposite);
final FormData sashData = isCurrentlyVertical ? defaultFormDataVertical(hideButton) : defaultFormDataHorizontal(hideButton);
final FormData bottomCompositeData = isCurrentlyVertical ? defaultFormDataVertical(sash) : defaultFormDataHorizontal(sash);
setStatusLineControlFormData();
scrolledUpperComposite.setLayoutData(upperScrolledCompositeData);
upperComposite.setLayoutData(upperCompositeData);
hideButton.setLayoutData(hideButtonData);
sash.setLayoutData(sashData);
bottomCompositeData.bottom = new FormAttachment(statusLineControl(), 0);
bottomComposite.setLayoutData(bottomCompositeData);
painter().adjustControlsLayoutData(upperScrolledCompositeData, hideButtonData, sashData, bottomCompositeData, hideButton, HIDE_CANVAS_HEIGHT);
}
private void setStatusLineControlFormData() {
final FormData pmFormData = new FormData();
pmFormData.left = new FormAttachment(0, 0);
pmFormData.right = new FormAttachment(100, 0);
pmFormData.bottom = new FormAttachment(100, 0);
statusLineControl().setLayoutData(pmFormData);
}
private FormData defaultFormDataVertical(final Control upNeighbour) {
final FormData fd = new FormData();
fd.top = upNeighbour != null ? new FormAttachment(upNeighbour, 0) : new FormAttachment(0, 0);
fd.left = new FormAttachment(0, 0);
fd.right = new FormAttachment(100, 0);
return fd;
}
private FormData defaultFormDataHorizontal(final Control leftNeighbour) {
final FormData fd = new FormData();
fd.top = new FormAttachment(0, 0);
fd.left = leftNeighbour != null ? new FormAttachment(leftNeighbour) : new FormAttachment(0, 0);
fd.bottom = new FormAttachment(viewProgressMonitor.getControl());
return fd;
}
public IProgressMonitor getProgressMonitor() {
return this.viewProgressMonitor;
}
private Control statusLineControl() {
return viewProgressMonitor.getControl();
}
private FormData getScrolledUpperCompositeLayoutData() {
return (FormData) scrolledUpperComposite.getLayoutData();
}
private FormData getHideButtonLayoutData() {
return (FormData) hideButton.getLayoutData();
}
private FormData getSashLayoutData() {
return (FormData) sash.getLayoutData();
}
private FormData getBottomCompositeLayoutData() {
return (FormData) bottomComposite.getLayoutData();
}
/**
* Move sash to the position specified. If the
* <code>position</code> is less then {@link #HIDE_CANVAS_HEIGHT}, the
* sash is moved to {@link #HIDE_CANVAS_HEIGHT} vertical offset. Also, the sash
* cannot be moved lower than the default height size of the upper composite
*
* @param position - position
*/
private void moveSashToPosition(final int position)
{
final int effectiveSashOffset = sashOffsetCalculator(getCurrentOrientation()).effectiveOffset(position);
final FormData hideButtonFormData = getHideButtonLayoutData();
final FormData sashFormData = getSashLayoutData();
final FormData upperScrolledCompositeFormData = getScrolledUpperCompositeLayoutData();
final FormData bottomCompositeFormData = getBottomCompositeLayoutData();
painter().arrangeControls(hideButtonFormData, sashFormData, upperScrolledCompositeFormData, bottomCompositeFormData , effectiveSashOffset, HIDE_CANVAS_HEIGHT);
this.getParentComposite().layout();
hideButton.redraw();
}
/**
* Hides the upper part of the composite
*/
protected void switchUpperVisibilityOff() {
assert upperVisible.get();
sash.setVisible(false);
moveSashToPosition(sashOffsetCalculator(getCurrentOrientation()).sashComponentHeight());
final FormData hideButtonFormData = getHideButtonLayoutData();
final FormData bottomCompositeFormData = getBottomCompositeLayoutData();
painter().arrangeControlsOnUpperVisibilityOff(hideButtonFormData, bottomCompositeFormData,HIDE_CANVAS_HEIGHT);
this.getParentComposite().layout();
}
/**
* Shows the upper part of the composite
*/
protected void switchUpperVisibilityOn() {
assert !upperVisible.get();
sash.setVisible(true);
adjustControlsLayoutData();
}
/**
* Listener for the hide/show bottom part button. When the bottom part is
* shown, its top edge is restored to the position before hiding or to the
* middle of the parent control
*
* @author Danail Branekov
*/
private class HideButtonSelectionListener implements Listener
{
private int lastKnownSashPosition;
@Override
public void handleEvent(Event event)
{
if (upperVisible.get())
{
lastKnownSashPosition = getSashOffset();
upperVisible.set(false);
} else
{
upperVisible.set(true);
moveSashToPosition(lastKnownSashPosition);
}
}
}
private int getSashOffset() {
return getCurrentOrientation() == ORIENTATION.VERTICAL ? sash.getBounds().y + sash.getBounds().height : sash.getBounds().x + sash.getBounds().width;
}
/**
* Listens for parent resize events and places the sash at a position which will be not hidden as a result of parent resizing
*
* @author Danail Branekov
*/
private class ParentCompositeResizeListener implements Listener
{
public void handleEvent(Event event)
{
final Rectangle rect = parentComposite.getClientArea();
_currentOrientation.set(rect.width >= rect.height ? ORIENTATION.HORIZONTAL : ORIENTATION.VERTICAL);
final int currentSashOffset = getSashOffset();
final IOffsetCalculator calc = sashOffsetCalculator(getCurrentOrientation());
// We want to preserve the current sash offset on resize. However, the current offset might be inappropriate and therefore compute the effective one
final int effectiveOffset = calc.effectiveOffset(currentSashOffset);
if(currentSashOffset != effectiveOffset)
{
moveSashToPosition(effectiveOffset);
}
}
}
private class OrientationChangedListener implements IPropertyAttributeListener<ORIENTATION>
{
@Override
public void attributeChanged(final PropertyAttributeChangedEvent<ORIENTATION> event)
{
createNewSash(event.getNewAttribute() == ORIENTATION.VERTICAL ? SWT.HORIZONTAL : SWT.VERTICAL);
onOrientationChange(event.getNewAttribute());
adjustControlsLayoutData();
scrolledUpperComposite.layout(true, true);
upperCompositeSizeProperty.set(upperComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
moveSashToPosition(sashOffsetCalculator(event.getNewAttribute()).determineDefaultOffest());
}
private void createNewSash(int orientation)
{
if (sash != null)
{
sash.dispose();
sash = new Sash(getParentComposite(), orientation);
sash.addListener(SWT.Selection, new SashListener());
}
}
}
/**
* Listens for sash events and moves the sash to the position specified. It
* also takes care the sash not to overlap the hide button
*
* @author Danail Branekov
*
*/
private class SashListener implements Listener
{
public void handleEvent(Event event)
{
final IOffsetCalculator offsetCalc = sashOffsetCalculator(getCurrentOrientation());
moveSashToPosition(offsetCalc.effectiveOffset(getCurrentOrientation() == ORIENTATION.VERTICAL ? Math.max(event.y, offsetCalc.sashComponentHeight()) : event.x));
}
}
protected FormToolkit getFormToolkit() {
return formToolkit;
}
/**
* This method will be invoked by clients whenever the upper composite size has been changed (e.g. after expanding an element)
*
* @param event
*/
public void upperCompositeSizeChanged(final PropertyAttributeChangedEvent<Point> event)
{
upperCompositeSizeProperty.set(event.getNewAttribute());
}
public void setMessage(final String message)
{
this.viewProgressMonitor.setMessage(message);
}
private class HideButtonPaintListener implements PaintListener
{
@Override
public void paintControl(final PaintEvent e)
{
if (upperComposite == null)
{
return;
}
final Rectangle clientArea = hideButton.getClientArea();
// fill whole rectangle with BG color
e.gc.setBackground(getBackgroundColor());
e.gc.fillRectangle(clientArea);
if (upperVisible.get())
{
painter().printSliderButtonUp(e, clientArea);
} else
{
painter().printSliderButtonDown(e, clientArea);
}
}
}
private SlidingCompositeStatePainter painter()
{
switch(getCurrentOrientation())
{
case VERTICAL:
return this._verticalPainter;
case HORIZONTAL:
return this._horizontalPainter;
default:
throw new IllegalStateException("Unsupported orientation"); //$NON-NLS-1$
}
}
/**
* Return a new instance of the sash offset calculator. The calculator instance is not chached as its input data is a subject of change
*/
private IOffsetCalculator sashOffsetCalculator(final ORIENTATION orientation)
{
return new SashOffsetCalculator(this.sash, this.scrolledUpperComposite, upperCompositeSizeProperty.get(), this.hideButton, orientation, upperVisible.get());
}
/**
* Notifies the user that the bottom composite content changed. The default implementation would show a balloon tooltip in case the bottom composite is not visible
*
* @param notificationMessage
* the message to notify with
*/
public void notifyBottomCompositeContentChanged(final String title, final String message)
{
if ((getCurrentOrientation() == ORIENTATION.VERTICAL ? bottomComposite.getSize().y : bottomComposite.getSize().x) < RESULT_HEIGHT_TO_SHOW_RESULT_TOOLTIP)
{
showInfoBalloon(title, message);
}
}
private void showInfoBalloon(final String title, final String message)
{
final ToolTip tt = tooltipFactory.newBallonTooltip(sash, title, message);
tt.setAutoHide(false);
tt.setVisible(true);
}
private class BalloonTooltipFactory
{
public ToolTip newBallonTooltip(final Control parentControl, final String title, final String message)
{
final ToolTip theTooltip = new ToolTip(parentControl.getShell(), SWT.BALLOON | SWT.ICON_INFORMATION);
final HideTooltipListener hideListener = new HideTooltipListener(theTooltip);
theTooltip.setText(title);
theTooltip.setMessage(message);
theTooltip.setLocation(parentControl.toDisplay((int)(parentControl.getSize().x*0.66), getCurrentOrientation() == ORIENTATION.VERTICAL ? 0 : (int)(parentControl.getSize().y*0.33)));
sash.addSelectionListener(hideListener);
sash.addListener(SWT.Resize, hideListener);
hideButton.addListener(SWT.MouseUp, hideListener);
hideButton.addListener(SWT.Resize, hideListener);
bottomComposite.addListener(SWT.Resize, hideListener);
return theTooltip;
}
private class HideTooltipListener extends SelectionAdapter implements SelectionListener, Listener
{
private final ToolTip tooltip;
public HideTooltipListener(final ToolTip tt)
{
this.tooltip = tt;
}
@Override
public void widgetSelected(SelectionEvent e)
{
hideTooltip();
}
@Override
public void handleEvent(Event event)
{
hideTooltip();
}
private void hideTooltip()
{
tooltip.setVisible(false);
tooltip.dispose();
sash.removeSelectionListener(this);
sash.removeListener(SWT.Resize, this);
hideButton.removeListener(SWT.MouseUp, this);
hideButton.removeListener(SWT.Resize, this);
bottomComposite.removeListener(SWT.Resize, this);
}
}
}
}