1 /*
   2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * Copyright (c) 2019, Red Hat Inc. All rights reserved.
   4  *
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * The contents of this file are subject to the terms of either the Universal Permissive License
   8  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   9  *
  10  * or the following license:
  11  *
  12  * Redistribution and use in source and binary forms, with or without modification, are permitted
  13  * provided that the following conditions are met:
  14  *
  15  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  16  * and the following disclaimer.
  17  *
  18  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  19  * conditions and the following disclaimer in the documentation and/or other materials provided with
  20  * the distribution.
  21  *
  22  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  23  * endorse or promote products derived from this software without specific prior written permission.
  24  *
  25  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  26  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  27  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  28  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  29  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  30  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  31  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  32  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  33  */
  34 package org.openjdk.jmc.ui.misc;
  35 
  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 import java.util.function.Consumer;
  42 
  43 import org.eclipse.jface.action.IMenuManager;
  44 import org.eclipse.jface.util.IPropertyChangeListener;
  45 import org.eclipse.jface.util.PropertyChangeEvent;
  46 import org.eclipse.swt.SWT;
  47 import org.eclipse.swt.custom.ScrolledComposite;
  48 import org.eclipse.swt.events.FocusEvent;
  49 import org.eclipse.swt.events.FocusListener;
  50 import org.eclipse.swt.events.KeyEvent;
  51 import org.eclipse.swt.events.KeyListener;
  52 import org.eclipse.swt.events.MouseAdapter;
  53 import org.eclipse.swt.events.MouseEvent;
  54 import org.eclipse.swt.events.MouseMoveListener;
  55 import org.eclipse.swt.events.MouseTrackListener;
  56 import org.eclipse.swt.events.PaintEvent;
  57 import org.eclipse.swt.events.PaintListener;
  58 import org.eclipse.swt.graphics.Cursor;
  59 import org.eclipse.swt.graphics.GC;
  60 import org.eclipse.swt.graphics.Point;
  61 import org.eclipse.swt.graphics.Rectangle;
  62 import org.eclipse.swt.widgets.Canvas;
  63 import org.eclipse.swt.widgets.Composite;
  64 import org.eclipse.swt.widgets.Control;
  65 import org.eclipse.swt.widgets.Display;
  66 import org.eclipse.swt.widgets.Event;
  67 import org.eclipse.swt.widgets.Listener;
  68 import org.openjdk.jmc.common.IDisplayable;
  69 import org.openjdk.jmc.common.unit.IQuantity;
  70 import org.openjdk.jmc.ui.UIPlugin;
  71 import org.openjdk.jmc.ui.accessibility.FocusTracker;
  72 import org.openjdk.jmc.ui.charts.IChartInfoVisitor;
  73 import org.openjdk.jmc.ui.charts.IXDataRenderer;
  74 import org.openjdk.jmc.ui.charts.XYChart;
  75 import org.openjdk.jmc.ui.common.util.Environment;
  76 import org.openjdk.jmc.ui.common.util.Environment.OSType;
  77 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
  78 import org.openjdk.jmc.ui.misc.PatternFly.Palette;
  79 
  80 public class ChartCanvas extends Canvas {
  81         private static int MIN_LANE_HEIGHT = 50;
  82         private int lastMouseX = -1;
  83         private int lastMouseY = -1;
  84         private List<Rectangle2D> highlightRects;
  85         private Object hoveredItemData;
  86 
  87         private class Selector extends MouseAdapter implements MouseMoveListener, MouseTrackListener {
  88 
  89                 int selectionStartX = -1;
  90                 int selectionStartY = -1;
  91                 Point highlightSelectionStart;
  92                 Point highlightSelectionEnd;
  93                 Point lastSelection;
  94                 boolean selectionIsClick = false;
  95 
  96                 @Override
  97                 public void mouseDown(MouseEvent e) {
  98                         /*
  99                          * On Mac OS X, CTRL + left mouse button can be used to trigger a context menu. (This is
 100                          * for historical reasons when the primary input device on Macs were a mouse with a
 101                          * single physical button. All modern Macs have other means to bring up the context
 102                          * menu, typically a two finger tap.)
 103                          * 
 104                          * Although I think it would be best to check that this MouseEvent does not cause a
 105                          * platform specific popup trigger, like java.awt.event.MouseEvent.isPopupTrigger() for
 106                          * AWT, SWT doesn't seem to have something as simple. It has the MenuDetectEvent, but
 107                          * the order in relation to this MouseEvent is unspecified.
 108                          * 
 109                          * The code below instead relies on ignoring mouse down events when SWT.MOD4 is
 110                          * depressed. Since MOD4 is CTRL on OS X and 0 on all other current platforms, this
 111                          * suffices. Except for an additional platform check, this approach is also used in
 112                          * org.eclipse.swt.custom.StyledText.handleMouseDown(Event).
 113                          */
 114                         if ((e.button == 1) && ((e.stateMask & SWT.MOD4) == 0) && ((e.stateMask & SWT.CTRL) == 0 ) && ((e.stateMask & SWT.SHIFT) == 0 )) {
 115                                 selectionStartX = e.x;
 116                                 selectionStartY = e.y;
 117                                 highlightSelectionEnd = new Point(-1, -1);
 118                                 lastSelection = new Point(-1, -1);
 119                                 selectionIsClick = true;
 120                                 toggleSelect(selectionStartX, selectionStartY);
 121                         } else if (((e.stateMask & SWT.CTRL) != 0) && (e.button == 1)) {
 122                                 select(e.x, e.x, e.y, e.y, false);
 123                                 if (selectionListener != null) {
 124                                         selectionListener.run();
 125                                 }
 126                         } else if (((e.stateMask & SWT.SHIFT) != 0) && (e.button == 1)) {
 127                                  if (highlightSelectionEnd.y == -1) {
 128                                         highlightSelectionEnd = new Point(e.x, e.y);
 129                                         lastSelection = highlightSelectionEnd;
 130                                         if (highlightSelectionStart.y > highlightSelectionEnd.y) {
 131                                                 Point temp = highlightSelectionStart;
 132                                                 highlightSelectionStart = highlightSelectionEnd;
 133                                                 highlightSelectionEnd = temp;
 134                                         }
 135                                 } else {
 136                                         if (e.y > highlightSelectionStart.y && e.y < highlightSelectionEnd.y) {
 137                                                 if (e.y < lastSelection.y) {
 138                                                         highlightSelectionEnd = new Point(e.x, e.y);
 139                                                 } else if (e.y > lastSelection.y) {
 140                                                         highlightSelectionStart = new Point(e.x, e.y);
 141                                                 }
 142                                         } else if (e.y < highlightSelectionStart.y) {
 143                                                 highlightSelectionStart = new Point(e.x, e.y);
 144                                                 lastSelection = highlightSelectionStart;
 145                                         } else if (e.y > highlightSelectionEnd.y) {
 146                                                 highlightSelectionEnd = new Point(e.x, e.y);
 147                                                 lastSelection = highlightSelectionEnd;
 148                                         }
 149                                 }
 150                                 select(highlightSelectionStart.x, highlightSelectionEnd.x, highlightSelectionStart.y, highlightSelectionEnd.y, true);
 151                                 if (selectionListener != null) {
 152                                         selectionListener.run();
 153                                 }
 154                         }
 155                 }
 156 
 157                 @Override
 158                 public void mouseMove(MouseEvent e) {
 159                         if (selectionStartX >= 0) {
 160                                 highlightRects = null;
 161                                 updateSelectionState(e);
 162                         } else {
 163                                 lastMouseX = e.x;
 164                                 lastMouseY = e.y;
 165                                 updateHighlightRects();
 166                         }
 167                 }
 168 
 169                 private void updateSelectionState(MouseEvent e) {
 170                         int x = e.x;
 171                         int y = e.y;
 172                         if (selectionIsClick && ((Math.abs(x - selectionStartX) > 3) || (Math.abs(y - selectionStartY) > 3))) {
 173                                 selectionIsClick = false;
 174                         }
 175                         if (!selectionIsClick) {
 176                                 select((int) (selectionStartX / xScale), (int) (x / xScale), (int) (selectionStartY / yScale),
 177                                                 (int) (y / yScale), true);
 178                         }
 179                 }
 180 
 181                 @Override
 182                 public void mouseUp(MouseEvent e) {
 183                         if (selectionStartX >= 0 && (e.button == 1)) {
 184                                 updateSelectionState(e);
 185                                 highlightSelectionStart = new Point(selectionStartX, selectionStartY);
 186                                 selectionStartX = -1;
 187                                 selectionStartY = -1;
 188                                 if (selectionIsClick) {
 189                                         notifyZoomOnClickListener(e.button);
 190                                 }
 191                                 if (selectionListener != null) {
 192                                         selectionListener.run();
 193                                         if (zoomToSelectionListener != null && !selectionIsClick) {
 194                                                 zoomToSelectionListener.run();
 195                                         }
 196                                 }
 197                         }
 198                 }
 199 
 200                 @Override
 201                 public void mouseEnter(MouseEvent e) {
 202                 }
 203 
 204                 @Override
 205                 public void mouseExit(MouseEvent e) {
 206                         if (!getClientArea().contains(e.x, e.y)) {
 207                                 resetHoveredItemData();
 208                         }
 209                         clearHighlightRects();
 210                 }
 211 
 212                 @Override
 213                 public void mouseHover(MouseEvent e) {
 214                 }
 215         }
 216 
 217         private int numItems = 0;
 218         public void setNumItems(int numItems) {
 219                 this.numItems = numItems;
 220         }
 221 
 222         private int getNumItems() {
 223                 return numItems;
 224         }
 225 
 226         class Painter implements PaintListener {
 227 
 228                 @Override
 229                 public void paintControl(PaintEvent e) {
 230                         Rectangle rect = new Rectangle(0, 0, getParent().getSize().x, getParent().getSize().y);
 231                         if (getNumItems() == 0) {
 232                                 rect = getClientArea();
 233                         } else if (getNumItems() == 1 || (MIN_LANE_HEIGHT * getNumItems() < rect.height)) {
 234                                 // it fills the height
 235                         } else {
 236                                 rect.height = MIN_LANE_HEIGHT * getNumItems();
 237                         }
 238 
 239                         if (awtNeedsRedraw || !awtCanvas.hasImage(rect.width, rect.height)) {
 240                                 Graphics2D g2d = awtCanvas.getGraphics(rect.width, rect.height);
 241                                 Point adjusted = translateDisplayToImageCoordinates(rect.width, rect.height);
 242                                 g2d.setColor(Palette.PF_BLACK_100.getAWTColor());
 243                                 g2d.fillRect(0, 0, adjusted.x, adjusted.y);
 244                                 render(g2d, adjusted.x, adjusted.y);
 245                                 if (getParent() instanceof ScrolledComposite) {
 246                                         ((ScrolledComposite) getParent()).setMinSize(rect.width, rect.height);
 247                                 }
 248                                 if (highlightRects != null) {
 249                                         updateHighlightRects();
 250                                 }
 251                                 awtNeedsRedraw = false;
 252                         }
 253                         awtCanvas.paint(e, 0, 0);
 254                         // Crude, flickering highlight of areas also delivered to tooltips.
 255                         // FIXME: Remove flicker by drawing in a buffered stage (AWT or SWT).
 256                         List<Rectangle2D> rs = highlightRects;
 257                         if (rs != null) {
 258                                 GC gc = e.gc;
 259                                 gc.setForeground(getForeground());
 260                                 for (Rectangle2D r : rs) {
 261                                         int x = (int) (((int) r.getX()) * xScale);
 262                                         int y = (int) (((int) r.getY()) * yScale);
 263                                         if ((r.getWidth() == 0) && (r.getHeight() == 0)) {
 264                                                 int width = (int) Math.round(4 * xScale);
 265                                                 int height = (int) Math.round(4 * yScale);
 266                                                 gc.drawOval(x - (int) Math.round(2 * xScale), y - (int) Math.round(2 * yScale), width, height);
 267                                         } else {
 268                                                 int width = (int) Math.round(r.getWidth() * xScale);
 269                                                 int height = (int) Math.round(r.getHeight() * yScale);
 270                                                 gc.drawRectangle(x, y, width, height);
 271                                         }
 272                                 }
 273                         }
 274                 }
 275         }
 276 
 277         class Zoomer implements Listener {
 278 
 279                 @Override
 280                 public void handleEvent(Event event) {
 281                         handleWheelEvent(event.stateMask, event.x, event.count);
 282                 }
 283 
 284         }
 285 
 286         /**
 287          * Steals the wheel events from the currently focused control while hovering over this
 288          * (ChartCanvas) control. Used on Windows to allow zooming without having to click in the chart
 289          * first as click causes a selection.
 290          */
 291         class WheelStealingZoomer implements Listener, MouseTrackListener, FocusListener {
 292 
 293                 private Control stealWheelFrom;
 294 
 295                 @Override
 296                 public void handleEvent(Event event) {
 297                         if (isDisposed()) {
 298                                 stop();
 299                         } else if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) {
 300                                 Point canvasSize = getSize();
 301                                 Point canvasPoint = toControl(stealWheelFrom.toDisplay(event.x, event.y));
 302                                 if (canvasPoint.x >= 0 && canvasPoint.y >= 0 && canvasPoint.x < canvasSize.x
 303                                                 && canvasPoint.y < canvasSize.y) {
 304                                         handleWheelEvent(event.stateMask, canvasPoint.x, event.count);
 305                                         event.doit = false;
 306                                 }
 307                         }
 308                 }
 309 
 310                 private void stop() {
 311                         if (stealWheelFrom != null && !stealWheelFrom.isDisposed()) {
 312                                 stealWheelFrom.removeListener(SWT.MouseVerticalWheel, this);
 313                                 stealWheelFrom.removeFocusListener(this);
 314                                 stealWheelFrom = null;
 315                         }
 316                 }
 317 
 318                 @Override
 319                 public void mouseEnter(MouseEvent e) {
 320                         stop();
 321                         Control stealWheelFrom = getDisplay().getFocusControl();
 322                         if (stealWheelFrom != null && stealWheelFrom != ChartCanvas.this) {
 323                                 stealWheelFrom.addListener(SWT.MouseVerticalWheel, this);
 324                                 stealWheelFrom.addFocusListener(this);
 325                                 this.stealWheelFrom = stealWheelFrom;
 326                         }
 327                 }
 328 
 329                 @Override
 330                 public void mouseExit(MouseEvent e) {
 331                 }
 332 
 333                 @Override
 334                 public void mouseHover(MouseEvent e) {
 335                 };
 336 
 337                 @Override
 338                 public void focusGained(FocusEvent e) {
 339                 }
 340 
 341                 @Override
 342                 public void focusLost(FocusEvent e) {
 343                         stop();
 344                 }
 345         }
 346 
 347         class KeyNavigator implements KeyListener {
 348 
 349                 @Override
 350                 public void keyPressed(KeyEvent event) {
 351                         switch (event.character) {
 352                         case '+':
 353                                 zoom(1);
 354                                 break;
 355                         case '-':
 356                                 zoom(-1);
 357                                 break;
 358                         default:
 359                                 switch (event.keyCode) {
 360                                 case SWT.ESC:
 361                                         awtChart.clearSelection();
 362                                         if (selectionListener != null) {
 363                                                 selectionListener.run();
 364                                         }
 365                                         redrawChart();
 366                                         redrawChartText();
 367                                         break;
 368                                 case SWT.ARROW_RIGHT:
 369                                         pan(10);
 370                                         break;
 371                                 case SWT.ARROW_LEFT:
 372                                         pan(-10);
 373                                         break;
 374                                 case SWT.ARROW_UP:
 375                                         zoom(1);
 376                                         break;
 377                                 case SWT.ARROW_DOWN:
 378                                         zoom(-1);
 379                                         break;
 380                                 default:
 381                                         // Ignore
 382                                 }
 383                         }
 384                 }
 385 
 386                 @Override
 387                 public void keyReleased(KeyEvent event) {
 388                         // Ignore
 389                 }
 390 
 391         }
 392 
 393         private class AntiAliasingListener implements IPropertyChangeListener {
 394 
 395                 @Override
 396                 public void propertyChange(PropertyChangeEvent event) {
 397                         redrawChart();
 398                 }
 399 
 400         }
 401 
 402         /**
 403          * This gets the "normal" DPI value for the system (72 on MacOS and 96 on Windows/Linux. It's
 404          * used to determine how much larger the current DPI is so that we can draw the charts based on
 405          * how large that area would be given the "normal" DPI value. Every draw on this smaller chart
 406          * is then scaled up by the Graphics2D objects DefaultTransform.
 407          */
 408         private final double xScale = Display.getDefault().getDPI().x / Environment.getNormalDPI();
 409         private final double yScale = Display.getDefault().getDPI().y / Environment.getNormalDPI();
 410 
 411         private final AwtCanvas awtCanvas = new AwtCanvas();
 412         private boolean awtNeedsRedraw;
 413         private Runnable selectionListener;
 414         private Runnable zoomToSelectionListener;
 415         private Consumer<Boolean> zoomOnClickListener;
 416         private IPropertyChangeListener aaListener;
 417         private XYChart awtChart;
 418         private MCContextMenuManager chartMenu;
 419         private ChartTextCanvas textCanvas;
 420 
 421         public ChartCanvas(Composite parent) {
 422                 super(parent, SWT.NO_BACKGROUND);
 423                 addPaintListener(new Painter());
 424                 Selector selector = new Selector();
 425                 addMouseListener(selector);
 426                 addMouseMoveListener(selector);
 427                 FocusTracker.enableFocusTracking(this);
 428                 addKeyListener(new KeyNavigator());
 429                 aaListener = new AntiAliasingListener();
 430                 UIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(aaListener);
 431                 addDisposeListener(e -> UIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(aaListener));
 432                 if (Environment.getOSType() == OSType.WINDOWS) {
 433                         addMouseTrackListener(new WheelStealingZoomer());
 434                 }
 435                 if (getParent() instanceof ScrolledComposite) { // JFR Threads Page
 436                         ((ScrolledComposite) getParent()).getVerticalBar()
 437                                 .addListener(SWT.Selection, e -> vBarScroll());
 438                 } else {
 439                         addMouseTrackListener(selector);
 440                         addListener(SWT.MouseVerticalWheel, new Zoomer());
 441                 }
 442         }
 443 
 444         private void vBarScroll() {
 445                 if (textCanvas != null) {
 446                         Point location = ((ScrolledComposite) getParent()).getOrigin();
 447                         textCanvas.syncScroll(location);
 448                 }
 449         }
 450 
 451         public IMenuManager getContextMenu() {
 452                 if (chartMenu == null) {
 453                         chartMenu = MCContextMenuManager.create(this);
 454                         chartMenu.addMenuListener(manager -> clearHighlightRects());
 455                 }
 456                 return chartMenu;
 457         }
 458 
 459         private void render(Graphics2D context, int width, int height) {
 460                 if (awtChart != null) {
 461                         awtChart.renderChart(context, width, height);
 462                         if (textCanvas == null) {
 463                                 awtChart.renderText(context, width, height);
 464                         }
 465                 }
 466         }
 467 
 468         /**
 469          * Translates display coordinates into image coordinates for the chart.
 470          *
 471          * @param x
 472          *            the provided x coordinate
 473          * @param y
 474          *            the provided y coordinate
 475          * @return a Point that represents the (x,y) coordinates in the chart's coordinate space
 476          */
 477         protected Point translateDisplayToImageCoordinates(int x, int y) {
 478                 int xImage = (int) Math.round(x / xScale);
 479                 int yImage = (int) Math.round(y / yScale);
 480                 return new Point(xImage, yImage);
 481         }
 482 
 483         /**
 484          * Translates a display x coordinate into an image x coordinate for the chart.
 485          *
 486          * @param x
 487          *            the provided display x coordinate
 488          * @return the x coordinate in the chart's coordinate space
 489          */
 490         protected int translateDisplayToImageXCoordinates(int x) {
 491                 return (int) Math.round(x / xScale);
 492         }
 493 
 494         /**
 495          * Translates a display x coordinate into an image x coordinate for the chart.
 496          *
 497          * @param x
 498          *            the provided display x coordinate
 499          * @return the x coordinate in the chart's coordinate space
 500          */
 501         protected int translateDisplayToImageYCoordinates(int y) {
 502                 return (int) Math.round(y / yScale);
 503         }
 504 
 505         public Object getHoveredItemData() {
 506                 return this.hoveredItemData;
 507         }
 508 
 509         public void setHoveredItemData(Object data) {
 510                 this.hoveredItemData = data;
 511         }
 512 
 513         public void resetHoveredItemData() {
 514                 this.hoveredItemData = null;
 515         }
 516 
 517         public void syncHighlightedRectangles (List<Rectangle2D> newRects) {
 518                 highlightRects = newRects;
 519                 redraw();
 520         }
 521 
 522         private void updateHighlightRects() {
 523                 List<Rectangle2D> newRects = new ArrayList<>();
 524                 infoAt(new IChartInfoVisitor.Adapter() {
 525                         @Override
 526                         public void visit(IBucket bucket) {
 527                                 newRects.add(bucket.getTarget());
 528                         }
 529 
 530                         @Override
 531                         public void visit(IPoint point) {
 532                                 Point2D target = point.getTarget();
 533                                 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0));
 534                         }
 535 
 536                         @Override
 537                         public void visit(ISpan span) {
 538                                 newRects.add(span.getTarget());
 539                         }
 540 
 541                         @Override
 542                         public void visit(ITick tick) {
 543                                 Point2D target = tick.getTarget();
 544                                 newRects.add(new Rectangle2D.Double(target.getX(), target.getY(), 0, 0));
 545                         }
 546 
 547                         @Override
 548                         public void visit(ILane lane) {
 549                                 // FIXME: Do we want this highlighted?
 550                         }
 551 
 552                         @Override
 553                         public void hover(Object data) {
 554                                 if (data != null) {
 555                                         setHoveredItemData(data);
 556                                 }
 557                         }
 558                 }, lastMouseX, lastMouseY);
 559                 // Attempt to reduce flicker by avoiding unnecessary updates.
 560                 if (!newRects.equals(highlightRects)) {
 561                         highlightRects = newRects;
 562                         if (textCanvas != null) {
 563                                 textCanvas.syncHighlightedRectangles(highlightRects);
 564                         }
 565                         redraw();
 566                 }
 567         }
 568 
 569         private void clearHighlightRects() {
 570                 if (highlightRects != null) {
 571                         highlightRects = null;
 572                         redraw();
 573                 }
 574         }
 575 
 576         private void handleWheelEvent(int stateMask, int x, int count) {
 577                 // SWT.MOD1 is CMD on OS X and CTRL elsewhere.
 578                 if ((stateMask & SWT.MOD1) != 0) {
 579                         pan(count * 3);
 580                 } else {
 581                         zoom(translateDisplayToImageXCoordinates(x), count);
 582                 }
 583         }
 584 
 585         private void pan(int rightPercent) {
 586                 if ((awtChart != null) && awtChart.pan(rightPercent)) {
 587                         redrawChart();
 588                 }
 589         }
 590 
 591         private void zoom(int zoomInSteps) {
 592                 if ((awtChart != null) && awtChart.zoom(zoomInSteps)) {
 593                         redrawChart();
 594                 }
 595         }
 596 
 597         private void zoom(int x, int zoomInSteps) {
 598                 if ((awtChart != null) && awtChart.zoom(x, zoomInSteps)) {
 599                         redrawChart();
 600                 }
 601         }
 602 
 603         private void select(int x1, int x2, int y1, int y2, boolean clear) {
 604                 Point p1 = translateDisplayToImageCoordinates(x1, y1);
 605                 Point p2 = translateDisplayToImageCoordinates(x2, y2);
 606                 if ((awtChart != null) && awtChart.select(p1.x, p2.x, p1.y, p2.y, clear)) {
 607                         redrawChart();
 608                         redrawChartText();
 609                 }
 610         }
 611 
 612         private void toggleSelect(int x, int y) {
 613                 Point p = translateDisplayToImageCoordinates(x, y);
 614                 if (awtChart != null) {
 615                         final IQuantity[] range = new IQuantity[2];
 616                         infoAt(new IChartInfoVisitor.Adapter() {
 617                                 @Override
 618                                 public void visit(IBucket bucket) {
 619                                         if (range[0] == null) {
 620                                                 range[0] = (IQuantity) bucket.getStartX();
 621                                                 range[1] = (IQuantity) bucket.getEndX();
 622                                         }
 623                                 }
 624 
 625                                 @Override
 626                                 public void visit(ISpan span) {
 627                                         if (range[0] == null) {
 628                                                 IDisplayable x0 = span.getStartX();
 629                                                 IDisplayable x1 = span.getEndX();
 630                                                 range[0] = (x0 instanceof IQuantity) ? (IQuantity) x0 : null;
 631                                                 range[1] = (x1 instanceof IQuantity) ? (IQuantity) x1 : null;
 632                                         }
 633                                 }
 634                         }, x, y);
 635                         if ((range[0] != null) || (range[1] != null)) {
 636                                 if (!awtChart.select(range[0], range[1], p.y, p.y, true)) {
 637                                         awtChart.clearSelection();
 638                                 }
 639                         } else {
 640                                 if (!awtChart.select(p.x, p.x, p.y, p.y, true)) {
 641                                         awtChart.clearSelection();
 642                                 }
 643                         }
 644                         notifyZoomOnClickListener(SWT.MouseDown);
 645                         redrawChart();
 646                         redrawChartText();
 647                 }
 648         }
 649 
 650         public void setChart(XYChart awtChart) {
 651                 this.awtChart = awtChart;
 652                 notifyListener();
 653                 redrawChart();
 654         }
 655 
 656         public void setTextCanvas(ChartTextCanvas textCanvas) {
 657                 this.textCanvas = textCanvas;
 658         }
 659 
 660         public void syncScroll(Point scrollPoint) {
 661                 ((ScrolledComposite) getParent()).setOrigin(scrollPoint);
 662         }
 663 
 664         public void replaceRenderer(IXDataRenderer rendererRoot) {
 665                 assert awtChart != null;
 666                 awtChart.setRendererRoot(rendererRoot);
 667                 notifyListener();
 668                 redrawChart();
 669         }
 670 
 671         public void setSelectionListener(Runnable selectionListener) {
 672                 this.selectionListener = selectionListener;
 673         }
 674 
 675         public void setZoomToSelectionListener(Runnable zoomListener) {
 676                 this.zoomToSelectionListener = zoomListener;
 677         }
 678 
 679         public void setZoomOnClickListener(Consumer<Boolean> clickListener) {
 680                 this.zoomOnClickListener = clickListener;
 681         }
 682 
 683         private void notifyZoomOnClickListener(Integer button) {
 684                 if (zoomOnClickListener != null) {
 685                         zoomOnClickListener.accept(button == SWT.MouseDown);
 686                 }
 687         }
 688 
 689         private void notifyListener() {
 690                 if (selectionListener != null) {
 691                         selectionListener.run();
 692                 }
 693         }
 694 
 695         public void changeCursor(Cursor cursor) {
 696                 setCursor(cursor);
 697         }
 698 
 699         public void infoAt(IChartInfoVisitor visitor, int x, int y) {
 700                 Point p = translateDisplayToImageCoordinates(x, y);
 701                 if (awtChart != null) {
 702                         awtChart.infoAt(visitor, p.x, p.y);
 703                 }
 704         }
 705 
 706         /**
 707          * Mark both the (AWT) chart and the SWT control as needing a redraw.
 708          */
 709         public void redrawChart() {
 710                 awtNeedsRedraw = true;
 711                 redraw();
 712         }
 713 
 714         private void redrawChartText() {
 715                 if (textCanvas != null) {
 716                         textCanvas.redrawChartText();
 717                 }
 718         }
 719 
 720 }