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.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.Timer; 41 import java.util.TimerTask; 42 43 import org.eclipse.swt.SWT; 44 import org.eclipse.swt.custom.ScrolledComposite; 45 import org.eclipse.swt.events.MouseAdapter; 46 import org.eclipse.swt.events.MouseEvent; 47 import org.eclipse.swt.events.MouseMoveListener; 48 import org.eclipse.swt.events.MouseWheelListener; 49 import org.eclipse.swt.events.PaintEvent; 50 import org.eclipse.swt.events.PaintListener; 51 import org.eclipse.swt.graphics.Cursor; 52 import org.eclipse.swt.graphics.GC; 53 import org.eclipse.swt.graphics.Point; 54 import org.eclipse.swt.graphics.Rectangle; 55 import org.eclipse.swt.layout.GridData; 56 import org.eclipse.swt.layout.GridLayout; 57 import org.eclipse.swt.widgets.Button; 58 import org.eclipse.swt.widgets.Canvas; 59 import org.eclipse.swt.widgets.Composite; 60 import org.eclipse.swt.widgets.Event; 61 import org.eclipse.swt.widgets.Listener; 62 import org.eclipse.swt.widgets.Scale; 63 import org.eclipse.swt.widgets.Text; 64 import org.openjdk.jmc.common.unit.IQuantity; 65 import org.openjdk.jmc.common.unit.IRange; 66 import org.openjdk.jmc.ui.UIPlugin; 67 import org.openjdk.jmc.ui.charts.SubdividedQuantityRange; 68 import org.openjdk.jmc.ui.charts.XYChart; 69 import org.openjdk.jmc.ui.misc.PatternFly.Palette; 70 71 public class ChartDisplayControlBar extends Composite { 72 private static final String ZOOM_IN_CURSOR = "zoomInCursor"; 73 private static final String ZOOM_OUT_CURSOR = "zoomOutCursor"; 74 private static final String DEFAULT_CURSOR = "defaultCursor"; 75 private static final String HAND_CURSOR = "handCursor"; 76 private static final int ZOOM_AMOUNT = 1; 77 private Map<String, Cursor> cursors; 78 private Scale scale; 79 private Text zoomText; 80 private XYChart chart; 81 private ChartCanvas chartCanvas; 82 private ChartTextCanvas textCanvas; 83 private List<Button> buttonGroup; 84 private Button zoomInBtn; 85 private Button zoomOutBtn; 86 private Button selectionBtn; 87 private Button zoomPanBtn; 88 private Button scaleToFitBtn; 89 private ZoomPan zoomPan; 90 91 public ChartDisplayControlBar(Composite parent) { 92 super(parent, SWT.NO_BACKGROUND); 93 94 this.setLayout(new GridLayout()); 95 this.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true)); 96 this.setBackground(Palette.PF_BLACK_300.getSWTColor()); 97 98 cursors = new HashMap<>(); 99 cursors.put(DEFAULT_CURSOR, getDisplay().getSystemCursor(SWT.CURSOR_ARROW)); 100 cursors.put(HAND_CURSOR, getDisplay().getSystemCursor(SWT.CURSOR_HAND)); 101 cursors.put(ZOOM_IN_CURSOR, new Cursor(getDisplay(), 102 UIPlugin.getDefault().getImage(UIPlugin.ICON_FA_ZOOM_IN).getImageData(), 0, 0)); 103 cursors.put(ZOOM_OUT_CURSOR, new Cursor(getDisplay(), 104 UIPlugin.getDefault().getImage(UIPlugin.ICON_FA_ZOOM_OUT).getImageData(), 0, 0)); 105 106 buttonGroup = new ArrayList<>(); 107 selectionBtn = new Button(this, SWT.TOGGLE); 108 selectionBtn.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); 109 selectionBtn.setImage(UIPlugin.getDefault().getImage(UIPlugin.ICON_FA_SELECTION)); 110 selectionBtn.setSelection(true); 111 selectionBtn.addListener(SWT.Selection, new Listener() { 112 @Override 113 public void handleEvent(Event event) { 114 setButtonSelectionStates(selectionBtn, null); 115 changeCursor(DEFAULT_CURSOR); 116 }; 117 }); 118 buttonGroup.add(selectionBtn); 119 120 zoomInBtn = new Button(this, SWT.TOGGLE); 121 zoomInBtn.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); 122 zoomInBtn.setImage(UIPlugin.getDefault().getImage(UIPlugin.ICON_FA_ZOOM_IN)); 123 zoomInBtn.setSelection(false); 124 zoomInBtn.addListener(SWT.Selection, new Listener() { 125 @Override 126 public void handleEvent(Event event) { 127 if (scale.getSelection() > 0) { 128 setButtonSelectionStates(zoomInBtn, zoomPanBtn); 129 changeCursor(ZOOM_IN_CURSOR); 130 } else { 131 setButtonSelectionStates(selectionBtn, null); 132 changeCursor(DEFAULT_CURSOR); 133 } 134 } 135 }); 136 zoomInBtn.addMouseListener(new LongPressListener(ZOOM_AMOUNT)); 137 buttonGroup.add(zoomInBtn); 138 139 scale = new Scale(this, SWT.VERTICAL); 140 scale.setMinimum(0); 141 scale.setMaximum(30); 142 scale.setIncrement(1); 143 scale.setSelection(30); 144 scale.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, true, true)); 145 scale.setEnabled(false); 146 147 zoomText = new Text(this, SWT.BORDER | SWT.READ_ONLY | SWT.SINGLE); 148 zoomText.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); 149 setZoomPercentageText(100); 150 151 zoomOutBtn = new Button(this, SWT.TOGGLE); 152 zoomOutBtn.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); 153 zoomOutBtn.setImage(UIPlugin.getDefault().getImage(UIPlugin.ICON_FA_ZOOM_OUT)); 154 zoomOutBtn.setSelection(false); 155 zoomOutBtn.addListener(SWT.Selection, new Listener() { 156 @Override 157 public void handleEvent(Event e) { 158 if (scale.getSelection() < scale.getMaximum()) { 159 setButtonSelectionStates(zoomOutBtn, zoomPanBtn); 160 changeCursor(ZOOM_OUT_CURSOR); 161 } else { 162 setButtonSelectionStates(selectionBtn, null); 163 changeCursor(DEFAULT_CURSOR); 164 } 165 } 166 }); 167 zoomOutBtn.addMouseListener(new LongPressListener(-ZOOM_AMOUNT)); 168 buttonGroup.add(zoomOutBtn); 169 170 zoomPanBtn = new Button(this, SWT.TOGGLE); 171 zoomPanBtn.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); 172 zoomPanBtn.setImage(UIPlugin.getDefault().getImage(UIPlugin.ICON_FA_ZOOM_PAN)); 173 zoomPanBtn.setSelection(false); 174 zoomPanBtn.addListener(SWT.Selection, new Listener() { 175 @Override 176 public void handleEvent(Event event) { 177 showZoomPanDisplay(zoomPanBtn.getSelection()); 178 } 179 }); 180 buttonGroup.add(zoomPanBtn); 181 182 scaleToFitBtn = new Button(this, SWT.PUSH); 183 scaleToFitBtn.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); 184 scaleToFitBtn.setImage(UIPlugin.getDefault().getImage(UIPlugin.ICON_FA_SCALE_TO_FIT)); 185 scaleToFitBtn.setSelection(false); 186 scaleToFitBtn.addListener(SWT.Selection, new Listener() { 187 @Override 188 public void handleEvent(Event event) { 189 resetZoomScale(); 190 chart.resetTimeline(); 191 chartCanvas.redrawChart(); 192 } 193 }); 194 buttonGroup.add(scaleToFitBtn); 195 } 196 197 public void setChart(XYChart chart) { 198 this.chart = chart; 199 } 200 201 public void setChartCanvas(ChartCanvas chartCanvas) { 202 this.chartCanvas = chartCanvas; 203 } 204 205 public void setTextCanvas(ChartTextCanvas textCanvas) { 206 this.textCanvas = textCanvas; 207 } 208 209 public void zoomOnClick(Boolean mouseDown) { 210 boolean shouldZoom = zoomInBtn.getSelection() || zoomOutBtn.getSelection() ; 211 if (shouldZoom) { 212 if (mouseDown) { 213 chart.clearSelection(); 214 } else { 215 int zoomAmount = zoomInBtn.getSelection() ? ZOOM_AMOUNT : -ZOOM_AMOUNT; 216 zoom(zoomAmount); 217 if (textCanvas != null) { 218 textCanvas.redrawChartText(); 219 } 220 } 221 } 222 } 223 224 public void zoomToSelection() { 225 if (zoomInBtn.getSelection()) { 226 IQuantity selectionStart = chart.getSelectionStart(); 227 IQuantity selectionEnd = chart.getSelectionEnd(); 228 if (selectionStart == null || selectionEnd == null) { 229 chart.clearVisibleRange(); 230 } else { 231 chart.setVisibleRange(selectionStart, selectionEnd); 232 chartCanvas.redrawChart(); 233 } 234 } 235 } 236 237 public void setZoomPercentageText(double zoom) { 238 zoomText.setText(String.format("%.2f %s", zoom, "%")); 239 } 240 241 public void setScaleValue(int value) { 242 scale.setSelection(scale.getMaximum() - value); 243 } 244 245 public void increaseScaleValue() { 246 scale.setSelection(scale.getSelection() - 1); 247 } 248 249 public void decreaseScaleValue() { 250 scale.setSelection(scale.getSelection() + 1); 251 } 252 253 public void resetZoomScale() { 254 scale.setSelection(scale.getMaximum()); 255 setZoomPercentageText(100); 256 } 257 258 private void changeCursor(String cursorName) { 259 chartCanvas.changeCursor(cursors.get(cursorName)); 260 } 261 262 private void setButtonSelectionStates(Button buttonSelected, Button dependentButton) { 263 for (Button button : buttonGroup) { 264 if ((button.getStyle() & SWT.TOGGLE) != 0) { 265 if (button.equals(buttonSelected)) { 266 button.setSelection(true); 267 } else if (dependentButton != null ) { 268 if (button.equals(dependentButton)) { 269 button.setSelection(true); 270 } else { 271 button.setSelection(false); 272 } 273 } else { 274 button.setSelection(false); 275 } 276 } 277 showZoomPanDisplay(zoomPanBtn.getSelection()); 278 } 279 } 280 281 private class LongPressListener extends MouseAdapter { 282 283 private static final long LONG_PRESS_TIME = 500; 284 private Timer timer; 285 private int zoomAmount; 286 287 LongPressListener(int zoomAmount) { 288 this.zoomAmount = zoomAmount; 289 } 290 291 @Override 292 public void mouseDown(MouseEvent e) { 293 if(e.button == 1) { 294 timer = new Timer(); 295 timer.schedule(new LongPress(), LONG_PRESS_TIME, (long) (LONG_PRESS_TIME * 1.5)); 296 } 297 } 298 299 @Override 300 public void mouseUp(MouseEvent e) { 301 timer.cancel(); 302 } 303 304 public class LongPress extends TimerTask { 305 306 @Override 307 public void run() { 308 doZoomInOut(zoomAmount); 309 } 310 } 311 312 private void doZoomInOut(int zoomAmount) { 313 DisplayToolkit.inDisplayThread().execute( () -> zoom(zoomAmount)); 314 } 315 } 316 317 private void zoom(int zoomAmount) { 318 int newScaleValue = scale.getSelection() - zoomAmount; 319 if (newScaleValue >= scale.getMinimum() && newScaleValue <= scale.getMaximum()) { 320 scale.setSelection(scale.getSelection() - zoomAmount); 321 chart.zoom(zoomAmount); 322 chartCanvas.redrawChart(); 323 } 324 } 325 326 public void createZoomPan(Composite parent) { 327 zoomPan = new ZoomPan(parent); 328 parent.setVisible(false); 329 } 330 331 private void showZoomPanDisplay(boolean show) { 332 if(show) { 333 zoomPan.getParent().setVisible(true); 334 zoomPan.redrawZoomPan(); 335 } else { 336 zoomPan.getParent().setVisible(false); 337 } 338 } 339 340 private class ZoomPan extends Canvas { 341 private static final int BORDER_PADDING = 2; 342 private IRange<IQuantity> chartRange; 343 private IRange<IQuantity> lastChartZoomedRange; 344 private Rectangle zoomRect; 345 346 public ZoomPan(Composite parent) { 347 super(parent, SWT.NO_BACKGROUND); 348 addPaintListener(new Painter()); 349 PanDetector panDetector = new PanDetector(); 350 addMouseListener(panDetector); 351 addMouseMoveListener(panDetector); 352 addMouseWheelListener(panDetector); 353 chartRange = chart.getVisibleRange(); 354 } 355 356 public void redrawZoomPan() { 357 redraw(); 358 } 359 360 private class PanDetector extends MouseAdapter implements MouseMoveListener, MouseWheelListener { 361 Point currentSelection; 362 Point lastSelection; 363 boolean isPan = false; 364 365 @Override 366 public void mouseDown(MouseEvent e) { 367 if (e.button == 1 && zoomRect.contains(e.x, e.y)) { 368 isPan = true; 369 chart.setIsZoomPanDrag(isPan); 370 currentSelection = chartCanvas.translateDisplayToImageCoordinates(e.x, e.y); 371 } 372 } 373 374 @Override 375 public void mouseUp(MouseEvent e) { 376 isPan = false; 377 chart.setIsZoomPanDrag(isPan); 378 } 379 380 @Override 381 public void mouseMove(MouseEvent e) { 382 zoomPan.setCursor(cursors.get(HAND_CURSOR)); 383 if (isPan && getParent().getSize().x >= e.x && getParent().getSize().y >= e.y ) { 384 lastSelection = currentSelection; 385 currentSelection = chartCanvas.translateDisplayToImageCoordinates(e.x, e.y); 386 int xdiff = currentSelection.x - lastSelection.x; 387 int ydiff = currentSelection.y - lastSelection.y; 388 updateZoomRectFromPan(xdiff, ydiff); 389 } 390 } 391 392 @Override 393 public void mouseScrolled(MouseEvent e) { 394 updateZoomRectFromPan(0, -e.count); 395 } 396 } 397 398 private void updateZoomRectFromPan(int xdiff, int ydiff) { 399 Point bounds = getParent().getSize(); 400 boolean xModified = false; 401 boolean yModified = false; 402 403 int xOld = zoomRect.x; 404 zoomRect.x += xdiff; 405 if (zoomRect.x > (bounds.x - zoomRect.width - BORDER_PADDING - 1)) { 406 zoomRect.x = bounds.x - zoomRect.width - BORDER_PADDING - 1; 407 } else if (zoomRect.x < BORDER_PADDING ) { 408 zoomRect.x = BORDER_PADDING; 409 } 410 xModified = xOld != zoomRect.x; 411 412 int yOld = zoomRect.y; 413 zoomRect.y += ydiff; 414 if (zoomRect.y < BORDER_PADDING ) { 415 zoomRect.y = BORDER_PADDING; 416 } else if (zoomRect.y > (bounds.y - zoomRect.height- BORDER_PADDING - 1)) { 417 zoomRect.y = bounds.y - zoomRect.height - BORDER_PADDING - 1; 418 } 419 yModified = yOld != zoomRect.y; 420 421 if (xModified || yModified) { 422 updateChartFromZoomRect(xModified, yModified); 423 chartCanvas.redrawChart(); 424 } 425 } 426 427 private void updateChartFromZoomRect(boolean updateXRange, boolean updateYRange) { 428 Rectangle zoomCanvasBounds = new Rectangle(0, 0, getParent().getSize().x, getParent().getSize().y); 429 Rectangle totalBounds = chartCanvas.getBounds(); 430 431 if (updateXRange) { 432 double ratio = getVisibilityRatio(zoomRect.x - BORDER_PADDING, 433 zoomCanvasBounds.x, zoomCanvasBounds.width - BORDER_PADDING); 434 int start = getPixelLocation(ratio, totalBounds.width, 0); 435 436 ratio = getVisibilityRatio(zoomRect.x + zoomRect.width + BORDER_PADDING + 1, 437 zoomCanvasBounds.width, zoomCanvasBounds.width - BORDER_PADDING); 438 int end = getPixelLocation(ratio, totalBounds.width, totalBounds.width); 439 440 SubdividedQuantityRange xAxis = new SubdividedQuantityRange(chartRange.getStart(), 441 chartRange.getEnd(), totalBounds.width, 1); 442 chart.setVisibleRange(xAxis.getQuantityAtPixel(start), xAxis.getQuantityAtPixel(end)); 443 lastChartZoomedRange = chart.getVisibleRange(); 444 } 445 if (updateYRange) { 446 double ratio = getVisibilityRatio(zoomRect.y - BORDER_PADDING, 0, 447 zoomCanvasBounds.height - BORDER_PADDING); 448 int top = getPixelLocation(ratio, totalBounds.height, 0); 449 450 Point p = ((ScrolledComposite) chartCanvas.getParent()).getOrigin(); 451 p.y = top; 452 453 if (textCanvas != null) { 454 textCanvas.syncScroll(p); 455 } 456 chartCanvas.syncScroll(p); 457 } 458 } 459 460 class Painter implements PaintListener { 461 @Override 462 public void paintControl(PaintEvent e) { 463 464 Rectangle backgroundRect = new Rectangle(0, 0, getParent().getSize().x, getParent().getSize().y); 465 GC gc = e.gc; 466 467 gc.setBackground(Palette.PF_BLACK_400.getSWTColor()); 468 gc.fillRectangle(backgroundRect); 469 gc.setForeground(Palette.PF_BLACK_900.getSWTColor()); 470 gc.drawRectangle(0, 0, backgroundRect.width - 1 , backgroundRect.height - 1); 471 472 updateZoomRectFromChart(); 473 474 gc.setBackground(Palette.PF_BLACK_100.getSWTColor()); 475 gc.fillRectangle(zoomRect); 476 gc.setForeground(Palette.PF_BLACK_900.getSWTColor()); 477 gc.drawRectangle(zoomRect); 478 } 479 } 480 481 private void updateZoomRectFromChart() { 482 Rectangle zoomCanvasBounds = new Rectangle(0, 0, getParent().getSize().x, getParent().getSize().y); 483 IRange<IQuantity> zoomedRange = chart.getVisibleRange(); 484 IQuantity visibleWidth = chartRange.getExtent(); 485 double visibleHeight = chartCanvas.getParent().getBounds().height; 486 Rectangle totalBounds = chartCanvas.getBounds(); 487 488 if (zoomRect == null ) { 489 zoomRect = new Rectangle(0, 0, 0, 0); 490 } 491 if (!chart.getVisibleRange().equals(lastChartZoomedRange)) { 492 double ratio = getVisibilityRatio(zoomedRange.getStart(), chartRange.getStart(), visibleWidth); 493 int start = getPixelLocation(ratio, zoomCanvasBounds.width, 0); 494 495 ratio = getVisibilityRatio(zoomedRange.getEnd(), chartRange.getEnd(), visibleWidth); 496 int end = getPixelLocation(ratio, zoomCanvasBounds.width, zoomCanvasBounds.width); 497 498 zoomRect.x = start + BORDER_PADDING; 499 zoomRect.width = end - start - 2 * BORDER_PADDING - 1; 500 lastChartZoomedRange = chart.getVisibleRange(); 501 } 502 double ratio = getVisibilityRatio(0, totalBounds.y, totalBounds.height); 503 int top = getPixelLocation(ratio, zoomCanvasBounds.height, 0); 504 505 ratio = getVisibilityRatio(visibleHeight, totalBounds.height + totalBounds.y, totalBounds.height); 506 int bottom = getPixelLocation(ratio, zoomCanvasBounds.height, zoomCanvasBounds.height); 507 508 zoomRect.y = top + BORDER_PADDING; 509 zoomRect.height = bottom - top - 2 * BORDER_PADDING - 1; 510 511 } 512 513 private double getVisibilityRatio(double visibleBound, double borderBound, double totalLength) { 514 double diff = visibleBound - borderBound; 515 return diff/totalLength; 516 } 517 518 private double getVisibilityRatio(IQuantity visibleBound, IQuantity borderBound, IQuantity totalLength) { 519 IQuantity diff = visibleBound.subtract(borderBound); 520 return diff.ratioTo(totalLength); 521 } 522 523 private int getPixelLocation(double visiblityRatio, int totalLength, int offset) { 524 return offset + (int) (visiblityRatio * totalLength); 525 } 526 } 527 }