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 }