/******************************************************************************* * Copyright (c) 2000, 2010 IBM Corporation 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: * IBM Corporation - initial API and implementation * Austin Riddle (Texas Center for Applied Technology) - initial RAP port *******************************************************************************/ package org.eclipse.draw2d; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DragDetectListener; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MenuDetectListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Layout; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Slider; import org.eclipse.draw2d.geometry.Dimension; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.Rectangle; /** * A scrolling Canvas that contains {@link Figure Figures} viewed through a * {@link Viewport}. Call {@link #setContents(IFigure)} to specify the root of * the tree of <tt>Figures</tt> to be viewed through the <tt>Viewport</tt>. * <p> * Normal procedure for using a FigureCanvas: * <ol> * <li>Create a FigureCanvas. * <li>Create a Draw2d Figure and call {@link #setContents(IFigure)}. This * Figure will be the top-level Figure of the Draw2d application. * </ol> * <dl> * <dt><b>Required Styles (when using certain constructors):</b></dt> * <dd>V_SCROLL, H_SCROLL, NO_REDRAW_RESIZE</dd> * <dt><b>Optional Styles:</b></dt> * <dd>DOUBLE_BUFFERED, RIGHT_TO_LEFT, LEFT_TO_RIGHT, NO_BACKGROUND, BORDER</dd> * </dl> * <p> * Note: Only one of the styles RIGHT_TO_LEFT, LEFT_TO_RIGHT may be specified. * </p> */ public class FigureCanvas extends Composite { private static final int ACCEPTED_STYLES = SWT.RIGHT_TO_LEFT | SWT.LEFT_TO_RIGHT | SWT.V_SCROLL | SWT.H_SCROLL | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE | SWT.DOUBLE_BUFFERED | SWT.BORDER; /** * The default styles are mixed in when certain constructors are used. This * constant is a bitwise OR of the following SWT style constants: * <UL> * <LI>{@link SWT#NO_REDRAW_RESIZE}</LI> * <LI>{@link SWT#NO_BACKGROUND}</LI> * <LI>{@link SWT#V_SCROLL}</LI> * <LI>{@link SWT#H_SCROLL}</LI> * </UL> */ static final int DEFAULT_STYLES = SWT.NO_REDRAW_RESIZE | SWT.NO_BACKGROUND | SWT.V_SCROLL | SWT.H_SCROLL; private static final int REQUIRED_STYLES = SWT.NO_REDRAW_RESIZE | SWT.V_SCROLL | SWT.H_SCROLL; /** Never show scrollbar */ public static int NEVER = 0; /** Automatically show scrollbar when needed */ public static int AUTOMATIC = 1; /** Always show scrollbar */ public static int ALWAYS = 2; protected Slider horizontalBar, verticalBar; protected Canvas innerCanvas; private int vBarVisibility = AUTOMATIC; private int hBarVisibility = AUTOMATIC; private Viewport viewport; private Font font; private int hBarOffset; private int vBarOffset; private PropertyChangeListener horizontalChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent event) { RangeModel model = getViewport().getHorizontalRangeModel(); hBarOffset = Math.max(0, -model.getMinimum()); Slider horizontalBar = getHorizontalSlider(); if (horizontalBar != null) { horizontalBar.setValues(model.getValue() + hBarOffset, model.getMinimum() + hBarOffset, model.getMaximum() + hBarOffset, model.getExtent(), Math.max(1, model.getExtent() / 20), Math.max(1, model.getExtent() * 3 / 4)); } } }; private PropertyChangeListener verticalChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent event) { RangeModel model = getViewport().getVerticalRangeModel(); vBarOffset = Math.max(0, -model.getMinimum()); Slider verticalBar = getVerticalSlider(); if (verticalBar != null) { verticalBar.setValues(model.getValue() + vBarOffset, model.getMinimum() + vBarOffset, model.getMaximum() + vBarOffset, model.getExtent(), Math.max(1, model.getExtent() / 20), Math.max(1, model.getExtent() * 3 / 4)); } } }; private final LightweightSystem lws; /** * Creates a new FigureCanvas with the given parent and the * {@link #DEFAULT_STYLES}. * * @param parent * the parent */ public FigureCanvas(Composite parent) { this(parent, SWT.DOUBLE_BUFFERED, new LightweightSystem()); } /** * Constructor which applies the default styles plus any optional styles * indicated. * * @param parent * the parent composite * @param style * see the class javadoc for optional styles * @since 3.1 */ public FigureCanvas(Composite parent, int style) { this(parent, style, new LightweightSystem()); } /** * Constructor which uses the given styles verbatim. Certain styles must be * used with this class. Refer to the class javadoc for more details. * * @param style * see the class javadoc for <b>required</b> and optional styles * @param parent * the parent composite * @since 3.4 */ public FigureCanvas(int style, Composite parent) { this(style, parent, new LightweightSystem()); } /** * Constructs a new FigureCanvas with the given parent and * LightweightSystem, using the {@link #DEFAULT_STYLES}. * * @param parent * the parent * @param lws * the LightweightSystem */ public FigureCanvas(Composite parent, LightweightSystem lws) { this(parent, SWT.DOUBLE_BUFFERED, lws); } /** * Constructor taking a lightweight system and SWT style, which is used * verbatim. Certain styles must be used with this class. Refer to the class * javadoc for more details. * * @param style * see the class javadoc for <b>required</b> and optional styles * @param parent * the parent composite * @param lws * the LightweightSystem * @since 3.4 */ public FigureCanvas(int style, Composite parent, LightweightSystem lws) { super(parent, checkStyle(style)); setLayout(new Layout() { protected org.eclipse.swt.graphics.Point computeSize( Composite composite, int wHint, int hHint, boolean flushCache) { return innerCanvas.computeSize(wHint, hHint, flushCache); } protected void layout(Composite composite, boolean flushCache) { org.eclipse.swt.graphics.Point size = composite.getSize(); int vScrollBarWidth = getVScrollBarWidth(); int hScrollBarHeight = getHScrollBarHeight(); if (!getHorizontalSlider().isVisible()) { hScrollBarHeight = 0; } if (!getVerticalSlider().isVisible()) { vScrollBarWidth = 0; } getHorizontalSlider().setBounds(0, size.y - hScrollBarHeight, size.x - vScrollBarWidth, hScrollBarHeight); getVerticalSlider().setBounds(size.x - vScrollBarWidth, 0, vScrollBarWidth, size.y - hScrollBarHeight); innerCanvas.setLocation(0, 0); innerCanvas.setSize(size.x - vScrollBarWidth, size.y - hScrollBarHeight); } }); Slider horizontalBar = getHorizontalSlider(); if (horizontalBar != null) { horizontalBar.setVisible(false); } Slider verticalBar = getVerticalSlider(); if (verticalBar != null) { verticalBar.setVisible(false); } this.lws = lws; lws.setControl(getInnerCanvas()); hook(); } public org.eclipse.swt.graphics.Rectangle getClientArea() { return innerCanvas.getClientArea(); } private Canvas getInnerCanvas() { if (innerCanvas == null) { innerCanvas = new Canvas(this, getStyle()); this.setData(Canvas.class.getName(), innerCanvas); } return innerCanvas; } /** * Constructor * * @param parent * the parent composite * @param style * look at class javadoc for valid styles * @param lws * the lightweight system * @since 3.1 */ public FigureCanvas(Composite parent, int style, LightweightSystem lws) { this(style | DEFAULT_STYLES, parent, lws); } private static int checkStyle(int style) { if ((style & REQUIRED_STYLES) != REQUIRED_STYLES) throw new IllegalArgumentException( "Required style missing on FigureCanvas"); //$NON-NLS-1$ if ((style & ~ACCEPTED_STYLES) != 0) throw new IllegalArgumentException( "Invalid style being set on FigureCanvas"); //$NON-NLS-1$ return style; } public void addFocusListener(FocusListener listener) { super.addFocusListener(listener); getInnerCanvas().addFocusListener(listener); } public void addDragDetectListener(DragDetectListener listener) { super.addDragDetectListener(listener); getInnerCanvas().addDragDetectListener(listener); } public void addMenuDetectListener(MenuDetectListener listener) { super.addMenuDetectListener(listener); getInnerCanvas().addMenuDetectListener(listener); } public void addKeyListener(KeyListener listener) { super.addKeyListener(listener); getInnerCanvas().addKeyListener(listener); } public void addListener(int eventType, Listener listener) { super.addListener(eventType, listener); getInnerCanvas().addListener(eventType, listener); } public void removeFocusListener(FocusListener listener) { super.removeFocusListener(listener); getInnerCanvas().removeFocusListener(listener); } public void removeDragDetectListener(DragDetectListener listener) { super.removeDragDetectListener(listener); getInnerCanvas().removeDragDetectListener(listener); } public void removeMenuDetectListener(MenuDetectListener listener) { super.removeMenuDetectListener(listener); getInnerCanvas().removeMenuDetectListener(listener); } public void removeKeyListener(KeyListener listener) { super.removeKeyListener(listener); getInnerCanvas().removeKeyListener(listener); } public void removeListener(int eventType, Listener listener) { super.removeListener(eventType, listener); getInnerCanvas().removeListener(eventType, listener); } /** * @see org.eclipse.swt.widgets.Composite#computeSize(int, int, boolean) */ public org.eclipse.swt.graphics.Point computeSize(int wHint, int hHint, boolean changed) { // TODO Still doesn't handle scrollbar cases, such as when a constrained // width // would require a horizontal scrollbar, and therefore additional // height. int borderSize = computeTrim(0, 0, 0, 0).x * -2; if (wHint >= 0) wHint = Math.max(0, wHint - borderSize); if (hHint >= 0) hHint = Math.max(0, hHint - borderSize); Dimension size = getLightweightSystem().getRootFigure() .getPreferredSize(wHint, hHint) .getExpanded(borderSize, borderSize); size.union(new Dimension(wHint, hHint)); return new org.eclipse.swt.graphics.Point(size.width, size.height); } /** * @return the contents of the {@link Viewport}. */ public IFigure getContents() { return getViewport().getContents(); } /** * @see org.eclipse.swt.widgets.Control#getFont() */ public Font getFont() { if (font == null) font = super.getFont(); return font; } /** * @return the horizontal scrollbar visibility. */ public int getHorizontalScrollBarVisibility() { return hBarVisibility; } int getVScrollBarWidth() { return 16; } int getHScrollBarHeight() { return 16; } /** * @return the LightweightSystem */ public LightweightSystem getLightweightSystem() { return lws; } /** * @return the vertical scrollbar visibility. */ public int getVerticalScrollBarVisibility() { return vBarVisibility; } /** * Returns the Viewport. If it's <code>null</code>, a new one is created. * * @return the viewport */ public Viewport getViewport() { if (viewport == null) setViewport(new Viewport(true)); return viewport; } /** * Adds listeners for scrolling. */ private void hook() { getLightweightSystem().getUpdateManager().addUpdateListener( new UpdateListener() { public void notifyPainting(Rectangle damage, java.util.Map dirtyRegions) { } public void notifyValidating() { if (!isDisposed()) layoutViewport(); } }); final Slider horizontalBar = getHorizontalSlider(); if (horizontalBar != null) { horizontalBar.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { scrollToX(horizontalBar.getSelection() - hBarOffset); } }); } final Slider verticalBar = getVerticalSlider(); if (verticalBar != null) { verticalBar.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { scrollToY(verticalBar.getSelection() - vBarOffset); } }); } } private void hookViewport() { getViewport().getHorizontalRangeModel().addPropertyChangeListener( horizontalChangeListener); getViewport().getVerticalRangeModel().addPropertyChangeListener( verticalChangeListener); } private void unhookViewport() { getViewport().getHorizontalRangeModel().removePropertyChangeListener( horizontalChangeListener); getViewport().getVerticalRangeModel().removePropertyChangeListener( verticalChangeListener); } private void layoutViewport() { ScrollPaneSolver.Result result; result = ScrollPaneSolver.solve( new Rectangle(innerCanvas.getBounds()).setLocation(0, 0), getViewport(), getHorizontalScrollBarVisibility(), getVerticalScrollBarVisibility(), innerCanvas.computeTrim(0, 0, 0, 0).width, innerCanvas.computeTrim(0, 0, 0, 0).height); getLightweightSystem().setIgnoreResize(true); try { Slider horizontalBar = getHorizontalSlider(); if (horizontalBar != null && horizontalBar.getVisible() != result.showH) { horizontalBar.setVisible(result.showH); layout(new Control[] { horizontalBar }); } Slider verticalBar = getVerticalSlider(); if (verticalBar != null && verticalBar.getVisible() != result.showV) { verticalBar.setVisible(result.showV); layout(new Control[] { verticalBar }); } Rectangle r = new Rectangle(getClientArea()); r.setLocation(0, 0); getLightweightSystem().getRootFigure().setBounds(r); } finally { getLightweightSystem().setIgnoreResize(false); } } /** * Scrolls in an animated way to the new x and y location. * * @param x * the x coordinate to scroll to * @param y * the y coordinate to scroll to */ public void scrollSmoothTo(int x, int y) { // Ensure newHOffset and newVOffset are within the appropriate ranges x = verifyScrollBarOffset(getViewport().getHorizontalRangeModel(), x); y = verifyScrollBarOffset(getViewport().getVerticalRangeModel(), y); int oldX = getViewport().getViewLocation().x; int oldY = getViewport().getViewLocation().y; int dx = x - oldX; int dy = y - oldY; if (dx == 0 && dy == 0) return; // Nothing to do. Dimension viewingArea = getViewport().getClientArea().getSize(); int minFrames = 3; int maxFrames = 6; if (dx == 0 || dy == 0) { minFrames = 6; maxFrames = 13; } int frames = (Math.abs(dx) + Math.abs(dy)) / 15; frames = Math.max(frames, minFrames); frames = Math.min(frames, maxFrames); int stepX = Math.min((dx / frames), (viewingArea.width / 3)); int stepY = Math.min((dy / frames), (viewingArea.height / 3)); for (int i = 1; i < frames; i++) { scrollTo(oldX + i * stepX, oldY + i * stepY); getViewport().getUpdateManager().performUpdate(); } scrollTo(x, y); } /** * Scrolls the contents to the new x and y location. If this scroll * operation only consists of a vertical or horizontal scroll, a call will * be made to {@link #scrollToY(int)} or {@link #scrollToX(int)}, * respectively, to increase performance. * * @param x * the x coordinate to scroll to * @param y * the y coordinate to scroll to */ public void scrollTo(int x, int y) { x = verifyScrollBarOffset(getViewport().getHorizontalRangeModel(), x); y = verifyScrollBarOffset(getViewport().getVerticalRangeModel(), y); if (x == getViewport().getViewLocation().x) scrollToY(y); else if (y == getViewport().getViewLocation().y) scrollToX(x); else getViewport().setViewLocation(x, y); } /** * Scrolls the contents horizontally so that they are offset by * <code>hOffset</code>. * * @param hOffset * the new horizontal offset */ public void scrollToX(int hOffset) { hOffset = verifyScrollBarOffset( getViewport().getHorizontalRangeModel(), hOffset); int hOffsetOld = getViewport().getViewLocation().x; if (hOffset == hOffsetOld) return; int dx = -hOffset + hOffsetOld; Rectangle clientArea = getViewport().getBounds().getCropped( getViewport().getInsets()); Rectangle blit = clientArea.getResized(-Math.abs(dx), 0); Rectangle expose = clientArea.getCopy(); Point dest = clientArea.getTopLeft(); expose.width = Math.abs(dx); if (dx < 0) { // Moving left? blit.translate(-dx, 0); // Move blit area to the right expose.x = dest.x + blit.width; } else // Moving right dest.x += dx; // Move expose area to the right // fix for bug 41111 Control[] children = getInnerCanvas().getChildren(); boolean[] manualMove = new boolean[children.length]; for (int i = 0; i < children.length; i++) { org.eclipse.swt.graphics.Rectangle bounds = children[i].getBounds(); manualMove[i] = blit.width <= 0 || bounds.x > blit.x + blit.width || bounds.y > blit.y + blit.height || bounds.x + bounds.width < blit.x || bounds.y + bounds.height < blit.y; } scroll(dest.x, dest.y, blit.x, blit.y, blit.width, blit.height, true); for (int i = 0; i < children.length; i++) { if (children[i].isDisposed()) continue; org.eclipse.swt.graphics.Rectangle bounds = children[i].getBounds(); if (manualMove[i]) children[i].setBounds(bounds.x + dx, bounds.y, bounds.width, bounds.height); } getViewport().setIgnoreScroll(true); getViewport().setHorizontalLocation(hOffset); getViewport().setIgnoreScroll(false); getInnerCanvas().redraw(/*expose.x, expose.y, expose.width, expose.height, true*/); } /** * Scrolls the contents vertically so that they are offset by * <code>vOffset</code>. * * @param vOffset * the new vertical offset */ public void scrollToY(int vOffset) { vOffset = verifyScrollBarOffset(getViewport().getVerticalRangeModel(), vOffset); int vOffsetOld = getViewport().getViewLocation().y; if (vOffset == vOffsetOld) return; int dy = -vOffset + vOffsetOld; Rectangle clientArea = getViewport().getBounds().getCropped( getViewport().getInsets()); Rectangle blit = clientArea.getResized(0, -Math.abs(dy)); Rectangle expose = clientArea.getCopy(); Point dest = clientArea.getTopLeft(); expose.height = Math.abs(dy); if (dy < 0) { // Moving up? blit.translate(0, -dy); // Move blit area down expose.y = dest.y + blit.height; // Move expose area down } else // Moving down dest.y += dy; // fix for bug 41111 Control[] children = getInnerCanvas().getChildren(); boolean[] manualMove = new boolean[children.length]; for (int i = 0; i < children.length; i++) { org.eclipse.swt.graphics.Rectangle bounds = children[i].getBounds(); manualMove[i] = blit.height <= 0 || bounds.x > blit.x + blit.width || bounds.y > blit.y + blit.height || bounds.x + bounds.width < blit.x || bounds.y + bounds.height < blit.y; } scroll(dest.x, dest.y, blit.x, blit.y, blit.width, blit.height, true); for (int i = 0; i < children.length; i++) { if (children[i].isDisposed()) continue; org.eclipse.swt.graphics.Rectangle bounds = children[i].getBounds(); if (manualMove[i]) children[i].setBounds(bounds.x, bounds.y + dy, bounds.width, bounds.height); } getViewport().setIgnoreScroll(true); getViewport().setVerticalLocation(vOffset); getViewport().setIgnoreScroll(false); getInnerCanvas().redraw(/*expose.x, expose.y, expose.width, expose.height, true*/); } public void scroll(int destX, int destY, int x, int y, int width, int height, boolean all) { checkWidget(); int deltaX = destX - x, deltaY = destY - y; if (all) { Control[] children = getInnerCanvas().getChildren(); for (int i = 0; i < children.length; i++) { Control child = children[i]; org.eclipse.swt.graphics.Rectangle rect = child.getBounds(); if (Math.min(x + width, rect.x + rect.width) >= Math.max(x, rect.x) && Math.min(y + height, rect.y + rect.height) >= Math .max(y, rect.y)) { child.setLocation(rect.x + deltaX, rect.y + deltaY); } } } } /** * Sets the given border on the LightweightSystem's root figure. * * @param border * The new border */ public void setBorder(Border border) { getLightweightSystem().getRootFigure().setBorder(border); } /** * Sets the contents of the {@link Viewport}. * * @param figure * the new contents */ public void setContents(IFigure figure) { getViewport().setContents(figure); } /** * @see org.eclipse.swt.widgets.Control#setFont(org.eclipse.swt.graphics.Font) */ public void setFont(Font font) { this.font = font; super.setFont(font); } public void setMenu(Menu menu) { super.setMenu(menu); getInnerCanvas().setMenu(menu); } /** * Sets the horizontal scrollbar visibility. Possible values are * {@link #AUTOMATIC}, {@link #ALWAYS}, and {@link #NEVER}. * * @param v * the new visibility */ public void setHorizontalScrollBarVisibility(int v) { hBarVisibility = v; } /** * Sets both the horizontal and vertical scrollbar visibility to the given * value. Possible values are {@link #AUTOMATIC}, {@link #ALWAYS}, and * {@link #NEVER}. * * @param both * the new visibility */ public void setScrollBarVisibility(int both) { setHorizontalScrollBarVisibility(both); setVerticalScrollBarVisibility(both); } /** * Sets the vertical scrollbar visibility. Possible values are * {@link #AUTOMATIC}, {@link #ALWAYS}, and {@link #NEVER}. * * @param v * the new visibility */ public void setVerticalScrollBarVisibility(int v) { vBarVisibility = v; } /** * Sets the Viewport. The given Viewport must use "fake" scrolling. That is, * it must be constructed using <code>new Viewport(true)</code>. * * @param vp * the new viewport */ public void setViewport(Viewport vp) { if (viewport != null) unhookViewport(); viewport = vp; lws.setContents(viewport); hookViewport(); } private int verifyScrollBarOffset(RangeModel model, int value) { value = Math.max(model.getMinimum(), value); return Math.min(model.getMaximum() - model.getExtent(), value); } public Slider getHorizontalSlider() { checkWidget(); if (horizontalBar == null) { horizontalBar = new Slider(this, SWT.HORIZONTAL); horizontalBar.setMaximum(100); horizontalBar.setThumb(10); } return horizontalBar; } public Slider getVerticalSlider() { checkWidget(); if (verticalBar == null) { verticalBar = new Slider(this, SWT.VERTICAL); verticalBar.setMaximum(100); verticalBar.setThumb(10); } return verticalBar; } }