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 resetHoveredItemData(); 157 clearHighlightRects(); 158 } 159 160 @Override 161 public void mouseHover(MouseEvent e) { 162 } 163 } 164 165 class Painter implements PaintListener { 166 167 @Override 168 public void paintControl(PaintEvent e) { 169 Rectangle rect = getClientArea(); 170 if (awtNeedsRedraw || !awtCanvas.hasImage(rect.width, rect.height)) { 171 Graphics2D g2d = awtCanvas.getGraphics(rect.width, rect.height); 172 g2d.setColor(Color.WHITE); 173 g2d.fillRect(0, 0, rect.width, rect.height); 174 Point adjusted = translateDisplayToImageCoordinates(rect.width, rect.height); 175 render(g2d, adjusted.x, adjusted.y); 176 if (highlightRects != null) { 177 updateHighlightRects(); 178 } 179 awtNeedsRedraw = false; 180 } 181 awtCanvas.paint(e, 0, 0); 182 // Crude, flickering highlight of areas also delivered to tooltips. 183 // FIXME: Remove flicker by drawing in a buffered stage (AWT or SWT). 184 List<Rectangle2D> rs = highlightRects; 185 if (rs != null) { 186 GC gc = e.gc; 187 gc.setForeground(getForeground()); 188 for (Rectangle2D r : rs) { 189 int x = (int) (((int) r.getX()) * xScale); 190 int y = (int) (((int) r.getY()) * yScale); 191 if ((r.getWidth() == 0) && (r.getHeight() == 0)) { 192 int width = (int) Math.round(4 * xScale); 193 int height = (int) Math.round(4 * yScale); 194 gc.drawOval(x - (int) Math.round(2 * xScale), y - (int) Math.round(2 * yScale), width, height); 195 } else { 196 int width = (int) Math.round(r.getWidth() * xScale); 197 int height = (int) Math.round(r.getHeight() * yScale); 198 gc.drawRectangle(x, y, width, height); 199 } 200 } 201 } 202 } 203 } 204 205 class Zoomer implements Listener { 206 207 @Override 208 public void handleEvent(Event event) { 209 handleWheelEvent(event.stateMask, event.x, event.count); 210 } 211 212 } 213 214 /** 215 * Steals the wheel events from the currently focused control while hovering over this 216 * (ChartCanvas) control. Used on Windows to allow zooming without having to click in the chart 217 * first as click causes a selection. 218 */ 219 class WheelStealingZoomer implements Listener, MouseTrackListener, FocusListener { 220 221 private Control stealWheelFrom; 222 223 @Override 224 public void handleEvent(Event event) { 225 if (isDisposed()) { 226 stop(); 227 } else if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) { 228 Point canvasSize = getSize(); 229 Point canvasPoint = toControl(stealWheelFrom.toDisplay(event.x, event.y)); 230 if (canvasPoint.x >= 0 && canvasPoint.y >= 0 && canvasPoint.x < canvasSize.x 231 && canvasPoint.y < canvasSize.y) { 232 handleWheelEvent(event.stateMask, canvasPoint.x, event.count); 233 event.doit = false; 234 } 235 } 236 } 237 238 private void stop() { 239 if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) { 240 stealWheelFrom.removeListener(SWT.MouseVerticalWheel, this); 241 stealWheelFrom.removeFocusListener(this); 242 stealWheelFrom = null; 243 } 244 } 245 246 @Override 247 public void mouseEnter(MouseEvent e) { 248 stop(); 249 Control stealWheelFrom = getDisplay().getFocusControl(); 250 if (stealWheelFrom != null && stealWheelFrom != ChartCanvas.this) { 251 stealWheelFrom.addListener(SWT.MouseVerticalWheel, this); 252 stealWheelFrom.addFocusListener(this); 253 this.stealWheelFrom = stealWheelFrom; 254 } 255 } 256 257 @Override 258 public void mouseExit(MouseEvent e) { 259 } 260 261 @Override 262 public void mouseHover(MouseEvent e) { 263 }; 264 265 @Override 266 public void focusGained(FocusEvent e) { 267 } 268 269 @Override 270 public void focusLost(FocusEvent e) { 271 stop(); 272 } 273 } 274 275 class KeyNavigator implements KeyListener { 276 277 @Override 278 public void keyPressed(KeyEvent event) { 279 switch (event.character) { 280 case '+': 281 zoom(1); 282 break; 283 case '-': 284 zoom(-1); 285 break; 286 default: 287 switch (event.keyCode) { 288 case SWT.ARROW_RIGHT: 289 pan(10); 290 break; 291 case SWT.ARROW_LEFT: 292 pan(-10); 293 break; 294 case SWT.ARROW_UP: 295 zoom(1); 296 break; 297 case SWT.ARROW_DOWN: 298 zoom(-1); 299 break; 300 default: 301 // Ignore 302 } 303 } 304 } 305 306 @Override 307 public void keyReleased(KeyEvent event) { 308 // Ignore 309 } 310 311 } 312 313 private class AntiAliasingListener implements IPropertyChangeListener { 314 315 @Override 316 public void propertyChange(PropertyChangeEvent event) { 317 redrawChart(); 318 } 319 320 } 321 322 /** 323 * This gets the "normal" DPI value for the system (72 on MacOS and 96 on Windows/Linux. It's 324 * used to determine how much larger the current DPI is so that we can draw the charts based on 325 * how large that area would be given the "normal" DPI value. Every draw on this smaller chart 326 * is then scaled up by the Graphics2D objects DefaultTransform. 327 */ 328 private final double xScale = Display.getDefault().getDPI().x / Environment.getNormalDPI(); 329 private final double yScale = Display.getDefault().getDPI().y / Environment.getNormalDPI(); 330 331 private final AwtCanvas awtCanvas = new AwtCanvas(); 332 private boolean awtNeedsRedraw; 333 private Runnable selectionListener; 334 private IPropertyChangeListener aaListener; 335 private XYChart awtChart; 336 private MCContextMenuManager chartMenu; 337 338 public ChartCanvas(Composite parent) { 339 super(parent, SWT.NO_BACKGROUND); 340 addPaintListener(new Painter()); 341 Selector selector = new Selector(); 342 addMouseListener(selector); 343 addMouseMoveListener(selector); 344 addMouseTrackListener(selector); 345 FocusTracker.enableFocusTracking(this); 346 addListener(SWT.MouseVerticalWheel, new Zoomer()); 347 addKeyListener(new KeyNavigator()); 348 aaListener = new AntiAliasingListener(); 349 UIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(aaListener); 350 addDisposeListener(e -> UIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(aaListener)); 351 if (Environment.getOSType() == OSType.WINDOWS) { 352 addMouseTrackListener(new WheelStealingZoomer()); 353 } 354 } 355 356 public IMenuManager getContextMenu() { 357 if (chartMenu == null) { 358 chartMenu = MCContextMenuManager.create(this); 359 chartMenu.addMenuListener(manager -> clearHighlightRects()); 360 } 361 return chartMenu; 362 } 363 364 private void render(Graphics2D context, int width, int height) { 365 if (awtChart != null) { 366 awtChart.render(context, width, height); 367 } 368 } 369 370 /** 371 * Translates display coordinates into image coordinates for the chart. 372 * 373 * @param x 374 * the provided x coordinate 375 * @param y 376 * the provided y coordinate 377 * @return a Point that represents the (x,y) coordinates in the chart's coordinate space 378 */ 379 private Point translateDisplayToImageCoordinates(int x, int y) { 380 int xImage = (int) Math.round(x / xScale); 381 int yImage = (int) Math.round(y / yScale); 382 return new Point(xImage, yImage); 383 } 384 385 /** 386 * Translates a display x coordinate into an image x coordinate for the chart. 387 * 388 * @param x 389 * the provided display x coordinate 390 * @return the x coordinate in the chart's coordinate space 391 */ 392 private int translateDisplayToImageXCoordinates(int x) { 393 return (int) Math.round(x / xScale); 394 } 395 396 public Object getHoveredItemData() { 397 return this.hoveredItemData; 398 } 399 400 public void setHoveredItemData(Object data) { 401 this.hoveredItemData = data; 402 } 403 404 public void resetHoveredItemData() { 405 this.hoveredItemData = null; 406 } 407 408 private void updateHighlightRects() { 409 List<Rectangle2D> newRects = new ArrayList<>(); 410 infoAt(new IChartInfoVisitor.Adapter() { 411 @Override 412 public void visit(IBucket bucket) { 413 newRects.add(bucket.getTarget()); 414 } 415 416 @Override 417 public void visit(IPoint point) { 418 Point2D target = point.getTarget(); 419 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0)); 420 } 421 422 @Override 423 public void visit(ISpan span) { 424 newRects.add(span.getTarget()); 425 } 426 427 @Override 428 public void visit(ITick tick) { 429 Point2D target = tick.getTarget(); 430 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0)); 431 } 432 433 @Override 434 public void visit(ILane lane) { 435 // FIXME: Do we want this highlighted? 436 } 437 438 @Override 439 public void hover(Object data) { 440 if (data != null) { 441 setHoveredItemData(data); 442 } 443 } 444 }, lastMouseX, lastMouseY); 445 // Attempt to reduce flicker by avoiding unnecessary updates. 446 if (!newRects.equals(highlightRects)) { 447 highlightRects = newRects; 448 redraw(); 449 } 450 } 451 452 private void clearHighlightRects() { 453 if (highlightRects != null) { 454 highlightRects = null; 455 redraw(); 456 } 457 } 458 459 private void handleWheelEvent(int stateMask, int x, int count) { 460 // SWT.MOD1 is CMD on OS X and CTRL elsewhere. 461 if ((stateMask & SWT.MOD1) != 0) { 462 pan(count * 3); 463 } else { 464 zoom(translateDisplayToImageXCoordinates(x), count); 465 } 466 } 467 468 private void pan(int rightPercent) { 469 if ((awtChart != null) && awtChart.pan(rightPercent)) { 470 redrawChart(); 471 } 472 } 473 474 private void zoom(int zoomInSteps) { 475 if ((awtChart != null) && awtChart.zoom(zoomInSteps)) { 476 redrawChart(); 477 } 478 } 479 480 private void zoom(int x, int zoomInSteps) { 481 if ((awtChart != null) && awtChart.zoom(x, zoomInSteps)) { 482 redrawChart(); 483 } 484 } 485 486 private void select(int x1, int x2, int y1, int y2) { 487 if ((awtChart != null) && awtChart.select(x1, x2, y1, y2)) { 488 redrawChart(); 489 } 490 } 491 492 private void toggleSelect(int x, int y) { 493 Point p = translateDisplayToImageCoordinates(x, y); 494 if (awtChart != null) { 495 final IQuantity[] range = new IQuantity[2]; 496 infoAt(new IChartInfoVisitor.Adapter() { 497 @Override 498 public void visit(IBucket bucket) { 499 if (range[0] == null) { 500 range[0] = (IQuantity) bucket.getStartX(); 501 range[1] = (IQuantity) bucket.getEndX(); 502 } 503 } 504 505 @Override 506 public void visit(ISpan span) { 507 if (range[0] == null) { 508 IDisplayable x0 = span.getStartX(); 509 IDisplayable x1 = span.getEndX(); 510 range[0] = (x0 instanceof IQuantity) ? (IQuantity) x0 : null; 511 range[1] = (x1 instanceof IQuantity) ? (IQuantity) x1 : null; 512 } 513 } 514 }, x, y); 515 if ((range[0] != null) || (range[1] != null)) { 516 if (!awtChart.select(range[0], range[1], p.y, p.y)) { 517 awtChart.clearSelection(); 518 } 519 } else { 520 if (!awtChart.select(p.x, p.x, p.y, p.y)) { 521 awtChart.clearSelection(); 522 } 523 } 524 redrawChart(); 525 } 526 } 527 528 public void setChart(XYChart awtChart) { 529 this.awtChart = awtChart; 530 notifyListener(); 531 redrawChart(); 532 } 533 534 public void replaceRenderer(IXDataRenderer rendererRoot) { 535 assert awtChart != null; 536 awtChart.setRendererRoot(rendererRoot); 537 notifyListener(); 538 redrawChart(); 539 } 540 541 public void setSelectionListener(Runnable selectionListener) { 542 this.selectionListener = selectionListener; 543 } 544 545 private void notifyListener() { 546 if (selectionListener != null) { 547 selectionListener.run(); 548 } 549 } 550 551 public void infoAt(IChartInfoVisitor visitor, int x, int y) { 552 Point p = translateDisplayToImageCoordinates(x, y); 553 if (awtChart != null) { 554 awtChart.infoAt(visitor, p.x, p.y); 555 } 556 } 557 558 /** 559 * Mark both the (AWT) chart and the SWT control as needing a redraw. 560 */ 561 public void redrawChart() { 562 awtNeedsRedraw = true; 563 redraw(); 564 } 565 }