1 /*
   2  * Copyright (c) 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.Rectangle2D;
  38 import java.util.HashSet;
  39 import java.util.List;
  40 import java.util.Set;
  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.custom.ScrolledComposite;
  47 import org.eclipse.swt.events.KeyEvent;
  48 import org.eclipse.swt.events.KeyListener;
  49 import org.eclipse.swt.events.MouseAdapter;
  50 import org.eclipse.swt.events.MouseEvent;
  51 import org.eclipse.swt.events.MouseMoveListener;
  52 import org.eclipse.swt.events.MouseTrackListener;
  53 import org.eclipse.swt.events.PaintEvent;
  54 import org.eclipse.swt.events.PaintListener;
  55 import org.eclipse.swt.graphics.Point;
  56 import org.eclipse.swt.graphics.Rectangle;
  57 import org.eclipse.swt.widgets.Canvas;
  58 import org.eclipse.swt.widgets.Composite;
  59 import org.eclipse.swt.widgets.Display;
  60 import org.openjdk.jmc.ui.UIPlugin;
  61 import org.openjdk.jmc.ui.accessibility.FocusTracker;
  62 import org.openjdk.jmc.ui.charts.IChartInfoVisitor;
  63 import org.openjdk.jmc.ui.charts.IXDataRenderer;
  64 import org.openjdk.jmc.ui.charts.XYChart;
  65 import org.openjdk.jmc.ui.common.util.Environment;
  66 import org.openjdk.jmc.ui.handlers.MCContextMenuManager;
  67 import org.openjdk.jmc.ui.misc.PatternFly.Palette;
  68 
  69 public class ChartTextCanvas extends Canvas {
  70         private static int MIN_LANE_HEIGHT = 50;
  71         private int numItems;
  72         private List<Rectangle2D> highlightRects;
  73 
  74         private class Selector extends MouseAdapter implements MouseMoveListener, MouseTrackListener {
  75 
  76                 int selectionStartX = -1;
  77                 int selectionStartY = -1;
  78                 Point highlightSelectionStart;
  79                 Point highlightSelectionEnd;
  80                 Point lastSelection;
  81                 boolean selectionIsClick = false;
  82                 Set<Point> highlightPoints;
  83 
  84 
  85                 @Override
  86                 public void mouseDown(MouseEvent e) {
  87                         /*
  88                          * On Mac OS X, CTRL + left mouse button can be used to trigger a context menu. (This is
  89                          * for historical reasons when the primary input device on Macs were a mouse with a
  90                          * single physical button. All modern Macs have other means to bring up the context
  91                          * menu, typically a two finger tap.)
  92                          * 
  93                          * Although I think it would be best to check that this MouseEvent does not cause a
  94                          * platform specific popup trigger, like java.awt.event.MouseEvent.isPopupTrigger() for
  95                          * AWT, SWT doesn't seem to have something as simple. It has the MenuDetectEvent, but
  96                          * the order in relation to this MouseEvent is unspecified.
  97                          * 
  98                          * The code below instead relies on ignoring mouse down events when SWT.MOD4 is
  99                          * depressed. Since MOD4 is CTRL on OS X and 0 on all other current platforms, this
 100                          * suffices. Except for an additional platform check, this approach is also used in
 101                          * org.eclipse.swt.custom.StyledText.handleMouseDown(Event).
 102                          */
 103                         if ((e.button == 1) && ((e.stateMask & SWT.MOD4) == 0) && ((e.stateMask & SWT.CTRL) == 0 ) && ((e.stateMask & SWT.SHIFT) == 0 )) {
 104                                 highlightPoints = new HashSet<>();
 105                                 highlightPoints.add(new Point(e.x, e.y));
 106                                 selectionStartX = e.x;
 107                                 selectionStartY = e.y;
 108                                 highlightSelectionEnd = new Point(-1, -1);
 109                                 lastSelection = new Point(-1, -1);
 110                                 selectionIsClick = true;
 111                                 toggleSelect(selectionStartX, selectionStartY);
 112                         } else if (((e.stateMask & SWT.CTRL) != 0) && (e.button == 1)) {
 113                                 highlightPoints.add(new Point(e.x, e.y));
 114                                 select(e.x, e.x, e.y, e.y, false);
 115                                 if (selectionListener != null) {
 116                                         selectionListener.run();
 117                                 }
 118                         } else if (((e.stateMask & SWT.SHIFT) != 0) && (e.button == 1)) {
 119                                  if (highlightSelectionEnd.y == -1) {
 120                                         highlightSelectionEnd = new Point(e.x, e.y);
 121                                         lastSelection = highlightSelectionEnd;
 122                                         if (highlightSelectionStart.y > highlightSelectionEnd.y) {
 123                                                 Point temp = highlightSelectionStart;
 124                                                 highlightSelectionStart = highlightSelectionEnd;
 125                                                 highlightSelectionEnd = temp;
 126                                         }
 127                                 } else {
 128                                         if (e.y > highlightSelectionStart.y && e.y < highlightSelectionEnd.y) {
 129                                                 if (e.y < lastSelection.y) {
 130                                                         highlightSelectionEnd = new Point(e.x, e.y);
 131                                                 } else if (e.y > lastSelection.y) {
 132                                                         highlightSelectionStart = new Point(e.x, e.y);
 133                                                 }
 134                                         } else if (e.y < highlightSelectionStart.y) {
 135                                                 highlightSelectionStart = new Point(e.x, e.y);
 136                                                 lastSelection = highlightSelectionStart;
 137                                         } else if (e.y > highlightSelectionEnd.y) {
 138                                                 highlightSelectionEnd = new Point(e.x, e.y);
 139                                                 lastSelection = highlightSelectionEnd;
 140                                         }
 141                                 }
 142                                 select(highlightSelectionStart.x, highlightSelectionStart.x, highlightSelectionStart.y, highlightSelectionEnd.y, true);
 143                                 if (selectionListener != null) {
 144                                         selectionListener.run();
 145                                 }
 146                         }
 147                 }
 148 
 149                 @Override
 150                 public void mouseMove(MouseEvent e) {
 151                         if (selectionStartX >= 0) {
 152                                 highlightRects = null;
 153                                 updateSelectionState(e);
 154                         } else {
 155                                 updateHighlightRects();
 156                         }
 157                 }
 158 
 159                 private void updateSelectionState(MouseEvent e) {
 160                         int x = e.x;
 161                         int y = e.y;
 162                         if (selectionIsClick && ((Math.abs(x - selectionStartX) > 3) || (Math.abs(y - selectionStartY) > 3))) {
 163                                 selectionIsClick = false;
 164                         }
 165                         if (!selectionIsClick) {
 166                                 select((int) (selectionStartX / xScale), (int) (selectionStartX / xScale), (int) (selectionStartY / yScale),
 167                                                 (int) (y / yScale), true);
 168                         }
 169                 }
 170 
 171                 @Override
 172                 public void mouseUp(MouseEvent e) {
 173                         if (selectionStartX >= 0 && (e.button == 1)) {
 174                                 updateSelectionState(e);
 175                                 highlightSelectionStart = new Point(selectionStartX, selectionStartY);
 176                                 selectionStartX = -1;
 177                                 selectionStartY = -1;
 178                                 if (selectionListener != null) {
 179                                         selectionListener.run();
 180                                 }
 181                         }
 182                 }
 183 
 184                 @Override
 185                 public void mouseEnter(MouseEvent e) {
 186                 }
 187 
 188                 @Override
 189                 public void mouseExit(MouseEvent e) {
 190                         clearHighlightRects();
 191                 }
 192 
 193                 @Override
 194                 public void mouseHover(MouseEvent e) {
 195                 }
 196         }
 197 
 198         public void setNumItems(int numItems) {
 199                 this.numItems = numItems;
 200         }
 201 
 202         private int getNumItems() {
 203                 return numItems;
 204         }
 205 
 206         class Painter implements PaintListener {
 207 
 208                 @Override
 209                 public void paintControl(PaintEvent e) {
 210                         Rectangle rect = new Rectangle(0, 0, getParent().getSize().x, getParent().getSize().y);
 211                         if (getNumItems() != 1 && !(MIN_LANE_HEIGHT * getNumItems() < rect.height)) {
 212                                 rect.height = MIN_LANE_HEIGHT * getNumItems();
 213                         }
 214 
 215                         if (awtNeedsRedraw || !awtCanvas.hasImage(rect.width, rect.height)) {
 216                                 Graphics2D g2d = awtCanvas.getGraphics(rect.width, rect.height);
 217                                 Point adjusted = chartCanvas.translateDisplayToImageCoordinates(rect.width, rect.height);
 218                                 g2d.setColor(Palette.PF_BLACK_100.getAWTColor());
 219                                 g2d.fillRect(0, 0, adjusted.x, adjusted.y);
 220                                 render(g2d, adjusted.x, adjusted.y);
 221                                 ((ScrolledComposite) getParent()).setMinSize(rect.width, rect.height);
 222                                 if (highlightRects != null) {
 223                                         updateHighlightRects();
 224                                 }
 225                                 awtNeedsRedraw = false;
 226                         }
 227                         awtCanvas.paint(e, 0, 0);
 228                 }
 229         }
 230 
 231         class KeyNavigator implements KeyListener {
 232 
 233                 @Override
 234                 public void keyPressed(KeyEvent event) {
 235                         switch (event.character) {
 236                         default:
 237                                 switch (event.keyCode) {
 238                                 case SWT.ESC:
 239                                         awtChart.clearSelection();
 240                                         if (selectionListener != null) {
 241                                                 selectionListener.run();
 242                                         }
 243                                         redrawChart();
 244                                         redrawChartText();
 245                                         break;
 246                                 default:
 247                                         // Ignore
 248                                 }
 249                         }
 250                 }
 251 
 252                 @Override
 253                 public void keyReleased(KeyEvent event) {
 254                         // Ignore
 255                 }
 256 
 257         }
 258 
 259         private class AntiAliasingListener implements IPropertyChangeListener {
 260 
 261                 @Override
 262                 public void propertyChange(PropertyChangeEvent event) {
 263                         redrawChartText();
 264                 }
 265 
 266         }
 267 
 268         /**
 269          * This gets the "normal" DPI value for the system (72 on MacOS and 96 on Windows/Linux. It's
 270          * used to determine how much larger the current DPI is so that we can draw the charts based on
 271          * how large that area would be given the "normal" DPI value. Every draw on this smaller chart
 272          * is then scaled up by the Graphics2D objects DefaultTransform.
 273          */
 274         private final double xScale = Display.getDefault().getDPI().x / Environment.getNormalDPI();
 275         private final double yScale = Display.getDefault().getDPI().y / Environment.getNormalDPI();
 276 
 277         public final AwtCanvas awtCanvas = new AwtCanvas();
 278         private boolean awtNeedsRedraw;
 279         private Runnable selectionListener;
 280         private IPropertyChangeListener aaListener;
 281         private XYChart awtChart;
 282         private ChartCanvas chartCanvas;
 283         private MCContextMenuManager chartMenu;
 284 
 285         public ChartTextCanvas(Composite parent) {
 286                 super(parent, SWT.NO_BACKGROUND);
 287                 numItems = 0;
 288                 addPaintListener(new Painter());
 289                 Selector selector = new Selector();
 290                 addMouseListener(selector);
 291                 addMouseMoveListener(selector);
 292                 FocusTracker.enableFocusTracking(this);
 293                 addKeyListener(new KeyNavigator());
 294                 aaListener = new AntiAliasingListener();
 295                 UIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(aaListener);
 296                 addDisposeListener(e -> UIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(aaListener));
 297                 ((ScrolledComposite) getParent()).getVerticalBar().addListener(SWT.Selection, e -> vBarScroll());
 298         }
 299 
 300         private void vBarScroll() {
 301                 if (chartCanvas != null) {
 302                         Point location = ((ScrolledComposite) getParent()).getOrigin();
 303                         chartCanvas.syncScroll(location);
 304                 }
 305         }
 306 
 307         public IMenuManager getContextMenu() {
 308                 if (chartMenu == null) {
 309                         chartMenu = MCContextMenuManager.create(this);
 310                         chartMenu.addMenuListener(manager -> clearHighlightRects());
 311                 }
 312                 return chartMenu;
 313         }
 314 
 315         private void render(Graphics2D context, int width, int height) {
 316                 if (awtChart != null) {
 317                         awtChart.renderTextCanvasText(context, width);
 318                 }
 319         }
 320 
 321         public void syncHighlightedRectangles (List<Rectangle2D> newRects) {
 322                 highlightRects = newRects;
 323                 redraw();
 324         }
 325 
 326         private void updateHighlightRects() {
 327                 redraw();
 328                 if (chartCanvas != null) {
 329                         chartCanvas.syncHighlightedRectangles(highlightRects);
 330                 }
 331         }
 332 
 333         private void clearHighlightRects() {
 334                 if (highlightRects != null) {
 335                         highlightRects = null;
 336                         redraw();
 337                 }
 338         }
 339 
 340         public void select(int x1, int x2, int y1, int y2, boolean clear) {
 341                 Point p1 = chartCanvas.translateDisplayToImageCoordinates(x1, y1);
 342                 Point p2 = chartCanvas.translateDisplayToImageCoordinates(x2, y2);
 343                 if ((awtChart != null) && awtChart.select(p1.x, p2.x, p1.y, p2.y, clear)) {
 344                         redrawChartText();
 345                         redrawChart();
 346                 }
 347         }
 348 
 349         private void toggleSelect(int x, int y) {
 350                 Point p = chartCanvas.translateDisplayToImageCoordinates(x, y);
 351                 if (awtChart != null) {
 352                         if (!awtChart.select(p.x, p.x, p.y, p.y, true)) {
 353                                 awtChart.clearSelection();
 354                         }
 355                         redrawChartText();
 356                         redrawChart();
 357                 }
 358         }
 359 
 360         public void setChart(XYChart awtChart) {
 361                 this.awtChart = awtChart;
 362                 notifyListener();
 363                 redrawChartText();
 364         }
 365 
 366         public void setChartCanvas(ChartCanvas chartCanvas) {
 367                 this.chartCanvas = chartCanvas;
 368         }
 369 
 370         public void syncScroll(Point scrollPoint) {
 371                 getParent().getVerticalBar().setVisible(false);
 372                 ((ScrolledComposite) getParent()).setOrigin(scrollPoint);
 373         }
 374 
 375         public void replaceRenderer(IXDataRenderer rendererRoot) {
 376                 assert awtChart != null;
 377                 awtChart.setRendererRoot(rendererRoot);
 378                 notifyListener();
 379                 redrawChartText();
 380         }
 381 
 382         public void setSelectionListener(Runnable selectionListener) {
 383                 this.selectionListener = selectionListener;
 384         }
 385 
 386         private void notifyListener() {
 387                 if (selectionListener != null) {
 388                         selectionListener.run();
 389                 }
 390         }
 391 
 392         public void infoAt(IChartInfoVisitor visitor, int x, int y) {
 393                 Point p = chartCanvas.translateDisplayToImageCoordinates(x, y);
 394                 if (awtChart != null) {
 395                         awtChart.infoAt(visitor, p.x, p.y);
 396                 }
 397         }
 398 
 399         /**
 400          * Mark both the (AWT) chart and the SWT control as needing a redraw.
 401          */
 402         public void redrawChartText() {
 403                 awtNeedsRedraw = true;
 404                 redraw();
 405         }
 406 
 407         private void redrawChart() {
 408                 if (chartCanvas != null) {
 409                         chartCanvas.redrawChart();
 410                 }
 411         }
 412 }