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 81 private class Selector extends MouseAdapter implements MouseMoveListener, MouseTrackListener { 82 83 int selectionStartX = -1; 84 int selectionStartY = -1; 85 boolean selectionIsClick = false; 86 87 @Override 88 public void mouseDown(MouseEvent e) { 89 /* 90 * On Mac OS X, CTRL + left mouse button can be used to trigger a context menu. (This is 91 * for historical reasons when the primary input device on Macs were a mouse with a 92 * single physical button. All modern Macs have other means to bring up the context 93 * menu, typically a two finger tap.) 94 * 95 * Although I think it would be best to check that this MouseEvent does not cause a 96 * platform specific popup trigger, like java.awt.event.MouseEvent.isPopupTrigger() for 97 * AWT, SWT doesn't seem to have something as simple. It has the MenuDetectEvent, but 98 * the order in relation to this MouseEvent is unspecified. 99 * 100 * The code below instead relies on ignoring mouse down events when SWT.MOD4 is 101 * depressed. Since MOD4 is CTRL on OS X and 0 on all other current platforms, this 102 * suffices. Except for an additional platform check, this approach is also used in 103 * org.eclipse.swt.custom.StyledText.handleMouseDown(Event). 104 */ 105 if ((e.button == 1) && ((e.stateMask & SWT.MOD4) == 0)) { 106 selectionStartX = e.x; 107 selectionStartY = e.y; 108 selectionIsClick = true; 109 toggleSelect(selectionStartX, selectionStartY); 110 } 111 } 112 113 @Override 114 public void mouseMove(MouseEvent e) { 115 if (selectionStartX >= 0) { 116 highlightRects = null; 117 updateSelectionState(e); 118 } else { 119 lastMouseX = e.x; 120 lastMouseY = e.y; 121 updateHighlightRects(); 122 } 123 } 124 125 private void updateSelectionState(MouseEvent e) { 126 int x = e.x; 127 int y = e.y; 128 if (selectionIsClick && ((Math.abs(x - selectionStartX) > 3) || (Math.abs(y - selectionStartY) > 3))) { 129 selectionIsClick = false; 130 } 131 if (!selectionIsClick) { 132 select((int) (selectionStartX / xScale), (int) (x / xScale), (int) (selectionStartY / yScale), 133 (int) (y / yScale)); 134 } 135 } 136 137 @Override 138 public void mouseUp(MouseEvent e) { 139 if (selectionStartX >= 0 && (e.button == 1)) { 140 updateSelectionState(e); 141 selectionStartX = -1; 142 selectionStartY = -1; 143 if (selectionListener != null) { 144 selectionListener.run(); 145 } 146 } 147 } 148 149 @Override 150 public void mouseEnter(MouseEvent e) { 151 } 152 153 @Override 154 public void mouseExit(MouseEvent e) { 155 clearHighlightRects(); 156 } 157 158 @Override 159 public void mouseHover(MouseEvent e) { 160 } 161 } 162 163 class Painter implements PaintListener { 164 165 @Override 166 public void paintControl(PaintEvent e) { 167 Rectangle rect = getClientArea(); 168 if (awtNeedsRedraw || !awtCanvas.hasImage(rect.width, rect.height)) { 169 Graphics2D g2d = awtCanvas.getGraphics(rect.width, rect.height); 170 g2d.setColor(Color.WHITE); 171 g2d.fillRect(0, 0, rect.width, rect.height); 172 Point adjusted = translateDisplayToImageCoordinates(rect.width, rect.height); 173 render(g2d, adjusted.x, adjusted.y); 174 if (highlightRects != null) { 175 updateHighlightRects(); 176 } 177 awtNeedsRedraw = false; 178 } 179 awtCanvas.paint(e, 0, 0); 180 // Crude, flickering highlight of areas also delivered to tooltips. 181 // FIXME: Remove flicker by drawing in a buffered stage (AWT or SWT). 182 List<Rectangle2D> rs = highlightRects; 183 if (rs != null) { 184 GC gc = e.gc; 185 gc.setForeground(getForeground()); 186 for (Rectangle2D r : rs) { 187 int x = (int) (((int) r.getX()) * xScale); 188 int y = (int) (((int) r.getY()) * yScale); 189 if ((r.getWidth() == 0) && (r.getHeight() == 0)) { 190 int width = (int) Math.round(4 * xScale); 191 int height = (int) Math.round(4 * yScale); 192 gc.drawOval(x - (int) Math.round(2 * xScale), y - (int) Math.round(2 * yScale), width, height); 193 } else { 194 int width = (int) Math.round(r.getWidth() * xScale); 195 int height = (int) Math.round(r.getHeight() * yScale); 196 gc.drawRectangle(x, y, width, height); 197 } 198 } 199 } 200 } 201 } 202 203 class Zoomer implements Listener { 204 205 @Override 206 public void handleEvent(Event event) { 207 handleWheelEvent(event.stateMask, event.x, event.count); 208 } 209 210 } 211 212 /** 213 * Steals the wheel events from the currently focused control while hovering over this 214 * (ChartCanvas) control. Used on Windows to allow zooming without having to click in the chart 215 * first as click causes a selection. 216 */ 217 class WheelStealingZoomer implements Listener, MouseTrackListener, FocusListener { 218 219 private Control stealWheelFrom; 220 221 @Override 222 public void handleEvent(Event event) { 223 if (isDisposed()) { 224 stop(); 225 } else if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) { 226 Point canvasSize = getSize(); 227 Point canvasPoint = toControl(stealWheelFrom.toDisplay(event.x, event.y)); 228 if (canvasPoint.x >= 0 && canvasPoint.y >= 0 && canvasPoint.x < canvasSize.x 229 && canvasPoint.y < canvasSize.y) { 230 handleWheelEvent(event.stateMask, canvasPoint.x, event.count); 231 event.doit = false; 232 } 233 } 234 } 235 236 private void stop() { 237 if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) { 238 stealWheelFrom.removeListener(SWT.MouseVerticalWheel, this); 239 stealWheelFrom.removeFocusListener(this); 240 stealWheelFrom = null; 241 } 242 } 243 244 @Override 245 public void mouseEnter(MouseEvent e) { 246 stop(); 247 Control stealWheelFrom = getDisplay().getFocusControl(); 248 if (stealWheelFrom != null && stealWheelFrom != ChartCanvas.this) { 249 stealWheelFrom.addListener(SWT.MouseVerticalWheel, this); 250 stealWheelFrom.addFocusListener(this); 251 this.stealWheelFrom = stealWheelFrom; 252 } 253 } 254 255 @Override 256 public void mouseExit(MouseEvent e) { 257 } 258 259 @Override 260 public void mouseHover(MouseEvent e) { 261 }; 262 263 @Override 264 public void focusGained(FocusEvent e) { 265 } 266 267 @Override 268 public void focusLost(FocusEvent e) { 269 stop(); 270 } 271 } 272 273 class KeyNavigator implements KeyListener { 274 275 @Override 276 public void keyPressed(KeyEvent event) { 277 switch (event.character) { 278 case '+': 279 zoom(1); 280 break; 281 case '-': 282 zoom(-1); 283 break; 284 default: 285 switch (event.keyCode) { 286 case SWT.ARROW_RIGHT: 287 pan(10); 288 break; 289 case SWT.ARROW_LEFT: 290 pan(-10); 291 break; 292 case SWT.ARROW_UP: 293 zoom(1); 294 break; 295 case SWT.ARROW_DOWN: 296 zoom(-1); 297 break; 298 default: 299 // Ignore 300 } 301 } 302 } 303 304 @Override 305 public void keyReleased(KeyEvent event) { 306 // Ignore 307 } 308 309 } 310 311 private class AntiAliasingListener implements IPropertyChangeListener { 312 313 @Override 314 public void propertyChange(PropertyChangeEvent event) { 315 redrawChart(); 316 } 317 318 } 319 320 /** 321 * This gets the "normal" DPI value for the system (72 on MacOS and 96 on Windows/Linux. It's 322 * used to determine how much larger the current DPI is so that we can draw the charts based on 323 * how large that area would be given the "normal" DPI value. Every draw on this smaller chart 324 * is then scaled up by the Graphics2D objects DefaultTransform. 325 */ 326 private final double xScale = Display.getDefault().getDPI().x / Environment.getNormalDPI(); 327 private final double yScale = Display.getDefault().getDPI().y / Environment.getNormalDPI(); 328 329 private final AwtCanvas awtCanvas = new AwtCanvas(); 330 private boolean awtNeedsRedraw; 331 private Runnable selectionListener; 332 private IPropertyChangeListener aaListener; 333 private XYChart awtChart; 334 private MCContextMenuManager chartMenu; 335 336 public ChartCanvas(Composite parent) { 337 super(parent, SWT.NO_BACKGROUND); 338 addPaintListener(new Painter()); 339 Selector selector = new Selector(); 340 addMouseListener(selector); 341 addMouseMoveListener(selector); 342 addMouseTrackListener(selector); 343 FocusTracker.enableFocusTracking(this); 344 addListener(SWT.MouseVerticalWheel, new Zoomer()); 345 addKeyListener(new KeyNavigator()); 346 aaListener = new AntiAliasingListener(); 347 UIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(aaListener); 348 addDisposeListener(e -> UIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(aaListener)); 349 if (Environment.getOSType() == OSType.WINDOWS) { 350 addMouseTrackListener(new WheelStealingZoomer()); 351 } 352 } 353 354 public IMenuManager getContextMenu() { 355 if (chartMenu == null) { 356 chartMenu = MCContextMenuManager.create(this); 357 chartMenu.addMenuListener(manager -> clearHighlightRects()); 358 } 359 return chartMenu; 360 } 361 362 private void render(Graphics2D context, int width, int height) { 363 if (awtChart != null) { 364 awtChart.render(context, width, height); 365 } 366 } 367 368 /** 369 * Translates display coordinates into image coordinates for the chart. 370 * 371 * @param x 372 * the provided x coordinate 373 * @param y 374 * the provided y coordinate 375 * @return a Point that represents the (x,y) coordinates in the chart's coordinate space 376 */ 377 private Point translateDisplayToImageCoordinates(int x, int y) { 378 int xImage = (int) Math.round(x / xScale); 379 int yImage = (int) Math.round(y / yScale); 380 return new Point(xImage, yImage); 381 } 382 383 /** 384 * Translates a display x coordinate into an image x coordinate for the chart. 385 * 386 * @param x 387 * the provided display x coordinate 388 * @return the x coordinate in the chart's coordinate space 389 */ 390 private int translateDisplayToImageXCoordinates(int x) { 391 return (int) Math.round(x / xScale); 392 } 393 394 private void updateHighlightRects() { 395 List<Rectangle2D> newRects = new ArrayList<>(); 396 infoAt(new IChartInfoVisitor.Adapter() { 397 @Override 398 public void visit(IBucket bucket) { 399 newRects.add(bucket.getTarget()); 400 } 401 402 @Override 403 public void visit(IPoint point) { 404 Point2D target = point.getTarget(); 405 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0)); 406 } 407 408 @Override 409 public void visit(ISpan span) { 410 newRects.add(span.getTarget()); 411 } 412 413 @Override 414 public void visit(ITick tick) { 415 Point2D target = tick.getTarget(); 416 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0)); 417 } 418 419 @Override 420 public void visit(ILane lane) { 421 // FIXME: Do we want this highlighted? 422 } 423 }, lastMouseX, lastMouseY); 424 // Attempt to reduce flicker by avoiding unnecessary updates. 425 if (!newRects.equals(highlightRects)) { 426 highlightRects = newRects; 427 redraw(); 428 } 429 } 430 431 private void clearHighlightRects() { 432 if (highlightRects != null) { 433 highlightRects = null; 434 redraw(); 435 } 436 } 437 438 private void handleWheelEvent(int stateMask, int x, int count) { 439 // SWT.MOD1 is CMD on OS X and CTRL elsewhere. 440 if ((stateMask & SWT.MOD1) != 0) { 441 pan(count * 3); 442 } else { 443 zoom(translateDisplayToImageXCoordinates(x), count); 444 } 445 } 446 447 private void pan(int rightPercent) { 448 if ((awtChart != null) && awtChart.pan(rightPercent)) { 449 redrawChart(); 450 } 451 } 452 453 private void zoom(int zoomInSteps) { 454 if ((awtChart != null) && awtChart.zoom(zoomInSteps)) { 455 redrawChart(); 456 } 457 } 458 459 private void zoom(int x, int zoomInSteps) { 460 if ((awtChart != null) && awtChart.zoom(x, zoomInSteps)) { 461 redrawChart(); 462 } 463 } 464 465 private void select(int x1, int x2, int y1, int y2) { 466 if ((awtChart != null) && awtChart.select(x1, x2, y1, y2)) { 467 redrawChart(); 468 } 469 } 470 471 private void toggleSelect(int x, int y) { 472 Point p = translateDisplayToImageCoordinates(x, y); 473 if (awtChart != null) { 474 final IQuantity[] range = new IQuantity[2]; 475 infoAt(new IChartInfoVisitor.Adapter() { 476 @Override 477 public void visit(IBucket bucket) { 478 if (range[0] == null) { 479 range[0] = (IQuantity) bucket.getStartX(); 480 range[1] = (IQuantity) bucket.getEndX(); 481 } 482 } 483 484 @Override 485 public void visit(ISpan span) { 486 if (range[0] == null) { 487 IDisplayable x0 = span.getStartX(); 488 IDisplayable x1 = span.getEndX(); 489 range[0] = (x0 instanceof IQuantity) ? (IQuantity) x0 : null; 490 range[1] = (x1 instanceof IQuantity) ? (IQuantity) x1 : null; 491 } 492 } 493 }, x, y); 494 if ((range[0] != null) || (range[1] != null)) { 495 if (!awtChart.select(range[0], range[1], p.y, p.y)) { 496 awtChart.clearSelection(); 497 } 498 } else { 499 if (!awtChart.select(p.x, p.x, p.y, p.y)) { 500 awtChart.clearSelection(); 501 } 502 } 503 redrawChart(); 504 } 505 } 506 507 public void setChart(XYChart awtChart) { 508 this.awtChart = awtChart; 509 notifyListener(); 510 redrawChart(); 511 } 512 513 public void replaceRenderer(IXDataRenderer rendererRoot) { 514 assert awtChart != null; 515 awtChart.setRendererRoot(rendererRoot); 516 notifyListener(); 517 redrawChart(); 518 } 519 520 public void setSelectionListener(Runnable selectionListener) { 521 this.selectionListener = selectionListener; 522 } 523 524 private void notifyListener() { 525 if (selectionListener != null) { 526 selectionListener.run(); 527 } 528 } 529 530 public void infoAt(IChartInfoVisitor visitor, int x, int y) { 531 Point p = translateDisplayToImageCoordinates(x, y); 532 if (awtChart != null) { 533 awtChart.infoAt(visitor, p.x, p.y); 534 } 535 } 536 537 /** 538 * Mark both the (AWT) chart and the SWT control as needing a redraw. 539 */ 540 public void redrawChart() { 541 awtNeedsRedraw = true; 542 redraw(); 543 } 544 }