Package org.eclipse.sapphire.ui.swt.gef.contextbuttons

Source Code of org.eclipse.sapphire.ui.swt.gef.contextbuttons.ContextButtonPad

/******************************************************************************
* Copyright (c) 2014 SAP and 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:
*    SAP - initial implementation
*    Shenxue Zhou - adaptation for Sapphire and ongoing maintenance
******************************************************************************/

package org.eclipse.sapphire.ui.swt.gef.contextbuttons;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.Shape;
import org.eclipse.draw2d.XYLayout;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.sapphire.ui.swt.gef.SapphireDiagramEditor;
import org.eclipse.sapphire.ui.swt.gef.contextbuttons.IContextButtonPadDeclaration.PadStyle;
import org.eclipse.sapphire.ui.swt.gef.figures.FigureUtil;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.widgets.Display;

/**
* A Shape depicting a context button pad. The context button pad contains
* several {@link ContextButton} as children. The visual definition of the
* context button pad is provided mostly by the
* {@link IContextButtonPadDeclaration}, which is set in the constructor, and
* not calculated in this class.
*
* @author SAP
* @author <a href="mailto:shenxue.zhou@oracle.com">Shenxue Zhou</a>
*/

public class ContextButtonPad extends Shape {

  /**
   * The default duration of the animation in milliseconds.
   */
  private static final int DEFAULT_ANIMATION_DURATION = 200;

  /**
   * The start transparency of the animation in the range 0.0 to 1.0.
   */
  private static final double ANIMATION_START_TRANSPARENCY = 0.0;

  /**
   * The end transparency of the animation in the range 0.0 to 1.0.
   */
  private static final double ANIMATION_END_TRANSPARENCY = 1.0;

  /**
   * The context button pad declaration as described in
   * {@link #getDeclaration()}.
   */
  private IContextButtonPadDeclaration declaration;

  /**
   * The zoom-level as described in {@link #getZoomLevel()}.
   */
  private double zoomLevel;

  /**
   * The editor as described in {@link #getEditor()}.
   */
  private SapphireDiagramEditor editor;
 
  private List<GraphicalEditPart> editParts;

  private Path pathOuterLine;

  private Path pathMiddleLine;

  private Path pathInnerLine;

  private Path pathFill;

  private List<Rectangle> containmentRectangles;

  private List<Rectangle> overlappingContainmentRectangles;

  private double currentTransparency = ANIMATION_END_TRANSPARENCY;

  private long animationDuration = DEFAULT_ANIMATION_DURATION;

  // ============================ constructors ==============================

  /**
   * Creates a new ContextButtonPad and calls {@link #initialize()}.
   *
   * @param declaration
   *            The context button pad declaration as described in
   *            {@link #getDeclaration()}.
   * @param zoomLevel
   *            The zoom-level as described in {@link #getZoomLevel()}.
   * @param editor
   *            The editor as described in {@link #getEditor()}.
   * @param sapphireParts
   *            The sapphire parts
   */
  public ContextButtonPad(IContextButtonPadDeclaration declaration, double zoomLevel, SapphireDiagramEditor editor,
            List<GraphicalEditPart> editParts)
  {
    this.declaration = declaration;
    this.zoomLevel = zoomLevel;
    this.editor = editor;
    this.editParts = new ArrayList<GraphicalEditPart>();
    this.editParts.addAll(editParts);

    initialize();
  }

  // ====================== getter/setter for fields ========================

  /**
   * Returns the declaration, which provides the visual definition and the
   * context buttons for this context button pad. It is set in the constructor
   * and not changed afterwards.
   *
   * @return The declaration, which provides the visual definition and the
   *         context buttons for this context button pad.
   */
  public final IContextButtonPadDeclaration getDeclaration() {
    return declaration;
  }

  /**
   * Returns the zoom-level, which shall be used when rendering the context
   * button pad. It is set in the constructor and not changed afterwards. The
   * values provided by the declaration (see {@link #getDeclaration()}) are
   * always using a zoom-level 1.0, so they have to be adjusted by the
   * zoom-level.
   *
   * @return The zoom-level, which shall be used when rendering the context
   *         button pad.
   */
  public final double getZoomLevel() {
    return zoomLevel;
  }

  /**
   * Returns the editor for which the context button pad belongs. It can be
   * used to access the environment. It is set in the constructor and not
   * changed afterwards.
   *
   * @return The editor, which can be used to access the environment.
   */
  public final SapphireDiagramEditor getEditor() {
    return editor;
  }
 
  // =========================== initialization =============================

  /**
   * Initializes several fields of this class. This method is called in the
   * constructor and calls {@link #initializePathes()},
   * {@link #initializeContainments()} and {@link #createContextButtons()}.
   */
  private void initialize() {
    initializeContainments();
    createContextButtons();
  }

  /**
   * Initializes the lists {@link #containmentRectangles} and
   * {@link #overlappingContainmentRectangles}, by transforming the rectangles
   * provided by the declaration (see {@link #getDeclaration()}) using
   * {@link #transformGenericRectangle(java.awt.Rectangle, int)}. Then it sets
   * the bounds of this figure dependent on the
   * {@link #overlappingContainmentRectangles}.
   */
  private void initializeContainments() {
    containmentRectangles = new ArrayList<Rectangle>();
    overlappingContainmentRectangles = new ArrayList<Rectangle>();

    for (java.awt.Rectangle rectangle : getDeclaration().getContainmentRectangles()) {
      Rectangle transformedRectangle = transformGenericRectangle(rectangle, 0);
      containmentRectangles.add(transformedRectangle);
    }
    for (java.awt.Rectangle rectangle : getDeclaration().getOverlappingContainmentRectangles()) {
      Rectangle transformedRectangle = transformGenericRectangle(rectangle, 0);
      overlappingContainmentRectangles.add(transformedRectangle);
    }

    if (overlappingContainmentRectangles.size() > 0) { // always true
      Rectangle unionRectangle = overlappingContainmentRectangles.get(0).getCopy();
      for (Rectangle containmentRectangle : overlappingContainmentRectangles) {
        unionRectangle.union(containmentRectangle);
      }
      unionRectangle.expand(2, 2); // expand slightly to avoid rounding problems
      setBounds(unionRectangle);
    }
  }

  /**
   * Creates a visual {@link ContextButton} for each logical
   * {@link PositionedContextButton}, which are provided by the declaration
   * {@link #getDeclaration()}. Those context buttons are then added to the
   * context button pad at the correct location.
   */
  private void createContextButtons() {
    List<PositionedContextButton> positionedButtons = getDeclaration().getPositionedContextButtons();

    setLayoutManager(new XYLayout());

    for (PositionedContextButton positionedButton : positionedButtons) {
      Rectangle position = transformGenericRectangle(positionedButton.getPosition(), 0);
      // translate position relative to bounds (after the bounds are set!)
      position.translate(-getBounds().getTopLeft().x, -getBounds().getTopLeft().y);
      ContextButton cb = new ContextButton(positionedButton, this);
      add(cb, position);
    }
  }

  /**
   * Initializes the fields for the paths {@link #pathOuterLine},
   * {@link #pathMiddleLine}, {@link #pathInnerLine} and {@link #pathFill}.
   * This is done by calling {@link #createPath(int)} with ascending values
   * for 'shrink-lines'. As a result those four paths all have the same
   * outline, but each path becomes one line smaller so that they lie directly
   * in each other.
   */
  private void createPathes() {
    pathOuterLine = createPath(0);
    pathMiddleLine = createPath(1);
    pathInnerLine = createPath(2);
    pathFill = createPath(3);
  }

  /**
   * Disposes all paths, which were created in {@link #createPathes()}.
   */
  private void disposePathes() {
    pathOuterLine.dispose();
    pathMiddleLine.dispose();
    pathInnerLine.dispose();
    pathFill.dispose();
    pathOuterLine = null;
    pathMiddleLine = null;
    pathInnerLine = null;
    pathFill = null;
  }

  /**
   * Creates and returns a path outlining the context button pad. The path
   * includes the three rectangles top, right and bottom, which are provided
   * by the declaration (see {@link #getDeclaration()}). A parameter
   * shrink-lines is used to shrink the path by the given number of lines to
   * the inside.
   * <p>
   * Basically this method just forwards to
   * {@link #createPath(Rectangle, Rectangle, Rectangle, int)}, but before it
   * adjusts the rectangles and corner by the zoom-level and shrink-lines.
   *
   * @param shrinkLines
   *            The number of lines to shrink the path to the inside.
   * @return A path outlining the context button pad.
   */
  private Path createPath(int shrinkLines) {
    double zoom = getZoomLevel();
    int lw = shrinkLines * ((int) (getDeclaration().getPadLineWidth() * zoom));

    // adjust corner for the inner path (formula found by experimenting)
    double corner = (getDeclaration().getPadCornerRadius() * zoom);
    corner = Math.max(1, corner - (lw + corner / 64));

    Rectangle topAdjusted = transformGenericRectangle(getDeclaration().getTopPad(), shrinkLines);
    Rectangle rightAdjusted = transformGenericRectangle(getDeclaration().getRightPad(), shrinkLines);
    Rectangle bottomAdjusted = transformGenericRectangle(getDeclaration().getBottomPad(), shrinkLines);

    Path path = createPath(topAdjusted, rightAdjusted, bottomAdjusted, (int) corner);
    return path;
  }

  /**
   * Creates and returns a path including the given rectangles and uses the
   * given corner-radius. Note, that all those values are already adjusted to
   * the given zoom-level and 'shrink-lines' (see {@link #createPath(int)}).
   *
   * @param topOutside
   *            The outside of the top rectangle to include. The path remains
   *            inside the rectangle even for a big line-width.
   * @param rightOutside
   *            The outside of the right rectangle to include. The path
   *            remains inside the rectangle even for a big line-width.
   * @param bottomOutside
   *            The outside of the bottom rectangle to include. The path
   *            remains inside the rectangle even for a big line-width.
   * @param corner
   *            The corner radius to use for the path.
   * @return A path surrounding the given rectangles and uses the given
   *         corner-radius.
   */
  private Path createPath(Rectangle topOutside, Rectangle rightOutside, Rectangle bottomOutside, int corner) {
    Path path = new Path(null);

    // currently we assume, that the inner corner radius is always half the outer corner radius
    int innerCorner = corner / 2;

    // first shrink all rectangles by the half line-width, so that painting remains inside the given 'outside' rectangles
    Rectangle top = FigureUtil.getAdjustedRectangle(topOutside, 1.0, (int) (getDeclaration().getPadLineWidth() * getZoomLevel()));
    Rectangle right = FigureUtil.getAdjustedRectangle(rightOutside, 1.0, (int) (getDeclaration().getPadLineWidth() * getZoomLevel()));
    Rectangle bottom = FigureUtil.getAdjustedRectangle(bottomOutside, 1.0,
        (int) (getDeclaration().getPadLineWidth() * getZoomLevel()));

    // differenciate the pad styles
    boolean hasTop = top != null;
    boolean hasRight = right != null;
    boolean hasStandardTop = hasTop && getDeclaration().getTopPadStyle().equals(PadStyle.STANDARD);
    boolean hasStandardRight = hasRight && getDeclaration().getRightPadStyle().equals(PadStyle.STANDARD);
    boolean hasAppendageTop = hasTop && getDeclaration().getTopPadStyle().equals(PadStyle.APPENDAGE);
    boolean hasAppendageRight = hasRight && getDeclaration().getRightPadStyle().equals(PadStyle.APPENDAGE);

    // create path

    if (hasStandardTop) {
      // curved line around top(top-right) -> top(top-left) -> top(bottom-left)
      path.addArc(top.getTopRight().x - corner, top.getTopRight().y, corner, corner, 0, 90);
      path.addArc(top.getTopLeft().x, top.getTopLeft().y, corner, corner, 90, 90);
      path.addArc(top.getBottomLeft().x, top.getBottomLeft().y - corner, corner, corner, 180, 90);
    } else if (hasAppendageTop) {
      // curved line around top(top-left) -> top(bottom-left)
      int appendageCorner = Math.min(corner, top.height * 2); // adjust for small sizes
      path.addArc(top.getTopLeft().x, top.getTopLeft().y, appendageCorner, appendageCorner, 90, 90);
      path.lineTo(top.getBottomLeft().x, top.getBottomLeft().y);
    } else { // !hasTop
      // curved line around right(top-left)
      path.addArc(right.getTopLeft().x, right.getTopLeft().y, corner, corner, 90, 90);
    }

    if (hasTop && hasRight) {
      // inside open curve connecting top and right
      path.addArc(right.getLeft().x - innerCorner, top.getBottom().y, innerCorner, innerCorner, 90, -90);
    }

    if (hasStandardRight) {
      if (bottom == null) {
        // curved line around right(bottom-left)
        path.addArc(right.getBottomLeft().x, right.getBottomLeft().y - corner, corner, corner, 180, 90);
      } else {
        // inside open curve connection right and bottom
        path.addArc(right.getLeft().x - innerCorner, bottom.getTop().y - innerCorner, innerCorner, innerCorner, 0, -90);
        // curved line around bottom(top-left) -> bottom(bottom-left) -> bottom(bottom-right)
        path.addArc(bottom.getTopLeft().x, bottom.getTopLeft().y, corner, corner, 90, 90);
        path.addArc(bottom.getBottomLeft().x, bottom.getBottomLeft().y - corner, corner, corner, 180, 90);
        path.addArc(bottom.getBottomRight().x - corner, bottom.getBottomRight().y - corner, corner, corner, 270, 90);
        // outside open curve connection bottom and right
        path.addArc(bottom.getRight().x, right.getBottom().y, corner, corner, 180, -90);
      }

      // curved line around right(bottom-right) -> right(top-right)
      path.addArc(right.getBottomRight().x - corner, right.getBottomRight().y - corner, corner, corner, 270, 90);
      path.addArc(right.getTopRight().x - corner, right.getTopRight().y, corner, corner, 0, 90);
    } else if (hasAppendageRight) {
      // curved line around right(bottom-left) -> right(bottom-right)
      int appendageCorner = Math.min(corner, right.width * 2); // adjust for small sizes
      path.lineTo(right.getBottomLeft().x, right.getBottomLeft().y);
      path.addArc(right.getBottomRight().x - appendageCorner, right.getBottomRight().y - appendageCorner, appendageCorner,
          appendageCorner, 270, 90);
    } else { // !hasRight
      // close curved rectangle around top (bottom-right)
      path.addArc(top.getBottomRight().x - corner, top.getBottomRight().y - corner, corner, corner, 270, 90);
    }

    if (hasStandardTop && hasStandardRight) {
      // outside open curve connecting right and top (appendages have direct line)
      path.addArc(top.getRight().x, right.getTop().y - corner, corner, corner, 270, -90);
    }

    path.close();

    return path;
  }

  /**
   * Returns a rectangle, which is calculating from the given rectangle by
   * shrinking it the given number of lines and scaling it with the
   * zoom-level. Note, that this method also makes a transformation from
   * java.awt.Rectangle to org.eclipse.draw2d.geometry.Rectangle.
   *
   * @param source
   *            The source rectangle from which to calculate the result.
   * @param shrinkLines
   *            The number of lines to shrink the rectangle.
   * @return A rectangle, which is calculating from the given rectangle by
   *         shrinking it the given number of lines and scaling it with the
   *         zoom-level.
   */
  private Rectangle transformGenericRectangle(java.awt.Rectangle source, int shrinkLines) {
    if (source == null) {
      return null;
    }

    double zoom = getZoomLevel();
    int lw = shrinkLines * ((int) (getDeclaration().getPadLineWidth() * zoom));

    Rectangle target = new Rectangle(source.x, source.y, source.width, source.height);
    target.scale(zoom);
    // shrink, but take care not to end up with a negative width or height
    int widthShrink = Math.min(target.width / 2, lw);
    int heightShrink = Math.min(target.height / 2, lw);
    target.shrink(widthShrink, heightShrink);
    return target;
  }

  // ============================== painting ================================

  /**
   * Outlines this Shape on the given Graphics. This will draw the paths
   * {@link #pathInnerLine}, {@link #pathMiddleLine} and
   * {@link #pathOuterLine}.
   *
   * @param graphics
   *            The Graphics on which to outline this Shape.
   */
  @Override
  protected void outlineShape(Graphics graphics) {
    int lw = (int) (getZoomLevel() * getDeclaration().getPadLineWidth());
    graphics.setLineWidth(lw);

    graphics.setForegroundColor(getEditor().getResourceCache().getColor(getDeclaration().getPadInnerLineColor()));
    graphics.drawPath(pathInnerLine);
    graphics.setForegroundColor(getEditor().getResourceCache().getColor(getDeclaration().getPadMiddleLineColor()));
    graphics.drawPath(pathMiddleLine);
    graphics.setForegroundColor(getEditor().getResourceCache().getColor(getDeclaration().getPadOuterLineColor()));
    graphics.drawPath(pathOuterLine);
  }

  /**
   * First initializes the given Graphics with settings like alpha-value,
   * antialias-value, ... Afterwards calls
   * <code>super.paintFigure(graphics)</code> to continue with the default
   * painting mechanisms.
   *
   * @param graphics
   *            The Graphics on which to paint.
   */
  @Override
  public void paintFigure(Graphics graphics) {
    graphics.setAntialias(SWT.ON);

    // double padDefaultOpacity = getDeclaration().getPadDefaultOpacity();
    // int endAlpha = (int) (padDefaultOpacity * 255.0);

    graphics.setAlpha((int) (currentTransparency * getDeclaration().getPadDefaultOpacity() * 255));

    createPathes();
    super.paintFigure(graphics);
    disposePathes();
  }

  /**
   * Fills this Shape on the given Graphics. This will draw and fill the path
   * {@link #pathFill}.
   *
   * @param graphics
   *            The Graphics on which to fill this Shape.
   */
  @Override
  protected void fillShape(Graphics graphics) {
    int lw = (int) (getZoomLevel() * getDeclaration().getPadLineWidth());
    graphics.setLineWidth(lw);

    graphics.setForegroundColor(getEditor().getResourceCache().getColor(getDeclaration().getPadFillColor()));
    graphics.setBackgroundColor(getEditor().getResourceCache().getColor(getDeclaration().getPadFillColor()));
    graphics.drawPath(pathFill);
    graphics.fillPath(pathFill);
  }

  /**
   * Increases the current transparency (see {@link #getCurrentTransparency()}
   * ) in a loop and forces a repaint after each increase. As a result this
   * will seem to the user as if the context button pad slowly 'fades in' from
   * invisible to visible.
   */
  @SuppressWarnings("unused")
  private void doAnimation() {
    // set start values for animation
    long startTime = System.currentTimeMillis();
    long stepsDone = 0;
    currentTransparency = ANIMATION_START_TRANSPARENCY;
    while (isPadShowing() && (currentTransparency < ANIMATION_END_TRANSPARENCY)) {
      // repaint context button pad with current transparency
      forceRepaint();
      stepsDone++;

      // increase current transparency (self adjusting algorithm)
      long elapsedTime = Math.max(1, System.currentTimeMillis() - startTime);
      long restTime = Math.max(1, animationDuration - elapsedTime);
      double restSteps = (((double) (stepsDone * restTime)) / elapsedTime);
      double deltaTransparency = (ANIMATION_END_TRANSPARENCY - currentTransparency) / restSteps;
      currentTransparency += deltaTransparency;
      if (elapsedTime > animationDuration) { // safeguard in case of rounding errors
        currentTransparency = ANIMATION_END_TRANSPARENCY;
      }
    }

    // finally paint using the end transparency
    currentTransparency = ANIMATION_END_TRANSPARENCY;
    forceRepaint();
  }

  /**
   * Forces a repaint of this figure, by first calling repaint() and then
   * waiting until all UI events are processed.
   */
  private void forceRepaint() {
    if (isPadShowing()) {
      repaint();
      while (Display.getCurrent().readAndDispatch()) {
        // do nothing
      }
    }
  }

  // ========================== helper methods ==============================

  /**
   * Returns true, if the given point is contained inside one of the visible
   * parts of the context button pad. Note, that this is a much smaller area
   * than the bounds of this shape, because the bounds are the outer invisible
   * rectangle around all visible parts of the context button pad.
   * <p>
   * Technically this is the union of all containment rectangles (see
   * {@link IContextButtonPadDeclaration#getContainmentRectangles()}).
   *
   * @param x
   *            The x-coordinate of the point to check.
   * @param y
   *            The y-coordinate of the point to check.
   * @return true, if the given point is contained inside one of the visible
   *         parts of the context button pad.
   */
  @Override
  public boolean containsPoint(int x, int y) {
    boolean ret = false;
    for (Rectangle rectangle : containmentRectangles) {
      if (rectangle.contains(x, y)) {
        ret = true;
        break;
      }
    }
    return ret;
  }

  public final List<GraphicalEditPart> getEditParts() {
    return this.editParts;
  }

  /**
   * Returns true, if the given point is contained inside the overlapping area
   * of all visible parts of the context button pad. Note, that this is a much
   * smaller area than the bounds of this shape, because the bounds are the
   * outer invisible rectangle around all visible parts of the context button
   * pad.
   *
   * @param x
   *            The x-coordinate of the point to check.
   * @param y
   *            The y-coordinate of the point to check.
   * @return true, if the given point is contained inside the overlapping area
   *         of all visible parts of the context button pad.
   */
  public boolean containsPointOverlapping(int x, int y) {
    boolean ret = false;
    for (Rectangle rectangle : overlappingContainmentRectangles) {
      if (rectangle.contains(x, y)) {
        ret = true;
        break;
      }
    }
    return ret;
  }

  /**
   * Returns the current transparency as described in
   * {@link ITransparencyProvider}.
   *
   * @return The current transparency as described in
   *         {@link ITransparencyProvider}.
   */
  public double getCurrentTransparency() {
    return currentTransparency;
  }

  /**
   * Returns true, if the pad is currently showing.
   *
   * @return true, if the pad is currently showing.
   */
  private boolean isPadShowing() {
    // returns true in the time, between addNotify() and removeNotify()
    boolean hasParent = getParent() != null;
    return hasParent;
  }
}
TOP

Related Classes of org.eclipse.sapphire.ui.swt.gef.contextbuttons.ContextButtonPad

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.