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 }