/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.annotations.NonNull; import com.android.ide.common.api.DrawingStyle; import com.android.ide.common.api.IColor; import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.FontMetrics; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.RGB; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Wraps an SWT {@link GC} into an {@link IGraphics} interface so that {@link IViewRule} objects * can directly draw on the canvas. * <p/> * The actual wrapped GC object is only non-null during the context of a paint operation. */ public class GCWrapper implements IGraphics { /** * The actual SWT {@link GC} being wrapped. This can change during the lifetime of the * object. It is generally set to something during an onPaint method and then changed * to null when not in the context of a paint. */ private GC mGc; /** * Current style being used for drawing. */ private SwtDrawingStyle mCurrentStyle = SwtDrawingStyle.INVALID; /** * Implementation of IColor wrapping an SWT color. */ private static class ColorWrapper implements IColor { private final Color mColor; public ColorWrapper(Color color) { mColor = color; } public Color getColor() { return mColor; } } /** A map of registered colors. All these colors must be disposed at the end. */ private final HashMap<Integer, ColorWrapper> mColorMap = new HashMap<Integer, ColorWrapper>(); /** * A map of the {@link SwtDrawingStyle} stroke colors that we have actually * used (to be disposed) */ private final Map<DrawingStyle, Color> mStyleStrokeMap = new EnumMap<DrawingStyle, Color>( DrawingStyle.class); /** * A map of the {@link SwtDrawingStyle} fill colors that we have actually * used (to be disposed) */ private final Map<DrawingStyle, Color> mStyleFillMap = new EnumMap<DrawingStyle, Color>( DrawingStyle.class); /** The cached pixel height of the default current font. */ private int mFontHeight = 0; /** The scaling of the canvas in X. */ private final CanvasTransform mHScale; /** The scaling of the canvas in Y. */ private final CanvasTransform mVScale; public GCWrapper(CanvasTransform hScale, CanvasTransform vScale) { mHScale = hScale; mVScale = vScale; mGc = null; } void setGC(GC gc) { mGc = gc; } private GC getGc() { return mGc; } void checkGC() { if (mGc == null) { throw new RuntimeException("IGraphics used without a valid context."); } } void dispose() { for (ColorWrapper c : mColorMap.values()) { c.getColor().dispose(); } mColorMap.clear(); for (Color c : mStyleStrokeMap.values()) { c.dispose(); } mStyleStrokeMap.clear(); for (Color c : mStyleFillMap.values()) { c.dispose(); } mStyleFillMap.clear(); } //------------- @Override public @NonNull IColor registerColor(int rgb) { checkGC(); Integer key = Integer.valueOf(rgb); ColorWrapper c = mColorMap.get(key); if (c == null) { c = new ColorWrapper(new Color(getGc().getDevice(), (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, (rgb >> 0) & 0xFF)); mColorMap.put(key, c); } return c; } /** Returns the (cached) pixel height of the current font. */ @Override public int getFontHeight() { if (mFontHeight < 1) { checkGC(); FontMetrics fm = getGc().getFontMetrics(); mFontHeight = fm.getHeight(); } return mFontHeight; } @Override public @NonNull IColor getForeground() { Color c = getGc().getForeground(); return new ColorWrapper(c); } @Override public @NonNull IColor getBackground() { Color c = getGc().getBackground(); return new ColorWrapper(c); } @Override public int getAlpha() { return getGc().getAlpha(); } @Override public void setForeground(@NonNull IColor color) { checkGC(); getGc().setForeground(((ColorWrapper) color).getColor()); } @Override public void setBackground(@NonNull IColor color) { checkGC(); getGc().setBackground(((ColorWrapper) color).getColor()); } @Override public void setAlpha(int alpha) { checkGC(); try { getGc().setAlpha(alpha); } catch (SWTException e) { // This means that we cannot set the alpha on this platform; this is // an acceptable no-op. } } @Override public void setLineStyle(@NonNull LineStyle style) { int swtStyle = 0; switch (style) { case LINE_SOLID: swtStyle = SWT.LINE_SOLID; break; case LINE_DASH: swtStyle = SWT.LINE_DASH; break; case LINE_DOT: swtStyle = SWT.LINE_DOT; break; case LINE_DASHDOT: swtStyle = SWT.LINE_DASHDOT; break; case LINE_DASHDOTDOT: swtStyle = SWT.LINE_DASHDOTDOT; break; default: assert false : style; break; } if (swtStyle != 0) { checkGC(); getGc().setLineStyle(swtStyle); } } @Override public void setLineWidth(int width) { checkGC(); if (width > 0) { getGc().setLineWidth(width); } } // lines @Override public void drawLine(int x1, int y1, int x2, int y2) { checkGC(); useStrokeAlpha(); x1 = mHScale.translate(x1); y1 = mVScale.translate(y1); x2 = mHScale.translate(x2); y2 = mVScale.translate(y2); getGc().drawLine(x1, y1, x2, y2); } @Override public void drawLine(@NonNull Point p1, @NonNull Point p2) { drawLine(p1.x, p1.y, p2.x, p2.y); } // rectangles @Override public void drawRect(int x1, int y1, int x2, int y2) { checkGC(); useStrokeAlpha(); int x = mHScale.translate(x1); int y = mVScale.translate(y1); int w = mHScale.scale(x2 - x1); int h = mVScale.scale(y2 - y1); getGc().drawRectangle(x, y, w, h); } @Override public void drawRect(@NonNull Point p1, @NonNull Point p2) { drawRect(p1.x, p1.y, p2.x, p2.y); } @Override public void drawRect(@NonNull Rect r) { checkGC(); useStrokeAlpha(); int x = mHScale.translate(r.x); int y = mVScale.translate(r.y); int w = mHScale.scale(r.w); int h = mVScale.scale(r.h); getGc().drawRectangle(x, y, w, h); } @Override public void fillRect(int x1, int y1, int x2, int y2) { checkGC(); useFillAlpha(); int x = mHScale.translate(x1); int y = mVScale.translate(y1); int w = mHScale.scale(x2 - x1); int h = mVScale.scale(y2 - y1); getGc().fillRectangle(x, y, w, h); } @Override public void fillRect(@NonNull Point p1, @NonNull Point p2) { fillRect(p1.x, p1.y, p2.x, p2.y); } @Override public void fillRect(@NonNull Rect r) { checkGC(); useFillAlpha(); int x = mHScale.translate(r.x); int y = mVScale.translate(r.y); int w = mHScale.scale(r.w); int h = mVScale.scale(r.h); getGc().fillRectangle(x, y, w, h); } // circles (actually ovals) public void drawOval(int x1, int y1, int x2, int y2) { checkGC(); useStrokeAlpha(); int x = mHScale.translate(x1); int y = mVScale.translate(y1); int w = mHScale.scale(x2 - x1); int h = mVScale.scale(y2 - y1); getGc().drawOval(x, y, w, h); } public void drawOval(Point p1, Point p2) { drawOval(p1.x, p1.y, p2.x, p2.y); } public void drawOval(Rect r) { checkGC(); useStrokeAlpha(); int x = mHScale.translate(r.x); int y = mVScale.translate(r.y); int w = mHScale.scale(r.w); int h = mVScale.scale(r.h); getGc().drawOval(x, y, w, h); } public void fillOval(int x1, int y1, int x2, int y2) { checkGC(); useFillAlpha(); int x = mHScale.translate(x1); int y = mVScale.translate(y1); int w = mHScale.scale(x2 - x1); int h = mVScale.scale(y2 - y1); getGc().fillOval(x, y, w, h); } public void fillOval(Point p1, Point p2) { fillOval(p1.x, p1.y, p2.x, p2.y); } public void fillOval(Rect r) { checkGC(); useFillAlpha(); int x = mHScale.translate(r.x); int y = mVScale.translate(r.y); int w = mHScale.scale(r.w); int h = mVScale.scale(r.h); getGc().fillOval(x, y, w, h); } // strings @Override public void drawString(@NonNull String string, int x, int y) { checkGC(); useStrokeAlpha(); x = mHScale.translate(x); y = mVScale.translate(y); // Background fill of text is not useful because it does not // use the alpha; we instead supply a separate method (drawBoxedStrings) which // first paints a semi-transparent mask for the text to sit on // top of (this ensures that the text is readable regardless of // colors of the pixels below the text) getGc().drawString(string, x, y, true /*isTransparent*/); } @Override public void drawBoxedStrings(int x, int y, @NonNull List<?> strings) { checkGC(); x = mHScale.translate(x); y = mVScale.translate(y); // Compute bounds of the box by adding up the sum of the text heights // and the max of the text widths int width = 0; int height = 0; int lineHeight = getGc().getFontMetrics().getHeight(); for (Object s : strings) { org.eclipse.swt.graphics.Point extent = getGc().stringExtent(s.toString()); height += extent.y; width = Math.max(width, extent.x); } // Paint a box below the text int padding = 2; useFillAlpha(); getGc().fillRectangle(x - padding, y - padding, width + 2 * padding, height + 2 * padding); // Finally draw strings on top useStrokeAlpha(); int lineY = y; for (Object s : strings) { getGc().drawString(s.toString(), x, lineY, true /* isTransparent */); lineY += lineHeight; } } @Override public void drawString(@NonNull String string, @NonNull Point topLeft) { drawString(string, topLeft.x, topLeft.y); } // Styles @Override public void useStyle(@NonNull DrawingStyle style) { checkGC(); // Look up the specific SWT style which defines the actual // colors and attributes to be used for the logical drawing style. SwtDrawingStyle swtStyle = SwtDrawingStyle.of(style); RGB stroke = swtStyle.getStrokeColor(); if (stroke != null) { Color color = getStrokeColor(style, stroke); mGc.setForeground(color); } RGB fill = swtStyle.getFillColor(); if (fill != null) { Color color = getFillColor(style, fill); mGc.setBackground(color); } mGc.setLineWidth(swtStyle.getLineWidth()); mGc.setLineStyle(swtStyle.getLineStyle()); if (swtStyle.getLineStyle() == SWT.LINE_CUSTOM) { mGc.setLineDash(new int[] { 8, 4 }); } mCurrentStyle = swtStyle; } /** Uses the stroke alpha for subsequent drawing operations. */ private void useStrokeAlpha() { mGc.setAlpha(mCurrentStyle.getStrokeAlpha()); } /** Uses the fill alpha for subsequent drawing operations. */ private void useFillAlpha() { mGc.setAlpha(mCurrentStyle.getFillAlpha()); } /** * Get the SWT stroke color (foreground/border) to use for the given style, * using the provided color description if we haven't seen this color yet. * The color will also be placed in the {@link #mStyleStrokeMap} such that * it can be disposed of at cleanup time. * * @param style The drawing style for which we want a color * @param defaultColorDesc The RGB values to initialize the color to if we * haven't seen this color before * @return The color object */ private Color getStrokeColor(DrawingStyle style, RGB defaultColorDesc) { return getStyleColor(style, defaultColorDesc, mStyleStrokeMap); } /** * Get the SWT fill (background/interior) color to use for the given style, * using the provided color description if we haven't seen this color yet. * The color will also be placed in the {@link #mStyleStrokeMap} such that * it can be disposed of at cleanup time. * * @param style The drawing style for which we want a color * @param defaultColorDesc The RGB values to initialize the color to if we * haven't seen this color before * @return The color object */ private Color getFillColor(DrawingStyle style, RGB defaultColorDesc) { return getStyleColor(style, defaultColorDesc, mStyleFillMap); } /** * Get the SWT color to use for the given style, using the provided color * description if we haven't seen this color yet. The color will also be * placed in the map referenced by the map parameter such that it can be * disposed of at cleanup time. * * @param style The drawing style for which we want a color * @param defaultColorDesc The RGB values to initialize the color to if we * haven't seen this color before * @param map The color map to use * @return The color object */ private Color getStyleColor(DrawingStyle style, RGB defaultColorDesc, Map<DrawingStyle, Color> map) { Color color = map.get(style); if (color == null) { color = new Color(getGc().getDevice(), defaultColorDesc); map.put(style, color); } return color; } // dots @Override public void drawPoint(int x, int y) { checkGC(); useStrokeAlpha(); x = mHScale.translate(x); y = mVScale.translate(y); getGc().drawPoint(x, y); } // arrows private static final int MIN_LENGTH = 10; @Override public void drawArrow(int x1, int y1, int x2, int y2, int size) { int arrowWidth = size; int arrowHeight = size; checkGC(); useStrokeAlpha(); x1 = mHScale.translate(x1); y1 = mVScale.translate(y1); x2 = mHScale.translate(x2); y2 = mVScale.translate(y2); GC graphics = getGc(); // Make size adjustments to ensure that the arrow has enough width to be visible if (x1 == x2 && Math.abs(y1 - y2) < MIN_LENGTH) { int delta = (MIN_LENGTH - Math.abs(y1 - y2)) / 2; if (y1 < y2) { y1 -= delta; y2 += delta; } else { y1 += delta; y2-= delta; } } else if (y1 == y2 && Math.abs(x1 - x2) < MIN_LENGTH) { int delta = (MIN_LENGTH - Math.abs(x1 - x2)) / 2; if (x1 < x2) { x1 -= delta; x2 += delta; } else { x1 += delta; x2-= delta; } } graphics.drawLine(x1, y1, x2, y2); // Arrowhead: if (x1 == x2) { // Vertical if (y2 > y1) { graphics.drawLine(x2 - arrowWidth, y2 - arrowHeight, x2, y2); graphics.drawLine(x2 + arrowWidth, y2 - arrowHeight, x2, y2); } else { graphics.drawLine(x2 - arrowWidth, y2 + arrowHeight, x2, y2); graphics.drawLine(x2 + arrowWidth, y2 + arrowHeight, x2, y2); } } else if (y1 == y2) { // Horizontal if (x2 > x1) { graphics.drawLine(x2 - arrowHeight, y2 - arrowWidth, x2, y2); graphics.drawLine(x2 - arrowHeight, y2 + arrowWidth, x2, y2); } else { graphics.drawLine(x2 + arrowHeight, y2 - arrowWidth, x2, y2); graphics.drawLine(x2 + arrowHeight, y2 + arrowWidth, x2, y2); } } else { // Compute angle: int dy = y2 - y1; int dx = x2 - x1; double angle = Math.atan2(dy, dx); double lineLength = Math.sqrt(dy * dy + dx * dx); // Imagine a line of the same length as the arrow, but with angle 0. // Its two arrow lines are at (-arrowWidth, -arrowHeight) relative // to the endpoint (x1 + lineLength, y1) stretching up to (x2,y2). // We compute the positions of (ax,ay) for the point above and // below this line and paint the lines to it: double ax = x1 + lineLength - arrowHeight; double ay = y1 - arrowWidth; int rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1); int ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1); graphics.drawLine(x2, y2, rx, ry); ay = y1 + arrowWidth; rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1); ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1); graphics.drawLine(x2, y2, rx, ry); } /* TODO: Experiment with filled arrow heads? if (x1 == x2) { // Vertical if (y2 > y1) { for (int i = 0; i < arrowWidth; i++) { graphics.drawLine(x2 - arrowWidth + i, y2 - arrowWidth + i, x2 + arrowWidth - i, y2 - arrowWidth + i); } } else { for (int i = 0; i < arrowWidth; i++) { graphics.drawLine(x2 - arrowWidth + i, y2 + arrowWidth - i, x2 + arrowWidth - i, y2 + arrowWidth - i); } } } else if (y1 == y2) { // Horizontal if (x2 > x1) { for (int i = 0; i < arrowHeight; i++) { graphics.drawLine(x2 - arrowHeight + i, y2 - arrowHeight + i, x2 - arrowHeight + i, y2 + arrowHeight - i); } } else { for (int i = 0; i < arrowHeight; i++) { graphics.drawLine(x2 + arrowHeight - i, y2 - arrowHeight + i, x2 + arrowHeight - i, y2 + arrowHeight - i); } } } else { // Arbitrary angle -- need to use trig // TODO: Implement this } */ } }