/*******************************************************************************
* 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.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.events.ControlListener;
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.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Scrollable;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.riena.ui.swt.CompletionCombo.DropDownListener;
import org.eclipse.riena.ui.swt.facades.SWTFacade;
import org.eclipse.riena.ui.swt.utils.SWTBindingPropertyLocator;
import org.eclipse.riena.ui.swt.utils.SwtUtilities;
/**
* The border drawer needs to be registered like follows:
* <ul>
* <li>as {@link PaintListener} to the control for which the border will be drawn</li>
* <li>as {@link ControlListener} to the entire parents hierarchy of the control (including the {@link Shell})</li>
* </ul>
* The registration/unregistration is implemented in the <tt>register()</tt> and <tt>unregister()</tt> methods, which must be called by clients.
*
* @since 4.0
*/
public class BorderDrawer implements Listener {
/**
* Set a data field with this key to <code>true</code> for widgets, for which the 'legacy' border decoration should be used. The legacy border decoration
* decorates always the entire widget bounds, while the current implementation considers widget areas that are eventually not visible (widget is partially
* hidden).
*
* @since 6.1
*/
public static final String LEGACY_BORDER_DECORATION = "BorderDrawer.LEGACY_BORDER_DECORATION"; //$NON-NLS-1$
public static final int DEFAULT_BORDER_WIDTH = 1;
private final IDecorationActivationStrategy activationStrategy;
private final Control control;
private Color borderColor;
private int borderWidth;
/**
* collect all listener registrations so they can be unregistered when unregister() is called
* <p>
* IMPORTANT: these runnables must be capable to handle the case when the control is already disposed
*/
private final List<Runnable> toUnregister = new ArrayList<Runnable>();
/**
* the currently visible control area in display-relative coordinates
*/
private Rectangle visibleControlAreaOnDisplay = new Rectangle(0, 0, 0, 0);
/**
* The area that needs updating before drawing the decoration somewhere else.
*/
private Rectangle updateArea;
private int specialWidgetWidthAdjustment;
private boolean computeBorderArea = true;
private Event lastMoveEvent;
private final Listener updateListener = new Listener() {
public void handleEvent(final Event event) {
update(false);
}
};
private boolean isMasterDetails;
private Control controlToDecorate;
private boolean layouting;
private Rectangle boundsToDecorate;
private final boolean useVisibleControlArea;
private Listener borderRenderingEnforcer;
private Event lastPaintEvent;
/**
* @param control
* the UI element for which the border will be drawn, not <code>null</code>
*/
public BorderDrawer(final Control control) {
this(control, DEFAULT_BORDER_WIDTH, null, null);
}
/**
* @param control
* the UI element for which the border will be drawn, not <code>null</code>
* @param borderWidth
* the desired width of the border that will be drawn
* @param borderColor
* the desired color of the border that will be drawn
* @param activationStrategy
* the strategy that determines when the border should be drawn or <code>null</code> if the border should be always shown
*/
public BorderDrawer(final Control control, final int borderWidth, final Color borderColor, final IDecorationActivationStrategy activationStrategy) {
this(control, DEFAULT_BORDER_WIDTH, null, false, null);
}
/**
* @param control
* the UI element for which the border will be drawn, not <code>null</code>
* @param borderWidth
* the desired width of the border that will be drawn
* @param borderColor
* the desired color of the border that will be drawn
* @param useVisibleControlArea
* <code>true</code> if the border should be drawn according to the visible control area. This does not work for all control types.
* @param activationStrategy
* the strategy that determines when the border should be drawn or <code>null</code> if the border should be always shown
* @since 5.0
*/
public BorderDrawer(final Control control, final int borderWidth, final Color borderColor, final boolean useVisibleControlArea,
final IDecorationActivationStrategy activationStrategy) {
Assert.isNotNull(control);
this.control = control;
this.borderWidth = borderWidth;
this.borderColor = borderColor;
this.useVisibleControlArea = useVisibleControlArea;
this.activationStrategy = activationStrategy;
}
/**
* Registers the {@link PaintListener} to the control and {@link ControlListener} to all parents from the hierarchy (including the {@link Shell}).
*/
public void register() {
enforcePostParentBorderRendering();
if (control instanceof DatePickerComposite || control instanceof CompletionCombo) {
specialWidgetWidthAdjustment = control.getDisplay().getDPI().x / 6 + 1;
final Control[] children = ((Composite) control).getChildren();
for (final Control child : children) {
if (child instanceof Text) {
registerToControl(child, SWTFacade.Paint);
}
}
if (control instanceof CompletionCombo) {
addDropDownListener((CompletionCombo) control);
}
} else if (control instanceof CCombo) {
registerToControl(control.getParent(), SWTFacade.Paint);
} else if (!useVisibleControlArea) {
registerToControlAndChildren(control, SWTFacade.Paint);
} else if (control instanceof ChoiceComposite) {
registerToControl(((ChoiceComposite) control).getContentComposite(), SWTFacade.Paint);
} else {
registerToControl(control, SWTFacade.Paint);
}
controlToDecorate = control;
if (MasterDetailsComposite.BIND_ID_TABLE.equals(SWTBindingPropertyLocator.getInstance().locateBindingProperty(getControlToDecorate()))) {
controlToDecorate = getControlToDecorate().getParent().getParent();
isMasterDetails = true;
}
Composite parent = getControlToDecorate().getParent();
do {
registerToControl(parent, SWT.Resize, SWT.Move);
registerToControl(parent, updateListener, SWTFacade.Paint);
} while ((parent = parent.getParent()) != null);
registerMnemonicsListener();
registerToControl(getControlToDecorate(), new Listener() {
public void handleEvent(final Event event) {
dispose();
}
}, SWT.Dispose);
}
/**
* Enforces the rendering of the marker border after parent paint events as these can over paint the marker border.
*/
private void enforcePostParentBorderRendering() {
borderRenderingEnforcer = new Listener() {
public void handleEvent(final Event event) {
if (lastPaintEvent != null) {
// reuse last paint event on this control
paintControl(lastPaintEvent);
}
}
};
control.getParent().addListener(SWT.Paint, borderRenderingEnforcer);
}
/**
* @param cc
*/
private void addDropDownListener(final CompletionCombo cc) {
final DropDownListener listener = new DropDownListener() {
public void hidden() {
computeBorderArea = true;
update(true);
}
};
cc.addDropDownListener(listener);
toUnregister.add(new Runnable() {
public void run() {
cc.removeDropDownListener(listener);
}
});
}
/**
* @param children
* @param paint
*/
private void registerToControlAndChildren(final Control control, final int... eventTypes) {
registerToControl(control, eventTypes);
if (control instanceof Composite) {
for (final Control child : ((Composite) control).getChildren()) {
registerToControlAndChildren(child, eventTypes);
}
}
}
/**
* Unregisters the {@link PaintListener} and {@link ControlListener} from the control and all parents.
*/
public void dispose() {
for (final Runnable r : toUnregister) {
r.run();
}
toUnregister.clear();
}
/**
* @param borderColor
* the borderColor to set
*/
public void setBorderColor(final Color borderColor) {
this.borderColor = borderColor;
}
/**
* @return the borderColor
*/
public Color getBorderColor() {
return borderColor;
}
/**
* @param borderWidth
* the borderWidth to set
*/
public void setBorderWidth(final int borderWidth) {
this.borderWidth = borderWidth;
}
/**
* @return the borderWidth
*/
public int getBorderWidth() {
return borderWidth;
}
public void paintControl(final Event event) {
if (computeBorderArea) {
Rectangle visibleControlArea;
final Object useLegacyBorderDecoration = getControlToDecorate().getData(LEGACY_BORDER_DECORATION);
if (!useVisibleControlArea || getControlToDecorate() instanceof CCombo || isMasterDetails || getControlToDecorate() instanceof ChoiceComposite
|| (useLegacyBorderDecoration instanceof Boolean && (Boolean) useLegacyBorderDecoration)) {
if (getControlToDecorate() instanceof Composite) {
visibleControlArea = ((Composite) getControlToDecorate()).getClientArea();
} else {
visibleControlArea = getControlToDecorate().getBounds();
visibleControlArea.x = 0;
visibleControlArea.y = 0;
}
} else {
visibleControlArea = getVisibleControlArea(event);
}
visibleControlAreaOnDisplay = includeBorder(toVisibleControlAreaOnDisplay(visibleControlArea));
computeBorderArea = false;
}
if (shouldShowDecoration()) {
Composite someParent = getControlToDecorate() instanceof Composite ? (Composite) getControlToDecorate() : getControlToDecorate().getParent();
Control someChild = getControlToDecorate();
boolean fullyPainted = false;
while (someParent != null && !fullyPainted) {
fullyPainted = includeAndDrawBorder(someParent, someChild);
someChild = someParent;
someParent = someParent.getParent();
}
}
}
/**
* request an update at the UI event queue end
*
* @since 5.0
*/
public void scheduleUpdate(final boolean redraw) {
getControlToDecorate().getDisplay().asyncExec(new Runnable() {
public void run() {
update(redraw);
}
});
}
/**
* Updates the area where the border is normally drawn
*/
public void update(final boolean redraw) {
if (SwtUtilities.isDisposed(getControlToDecorate())) {
return;
}
final Shell shell = getControlToDecorate().getShell();
final boolean redrawLater = updateArea == null;
if (redraw && !redrawLater) {
shell.redraw(updateArea.x, updateArea.y, updateArea.width, updateArea.height, true);
}
Rectangle bounds = getControlToDecorate().getBounds();
bounds.x = bounds.y = 0;
bounds = getVisibleControlAreaStartingWith(bounds, null);
final Rectangle onDisplay = toVisibleControlAreaOnDisplay(bounds);
updateArea = includeBorder(toAreaOnControl(onDisplay, shell));
updateArea.x = updateArea.x - 1;
updateArea.y = updateArea.y - 1;
updateArea.width = updateArea.width + 2;
updateArea.height = updateArea.height + 2;
if (redraw && redrawLater) {
shell.redraw(updateArea.x, updateArea.y, updateArea.width, updateArea.height, true);
}
}
// helping methods
//////////////////
/**
* Needed for the mnemonics handling in windows:
* <p>
* Pressing the ALT key causes the mnemonics to appear -> all mnemonic-able controls will be redrawn. This redraw causes the borders on non-mnemonic-able
* widgets to disappear. This happens only the first time the mnemonics are triggered (the first time ALT is pressed).
*/
private void registerMnemonicsListener() {
final Listener altListener = new Listener() {
public void handleEvent(final Event event) {
if (event.keyCode == SWT.ALT) {
update(true);
getControlToDecorate().getDisplay().removeFilter(SWT.KeyDown, this);
}
}
};
final Display display = getControlToDecorate().getDisplay();
display.addFilter(SWT.KeyDown, altListener);
toUnregister.add(new Runnable() {
public void run() {
display.removeFilter(SWT.KeyDown, altListener);
}
});
}
/**
* Register as listener for the given event types on the given control. This method will automatically add an unregister runnable. There is no need to
* manually unregister this registration.
*
* @param control
* the control to register the listener
* @param eventTypes
* the event types to listen for
*/
private void registerToControl(final Control control, final int... eventTypes) {
registerToControl(control, this, eventTypes);
}
private void registerToControl(final Control control, final Listener listener, final int... eventTypes) {
for (final int eventType : eventTypes) {
control.addListener(eventType, listener);
}
toUnregister.add(new Runnable() {
public void run() {
if (!control.isDisposed()) {
for (final int eventType : eventTypes) {
control.removeListener(eventType, listener);
}
}
}
});
}
/**
* Returns whether the decoration should be shown or it should not.
*
* @return {@code true} if the decoration should be shown, {@code false} if it should not.
*/
private boolean shouldShowDecoration() {
if (SwtUtilities.isDisposed(getControlToDecorate())) {
return false;
}
if (!getControlToDecorate().isVisible()) {
return false;
}
if (borderWidth <= 0) {
return false;
}
if (activationStrategy != null && !activationStrategy.isActive()) {
return false;
}
return !layouting;
}
/**
* @param someChild
* @return true if the border could be completely painted on the available client area
*/
private boolean includeAndDrawBorder(final Composite someParent, final Control someChild) {
final Rectangle areaOnControl = toAreaOnControl(visibleControlAreaOnDisplay, someParent);
final Rectangle clientArea = someParent.getClientArea();
final GC gc = new GC(someParent);
drawBorder(areaOnControl, gc);
gc.dispose();
return someChild.getBounds().x > getBorderWidth() && someChild.getBounds().y > getBorderWidth()
&& areaOnControl.x + areaOnControl.width < clientArea.width && areaOnControl.y + areaOnControl.width < clientArea.height;
}
/**
* draws the border <i>inside</i> the given area
*/
private void drawBorder(final Rectangle rect, final GC gc) {
if ((rect.width == 0) && (rect.height == 0)) {
return;
}
final Color previousForeground = gc.getForeground();
if (borderColor != null) {
gc.setForeground(borderColor);
}
for (int i = 0; i < borderWidth; i++) {
gc.drawRectangle(rect.x + i, rect.y + i, rect.width - 2 * i, rect.height - 2 * i);
}
gc.setForeground(previousForeground);
}
/**
* retrieve the visible area of the given control (including scroll bars)
*/
private Rectangle getVisibleControlArea(final Event event) {
final Rectangle visibleControlArea = new Rectangle(0, 0, 0, 0);
visibleControlArea.width = event.x > 0 ? event.x + event.width : event.width;
visibleControlArea.height = event.y > 0 ? event.y + event.height : event.height;
return getVisibleControlAreaStartingWith(visibleControlArea, event);
}
/**
* @param visibleControlArea
* @param event
* @return
*/
private Rectangle getVisibleControlAreaStartingWith(final Rectangle visibleControlArea, final Event event) {
// if some scroll bars are visible, their size must be also included
if (getControlToDecorate() instanceof Scrollable) {
final ScrollBar horizontalBar = ((Scrollable) getControlToDecorate()).getHorizontalBar();
if (horizontalBar != null && horizontalBar.isVisible()) {
visibleControlArea.height += horizontalBar.getSize().y;
}
final ScrollBar verticalBar = ((Scrollable) getControlToDecorate()).getVerticalBar();
if (verticalBar != null && verticalBar.isVisible()) {
visibleControlArea.width += verticalBar.getSize().x;
}
}
// some special handling for the tree widget
if (getControlToDecorate() instanceof Tree) {
final Tree t = (Tree) getControlToDecorate();
if (t.getColumnCount() > 0 && event != null) {
visibleControlArea.x = event.x;
visibleControlArea.width -= event.x;
}
if (t.getHeaderVisible()) {
visibleControlArea.y -= t.getHeaderHeight();
visibleControlArea.height += t.getHeaderHeight();
}
// System.out.println(visibleControlArea);
} else if (getControlToDecorate() instanceof DatePickerComposite && visibleControlArea.width + specialWidgetWidthAdjustment
+ 2 * getControlToDecorate().getBorderWidth() - 1 == getControlToDecorate().getBounds().width) {
visibleControlArea.width += specialWidgetWidthAdjustment;
} else if (getControlToDecorate() instanceof CompletionCombo) {
final Control[] children = ((Composite) getControlToDecorate()).getChildren();
if (children.length == 3) {
// we also have a label
// to understand this code, look at CompletionCombo.computeSize(...)
final GC gc = new GC(children[0]);
final int spacer = gc.stringExtent(" ").x; //$NON-NLS-1$
gc.dispose();
visibleControlArea.width += children[0].getBounds().width + spacer - 1;
}
if (visibleControlArea.width + specialWidgetWidthAdjustment + 2 * getControlToDecorate().getBorderWidth()
+ 1 >= getControlToDecorate().getBounds().width) {
visibleControlArea.width += specialWidgetWidthAdjustment;
}
}
return visibleControlArea;
}
/**
* transform the given area from display-relative to target control relative coordinates
*/
private Rectangle toAreaOnControl(final Rectangle onDisplay, final Control target) {
final Point onControl = target.toControl(onDisplay.x, onDisplay.y);
return new Rectangle(onControl.x, onControl.y, onDisplay.width, onDisplay.height);
}
/**
* @return include the border size on each side of the given rectangle
*/
private Rectangle includeBorder(final Rectangle visibleControlArea) {
final int controlBorder = getControlToDecorate().getBorderWidth();
final int border = controlBorder + borderWidth;
final int x = visibleControlArea.x - border;
final int y = visibleControlArea.y - border;
final int width = visibleControlArea.width + 2 * border - 1;
final int height = visibleControlArea.height + 2 * border - 1;
return new Rectangle(x, y, width, height);
}
/**
* @return the visible control area relative to its display
*/
private Rectangle toVisibleControlAreaOnDisplay(final Rectangle visibleControlArea) {
final Point onDisplay = getControlToDecorate().toDisplay(visibleControlArea.x, visibleControlArea.y);
return new Rectangle(onDisplay.x, onDisplay.y, visibleControlArea.width, visibleControlArea.height);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event)
*/
public void handleEvent(final Event event) {
switch (event.type) {
case SWT.Move:
computeBorderArea = true;
boundsToDecorate = getControlToDecorate().getDisplay().map(getControlToDecorate(), null, getControlToDecorate().getBounds());
lastMoveEvent = event;
if (SwtUtilities.isDisposed(getControlToDecorate())) {
break;
}
update(false);
getControlToDecorate().getDisplay().timerExec(200, new Runnable() {
public void run() {
if (event == lastMoveEvent) {
if (!SwtUtilities.isDisposed(getControlToDecorate())) {
getControlToDecorate().redraw();
}
}
}
});
break;
case SWT.Resize:
computeBorderArea = true;
boundsToDecorate = getControlToDecorate().getDisplay().map(getControlToDecorate(), null, getControlToDecorate().getBounds());
update(true);
break;
case SWTFacade.Paint:
//cache the paint event
lastPaintEvent = event;
final Rectangle onDisplay = getControlToDecorate().getDisplay().map(getControlToDecorate(), null, getControlToDecorate().getBounds());
if (boundsToDecorate == null) {
boundsToDecorate = onDisplay;
}
// this hacky workaround is needed for the case when a layout()
// caused the control to decorate to be moved or resized
// in this case, we don't get a resize or move event
if (!onDisplay.equals(boundsToDecorate) && !computeBorderArea) {
boundsToDecorate = onDisplay;
layouting = true;
final Shell shell = getControlToDecorate().getShell();
shell.redraw();
layouting = false;
computeBorderArea = true;
final Rectangle b = shell.getBounds();
shell.redraw(0, 0, b.width, b.height, true);
} else {
paintControl(event);
}
break;
default:
break;
}
}
/**
* @return the control
*/
private Control getControlToDecorate() {
return controlToDecorate;
}
}