1 /* 2 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. 3 * 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * The contents of this file are subject to the terms of either the Universal Permissive License 7 * v 1.0 as shown at http://oss.oracle.com/licenses/upl 8 * 9 * or the following license: 10 * 11 * Redistribution and use in source and binary forms, with or without modification, are permitted 12 * provided that the following conditions are met: 13 * 14 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions 15 * and the following disclaimer. 16 * 17 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of 18 * conditions and the following disclaimer in the documentation and/or other materials provided with 19 * the distribution. 20 * 21 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to 22 * endorse or promote products derived from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 25 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 31 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 package org.openjdk.jmc.ui.misc; 34 35 import java.awt.Color; 36 import java.awt.Graphics2D; 37 import java.awt.geom.Point2D; 38 import java.awt.geom.Rectangle2D; 39 import java.util.ArrayList; 40 import java.util.List; 41 42 import org.eclipse.jface.action.IMenuManager; 43 import org.eclipse.jface.util.IPropertyChangeListener; 44 import org.eclipse.jface.util.PropertyChangeEvent; 45 import org.eclipse.swt.SWT; 46 import org.eclipse.swt.events.FocusEvent; 47 import org.eclipse.swt.events.FocusListener; 48 import org.eclipse.swt.events.KeyEvent; 49 import org.eclipse.swt.events.KeyListener; 50 import org.eclipse.swt.events.MouseAdapter; 51 import org.eclipse.swt.events.MouseEvent; 52 import org.eclipse.swt.events.MouseMoveListener; 53 import org.eclipse.swt.events.MouseTrackListener; 54 import org.eclipse.swt.events.PaintEvent; 55 import org.eclipse.swt.events.PaintListener; 56 import org.eclipse.swt.graphics.GC; 57 import org.eclipse.swt.graphics.Point; 58 import org.eclipse.swt.graphics.Rectangle; 59 import org.eclipse.swt.widgets.Canvas; 60 import org.eclipse.swt.widgets.Composite; 61 import org.eclipse.swt.widgets.Control; 62 import org.eclipse.swt.widgets.Display; 63 import org.eclipse.swt.widgets.Event; 64 import org.eclipse.swt.widgets.Listener; 65 import org.openjdk.jmc.common.IDisplayable; 66 import org.openjdk.jmc.common.unit.IQuantity; 67 import org.openjdk.jmc.ui.UIPlugin; 68 import org.openjdk.jmc.ui.accessibility.FocusTracker; 69 import org.openjdk.jmc.ui.charts.IChartInfoVisitor; 70 import org.openjdk.jmc.ui.charts.IXDataRenderer; 71 import org.openjdk.jmc.ui.charts.XYChart; 72 import org.openjdk.jmc.ui.common.util.Environment; 73 import org.openjdk.jmc.ui.common.util.Environment.OSType; 74 import org.openjdk.jmc.ui.handlers.MCContextMenuManager; 75 76 public class ChartCanvas extends Canvas { 77 private int lastMouseX = -1; 78 private int lastMouseY = -1; 79 private List<Rectangle2D> highlightRects; 80 private Object hoveredItemData; 81 82 private class Selector extends MouseAdapter implements MouseMoveListener, MouseTrackListener { 83 84 int selectionStartX = -1; 85 int selectionStartY = -1; 86 boolean selectionIsClick = false; 87 88 @Override 89 public void mouseDown(MouseEvent e) { 90 /* 91 * On Mac OS X, CTRL + left mouse button can be used to trigger a context menu. (This is 92 * for historical reasons when the primary input device on Macs were a mouse with a 93 * single physical button. All modern Macs have other means to bring up the context 94 * menu, typically a two finger tap.) 95 * 96 * Although I think it would be best to check that this MouseEvent does not cause a 97 * platform specific popup trigger, like java.awt.event.MouseEvent.isPopupTrigger() for 98 * AWT, SWT doesn't seem to have something as simple. It has the MenuDetectEvent, but 99 * the order in relation to this MouseEvent is unspecified. 100 * 101 * The code below instead relies on ignoring mouse down events when SWT.MOD4 is 102 * depressed. Since MOD4 is CTRL on OS X and 0 on all other current platforms, this 103 * suffices. Except for an additional platform check, this approach is also used in 104 * org.eclipse.swt.custom.StyledText.handleMouseDown(Event). 105 */ 106 if ((e.button == 1) && ((e.stateMask & SWT.MOD4) == 0)) { 107 selectionStartX = e.x; 108 selectionStartY = e.y; 109 selectionIsClick = true; 110 toggleSelect(selectionStartX, selectionStartY); 111 } 112 } 113 114 @Override 115 public void mouseMove(MouseEvent e) { 116 if (selectionStartX >= 0) { 117 highlightRects = null; 118 updateSelectionState(e); 119 } else { 120 lastMouseX = e.x; 121 lastMouseY = e.y; 122 updateHighlightRects(); 123 } 124 } 125 126 private void updateSelectionState(MouseEvent e) { 127 int x = e.x; 128 int y = e.y; 129 if (selectionIsClick && ((Math.abs(x - selectionStartX) > 3) || (Math.abs(y - selectionStartY) > 3))) { 130 selectionIsClick = false; 131 } 132 if (!selectionIsClick) { 133 select((int) (selectionStartX / xScale), (int) (x / xScale), (int) (selectionStartY / yScale), 134 (int) (y / yScale)); 135 } 136 } 137 138 @Override 139 public void mouseUp(MouseEvent e) { 140 if (selectionStartX >= 0 && (e.button == 1)) { 141 updateSelectionState(e); 142 selectionStartX = -1; 143 selectionStartY = -1; 144 if (selectionListener != null) { 145 selectionListener.run(); 146 } 147 } 148 } 149 150 @Override 151 public void mouseEnter(MouseEvent e) { 152 } 153 154 @Override 155 public void mouseExit(MouseEvent e) { 156 if (!getClientArea().contains(e.x, e.y)) { 157 resetHoveredItemData(); 158 } 159 clearHighlightRects(); 160 } 161 162 @Override 163 public void mouseHover(MouseEvent e) { 164 } 165 } 166 167 class Painter implements PaintListener { 168 169 @Override 170 public void paintControl(PaintEvent e) { 171 Rectangle rect = getClientArea(); 172 if (awtNeedsRedraw || !awtCanvas.hasImage(rect.width, rect.height)) { 173 Graphics2D g2d = awtCanvas.getGraphics(rect.width, rect.height); 174 g2d.setColor(Color.WHITE); 175 g2d.fillRect(0, 0, rect.width, rect.height); 176 Point adjusted = translateDisplayToImageCoordinates(rect.width, rect.height); 177 render(g2d, adjusted.x, adjusted.y); 178 if (highlightRects != null) { 179 updateHighlightRects(); 180 } 181 awtNeedsRedraw = false; 182 } 183 awtCanvas.paint(e, 0, 0); 184 // Crude, flickering highlight of areas also delivered to tooltips. 185 // FIXME: Remove flicker by drawing in a buffered stage (AWT or SWT). 186 List<Rectangle2D> rs = highlightRects; 187 if (rs != null) { 188 GC gc = e.gc; 189 gc.setForeground(getForeground()); 190 for (Rectangle2D r : rs) { 191 int x = (int) (((int) r.getX()) * xScale); 192 int y = (int) (((int) r.getY()) * yScale); 193 if ((r.getWidth() == 0) && (r.getHeight() == 0)) { 194 int width = (int) Math.round(4 * xScale); 195 int height = (int) Math.round(4 * yScale); 196 gc.drawOval(x - (int) Math.round(2 * xScale), y - (int) Math.round(2 * yScale), width, height); 197 } else { 198 int width = (int) Math.round(r.getWidth() * xScale); 199 int height = (int) Math.round(r.getHeight() * yScale); 200 gc.drawRectangle(x, y, width, height); 201 } 202 } 203 } 204 } 205 } 206 207 class Zoomer implements Listener { 208 209 @Override 210 public void handleEvent(Event event) { 211 handleWheelEvent(event.stateMask, event.x, event.count); 212 } 213 214 } 215 216 /** 217 * Steals the wheel events from the currently focused control while hovering over this 218 * (ChartCanvas) control. Used on Windows to allow zooming without having to click in the chart 219 * first as click causes a selection. 220 */ 221 class WheelStealingZoomer implements Listener, MouseTrackListener, FocusListener { 222 223 private Control stealWheelFrom; 224 225 @Override 226 public void handleEvent(Event event) { 227 if (isDisposed()) { 228 stop(); 229 } else if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) { 230 Point canvasSize = getSize(); 231 Point canvasPoint = toControl(stealWheelFrom.toDisplay(event.x, event.y)); 232 if (canvasPoint.x >= 0 && canvasPoint.y >= 0 && canvasPoint.x < canvasSize.x 233 && canvasPoint.y < canvasSize.y) { 234 handleWheelEvent(event.stateMask, canvasPoint.x, event.count); 235 event.doit = false; 236 } 237 } 238 } 239 240 private void stop() { 241 if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) { 242 stealWheelFrom.removeListener(SWT.MouseVerticalWheel, this); 243 stealWheelFrom.removeFocusListener(this); 244 stealWheelFrom = null; 245 } 246 } 247 248 @Override 249 public void mouseEnter(MouseEvent e) { 250 stop(); 251 Control stealWheelFrom = getDisplay().getFocusControl(); 252 if (stealWheelFrom != null && stealWheelFrom != ChartCanvas.this) { 253 stealWheelFrom.addListener(SWT.MouseVerticalWheel, this); 254 stealWheelFrom.addFocusListener(this); 255 this.stealWheelFrom = stealWheelFrom; 256 } 257 } 258 259 @Override 260 public void mouseExit(MouseEvent e) { 261 } 262 263 @Override 264 public void mouseHover(MouseEvent e) { 265 }; 266 267 @Override 268 public void focusGained(FocusEvent e) { 269 } 270 271 @Override 272 public void focusLost(FocusEvent e) { 273 stop(); 274 } 275 } 276 277 class KeyNavigator implements KeyListener { 278 279 @Override 280 public void keyPressed(KeyEvent event) { 281 switch (event.character) { 282 case '+': 283 zoom(1); 284 break; 285 case '-': 286 zoom(-1); 287 break; 288 default: 289 switch (event.keyCode) { 290 case SWT.ARROW_RIGHT: 291 pan(10); 292 break; 293 case SWT.ARROW_LEFT: 294 pan(-10); 295 break; 296 case SWT.ARROW_UP: 297 zoom(1); 298 break; 299 case SWT.ARROW_DOWN: 300 zoom(-1); 301 break; 302 default: 303 // Ignore 304 } 305 } 306 } 307 308 @Override 309 public void keyReleased(KeyEvent event) { 310 // Ignore 311 } 312 313 } 314 315 private class AntiAliasingListener implements IPropertyChangeListener { 316 317 @Override 318 public void propertyChange(PropertyChangeEvent event) { 319 redrawChart(); 320 } 321 322 } 323 324 /** 325 * This gets the "normal" DPI value for the system (72 on MacOS and 96 on Windows/Linux. It's 326 * used to determine how much larger the current DPI is so that we can draw the charts based on 327 * how large that area would be given the "normal" DPI value. Every draw on this smaller chart 328 * is then scaled up by the Graphics2D objects DefaultTransform. 329 */ 330 private final double xScale = Display.getDefault().getDPI().x / Environment.getNormalDPI(); 331 private final double yScale = Display.getDefault().getDPI().y / Environment.getNormalDPI(); 332 333 private final AwtCanvas awtCanvas = new AwtCanvas(); 334 private boolean awtNeedsRedraw; 335 private Runnable selectionListener; 336 private IPropertyChangeListener aaListener; 337 private XYChart awtChart; 338 private MCContextMenuManager chartMenu; 339 340 public ChartCanvas(Composite parent) { 341 super(parent, SWT.NO_BACKGROUND); 342 addPaintListener(new Painter()); 343 Selector selector = new Selector(); 344 addMouseListener(selector); 345 addMouseMoveListener(selector); 346 addMouseTrackListener(selector); 347 FocusTracker.enableFocusTracking(this); 348 addListener(SWT.MouseVerticalWheel, new Zoomer()); 349 addKeyListener(new KeyNavigator()); 350 aaListener = new AntiAliasingListener(); 351 UIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(aaListener); 352 addDisposeListener(e -> UIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(aaListener)); 353 if (Environment.getOSType() == OSType.WINDOWS) { 354 addMouseTrackListener(new WheelStealingZoomer()); 355 } 356 } 357 358 public IMenuManager getContextMenu() { 359 if (chartMenu == null) { 360 chartMenu = MCContextMenuManager.create(this); 361 chartMenu.addMenuListener(manager -> clearHighlightRects()); 362 } 363 return chartMenu; 364 } 365 366 private void render(Graphics2D context, int width, int height) { 367 if (awtChart != null) { 368 awtChart.render(context, width, height); 369 } 370 } 371 372 /** 373 * Translates display coordinates into image coordinates for the chart. 374 * 375 * @param x 376 * the provided x coordinate 377 * @param y 378 * the provided y coordinate 379 * @return a Point that represents the (x,y) coordinates in the chart's coordinate space 380 */ 381 private Point translateDisplayToImageCoordinates(int x, int y) { 382 int xImage = (int) Math.round(x / xScale); 383 int yImage = (int) Math.round(y / yScale); 384 return new Point(xImage, yImage); 385 } 386 387 /** 388 * Translates a display x coordinate into an image x coordinate for the chart. 389 * 390 * @param x 391 * the provided display x coordinate 392 * @return the x coordinate in the chart's coordinate space 393 */ 394 private int translateDisplayToImageXCoordinates(int x) { 395 return (int) Math.round(x / xScale); 396 } 397 398 public Object getHoveredItemData() { 399 return this.hoveredItemData; 400 } 401 402 public void setHoveredItemData(Object data) { 403 this.hoveredItemData = data; 404 } 405 406 public void resetHoveredItemData() { 407 this.hoveredItemData = null; 408 } 409 410 private void updateHighlightRects() { 411 List<Rectangle2D> newRects = new ArrayList<>(); 412 infoAt(new IChartInfoVisitor.Adapter() { 413 @Override 414 public void visit(IBucket bucket) { 415 newRects.add(bucket.getTarget()); 416 } 417 418 @Override 419 public void visit(IPoint point) { 420 Point2D target = point.getTarget(); 421 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0)); 422 } 423 424 @Override 425 public void visit(ISpan span) { 426 newRects.add(span.getTarget()); 427 } 428 429 @Override 430 public void visit(ITick tick) { 431 Point2D target = tick.getTarget(); 432 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0)); 433 } 434 435 @Override 436 public void visit(ILane lane) { 437 // FIXME: Do we want this highlighted? 438 } 439 440 @Override 441 public void hover(Object data) { 442 if (data != null) { 443 setHoveredItemData(data); 444 } 445 } 446 }, lastMouseX, lastMouseY); 447 // Attempt to reduce flicker by avoiding unnecessary updates. 448 if (!newRects.equals(highlightRects)) { 449 highlightRects = newRects; 450 redraw(); 451 } 452 } 453 454 private void clearHighlightRects() { 455 if (highlightRects != null) { 456 highlightRects = null; 457 redraw(); 458 } 459 } 460 461 private void handleWheelEvent(int stateMask, int x, int count) { 462 // SWT.MOD1 is CMD on OS X and CTRL elsewhere. 463 if ((stateMask & SWT.MOD1) != 0) { 464 pan(count * 3); 465 } else { 466 zoom(translateDisplayToImageXCoordinates(x), count); 467 } 468 } 469 470 private void pan(int rightPercent) { 471 if ((awtChart != null) && awtChart.pan(rightPercent)) { 472 redrawChart(); 473 } 474 } 475 476 private void zoom(int zoomInSteps) { 477 if ((awtChart != null) && awtChart.zoom(zoomInSteps)) { 478 redrawChart(); 479 } 480 } 481 482 private void zoom(int x, int zoomInSteps) { 483 if ((awtChart != null) && awtChart.zoom(x, zoomInSteps)) { 484 redrawChart(); 485 } 486 } 487 488 private void select(int x1, int x2, int y1, int y2) { 489 if ((awtChart != null) && awtChart.select(x1, x2, y1, y2)) { 490 redrawChart(); 491 } 492 } 493 494 private void toggleSelect(int x, int y) { 495 Point p = translateDisplayToImageCoordinates(x, y); 496 if (awtChart != null) { 497 final IQuantity[] range = new IQuantity[2]; 498 infoAt(new IChartInfoVisitor.Adapter() { 499 @Override 500 public void visit(IBucket bucket) { 501 if (range[0] == null) { 502 range[0] = (IQuantity) bucket.getStartX(); 503 range[1] = (IQuantity) bucket.getEndX(); 504 } 505 } 506 507 @Override 508 public void visit(ISpan span) { 509 if (range[0] == null) { 510 IDisplayable x0 = span.getStartX(); 511 IDisplayable x1 = span.getEndX(); 512 range[0] = (x0 instanceof IQuantity) ? (IQuantity) x0 : null; 513 range[1] = (x1 instanceof IQuantity) ? (IQuantity) x1 : null; 514 } 515 } 516 }, x, y); 517 if ((range[0] != null) || (range[1] != null)) { 518 if (!awtChart.select(range[0], range[1], p.y, p.y)) { 519 awtChart.clearSelection(); 520 } 521 } else { 522 if (!awtChart.select(p.x, p.x, p.y, p.y)) { 523 awtChart.clearSelection(); 524 } 525 } 526 redrawChart(); 527 } 528 } 529 530 public void setChart(XYChart awtChart) { 531 this.awtChart = awtChart; 532 notifyListener(); 533 redrawChart(); 534 } 535 536 public void replaceRenderer(IXDataRenderer rendererRoot) { 537 assert awtChart != null; 538 awtChart.setRendererRoot(rendererRoot); 539 notifyListener(); 540 redrawChart(); 541 } 542 543 public void setSelectionListener(Runnable selectionListener) { 544 this.selectionListener = selectionListener; 545 } 546 547 private void notifyListener() { 548 if (selectionListener != null) { 549 selectionListener.run(); 550 } 551 } 552 553 public void infoAt(IChartInfoVisitor visitor, int x, int y) { 554 Point p = translateDisplayToImageCoordinates(x, y); 555 if (awtChart != null) { 556 awtChart.infoAt(visitor, p.x, p.y); 557 } 558 } 559 560 /** 561 * Mark both the (AWT) chart and the SWT control as needing a redraw. 562 */ 563 public void redrawChart() { 564 awtNeedsRedraw = true; 565 redraw(); 566 } 567 }