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 }