< prev index next >

application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/misc/ChartCanvas.java

Print this page


   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 {


 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) {


 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 }
   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 {


 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) {


 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 }
< prev index next >