1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import com.android.annotations.NonNull; 20 import com.android.ide.common.api.DrawingStyle; 21 import com.android.ide.common.api.IColor; 22 import com.android.ide.common.api.IGraphics; 23 import com.android.ide.common.api.IViewRule; 24 import com.android.ide.common.api.Point; 25 import com.android.ide.common.api.Rect; 26 27 import org.eclipse.swt.SWT; 28 import org.eclipse.swt.SWTException; 29 import org.eclipse.swt.graphics.Color; 30 import org.eclipse.swt.graphics.FontMetrics; 31 import org.eclipse.swt.graphics.GC; 32 import org.eclipse.swt.graphics.RGB; 33 34 import java.util.EnumMap; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 39 /** 40 * Wraps an SWT {@link GC} into an {@link IGraphics} interface so that {@link IViewRule} objects 41 * can directly draw on the canvas. 42 * <p/> 43 * The actual wrapped GC object is only non-null during the context of a paint operation. 44 */ 45 public class GCWrapper implements IGraphics { 46 47 /** 48 * The actual SWT {@link GC} being wrapped. This can change during the lifetime of the 49 * object. It is generally set to something during an onPaint method and then changed 50 * to null when not in the context of a paint. 51 */ 52 private GC mGc; 53 54 /** 55 * Current style being used for drawing. 56 */ 57 private SwtDrawingStyle mCurrentStyle = SwtDrawingStyle.INVALID; 58 59 /** 60 * Implementation of IColor wrapping an SWT color. 61 */ 62 private static class ColorWrapper implements IColor { 63 private final Color mColor; 64 ColorWrapper(Color color)65 public ColorWrapper(Color color) { 66 mColor = color; 67 } 68 getColor()69 public Color getColor() { 70 return mColor; 71 } 72 } 73 74 /** A map of registered colors. All these colors must be disposed at the end. */ 75 private final HashMap<Integer, ColorWrapper> mColorMap = new HashMap<Integer, ColorWrapper>(); 76 77 /** 78 * A map of the {@link SwtDrawingStyle} stroke colors that we have actually 79 * used (to be disposed) 80 */ 81 private final Map<DrawingStyle, Color> mStyleStrokeMap = new EnumMap<DrawingStyle, Color>( 82 DrawingStyle.class); 83 84 /** 85 * A map of the {@link SwtDrawingStyle} fill colors that we have actually 86 * used (to be disposed) 87 */ 88 private final Map<DrawingStyle, Color> mStyleFillMap = new EnumMap<DrawingStyle, Color>( 89 DrawingStyle.class); 90 91 /** The cached pixel height of the default current font. */ 92 private int mFontHeight = 0; 93 94 /** The scaling of the canvas in X. */ 95 private final CanvasTransform mHScale; 96 /** The scaling of the canvas in Y. */ 97 private final CanvasTransform mVScale; 98 GCWrapper(CanvasTransform hScale, CanvasTransform vScale)99 public GCWrapper(CanvasTransform hScale, CanvasTransform vScale) { 100 mHScale = hScale; 101 mVScale = vScale; 102 mGc = null; 103 } 104 setGC(GC gc)105 void setGC(GC gc) { 106 mGc = gc; 107 } 108 getGc()109 private GC getGc() { 110 return mGc; 111 } 112 checkGC()113 void checkGC() { 114 if (mGc == null) { 115 throw new RuntimeException("IGraphics used without a valid context."); 116 } 117 } 118 dispose()119 void dispose() { 120 for (ColorWrapper c : mColorMap.values()) { 121 c.getColor().dispose(); 122 } 123 mColorMap.clear(); 124 125 for (Color c : mStyleStrokeMap.values()) { 126 c.dispose(); 127 } 128 mStyleStrokeMap.clear(); 129 130 for (Color c : mStyleFillMap.values()) { 131 c.dispose(); 132 } 133 mStyleFillMap.clear(); 134 } 135 136 //------------- 137 138 @Override registerColor(int rgb)139 public @NonNull IColor registerColor(int rgb) { 140 checkGC(); 141 142 Integer key = Integer.valueOf(rgb); 143 ColorWrapper c = mColorMap.get(key); 144 if (c == null) { 145 c = new ColorWrapper(new Color(getGc().getDevice(), 146 (rgb >> 16) & 0xFF, 147 (rgb >> 8) & 0xFF, 148 (rgb >> 0) & 0xFF)); 149 mColorMap.put(key, c); 150 } 151 152 return c; 153 } 154 155 /** Returns the (cached) pixel height of the current font. */ 156 @Override getFontHeight()157 public int getFontHeight() { 158 if (mFontHeight < 1) { 159 checkGC(); 160 FontMetrics fm = getGc().getFontMetrics(); 161 mFontHeight = fm.getHeight(); 162 } 163 return mFontHeight; 164 } 165 166 @Override getForeground()167 public @NonNull IColor getForeground() { 168 Color c = getGc().getForeground(); 169 return new ColorWrapper(c); 170 } 171 172 @Override getBackground()173 public @NonNull IColor getBackground() { 174 Color c = getGc().getBackground(); 175 return new ColorWrapper(c); 176 } 177 178 @Override getAlpha()179 public int getAlpha() { 180 return getGc().getAlpha(); 181 } 182 183 @Override setForeground(@onNull IColor color)184 public void setForeground(@NonNull IColor color) { 185 checkGC(); 186 getGc().setForeground(((ColorWrapper) color).getColor()); 187 } 188 189 @Override setBackground(@onNull IColor color)190 public void setBackground(@NonNull IColor color) { 191 checkGC(); 192 getGc().setBackground(((ColorWrapper) color).getColor()); 193 } 194 195 @Override setAlpha(int alpha)196 public void setAlpha(int alpha) { 197 checkGC(); 198 try { 199 getGc().setAlpha(alpha); 200 } catch (SWTException e) { 201 // This means that we cannot set the alpha on this platform; this is 202 // an acceptable no-op. 203 } 204 } 205 206 @Override setLineStyle(@onNull LineStyle style)207 public void setLineStyle(@NonNull LineStyle style) { 208 int swtStyle = 0; 209 switch (style) { 210 case LINE_SOLID: 211 swtStyle = SWT.LINE_SOLID; 212 break; 213 case LINE_DASH: 214 swtStyle = SWT.LINE_DASH; 215 break; 216 case LINE_DOT: 217 swtStyle = SWT.LINE_DOT; 218 break; 219 case LINE_DASHDOT: 220 swtStyle = SWT.LINE_DASHDOT; 221 break; 222 case LINE_DASHDOTDOT: 223 swtStyle = SWT.LINE_DASHDOTDOT; 224 break; 225 default: 226 assert false : style; 227 break; 228 } 229 230 if (swtStyle != 0) { 231 checkGC(); 232 getGc().setLineStyle(swtStyle); 233 } 234 } 235 236 @Override setLineWidth(int width)237 public void setLineWidth(int width) { 238 checkGC(); 239 if (width > 0) { 240 getGc().setLineWidth(width); 241 } 242 } 243 244 // lines 245 246 @Override drawLine(int x1, int y1, int x2, int y2)247 public void drawLine(int x1, int y1, int x2, int y2) { 248 checkGC(); 249 useStrokeAlpha(); 250 x1 = mHScale.translate(x1); 251 y1 = mVScale.translate(y1); 252 x2 = mHScale.translate(x2); 253 y2 = mVScale.translate(y2); 254 getGc().drawLine(x1, y1, x2, y2); 255 } 256 257 @Override drawLine(@onNull Point p1, @NonNull Point p2)258 public void drawLine(@NonNull Point p1, @NonNull Point p2) { 259 drawLine(p1.x, p1.y, p2.x, p2.y); 260 } 261 262 // rectangles 263 264 @Override drawRect(int x1, int y1, int x2, int y2)265 public void drawRect(int x1, int y1, int x2, int y2) { 266 checkGC(); 267 useStrokeAlpha(); 268 int x = mHScale.translate(x1); 269 int y = mVScale.translate(y1); 270 int w = mHScale.scale(x2 - x1); 271 int h = mVScale.scale(y2 - y1); 272 getGc().drawRectangle(x, y, w, h); 273 } 274 275 @Override drawRect(@onNull Point p1, @NonNull Point p2)276 public void drawRect(@NonNull Point p1, @NonNull Point p2) { 277 drawRect(p1.x, p1.y, p2.x, p2.y); 278 } 279 280 @Override drawRect(@onNull Rect r)281 public void drawRect(@NonNull Rect r) { 282 checkGC(); 283 useStrokeAlpha(); 284 int x = mHScale.translate(r.x); 285 int y = mVScale.translate(r.y); 286 int w = mHScale.scale(r.w); 287 int h = mVScale.scale(r.h); 288 getGc().drawRectangle(x, y, w, h); 289 } 290 291 @Override fillRect(int x1, int y1, int x2, int y2)292 public void fillRect(int x1, int y1, int x2, int y2) { 293 checkGC(); 294 useFillAlpha(); 295 int x = mHScale.translate(x1); 296 int y = mVScale.translate(y1); 297 int w = mHScale.scale(x2 - x1); 298 int h = mVScale.scale(y2 - y1); 299 getGc().fillRectangle(x, y, w, h); 300 } 301 302 @Override fillRect(@onNull Point p1, @NonNull Point p2)303 public void fillRect(@NonNull Point p1, @NonNull Point p2) { 304 fillRect(p1.x, p1.y, p2.x, p2.y); 305 } 306 307 @Override fillRect(@onNull Rect r)308 public void fillRect(@NonNull Rect r) { 309 checkGC(); 310 useFillAlpha(); 311 int x = mHScale.translate(r.x); 312 int y = mVScale.translate(r.y); 313 int w = mHScale.scale(r.w); 314 int h = mVScale.scale(r.h); 315 getGc().fillRectangle(x, y, w, h); 316 } 317 318 // circles (actually ovals) 319 drawOval(int x1, int y1, int x2, int y2)320 public void drawOval(int x1, int y1, int x2, int y2) { 321 checkGC(); 322 useStrokeAlpha(); 323 int x = mHScale.translate(x1); 324 int y = mVScale.translate(y1); 325 int w = mHScale.scale(x2 - x1); 326 int h = mVScale.scale(y2 - y1); 327 getGc().drawOval(x, y, w, h); 328 } 329 drawOval(Point p1, Point p2)330 public void drawOval(Point p1, Point p2) { 331 drawOval(p1.x, p1.y, p2.x, p2.y); 332 } 333 drawOval(Rect r)334 public void drawOval(Rect r) { 335 checkGC(); 336 useStrokeAlpha(); 337 int x = mHScale.translate(r.x); 338 int y = mVScale.translate(r.y); 339 int w = mHScale.scale(r.w); 340 int h = mVScale.scale(r.h); 341 getGc().drawOval(x, y, w, h); 342 } 343 fillOval(int x1, int y1, int x2, int y2)344 public void fillOval(int x1, int y1, int x2, int y2) { 345 checkGC(); 346 useFillAlpha(); 347 int x = mHScale.translate(x1); 348 int y = mVScale.translate(y1); 349 int w = mHScale.scale(x2 - x1); 350 int h = mVScale.scale(y2 - y1); 351 getGc().fillOval(x, y, w, h); 352 } 353 fillOval(Point p1, Point p2)354 public void fillOval(Point p1, Point p2) { 355 fillOval(p1.x, p1.y, p2.x, p2.y); 356 } 357 fillOval(Rect r)358 public void fillOval(Rect r) { 359 checkGC(); 360 useFillAlpha(); 361 int x = mHScale.translate(r.x); 362 int y = mVScale.translate(r.y); 363 int w = mHScale.scale(r.w); 364 int h = mVScale.scale(r.h); 365 getGc().fillOval(x, y, w, h); 366 } 367 368 369 // strings 370 371 @Override drawString(@onNull String string, int x, int y)372 public void drawString(@NonNull String string, int x, int y) { 373 checkGC(); 374 useStrokeAlpha(); 375 x = mHScale.translate(x); 376 y = mVScale.translate(y); 377 // Background fill of text is not useful because it does not 378 // use the alpha; we instead supply a separate method (drawBoxedStrings) which 379 // first paints a semi-transparent mask for the text to sit on 380 // top of (this ensures that the text is readable regardless of 381 // colors of the pixels below the text) 382 getGc().drawString(string, x, y, true /*isTransparent*/); 383 } 384 385 @Override drawBoxedStrings(int x, int y, @NonNull List<?> strings)386 public void drawBoxedStrings(int x, int y, @NonNull List<?> strings) { 387 checkGC(); 388 389 x = mHScale.translate(x); 390 y = mVScale.translate(y); 391 392 // Compute bounds of the box by adding up the sum of the text heights 393 // and the max of the text widths 394 int width = 0; 395 int height = 0; 396 int lineHeight = getGc().getFontMetrics().getHeight(); 397 for (Object s : strings) { 398 org.eclipse.swt.graphics.Point extent = getGc().stringExtent(s.toString()); 399 height += extent.y; 400 width = Math.max(width, extent.x); 401 } 402 403 // Paint a box below the text 404 int padding = 2; 405 useFillAlpha(); 406 getGc().fillRectangle(x - padding, y - padding, width + 2 * padding, height + 2 * padding); 407 408 // Finally draw strings on top 409 useStrokeAlpha(); 410 int lineY = y; 411 for (Object s : strings) { 412 getGc().drawString(s.toString(), x, lineY, true /* isTransparent */); 413 lineY += lineHeight; 414 } 415 } 416 417 @Override drawString(@onNull String string, @NonNull Point topLeft)418 public void drawString(@NonNull String string, @NonNull Point topLeft) { 419 drawString(string, topLeft.x, topLeft.y); 420 } 421 422 // Styles 423 424 @Override useStyle(@onNull DrawingStyle style)425 public void useStyle(@NonNull DrawingStyle style) { 426 checkGC(); 427 428 // Look up the specific SWT style which defines the actual 429 // colors and attributes to be used for the logical drawing style. 430 SwtDrawingStyle swtStyle = SwtDrawingStyle.of(style); 431 RGB stroke = swtStyle.getStrokeColor(); 432 if (stroke != null) { 433 Color color = getStrokeColor(style, stroke); 434 mGc.setForeground(color); 435 } 436 RGB fill = swtStyle.getFillColor(); 437 if (fill != null) { 438 Color color = getFillColor(style, fill); 439 mGc.setBackground(color); 440 } 441 mGc.setLineWidth(swtStyle.getLineWidth()); 442 mGc.setLineStyle(swtStyle.getLineStyle()); 443 if (swtStyle.getLineStyle() == SWT.LINE_CUSTOM) { 444 mGc.setLineDash(new int[] { 445 8, 4 446 }); 447 } 448 mCurrentStyle = swtStyle; 449 } 450 451 /** Uses the stroke alpha for subsequent drawing operations. */ useStrokeAlpha()452 private void useStrokeAlpha() { 453 mGc.setAlpha(mCurrentStyle.getStrokeAlpha()); 454 } 455 456 /** Uses the fill alpha for subsequent drawing operations. */ useFillAlpha()457 private void useFillAlpha() { 458 mGc.setAlpha(mCurrentStyle.getFillAlpha()); 459 } 460 461 /** 462 * Get the SWT stroke color (foreground/border) to use for the given style, 463 * using the provided color description if we haven't seen this color yet. 464 * The color will also be placed in the {@link #mStyleStrokeMap} such that 465 * it can be disposed of at cleanup time. 466 * 467 * @param style The drawing style for which we want a color 468 * @param defaultColorDesc The RGB values to initialize the color to if we 469 * haven't seen this color before 470 * @return The color object 471 */ getStrokeColor(DrawingStyle style, RGB defaultColorDesc)472 private Color getStrokeColor(DrawingStyle style, RGB defaultColorDesc) { 473 return getStyleColor(style, defaultColorDesc, mStyleStrokeMap); 474 } 475 476 /** 477 * Get the SWT fill (background/interior) color to use for the given style, 478 * using the provided color description if we haven't seen this color yet. 479 * The color will also be placed in the {@link #mStyleStrokeMap} such that 480 * it can be disposed of at cleanup time. 481 * 482 * @param style The drawing style for which we want a color 483 * @param defaultColorDesc The RGB values to initialize the color to if we 484 * haven't seen this color before 485 * @return The color object 486 */ getFillColor(DrawingStyle style, RGB defaultColorDesc)487 private Color getFillColor(DrawingStyle style, RGB defaultColorDesc) { 488 return getStyleColor(style, defaultColorDesc, mStyleFillMap); 489 } 490 491 /** 492 * Get the SWT color to use for the given style, using the provided color 493 * description if we haven't seen this color yet. The color will also be 494 * placed in the map referenced by the map parameter such that it can be 495 * disposed of at cleanup time. 496 * 497 * @param style The drawing style for which we want a color 498 * @param defaultColorDesc The RGB values to initialize the color to if we 499 * haven't seen this color before 500 * @param map The color map to use 501 * @return The color object 502 */ getStyleColor(DrawingStyle style, RGB defaultColorDesc, Map<DrawingStyle, Color> map)503 private Color getStyleColor(DrawingStyle style, RGB defaultColorDesc, 504 Map<DrawingStyle, Color> map) { 505 Color color = map.get(style); 506 if (color == null) { 507 color = new Color(getGc().getDevice(), defaultColorDesc); 508 map.put(style, color); 509 } 510 511 return color; 512 } 513 514 // dots 515 516 @Override drawPoint(int x, int y)517 public void drawPoint(int x, int y) { 518 checkGC(); 519 useStrokeAlpha(); 520 x = mHScale.translate(x); 521 y = mVScale.translate(y); 522 523 getGc().drawPoint(x, y); 524 } 525 526 // arrows 527 528 private static final int MIN_LENGTH = 10; 529 530 531 @Override drawArrow(int x1, int y1, int x2, int y2, int size)532 public void drawArrow(int x1, int y1, int x2, int y2, int size) { 533 int arrowWidth = size; 534 int arrowHeight = size; 535 536 checkGC(); 537 useStrokeAlpha(); 538 x1 = mHScale.translate(x1); 539 y1 = mVScale.translate(y1); 540 x2 = mHScale.translate(x2); 541 y2 = mVScale.translate(y2); 542 GC graphics = getGc(); 543 544 // Make size adjustments to ensure that the arrow has enough width to be visible 545 if (x1 == x2 && Math.abs(y1 - y2) < MIN_LENGTH) { 546 int delta = (MIN_LENGTH - Math.abs(y1 - y2)) / 2; 547 if (y1 < y2) { 548 y1 -= delta; 549 y2 += delta; 550 } else { 551 y1 += delta; 552 y2-= delta; 553 } 554 555 } else if (y1 == y2 && Math.abs(x1 - x2) < MIN_LENGTH) { 556 int delta = (MIN_LENGTH - Math.abs(x1 - x2)) / 2; 557 if (x1 < x2) { 558 x1 -= delta; 559 x2 += delta; 560 } else { 561 x1 += delta; 562 x2-= delta; 563 } 564 } 565 566 graphics.drawLine(x1, y1, x2, y2); 567 568 // Arrowhead: 569 570 if (x1 == x2) { 571 // Vertical 572 if (y2 > y1) { 573 graphics.drawLine(x2 - arrowWidth, y2 - arrowHeight, x2, y2); 574 graphics.drawLine(x2 + arrowWidth, y2 - arrowHeight, x2, y2); 575 } else { 576 graphics.drawLine(x2 - arrowWidth, y2 + arrowHeight, x2, y2); 577 graphics.drawLine(x2 + arrowWidth, y2 + arrowHeight, x2, y2); 578 } 579 } else if (y1 == y2) { 580 // Horizontal 581 if (x2 > x1) { 582 graphics.drawLine(x2 - arrowHeight, y2 - arrowWidth, x2, y2); 583 graphics.drawLine(x2 - arrowHeight, y2 + arrowWidth, x2, y2); 584 } else { 585 graphics.drawLine(x2 + arrowHeight, y2 - arrowWidth, x2, y2); 586 graphics.drawLine(x2 + arrowHeight, y2 + arrowWidth, x2, y2); 587 } 588 } else { 589 // Compute angle: 590 int dy = y2 - y1; 591 int dx = x2 - x1; 592 double angle = Math.atan2(dy, dx); 593 double lineLength = Math.sqrt(dy * dy + dx * dx); 594 595 // Imagine a line of the same length as the arrow, but with angle 0. 596 // Its two arrow lines are at (-arrowWidth, -arrowHeight) relative 597 // to the endpoint (x1 + lineLength, y1) stretching up to (x2,y2). 598 // We compute the positions of (ax,ay) for the point above and 599 // below this line and paint the lines to it: 600 double ax = x1 + lineLength - arrowHeight; 601 double ay = y1 - arrowWidth; 602 int rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1); 603 int ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1); 604 graphics.drawLine(x2, y2, rx, ry); 605 606 ay = y1 + arrowWidth; 607 rx = (int) (Math.cos(angle) * (ax-x1) - Math.sin(angle) * (ay-y1) + x1); 608 ry = (int) (Math.sin(angle) * (ax-x1) + Math.cos(angle) * (ay-y1) + y1); 609 graphics.drawLine(x2, y2, rx, ry); 610 } 611 612 /* TODO: Experiment with filled arrow heads? 613 if (x1 == x2) { 614 // Vertical 615 if (y2 > y1) { 616 for (int i = 0; i < arrowWidth; i++) { 617 graphics.drawLine(x2 - arrowWidth + i, y2 - arrowWidth + i, 618 x2 + arrowWidth - i, y2 - arrowWidth + i); 619 } 620 } else { 621 for (int i = 0; i < arrowWidth; i++) { 622 graphics.drawLine(x2 - arrowWidth + i, y2 + arrowWidth - i, 623 x2 + arrowWidth - i, y2 + arrowWidth - i); 624 } 625 } 626 } else if (y1 == y2) { 627 // Horizontal 628 if (x2 > x1) { 629 for (int i = 0; i < arrowHeight; i++) { 630 graphics.drawLine(x2 - arrowHeight + i, y2 - arrowHeight + i, x2 631 - arrowHeight + i, y2 + arrowHeight - i); 632 } 633 } else { 634 for (int i = 0; i < arrowHeight; i++) { 635 graphics.drawLine(x2 + arrowHeight - i, y2 - arrowHeight + i, x2 636 + arrowHeight - i, y2 + arrowHeight - i); 637 } 638 } 639 } else { 640 // Arbitrary angle -- need to use trig 641 // TODO: Implement this 642 } 643 */ 644 } 645 } 646