Commit | Line | Data |
---|---|---|
c392540b | 1 | /******************************************************************************* |
c8422608 | 2 | * Copyright (c) 2011, 2013 Ericsson |
20ff3b75 | 3 | * |
c392540b FC |
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 | |
20ff3b75 | 8 | * |
c392540b FC |
9 | * Contributors: |
10 | * Francois Chouinard - Initial API and implementation | |
fbd124dd | 11 | * Bernd Hufmann - Changed to updated histogram data model |
f8177ba2 | 12 | * Francois Chouinard - Reformat histogram labels on format change |
0fcf3b09 | 13 | * Patrick Tasse - Support selection range |
c392540b FC |
14 | *******************************************************************************/ |
15 | ||
e0752744 | 16 | package org.eclipse.linuxtools.tmf.ui.views.histogram; |
c392540b | 17 | |
f8177ba2 FC |
18 | import org.eclipse.linuxtools.tmf.core.signal.TmfSignalHandler; |
19 | import org.eclipse.linuxtools.tmf.core.signal.TmfSignalManager; | |
20 | import org.eclipse.linuxtools.tmf.core.signal.TmfTimestampFormatUpdateSignal; | |
3bd46eef AM |
21 | import org.eclipse.linuxtools.tmf.core.timestamp.ITmfTimestamp; |
22 | import org.eclipse.linuxtools.tmf.core.timestamp.TmfTimestamp; | |
23 | import org.eclipse.linuxtools.tmf.core.timestamp.TmfTimestampFormat; | |
c392540b FC |
24 | import org.eclipse.linuxtools.tmf.ui.views.TmfView; |
25 | import org.eclipse.swt.SWT; | |
26 | import org.eclipse.swt.events.ControlEvent; | |
27 | import org.eclipse.swt.events.ControlListener; | |
65cdf787 PT |
28 | import org.eclipse.swt.events.FocusAdapter; |
29 | import org.eclipse.swt.events.FocusEvent; | |
30 | import org.eclipse.swt.events.FocusListener; | |
c392540b FC |
31 | import org.eclipse.swt.events.KeyEvent; |
32 | import org.eclipse.swt.events.KeyListener; | |
33 | import org.eclipse.swt.events.MouseEvent; | |
34 | import org.eclipse.swt.events.MouseListener; | |
35 | import org.eclipse.swt.events.MouseTrackListener; | |
65cdf787 | 36 | import org.eclipse.swt.events.MouseWheelListener; |
c392540b FC |
37 | import org.eclipse.swt.events.PaintEvent; |
38 | import org.eclipse.swt.events.PaintListener; | |
39 | import org.eclipse.swt.graphics.Color; | |
40 | import org.eclipse.swt.graphics.Font; | |
41 | import org.eclipse.swt.graphics.FontData; | |
42 | import org.eclipse.swt.graphics.GC; | |
43 | import org.eclipse.swt.graphics.Image; | |
e60df94a | 44 | import org.eclipse.swt.layout.FillLayout; |
c392540b FC |
45 | import org.eclipse.swt.layout.GridData; |
46 | import org.eclipse.swt.layout.GridLayout; | |
47 | import org.eclipse.swt.widgets.Canvas; | |
48 | import org.eclipse.swt.widgets.Composite; | |
49 | import org.eclipse.swt.widgets.Display; | |
65cdf787 | 50 | import org.eclipse.swt.widgets.Label; |
c392540b FC |
51 | import org.eclipse.swt.widgets.Text; |
52 | ||
53 | /** | |
b544077e | 54 | * Re-usable histogram widget. |
20ff3b75 | 55 | * |
b544077e | 56 | * It has the following features: |
c392540b FC |
57 | * <ul> |
58 | * <li>Y-axis labels displaying min/max count values | |
59 | * <li>X-axis labels displaying time range | |
60 | * <li>a histogram displaying the distribution of values over time (note that | |
61 | * the histogram might not necessarily fill the whole canvas) | |
62 | * </ul> | |
63 | * The widget also has 2 'markers' to identify: | |
64 | * <ul> | |
65 | * <li>a red dashed line over the bar that contains the currently selected event | |
66 | * <li>a dark red dashed line that delimits the right end of the histogram (if | |
67 | * it doesn't fill the canvas) | |
68 | * </ul> | |
69 | * Clicking on the histogram will select the current event at the mouse | |
70 | * location. | |
71 | * <p> | |
72 | * Once the histogram is selected, there is some limited keyboard support: | |
73 | * <ul> | |
74 | * <li>Home: go to the first histogram bar | |
75 | * <li>End: go to the last histogram bar | |
76 | * <li>Left: go to the previous histogram | |
77 | * <li>Right: go to the next histogram bar | |
78 | * </ul> | |
79 | * Finally, when the mouse hovers over the histogram, a tool tip showing the | |
80 | * following information about the corresponding histogram bar time range: | |
81 | * <ul> | |
82 | * <li>start of the time range | |
83 | * <li>end of the time range | |
84 | * <li>number of events in that time range | |
85 | * </ul> | |
20ff3b75 | 86 | * |
f8177ba2 | 87 | * @version 1.1 |
b544077e | 88 | * @author Francois Chouinard |
c392540b | 89 | */ |
fbd124dd | 90 | public abstract class Histogram implements ControlListener, PaintListener, KeyListener, MouseListener, MouseTrackListener, IHistogramModelListener { |
c392540b FC |
91 | |
92 | // ------------------------------------------------------------------------ | |
93 | // Constants | |
94 | // ------------------------------------------------------------------------ | |
95 | ||
c392540b FC |
96 | // Histogram colors |
97 | private final Color fBackgroundColor = Display.getCurrent().getSystemColor(SWT.COLOR_WHITE); | |
0fcf3b09 PT |
98 | private final Color fSelectionForegroundColor = Display.getCurrent().getSystemColor(SWT.COLOR_BLUE); |
99 | private final Color fSelectionBackgroundColor = Display.getCurrent().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); | |
c392540b FC |
100 | private final Color fLastEventColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_RED); |
101 | private final Color fHistoBarColor = new Color(Display.getDefault(), 74, 112, 139); | |
102 | ||
c392540b FC |
103 | // ------------------------------------------------------------------------ |
104 | // Attributes | |
105 | // ------------------------------------------------------------------------ | |
106 | ||
b544077e BH |
107 | /** |
108 | * The parent TMF view. | |
109 | */ | |
c392540b FC |
110 | protected TmfView fParentView; |
111 | ||
da7bdcbc | 112 | private Composite fComposite; |
f8177ba2 FC |
113 | private Font fFont; |
114 | ||
c392540b FC |
115 | // Histogram text fields |
116 | private Text fMaxNbEventsText; | |
117 | private Text fMinNbEventsText; | |
118 | private Text fTimeRangeStartText; | |
119 | private Text fTimeRangeEndText; | |
120 | ||
b544077e BH |
121 | /** |
122 | * Histogram drawing area | |
123 | */ | |
c392540b | 124 | protected Canvas fCanvas; |
f8177ba2 | 125 | |
b544077e BH |
126 | /** |
127 | * The histogram data model. | |
128 | */ | |
c392540b | 129 | protected final HistogramDataModel fDataModel; |
f8177ba2 | 130 | |
b544077e | 131 | /** |
20ff3b75 | 132 | * The histogram data model scaled to current resolution and screen width. |
b544077e | 133 | */ |
c392540b FC |
134 | protected HistogramScaledData fScaledData; |
135 | ||
f8177ba2 FC |
136 | /** |
137 | * The current event value | |
138 | */ | |
139 | protected long fCurrentEventTime = 0L; | |
c392540b | 140 | |
0fcf3b09 PT |
141 | /** |
142 | * The current selection begin time | |
143 | */ | |
144 | private long fSelectionBegin = 0L; | |
145 | ||
146 | /** | |
147 | * The current selection end time | |
148 | */ | |
149 | private long fSelectionEnd = 0L; | |
150 | ||
c392540b FC |
151 | // ------------------------------------------------------------------------ |
152 | // Construction | |
153 | // ------------------------------------------------------------------------ | |
154 | ||
b544077e | 155 | /** |
f8177ba2 | 156 | * Full constructor. |
20ff3b75 | 157 | * |
b544077e BH |
158 | * @param view A reference to the parent TMF view. |
159 | * @param parent A parent composite | |
160 | */ | |
09e86496 | 161 | public Histogram(final TmfView view, final Composite parent) { |
c392540b FC |
162 | fParentView = view; |
163 | ||
da7bdcbc | 164 | fComposite = createWidget(parent); |
c392540b | 165 | fDataModel = new HistogramDataModel(); |
fbd124dd | 166 | fDataModel.addHistogramListener(this); |
c392540b FC |
167 | clear(); |
168 | ||
169 | fCanvas.addControlListener(this); | |
170 | fCanvas.addPaintListener(this); | |
171 | fCanvas.addKeyListener(this); | |
172 | fCanvas.addMouseListener(this); | |
173 | fCanvas.addMouseTrackListener(this); | |
f8177ba2 FC |
174 | |
175 | TmfSignalManager.register(this); | |
c392540b FC |
176 | } |
177 | ||
b544077e | 178 | /** |
f8177ba2 | 179 | * Dispose resources and unregisters listeners. |
b544077e | 180 | */ |
c392540b | 181 | public void dispose() { |
f8177ba2 FC |
182 | TmfSignalManager.deregister(this); |
183 | ||
c392540b | 184 | fHistoBarColor.dispose(); |
fbd124dd | 185 | fDataModel.removeHistogramListener(this); |
c392540b FC |
186 | } |
187 | ||
da7bdcbc | 188 | private Composite createWidget(final Composite parent) { |
c392540b | 189 | |
0301dc43 | 190 | final Color labelColor = parent.getBackground(); |
f8177ba2 | 191 | fFont = adjustFont(parent); |
c392540b FC |
192 | |
193 | final int initalWidth = 10; | |
194 | ||
195 | // -------------------------------------------------------------------- | |
196 | // Define the histogram | |
197 | // -------------------------------------------------------------------- | |
198 | ||
09e86496 | 199 | final GridLayout gridLayout = new GridLayout(); |
c392540b FC |
200 | gridLayout.numColumns = 3; |
201 | gridLayout.marginHeight = 0; | |
202 | gridLayout.marginWidth = 0; | |
203 | gridLayout.marginTop = 0; | |
204 | gridLayout.horizontalSpacing = 0; | |
205 | gridLayout.verticalSpacing = 0; | |
206 | gridLayout.marginLeft = 0; | |
207 | gridLayout.marginRight = 0; | |
09e86496 | 208 | final Composite composite = new Composite(parent, SWT.FILL); |
c392540b FC |
209 | composite.setLayout(gridLayout); |
210 | ||
211 | // Use all the horizontal space | |
212 | GridData gridData = new GridData(); | |
213 | gridData.horizontalAlignment = SWT.FILL; | |
214 | gridData.verticalAlignment = SWT.FILL; | |
215 | gridData.grabExcessHorizontalSpace = true; | |
216 | composite.setLayoutData(gridData); | |
217 | ||
218 | // Y-axis max event | |
219 | gridData = new GridData(); | |
220 | gridData.horizontalAlignment = SWT.RIGHT; | |
221 | gridData.verticalAlignment = SWT.TOP; | |
222 | fMaxNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT); | |
223 | fMaxNbEventsText.setFont(fFont); | |
224 | fMaxNbEventsText.setBackground(labelColor); | |
225 | fMaxNbEventsText.setEditable(false); | |
226 | fMaxNbEventsText.setText("0"); //$NON-NLS-1$ | |
227 | fMaxNbEventsText.setLayoutData(gridData); | |
228 | ||
229 | // Histogram itself | |
e60df94a | 230 | Composite canvasComposite = new Composite(composite, SWT.BORDER); |
c392540b FC |
231 | gridData = new GridData(); |
232 | gridData.horizontalSpan = 2; | |
233 | gridData.verticalSpan = 2; | |
234 | gridData.horizontalAlignment = SWT.FILL; | |
235 | gridData.verticalAlignment = SWT.FILL; | |
236 | gridData.grabExcessHorizontalSpace = true; | |
e60df94a PT |
237 | canvasComposite.setLayoutData(gridData); |
238 | canvasComposite.setLayout(new FillLayout()); | |
239 | fCanvas = new Canvas(canvasComposite, SWT.DOUBLE_BUFFERED); | |
c392540b FC |
240 | |
241 | // Y-axis min event (always 0...) | |
242 | gridData = new GridData(); | |
243 | gridData.horizontalAlignment = SWT.RIGHT; | |
244 | gridData.verticalAlignment = SWT.BOTTOM; | |
245 | fMinNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT); | |
246 | fMinNbEventsText.setFont(fFont); | |
247 | fMinNbEventsText.setBackground(labelColor); | |
248 | fMinNbEventsText.setEditable(false); | |
249 | fMinNbEventsText.setText("0"); //$NON-NLS-1$ | |
250 | fMinNbEventsText.setLayoutData(gridData); | |
251 | ||
252 | // Dummy cell | |
253 | gridData = new GridData(initalWidth, SWT.DEFAULT); | |
254 | gridData.horizontalAlignment = SWT.RIGHT; | |
255 | gridData.verticalAlignment = SWT.BOTTOM; | |
65cdf787 PT |
256 | final Label dummyLabel = new Label(composite, SWT.NONE); |
257 | dummyLabel.setLayoutData(gridData); | |
c392540b FC |
258 | |
259 | // Window range start time | |
260 | gridData = new GridData(); | |
261 | gridData.horizontalAlignment = SWT.LEFT; | |
262 | gridData.verticalAlignment = SWT.BOTTOM; | |
263 | fTimeRangeStartText = new Text(composite, SWT.READ_ONLY); | |
264 | fTimeRangeStartText.setFont(fFont); | |
265 | fTimeRangeStartText.setBackground(labelColor); | |
c392540b FC |
266 | fTimeRangeStartText.setLayoutData(gridData); |
267 | ||
268 | // Window range end time | |
269 | gridData = new GridData(); | |
270 | gridData.horizontalAlignment = SWT.RIGHT; | |
271 | gridData.verticalAlignment = SWT.BOTTOM; | |
272 | fTimeRangeEndText = new Text(composite, SWT.READ_ONLY); | |
273 | fTimeRangeEndText.setFont(fFont); | |
274 | fTimeRangeEndText.setBackground(labelColor); | |
c392540b | 275 | fTimeRangeEndText.setLayoutData(gridData); |
da7bdcbc | 276 | |
65cdf787 PT |
277 | FocusListener listener = new FocusAdapter() { |
278 | @Override | |
279 | public void focusGained(FocusEvent e) { | |
280 | fCanvas.setFocus(); | |
281 | } | |
282 | }; | |
283 | fMaxNbEventsText.addFocusListener(listener); | |
284 | fMinNbEventsText.addFocusListener(listener); | |
285 | fTimeRangeStartText.addFocusListener(listener); | |
286 | fTimeRangeEndText.addFocusListener(listener); | |
287 | ||
da7bdcbc | 288 | return composite; |
c392540b FC |
289 | } |
290 | ||
abbdd66a | 291 | private static Font adjustFont(final Composite composite) { |
c392540b | 292 | // Reduce font size for a more pleasing rendering |
09e86496 FC |
293 | final int fontSizeAdjustment = -2; |
294 | final Font font = composite.getFont(); | |
295 | final FontData fontData = font.getFontData()[0]; | |
c392540b FC |
296 | return new Font(font.getDevice(), fontData.getName(), fontData.getHeight() + fontSizeAdjustment, fontData.getStyle()); |
297 | } | |
298 | ||
299 | // ------------------------------------------------------------------------ | |
300 | // Accessors | |
301 | // ------------------------------------------------------------------------ | |
302 | ||
b544077e | 303 | /** |
f8177ba2 | 304 | * Returns the start time (equal first bucket time) |
b544077e BH |
305 | * @return the start time. |
306 | */ | |
c392540b | 307 | public long getStartTime() { |
fbd124dd | 308 | return fDataModel.getFirstBucketTime(); |
c392540b FC |
309 | } |
310 | ||
b544077e BH |
311 | /** |
312 | * Returns the end time. | |
313 | * @return the end time. | |
314 | */ | |
c392540b FC |
315 | public long getEndTime() { |
316 | return fDataModel.getEndTime(); | |
317 | } | |
318 | ||
b544077e BH |
319 | /** |
320 | * Returns the time limit (end of last bucket) | |
321 | * @return the time limit. | |
322 | */ | |
c392540b FC |
323 | public long getTimeLimit() { |
324 | return fDataModel.getTimeLimit(); | |
325 | } | |
09e86496 | 326 | |
20ff3b75 AM |
327 | /** |
328 | * Returns a data model reference. | |
b544077e BH |
329 | * @return data model. |
330 | */ | |
fbd124dd BH |
331 | public HistogramDataModel getDataModel() { |
332 | return fDataModel; | |
333 | } | |
c392540b FC |
334 | |
335 | // ------------------------------------------------------------------------ | |
336 | // Operations | |
337 | // ------------------------------------------------------------------------ | |
b544077e | 338 | /** |
20ff3b75 | 339 | * Updates the time range. |
b544077e BH |
340 | * @param startTime A start time |
341 | * @param endTime A end time. | |
342 | */ | |
c392540b FC |
343 | public abstract void updateTimeRange(long startTime, long endTime); |
344 | ||
345 | /** | |
346 | * Clear the histogram and reset the data | |
347 | */ | |
348 | public void clear() { | |
349 | fDataModel.clear(); | |
dcb3cda5 BH |
350 | synchronized (fDataModel) { |
351 | fScaledData = null; | |
352 | } | |
c392540b FC |
353 | } |
354 | ||
355 | /** | |
356 | * Increase the histogram bucket corresponding to [timestamp] | |
20ff3b75 AM |
357 | * |
358 | * @param eventCount | |
359 | * The new event count | |
c392540b | 360 | * @param timestamp |
20ff3b75 | 361 | * The latest timestamp |
c392540b | 362 | */ |
09e86496 | 363 | public void countEvent(final long eventCount, final long timestamp) { |
fbd124dd | 364 | fDataModel.countEvent(eventCount, timestamp); |
c392540b FC |
365 | } |
366 | ||
367 | /** | |
368 | * Sets the current event time and refresh the display | |
20ff3b75 | 369 | * |
c392540b | 370 | * @param timestamp |
20ff3b75 | 371 | * The time of the current event |
0fcf3b09 | 372 | * @deprecated As of 2.1, use {@link #setSelection(long, long)} |
c392540b | 373 | */ |
0fcf3b09 | 374 | @Deprecated |
09e86496 | 375 | public void setCurrentEvent(final long timestamp) { |
0fcf3b09 PT |
376 | fSelectionBegin = (timestamp > 0) ? timestamp : 0; |
377 | fSelectionEnd = (timestamp > 0) ? timestamp : 0; | |
378 | fDataModel.setSelectionNotifyListeners(timestamp, timestamp); | |
379 | } | |
380 | ||
381 | /** | |
382 | * Sets the current selection time range and refresh the display | |
383 | * | |
384 | * @param beginTime | |
385 | * The begin time of the current selection | |
386 | * @param endTime | |
387 | * The end time of the current selection | |
00ff7819 | 388 | * @since 3.0 |
0fcf3b09 PT |
389 | */ |
390 | public void setSelection(final long beginTime, final long endTime) { | |
391 | fSelectionBegin = (beginTime > 0) ? beginTime : 0; | |
392 | fSelectionEnd = (endTime > 0) ? endTime : 0; | |
393 | fDataModel.setSelectionNotifyListeners(beginTime, endTime); | |
c392540b FC |
394 | } |
395 | ||
396 | /** | |
397 | * Computes the timestamp of the bucket at [offset] | |
20ff3b75 | 398 | * |
c392540b FC |
399 | * @param offset offset from the left on the histogram |
400 | * @return the start timestamp of the corresponding bucket | |
401 | */ | |
09e86496 | 402 | public synchronized long getTimestamp(final int offset) { |
c392540b FC |
403 | assert offset > 0 && offset < fScaledData.fWidth; |
404 | try { | |
fbd124dd | 405 | return fDataModel.getFirstBucketTime() + fScaledData.fBucketDuration * offset; |
09e86496 | 406 | } catch (final Exception e) { |
c392540b FC |
407 | return 0; // TODO: Fix that racing condition (NPE) |
408 | } | |
409 | } | |
410 | ||
411 | /** | |
412 | * Computes the offset of the timestamp in the histogram | |
20ff3b75 | 413 | * |
c392540b FC |
414 | * @param timestamp the timestamp |
415 | * @return the offset of the corresponding bucket (-1 if invalid) | |
416 | */ | |
09e86496 | 417 | public synchronized int getOffset(final long timestamp) { |
20ff3b75 | 418 | if (timestamp < fDataModel.getFirstBucketTime() || timestamp > fDataModel.getEndTime()) { |
c392540b | 419 | return -1; |
20ff3b75 | 420 | } |
fbd124dd | 421 | return (int) ((timestamp - fDataModel.getFirstBucketTime()) / fScaledData.fBucketDuration); |
c392540b FC |
422 | } |
423 | ||
424 | /** | |
425 | * Move the currently selected bar cursor to a non-empty bucket. | |
20ff3b75 | 426 | * |
c392540b FC |
427 | * @param keyCode the SWT key code |
428 | */ | |
09e86496 | 429 | protected void moveCursor(final int keyCode) { |
c392540b | 430 | |
c392540b FC |
431 | int index; |
432 | switch (keyCode) { | |
433 | ||
434 | case SWT.HOME: | |
435 | index = 0; | |
20ff3b75 | 436 | while (index < fScaledData.fLastBucket && fScaledData.fData[index] == 0) { |
c392540b | 437 | index++; |
20ff3b75 AM |
438 | } |
439 | if (index < fScaledData.fLastBucket) { | |
0fcf3b09 | 440 | fScaledData.fSelectionBeginBucket = index; |
20ff3b75 | 441 | } |
c392540b FC |
442 | break; |
443 | ||
444 | case SWT.ARROW_RIGHT: | |
0fcf3b09 | 445 | index = Math.max(0, fScaledData.fSelectionBeginBucket + 1); |
20ff3b75 | 446 | while (index < fScaledData.fWidth && fScaledData.fData[index] == 0) { |
c392540b | 447 | index++; |
20ff3b75 AM |
448 | } |
449 | if (index < fScaledData.fLastBucket) { | |
0fcf3b09 | 450 | fScaledData.fSelectionBeginBucket = index; |
20ff3b75 | 451 | } |
c392540b FC |
452 | break; |
453 | ||
454 | case SWT.END: | |
455 | index = fScaledData.fLastBucket; | |
20ff3b75 | 456 | while (index >= 0 && fScaledData.fData[index] == 0) { |
c392540b | 457 | index--; |
20ff3b75 AM |
458 | } |
459 | if (index >= 0) { | |
0fcf3b09 | 460 | fScaledData.fSelectionBeginBucket = index; |
20ff3b75 | 461 | } |
c392540b FC |
462 | break; |
463 | ||
464 | case SWT.ARROW_LEFT: | |
0fcf3b09 | 465 | index = Math.min(fScaledData.fLastBucket - 1, fScaledData.fSelectionBeginBucket - 1); |
20ff3b75 | 466 | while (index >= 0 && fScaledData.fData[index] == 0) { |
c392540b | 467 | index--; |
20ff3b75 AM |
468 | } |
469 | if (index >= 0) { | |
0fcf3b09 | 470 | fScaledData.fSelectionBeginBucket = index; |
20ff3b75 | 471 | } |
c392540b FC |
472 | break; |
473 | ||
474 | default: | |
475 | return; | |
476 | } | |
477 | ||
0fcf3b09 PT |
478 | fScaledData.fSelectionEndBucket = fScaledData.fSelectionBeginBucket; |
479 | fSelectionBegin = getTimestamp(fScaledData.fSelectionBeginBucket); | |
480 | fSelectionEnd = fSelectionBegin; | |
481 | updateSelectionTime(); | |
c392540b FC |
482 | } |
483 | ||
484 | /** | |
485 | * Refresh the histogram display | |
486 | */ | |
fbd124dd BH |
487 | @Override |
488 | public void modelUpdated() { | |
20ff3b75 | 489 | if (!fCanvas.isDisposed() && fCanvas.getDisplay() != null) { |
c392540b FC |
490 | fCanvas.getDisplay().asyncExec(new Runnable() { |
491 | @Override | |
492 | public void run() { | |
493 | if (!fCanvas.isDisposed()) { | |
494 | // Retrieve and normalize the data | |
09e86496 FC |
495 | final int canvasWidth = fCanvas.getBounds().width; |
496 | final int canvasHeight = fCanvas.getBounds().height; | |
20ff3b75 | 497 | if (canvasWidth <= 0 || canvasHeight <= 0) { |
40890fec | 498 | return; |
20ff3b75 | 499 | } |
0fcf3b09 | 500 | fDataModel.setSelection(fSelectionBegin, fSelectionEnd); |
f8177ba2 | 501 | fScaledData = fDataModel.scaleTo(canvasWidth, canvasHeight, 1); |
dcb3cda5 | 502 | synchronized(fDataModel) { |
0316808c FC |
503 | if (fScaledData != null) { |
504 | fCanvas.redraw(); | |
da7bdcbc PT |
505 | if (fDataModel.getNbEvents() != 0) { |
506 | // Display histogram and update X-,Y-axis labels | |
507 | fTimeRangeStartText.setText(TmfTimestampFormat.getDefaulTimeFormat().format(fDataModel.getFirstBucketTime())); | |
508 | fTimeRangeEndText.setText(TmfTimestampFormat.getDefaulTimeFormat().format(fDataModel.getEndTime())); | |
509 | } else { | |
510 | fTimeRangeStartText.setText(""); //$NON-NLS-1$ | |
511 | fTimeRangeEndText.setText(""); //$NON-NLS-1$ | |
512 | } | |
0316808c FC |
513 | fMaxNbEventsText.setText(Long.toString(fScaledData.fMaxValue)); |
514 | // The Y-axis area might need to be re-sized | |
515 | fMaxNbEventsText.getParent().layout(); | |
516 | } | |
09e86496 | 517 | } |
c392540b FC |
518 | } |
519 | } | |
520 | }); | |
20ff3b75 | 521 | } |
c392540b FC |
522 | } |
523 | ||
65cdf787 PT |
524 | /** |
525 | * Add a mouse wheel listener to the histogram | |
526 | * @param listener the mouse wheel listener | |
527 | * @since 2.0 | |
528 | */ | |
529 | public void addMouseWheelListener(MouseWheelListener listener) { | |
530 | fCanvas.addMouseWheelListener(listener); | |
531 | } | |
532 | ||
533 | /** | |
534 | * Remove a mouse wheel listener from the histogram | |
535 | * @param listener the mouse wheel listener | |
536 | * @since 2.0 | |
537 | */ | |
538 | public void removeMouseWheelListener(MouseWheelListener listener) { | |
539 | fCanvas.removeMouseWheelListener(listener); | |
540 | } | |
541 | ||
c392540b FC |
542 | // ------------------------------------------------------------------------ |
543 | // Helper functions | |
544 | // ------------------------------------------------------------------------ | |
545 | ||
0fcf3b09 PT |
546 | private void updateSelectionTime() { |
547 | ((HistogramView) fParentView).updateSelectionTime(fSelectionBegin, fSelectionEnd); | |
c392540b FC |
548 | } |
549 | ||
550 | // ------------------------------------------------------------------------ | |
551 | // PaintListener | |
552 | // ------------------------------------------------------------------------ | |
b544077e BH |
553 | /** |
554 | * Image key string for the canvas. | |
555 | */ | |
c392540b FC |
556 | protected final String IMAGE_KEY = "double-buffer-image"; //$NON-NLS-1$ |
557 | ||
558 | @Override | |
09e86496 | 559 | public void paintControl(final PaintEvent event) { |
c392540b FC |
560 | |
561 | // Get the geometry | |
09e86496 FC |
562 | final int canvasWidth = fCanvas.getBounds().width; |
563 | final int canvasHeight = fCanvas.getBounds().height; | |
c392540b FC |
564 | |
565 | // Make sure we have something to draw upon | |
20ff3b75 | 566 | if (canvasWidth <= 0 || canvasHeight <= 0) { |
c392540b | 567 | return; |
20ff3b75 | 568 | } |
c392540b FC |
569 | |
570 | // Retrieve image; re-create only if necessary | |
571 | Image image = (Image) fCanvas.getData(IMAGE_KEY); | |
572 | if (image == null || image.getBounds().width != canvasWidth || image.getBounds().height != canvasHeight) { | |
573 | image = new Image(event.display, canvasWidth, canvasHeight); | |
574 | fCanvas.setData(IMAGE_KEY, image); | |
575 | } | |
576 | ||
577 | // Draw the histogram on its canvas | |
09e86496 | 578 | final GC imageGC = new GC(image); |
c392540b FC |
579 | formatImage(imageGC, image); |
580 | event.gc.drawImage(image, 0, 0); | |
581 | imageGC.dispose(); | |
582 | } | |
583 | ||
09e86496 | 584 | private void formatImage(final GC imageGC, final Image image) { |
c392540b | 585 | |
20ff3b75 | 586 | if (fScaledData == null) { |
c392540b | 587 | return; |
20ff3b75 | 588 | } |
c392540b | 589 | |
09e86496 | 590 | final HistogramScaledData scaledData = new HistogramScaledData(fScaledData); |
c392540b FC |
591 | |
592 | try { | |
593 | // Get drawing boundaries | |
09e86496 FC |
594 | final int width = image.getBounds().width; |
595 | final int height = image.getBounds().height; | |
c392540b FC |
596 | |
597 | // Clear the drawing area | |
598 | imageGC.setBackground(fBackgroundColor); | |
599 | imageGC.fillRectangle(0, 0, image.getBounds().width + 1, image.getBounds().height + 1); | |
600 | ||
601 | // Draw the histogram bars | |
602 | imageGC.setBackground(fHistoBarColor); | |
09e86496 | 603 | final int limit = width < scaledData.fWidth ? width : scaledData.fWidth; |
e6953950 | 604 | for (int i = 0; i < limit; i++) { |
e60df94a | 605 | final int value = (int) Math.ceil(scaledData.fData[i] * scaledData.fScalingFactor); |
c392540b FC |
606 | imageGC.fillRectangle(i, height - value, 1, value); |
607 | } | |
608 | ||
c392540b FC |
609 | // Add a dashed line as a delimiter (at the right of the last bar) |
610 | int lastEventIndex = limit - 1; | |
20ff3b75 | 611 | while (lastEventIndex >= 0 && scaledData.fData[lastEventIndex] == 0) { |
c392540b | 612 | lastEventIndex--; |
20ff3b75 | 613 | } |
c392540b FC |
614 | lastEventIndex += (lastEventIndex < limit - 1) ? 1 : 0; |
615 | drawDelimiter(imageGC, fLastEventColor, height, lastEventIndex); | |
0fcf3b09 PT |
616 | |
617 | // Draw the selection bars | |
618 | int alpha = imageGC.getAlpha(); | |
619 | imageGC.setAlpha(100); | |
620 | imageGC.setForeground(fSelectionForegroundColor); | |
621 | imageGC.setBackground(fSelectionBackgroundColor); | |
622 | final int beginBucket = scaledData.fSelectionBeginBucket; | |
623 | if (beginBucket >= 0 && beginBucket < limit) { | |
624 | imageGC.drawLine(beginBucket, 0, beginBucket, height); | |
625 | } | |
626 | final int endBucket = Math.min(lastEventIndex, scaledData.fSelectionEndBucket); | |
627 | if (endBucket >= 0 && endBucket < limit && endBucket != beginBucket) { | |
628 | imageGC.drawLine(endBucket, 0, endBucket, height); | |
629 | } | |
630 | if (endBucket - beginBucket > 1) { | |
631 | imageGC.fillRectangle(beginBucket + 1, 0, endBucket - beginBucket - 1, height); | |
632 | } | |
633 | imageGC.setAlpha(alpha); | |
09e86496 | 634 | } catch (final Exception e) { |
c392540b FC |
635 | // Do nothing |
636 | } | |
637 | } | |
638 | ||
abbdd66a AM |
639 | private static void drawDelimiter(final GC imageGC, final Color color, |
640 | final int height, final int index) { | |
c392540b | 641 | imageGC.setBackground(color); |
09e86496 | 642 | final int dash = height / 4; |
c392540b FC |
643 | imageGC.fillRectangle(index, 0 * dash, 1, dash - 1); |
644 | imageGC.fillRectangle(index, 1 * dash, 1, dash - 1); | |
645 | imageGC.fillRectangle(index, 2 * dash, 1, dash - 1); | |
646 | imageGC.fillRectangle(index, 3 * dash, 1, height - 3 * dash); | |
647 | } | |
648 | ||
649 | // ------------------------------------------------------------------------ | |
650 | // KeyListener | |
651 | // ------------------------------------------------------------------------ | |
652 | ||
653 | @Override | |
09e86496 | 654 | public void keyPressed(final KeyEvent event) { |
c392540b FC |
655 | moveCursor(event.keyCode); |
656 | } | |
657 | ||
658 | @Override | |
09e86496 | 659 | public void keyReleased(final KeyEvent event) { |
c392540b FC |
660 | } |
661 | ||
662 | // ------------------------------------------------------------------------ | |
663 | // MouseListener | |
664 | // ------------------------------------------------------------------------ | |
665 | ||
666 | @Override | |
09e86496 | 667 | public void mouseDoubleClick(final MouseEvent event) { |
c392540b FC |
668 | } |
669 | ||
670 | @Override | |
09e86496 | 671 | public void mouseDown(final MouseEvent event) { |
c392540b | 672 | if (fDataModel.getNbEvents() > 0 && fScaledData.fLastBucket >= event.x) { |
0fcf3b09 PT |
673 | if ((event.stateMask & SWT.MODIFIER_MASK) == 0) { |
674 | fScaledData.fSelectionBeginBucket = event.x; | |
675 | fScaledData.fSelectionEndBucket = event.x; | |
676 | fSelectionBegin = getTimestamp(event.x); | |
677 | fSelectionEnd = fSelectionBegin; | |
678 | } else if ((event.stateMask & SWT.MODIFIER_MASK) == SWT.SHIFT) { | |
679 | if (fSelectionBegin == fSelectionEnd) { | |
680 | if (event.x < fScaledData.fSelectionBeginBucket) { | |
681 | fScaledData.fSelectionBeginBucket = event.x; | |
682 | fSelectionBegin = getTimestamp(event.x); | |
683 | } else { | |
684 | fScaledData.fSelectionEndBucket = event.x; | |
685 | fSelectionEnd = getTimestamp(event.x); | |
686 | } | |
687 | } else if (Math.abs(event.x - fScaledData.fSelectionBeginBucket) <= Math.abs(event.x - fScaledData.fSelectionEndBucket)) { | |
688 | fScaledData.fSelectionBeginBucket = event.x; | |
689 | fSelectionBegin = getTimestamp(event.x); | |
690 | } else { | |
691 | fScaledData.fSelectionEndBucket = event.x; | |
692 | fSelectionEnd = getTimestamp(event.x); | |
693 | } | |
694 | } | |
695 | updateSelectionTime(); | |
c392540b FC |
696 | } |
697 | } | |
698 | ||
699 | @Override | |
09e86496 | 700 | public void mouseUp(final MouseEvent event) { |
c392540b FC |
701 | } |
702 | ||
703 | // ------------------------------------------------------------------------ | |
704 | // MouseTrackListener | |
705 | // ------------------------------------------------------------------------ | |
706 | ||
707 | @Override | |
09e86496 | 708 | public void mouseEnter(final MouseEvent event) { |
c392540b FC |
709 | } |
710 | ||
711 | @Override | |
09e86496 | 712 | public void mouseExit(final MouseEvent event) { |
c392540b FC |
713 | } |
714 | ||
715 | @Override | |
09e86496 | 716 | public void mouseHover(final MouseEvent event) { |
74237cc3 | 717 | if (fDataModel.getNbEvents() > 0 && fScaledData != null && fScaledData.fLastBucket >= event.x) { |
09e86496 | 718 | final String tooltip = formatToolTipLabel(event.x); |
c392540b | 719 | fCanvas.setToolTipText(tooltip); |
1be49d83 PT |
720 | } else { |
721 | fCanvas.setToolTipText(null); | |
c392540b FC |
722 | } |
723 | } | |
724 | ||
09e86496 | 725 | private String formatToolTipLabel(final int index) { |
466857f6 | 726 | long startTime = fScaledData.getBucketStartTime(index); |
09e86496 | 727 | // negative values are possible if time values came into the model in decreasing order |
20ff3b75 | 728 | if (startTime < 0) { |
fbd124dd | 729 | startTime = 0; |
20ff3b75 | 730 | } |
466857f6 | 731 | final long endTime = fScaledData.getBucketEndTime(index); |
09e86496 | 732 | final int nbEvents = (index >= 0) ? fScaledData.fData[index] : 0; |
c392540b | 733 | |
09e86496 | 734 | final StringBuffer buffer = new StringBuffer(); |
c392540b | 735 | buffer.append("Range = ["); //$NON-NLS-1$ |
f8177ba2 | 736 | buffer.append(new TmfTimestamp(startTime, ITmfTimestamp.NANOSECOND_SCALE).toString()); |
c392540b | 737 | buffer.append(","); //$NON-NLS-1$ |
f8177ba2 | 738 | buffer.append(new TmfTimestamp(endTime, ITmfTimestamp.NANOSECOND_SCALE).toString()); |
c392540b FC |
739 | buffer.append(")\n"); //$NON-NLS-1$ |
740 | buffer.append("Event count = "); //$NON-NLS-1$ | |
741 | buffer.append(nbEvents); | |
742 | return buffer.toString(); | |
743 | } | |
744 | ||
745 | // ------------------------------------------------------------------------ | |
746 | // ControlListener | |
747 | // ------------------------------------------------------------------------ | |
748 | ||
749 | @Override | |
09e86496 | 750 | public void controlMoved(final ControlEvent event) { |
fbd124dd | 751 | fDataModel.complete(); |
c392540b FC |
752 | } |
753 | ||
754 | @Override | |
09e86496 | 755 | public void controlResized(final ControlEvent event) { |
fbd124dd | 756 | fDataModel.complete(); |
c392540b | 757 | } |
f8177ba2 FC |
758 | |
759 | // ------------------------------------------------------------------------ | |
760 | // Signal Handlers | |
761 | // ------------------------------------------------------------------------ | |
762 | ||
763 | /** | |
764 | * Format the timestamp and update the display | |
765 | * | |
766 | * @param signal the incoming signal | |
767 | * @since 2.0 | |
768 | */ | |
769 | @TmfSignalHandler | |
770 | public void timestampFormatUpdated(TmfTimestampFormatUpdateSignal signal) { | |
da7bdcbc PT |
771 | if (fDataModel.getNbEvents() == 0) { |
772 | return; | |
773 | } | |
774 | ||
f8177ba2 | 775 | String newTS = TmfTimestampFormat.getDefaulTimeFormat().format(fDataModel.getFirstBucketTime()); |
f8177ba2 FC |
776 | fTimeRangeStartText.setText(newTS); |
777 | ||
778 | newTS = TmfTimestampFormat.getDefaulTimeFormat().format(fDataModel.getEndTime()); | |
f8177ba2 | 779 | fTimeRangeEndText.setText(newTS); |
f8177ba2 | 780 | |
da7bdcbc | 781 | fComposite.layout(); |
f8177ba2 FC |
782 | } |
783 | ||
c392540b | 784 | } |