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 }