Fix for Histogram widget minor display issues.
[deliverable/tracecompass.git] / org.eclipse.linuxtools.lttng.ui / src / org / eclipse / linuxtools / lttng / ui / views / histogram / Histogram.java
1 /*******************************************************************************
2 * Copyright (c) 2011 Ericsson
3 *
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *
9 * Contributors:
10 * Francois Chouinard - Initial API and implementation
11 * Bernd Hufmann - Changed to updated histogram data model
12 *******************************************************************************/
13
14 package org.eclipse.linuxtools.lttng.ui.views.histogram;
15
16 import org.eclipse.linuxtools.tmf.ui.views.TmfView;
17 import org.eclipse.swt.SWT;
18 import org.eclipse.swt.events.ControlEvent;
19 import org.eclipse.swt.events.ControlListener;
20 import org.eclipse.swt.events.KeyEvent;
21 import org.eclipse.swt.events.KeyListener;
22 import org.eclipse.swt.events.MouseEvent;
23 import org.eclipse.swt.events.MouseListener;
24 import org.eclipse.swt.events.MouseTrackListener;
25 import org.eclipse.swt.events.PaintEvent;
26 import org.eclipse.swt.events.PaintListener;
27 import org.eclipse.swt.graphics.Color;
28 import org.eclipse.swt.graphics.Font;
29 import org.eclipse.swt.graphics.FontData;
30 import org.eclipse.swt.graphics.GC;
31 import org.eclipse.swt.graphics.Image;
32 import org.eclipse.swt.layout.GridData;
33 import org.eclipse.swt.layout.GridLayout;
34 import org.eclipse.swt.widgets.Canvas;
35 import org.eclipse.swt.widgets.Composite;
36 import org.eclipse.swt.widgets.Display;
37 import org.eclipse.swt.widgets.Text;
38
39 /**
40 * <b><u>Histogram</u></b>
41 * <p>
42 * Re-usable histogram widget with the following features:
43 * <ul>
44 * <li>Y-axis labels displaying min/max count values
45 * <li>X-axis labels displaying time range
46 * <li>a histogram displaying the distribution of values over time (note that
47 * the histogram might not necessarily fill the whole canvas)
48 * </ul>
49 * The widget also has 2 'markers' to identify:
50 * <ul>
51 * <li>a red dashed line over the bar that contains the currently selected event
52 * <li>a dark red dashed line that delimits the right end of the histogram (if
53 * it doesn't fill the canvas)
54 * </ul>
55 * Clicking on the histogram will select the current event at the mouse
56 * location.
57 * <p>
58 * Once the histogram is selected, there is some limited keyboard support:
59 * <ul>
60 * <li>Home: go to the first histogram bar
61 * <li>End: go to the last histogram bar
62 * <li>Left: go to the previous histogram
63 * <li>Right: go to the next histogram bar
64 * </ul>
65 * Finally, when the mouse hovers over the histogram, a tool tip showing the
66 * following information about the corresponding histogram bar time range:
67 * <ul>
68 * <li>start of the time range
69 * <li>end of the time range
70 * <li>number of events in that time range
71 * </ul>
72 */
73 public abstract class Histogram implements ControlListener, PaintListener, KeyListener, MouseListener, MouseTrackListener, IHistogramModelListener {
74
75 // ------------------------------------------------------------------------
76 // Constants
77 // ------------------------------------------------------------------------
78
79 // Histogram colors
80 private final Color fBackgroundColor = Display.getCurrent().getSystemColor(SWT.COLOR_WHITE);
81 private final Color fCurrentEventColor = Display.getCurrent().getSystemColor(SWT.COLOR_RED);
82 private final Color fLastEventColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_RED);
83 private final Color fHistoBarColor = new Color(Display.getDefault(), 74, 112, 139);
84
85 // Timestamp scale (nanosecond)
86 public static final byte TIME_SCALE = -9;
87
88 public static final int HISTOGRAM_BAR_WIDTH = 1;
89
90 // ------------------------------------------------------------------------
91 // Attributes
92 // ------------------------------------------------------------------------
93
94 // Owner view
95 protected TmfView fParentView;
96
97 // Histogram text fields
98 private Text fMaxNbEventsText;
99 private Text fMinNbEventsText;
100 private Text fTimeRangeStartText;
101 private Text fTimeRangeEndText;
102
103 // Histogram drawing area
104 protected Canvas fCanvas;
105
106 // Data model
107 protected final HistogramDataModel fDataModel;
108 protected HistogramScaledData fScaledData;
109
110 protected long fCurrentEventTime = 0;
111
112 // ------------------------------------------------------------------------
113 // Construction
114 // ------------------------------------------------------------------------
115
116 public Histogram(TmfView view, Composite parent) {
117 fParentView = view;
118
119 createWidget(parent);
120 fDataModel = new HistogramDataModel();
121 fDataModel.addHistogramListener(this);
122 clear();
123
124 fCanvas.addControlListener(this);
125 fCanvas.addPaintListener(this);
126 fCanvas.addKeyListener(this);
127 fCanvas.addMouseListener(this);
128 fCanvas.addMouseTrackListener(this);
129 }
130
131 public void dispose() {
132 fHistoBarColor.dispose();
133 fDataModel.removeHistogramListener(this);
134 }
135
136 private void createWidget(Composite parent) {
137
138 final Color labelColor = parent.getBackground();
139 final Font fFont = adjustFont(parent);
140
141 final int initalWidth = 10;
142
143 // --------------------------------------------------------------------
144 // Define the histogram
145 // --------------------------------------------------------------------
146
147 GridLayout gridLayout = new GridLayout();
148 gridLayout.numColumns = 3;
149 gridLayout.marginHeight = 0;
150 gridLayout.marginWidth = 0;
151 gridLayout.marginTop = 0;
152 gridLayout.horizontalSpacing = 0;
153 gridLayout.verticalSpacing = 0;
154 gridLayout.marginLeft = 0;
155 gridLayout.marginRight = 0;
156 Composite composite = new Composite(parent, SWT.FILL);
157 composite.setLayout(gridLayout);
158
159 // Use all the horizontal space
160 GridData gridData = new GridData();
161 gridData.horizontalAlignment = SWT.FILL;
162 gridData.verticalAlignment = SWT.FILL;
163 gridData.grabExcessHorizontalSpace = true;
164 composite.setLayoutData(gridData);
165
166 // Y-axis max event
167 gridData = new GridData();
168 gridData.horizontalAlignment = SWT.RIGHT;
169 gridData.verticalAlignment = SWT.TOP;
170 fMaxNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT);
171 fMaxNbEventsText.setFont(fFont);
172 fMaxNbEventsText.setBackground(labelColor);
173 fMaxNbEventsText.setEditable(false);
174 fMaxNbEventsText.setText("0"); //$NON-NLS-1$
175 fMaxNbEventsText.setLayoutData(gridData);
176
177 // Histogram itself
178 gridData = new GridData();
179 gridData.horizontalSpan = 2;
180 gridData.verticalSpan = 2;
181 gridData.horizontalAlignment = SWT.FILL;
182 gridData.verticalAlignment = SWT.FILL;
183 gridData.grabExcessHorizontalSpace = true;
184 fCanvas = new Canvas(composite, SWT.BORDER | SWT.DOUBLE_BUFFERED);
185 fCanvas.setLayoutData(gridData);
186
187 // Y-axis min event (always 0...)
188 gridData = new GridData();
189 gridData.horizontalAlignment = SWT.RIGHT;
190 gridData.verticalAlignment = SWT.BOTTOM;
191 fMinNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT);
192 fMinNbEventsText.setFont(fFont);
193 fMinNbEventsText.setBackground(labelColor);
194 fMinNbEventsText.setEditable(false);
195 fMinNbEventsText.setText("0"); //$NON-NLS-1$
196 fMinNbEventsText.setLayoutData(gridData);
197
198 // Dummy cell
199 gridData = new GridData(initalWidth, SWT.DEFAULT);
200 gridData.horizontalAlignment = SWT.RIGHT;
201 gridData.verticalAlignment = SWT.BOTTOM;
202 Text dummyText = new Text(composite, SWT.READ_ONLY);
203 dummyText.setFont(fFont);
204 dummyText.setBackground(labelColor);
205 dummyText.setEditable(false);
206 dummyText.setText(""); //$NON-NLS-1$
207 dummyText.setLayoutData(gridData);
208
209 // Window range start time
210 gridData = new GridData();
211 gridData.horizontalAlignment = SWT.LEFT;
212 gridData.verticalAlignment = SWT.BOTTOM;
213 fTimeRangeStartText = new Text(composite, SWT.READ_ONLY);
214 fTimeRangeStartText.setFont(fFont);
215 fTimeRangeStartText.setBackground(labelColor);
216 fTimeRangeStartText.setText(HistogramUtils.nanosecondsToString(0));
217 fTimeRangeStartText.setLayoutData(gridData);
218
219 // Window range end time
220 gridData = new GridData();
221 gridData.horizontalAlignment = SWT.RIGHT;
222 gridData.verticalAlignment = SWT.BOTTOM;
223 fTimeRangeEndText = new Text(composite, SWT.READ_ONLY);
224 fTimeRangeEndText.setFont(fFont);
225 fTimeRangeEndText.setBackground(labelColor);
226 fTimeRangeEndText.setText(HistogramUtils.nanosecondsToString(0));
227 fTimeRangeEndText.setLayoutData(gridData);
228 }
229
230 private Font adjustFont(Composite composite) {
231 // Reduce font size for a more pleasing rendering
232 int fontSizeAdjustment = -2;
233 Font font = composite.getFont();
234 FontData fontData = font.getFontData()[0];
235 return new Font(font.getDevice(), fontData.getName(), fontData.getHeight() + fontSizeAdjustment, fontData.getStyle());
236 }
237
238 // ------------------------------------------------------------------------
239 // Accessors
240 // ------------------------------------------------------------------------
241
242 public long getStartTime() {
243 return fDataModel.getFirstBucketTime();
244 }
245
246 public long getEndTime() {
247 return fDataModel.getEndTime();
248 }
249
250 public long getTimeLimit() {
251 return fDataModel.getTimeLimit();
252 }
253
254 public HistogramDataModel getDataModel() {
255 return fDataModel;
256 }
257
258 // ------------------------------------------------------------------------
259 // Operations
260 // ------------------------------------------------------------------------
261
262 public abstract void updateTimeRange(long startTime, long endTime);
263
264 /**
265 * Clear the histogram and reset the data
266 */
267 public void clear() {
268 fDataModel.clear();
269 fScaledData = null;
270 }
271
272 /**
273 * Increase the histogram bucket corresponding to [timestamp]
274 *
275 * @param timestamp
276 */
277 public void countEvent(long eventCount, long timestamp) {
278 fDataModel.countEvent(eventCount, timestamp);
279 }
280
281 /**
282 * Sets the current event time and refresh the display
283 *
284 * @param timestamp
285 */
286 public void setCurrentEvent(long timestamp) {
287 fCurrentEventTime = (timestamp > 0) ? timestamp : 0;
288 fDataModel.setCurrentEventNotifyListeners(timestamp);
289 }
290
291 /**
292 * Computes the timestamp of the bucket at [offset]
293 *
294 * @param offset offset from the left on the histogram
295 * @return the start timestamp of the corresponding bucket
296 */
297 public synchronized long getTimestamp(int offset) {
298 assert offset > 0 && offset < fScaledData.fWidth;
299 try {
300 return fDataModel.getFirstBucketTime() + fScaledData.fBucketDuration * offset;
301 } catch (Exception e) {
302 return 0; // TODO: Fix that racing condition (NPE)
303 }
304 }
305
306 /**
307 * Computes the offset of the timestamp in the histogram
308 *
309 * @param timestamp the timestamp
310 * @return the offset of the corresponding bucket (-1 if invalid)
311 */
312 public synchronized int getOffset(long timestamp) {
313 if (timestamp < fDataModel.getFirstBucketTime() || timestamp > fDataModel.getEndTime())
314 return -1;
315 return (int) ((timestamp - fDataModel.getFirstBucketTime()) / fScaledData.fBucketDuration);
316 }
317
318 /**
319 * Move the currently selected bar cursor to a non-empty bucket.
320 *
321 * @param keyCode the SWT key code
322 */
323 protected void moveCursor(int keyCode) {
324
325 if (fScaledData.fCurrentBucket == HistogramScaledData.OUT_OF_RANGE_BUCKET)
326 return;
327
328 int index;
329 switch (keyCode) {
330
331 case SWT.HOME:
332 index = 0;
333 while (index < fScaledData.fLastBucket && fScaledData.fData[index] == 0)
334 index++;
335 if (index < fScaledData.fLastBucket)
336 fScaledData.fCurrentBucket = index;
337 break;
338
339 case SWT.ARROW_RIGHT:
340 index = fScaledData.fCurrentBucket + 1;
341 while (index < fScaledData.fWidth && fScaledData.fData[index] == 0)
342 index++;
343 if (index < fScaledData.fLastBucket)
344 fScaledData.fCurrentBucket = index;
345 break;
346
347 case SWT.END:
348 index = fScaledData.fLastBucket;
349 while (index >= 0 && fScaledData.fData[index] == 0)
350 index--;
351 if (index >= 0)
352 fScaledData.fCurrentBucket = index;
353 break;
354
355 case SWT.ARROW_LEFT:
356 index = fScaledData.fCurrentBucket - 1;
357 while (index >= 0 && fScaledData.fData[index] == 0)
358 index--;
359 if (index >= 0)
360 fScaledData.fCurrentBucket = index;
361 break;
362
363 default:
364 return;
365 }
366
367 updateCurrentEventTime();
368 }
369
370 /**
371 * Refresh the histogram display
372 */
373 @Override
374 public void modelUpdated() {
375 if (!fCanvas.isDisposed() && fCanvas.getDisplay() != null) {
376 fCanvas.getDisplay().asyncExec(new Runnable() {
377 @Override
378 public void run() {
379 if (!fCanvas.isDisposed()) {
380 // Retrieve and normalize the data
381 int canvasWidth = fCanvas.getBounds().width;
382 int canvasHeight = fCanvas.getBounds().height;
383 if (canvasWidth <= 0 || canvasHeight <= 0)
384 return;
385 fDataModel.setCurrentEvent(fCurrentEventTime);
386 fScaledData = fDataModel.scaleTo(canvasWidth, canvasHeight, HISTOGRAM_BAR_WIDTH);
387 fCanvas.redraw();
388 // Display histogram and update X-,Y-axis labels
389 fTimeRangeStartText.setText(HistogramUtils.nanosecondsToString(fDataModel.getFirstBucketTime()));
390 fTimeRangeEndText.setText(HistogramUtils.nanosecondsToString(fDataModel.getEndTime()));
391 fMaxNbEventsText.setText(Long.toString(fScaledData.fMaxValue));
392 // The Y-axis area might need to be re-sized
393 fMaxNbEventsText.getParent().layout();
394 }
395 }
396 });
397 }
398 }
399
400 // ------------------------------------------------------------------------
401 // Helper functions
402 // ------------------------------------------------------------------------
403
404 private void updateCurrentEventTime() {
405 long bucketStartTime = getTimestamp(fScaledData.fCurrentBucket);
406 ((HistogramView) fParentView).updateCurrentEventTime(bucketStartTime);
407 }
408
409 // ------------------------------------------------------------------------
410 // PaintListener
411 // ------------------------------------------------------------------------
412
413 protected final String IMAGE_KEY = "double-buffer-image"; //$NON-NLS-1$
414
415 @Override
416 public void paintControl(PaintEvent event) {
417
418 // Get the geometry
419 int canvasWidth = fCanvas.getBounds().width;
420 int canvasHeight = fCanvas.getBounds().height;
421
422 // Make sure we have something to draw upon
423 if (canvasWidth <= 0 || canvasHeight <= 0)
424 return;
425
426 // Retrieve image; re-create only if necessary
427 Image image = (Image) fCanvas.getData(IMAGE_KEY);
428 if (image == null || image.getBounds().width != canvasWidth || image.getBounds().height != canvasHeight) {
429 image = new Image(event.display, canvasWidth, canvasHeight);
430 fCanvas.setData(IMAGE_KEY, image);
431 }
432
433 // Draw the histogram on its canvas
434 GC imageGC = new GC(image);
435 formatImage(imageGC, image);
436 event.gc.drawImage(image, 0, 0);
437 imageGC.dispose();
438 }
439
440 private void formatImage(GC imageGC, Image image) {
441
442 if (fScaledData == null)
443 return;
444
445 HistogramScaledData scaledData = new HistogramScaledData(fScaledData);
446
447 try {
448 // Get drawing boundaries
449 int width = image.getBounds().width;
450 int height = image.getBounds().height;
451
452 // Clear the drawing area
453 imageGC.setBackground(fBackgroundColor);
454 imageGC.fillRectangle(0, 0, image.getBounds().width + 1, image.getBounds().height + 1);
455
456 // Draw the histogram bars
457 imageGC.setBackground(fHistoBarColor);
458 int limit = width < scaledData.fWidth ? width : scaledData.fWidth;
459 for (int i = 1; i < limit; i++) {
460 int value = (int) (scaledData.fData[i] * scaledData.fScalingFactor);
461 imageGC.fillRectangle(i, height - value, 1, value);
462 }
463
464 // Draw the current event bar
465 int currentBucket = scaledData.fCurrentBucket;
466 if (currentBucket >= 0 && currentBucket < limit) {
467 drawDelimiter(imageGC, fCurrentEventColor, height, currentBucket);
468 }
469
470 // Add a dashed line as a delimiter (at the right of the last bar)
471 int lastEventIndex = limit - 1;
472 while (lastEventIndex >= 0 && scaledData.fData[lastEventIndex] == 0)
473 lastEventIndex--;
474 lastEventIndex += (lastEventIndex < limit - 1) ? 1 : 0;
475 drawDelimiter(imageGC, fLastEventColor, height, lastEventIndex);
476 } catch (Exception e) {
477 // Do nothing
478 }
479 }
480
481 private void drawDelimiter(GC imageGC, Color color, int height, int index) {
482 imageGC.setBackground(color);
483 int dash = height / 4;
484 imageGC.fillRectangle(index, 0 * dash, 1, dash - 1);
485 imageGC.fillRectangle(index, 1 * dash, 1, dash - 1);
486 imageGC.fillRectangle(index, 2 * dash, 1, dash - 1);
487 imageGC.fillRectangle(index, 3 * dash, 1, height - 3 * dash);
488 }
489
490 // ------------------------------------------------------------------------
491 // KeyListener
492 // ------------------------------------------------------------------------
493
494 @Override
495 public void keyPressed(KeyEvent event) {
496 moveCursor(event.keyCode);
497 }
498
499 @Override
500 public void keyReleased(KeyEvent event) {
501 }
502
503 // ------------------------------------------------------------------------
504 // MouseListener
505 // ------------------------------------------------------------------------
506
507 @Override
508 public void mouseDoubleClick(MouseEvent event) {
509 }
510
511 @Override
512 public void mouseDown(MouseEvent event) {
513 if (fDataModel.getNbEvents() > 0 && fScaledData.fLastBucket >= event.x) {
514 fScaledData.fCurrentBucket = event.x;
515 updateCurrentEventTime();
516 }
517 }
518
519 @Override
520 public void mouseUp(MouseEvent event) {
521 }
522
523 // ------------------------------------------------------------------------
524 // MouseTrackListener
525 // ------------------------------------------------------------------------
526
527 @Override
528 public void mouseEnter(MouseEvent event) {
529 }
530
531 @Override
532 public void mouseExit(MouseEvent event) {
533 }
534
535 @Override
536 public void mouseHover(MouseEvent event) {
537 if (fDataModel.getNbEvents() > 0 && fScaledData != null && fScaledData.fLastBucket >= event.x) {
538 String tooltip = formatToolTipLabel(event.x);
539 fCanvas.setToolTipText(tooltip);
540 }
541 }
542
543 private String formatToolTipLabel(int index) {
544 long startTime = fScaledData.getBucketStartTime(fScaledData.fCurrentBucket);
545 // negative values are possible if time values came into the model in decreasing order
546 if (startTime < 0) {
547 startTime = 0;
548 }
549 long endTime = fScaledData.getBucketEndTime(fScaledData.fCurrentBucket);
550 int nbEvents = (index >= 0) ? fScaledData.fData[index] : 0;
551
552 StringBuffer buffer = new StringBuffer();
553 buffer.append("Range = ["); //$NON-NLS-1$
554 buffer.append(HistogramUtils.nanosecondsToString(startTime));
555 buffer.append(","); //$NON-NLS-1$
556 buffer.append(HistogramUtils.nanosecondsToString(endTime));
557 buffer.append(")\n"); //$NON-NLS-1$
558 buffer.append("Event count = "); //$NON-NLS-1$
559 buffer.append(nbEvents);
560 return buffer.toString();
561 }
562
563 // ------------------------------------------------------------------------
564 // ControlListener
565 // ------------------------------------------------------------------------
566
567 @Override
568 public void controlMoved(ControlEvent event) {
569 fDataModel.complete();
570 }
571
572 @Override
573 public void controlResized(ControlEvent event) {
574 fDataModel.complete();
575 }
576 }
This page took 0.0432439999999999 seconds and 5 git commands to generate.