Commit | Line | Data |
---|---|---|
4208b510 AM |
1 | /******************************************************************************* |
2 | * Copyright (c) 2015, 2016 EfficiOS Inc., Michael Jeanson | |
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 | ||
10 | package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.viewers; | |
11 | ||
12 | import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; | |
13 | import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString; | |
14 | ||
15 | import java.text.Format; | |
16 | import java.util.ArrayList; | |
17 | import java.util.HashSet; | |
18 | import java.util.List; | |
19 | import java.util.Set; | |
20 | import java.util.concurrent.TimeUnit; | |
21 | import java.util.function.ToDoubleFunction; | |
22 | ||
23 | import org.eclipse.jdt.annotation.NonNull; | |
24 | import org.eclipse.jdt.annotation.Nullable; | |
25 | import org.eclipse.swt.SWT; | |
7710e6ed JR |
26 | import org.eclipse.swt.events.SelectionEvent; |
27 | import org.eclipse.swt.events.SelectionListener; | |
4208b510 AM |
28 | import org.eclipse.swt.graphics.Color; |
29 | import org.eclipse.swt.graphics.Font; | |
30 | import org.eclipse.swt.graphics.GC; | |
7710e6ed | 31 | import org.eclipse.swt.graphics.Image; |
4208b510 AM |
32 | import org.eclipse.swt.graphics.Point; |
33 | import org.eclipse.swt.graphics.Rectangle; | |
34 | import org.eclipse.swt.widgets.Composite; | |
35 | import org.eclipse.swt.widgets.Control; | |
36 | import org.eclipse.swt.widgets.Display; | |
7710e6ed | 37 | import org.eclipse.swt.widgets.Event; |
4208b510 | 38 | import org.eclipse.swt.widgets.Listener; |
7710e6ed JR |
39 | import org.eclipse.swt.widgets.ToolBar; |
40 | import org.eclipse.swt.widgets.ToolItem; | |
4208b510 AM |
41 | import org.eclipse.tracecompass.common.core.format.DecimalUnitFormat; |
42 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect; | |
43 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiChartModel; | |
44 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiResultTable; | |
45 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTableEntry; | |
46 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTimeStampFormat; | |
47 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.signals.LamiSelectionUpdateSignal; | |
48 | import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler; | |
49 | import org.eclipse.tracecompass.tmf.ui.viewers.TmfViewer; | |
7710e6ed JR |
50 | import org.eclipse.ui.ISharedImages; |
51 | import org.eclipse.ui.PlatformUI; | |
4208b510 AM |
52 | import org.swtchart.Chart; |
53 | import org.swtchart.ITitle; | |
54 | ||
55 | import com.google.common.collect.ImmutableList; | |
56 | ||
57 | /** | |
58 | * Abstract XYChart Viewer for LAMI views. | |
59 | * | |
60 | * @author Michael Jeanson | |
61 | * | |
62 | */ | |
63 | public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer { | |
64 | ||
65 | /** Ellipsis character */ | |
66 | protected static final String ELLIPSIS = "…"; //$NON-NLS-1$ | |
67 | ||
68 | /** | |
69 | * String representing unknown values. Can be present even in numerical | |
70 | * aspects! | |
71 | */ | |
72 | protected static final String UNKNOWN = "?"; //$NON-NLS-1$ | |
73 | ||
74 | /** Zero value */ | |
75 | protected static final double ZERO = 0.0; | |
76 | ||
77 | /** Symbol for seconds (used in the custom ns -> s conversion) */ | |
78 | private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$ | |
79 | ||
80 | /** Symbol for nanoseconds (used in the custom ns -> s conversion) */ | |
81 | private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$ | |
82 | ||
83 | /** | |
84 | * Function to use to map Strings read from the data table to doubles for | |
85 | * use in SWTChart series. | |
86 | */ | |
87 | protected static final ToDoubleFunction<@Nullable String> DOUBLE_MAPPER = str -> { | |
88 | if (str == null || str.equals(UNKNOWN)) { | |
89 | return ZERO; | |
90 | } | |
91 | return Double.parseDouble(str); | |
92 | }; | |
93 | ||
94 | /** | |
95 | * List of standard colors | |
96 | */ | |
97 | protected static final List<@NonNull Color> COLORS = ImmutableList.of( | |
98 | new Color(Display.getDefault(), 72, 120, 207), | |
99 | new Color(Display.getDefault(), 106, 204, 101), | |
100 | new Color(Display.getDefault(), 214, 95, 95), | |
101 | new Color(Display.getDefault(), 180, 124, 199), | |
102 | new Color(Display.getDefault(), 196, 173, 102), | |
103 | new Color(Display.getDefault(), 119, 190, 219) | |
104 | ); | |
105 | ||
106 | /** | |
107 | * List of "light" colors (when unselected) | |
108 | */ | |
109 | protected static final List<@NonNull Color> LIGHT_COLORS = ImmutableList.of( | |
110 | new Color(Display.getDefault(), 173, 195, 233), | |
111 | new Color(Display.getDefault(), 199, 236, 197), | |
112 | new Color(Display.getDefault(), 240, 196, 196), | |
113 | new Color(Display.getDefault(), 231, 213, 237), | |
114 | new Color(Display.getDefault(), 231, 222, 194), | |
115 | new Color(Display.getDefault(), 220, 238, 246) | |
116 | ); | |
117 | ||
118 | /** | |
119 | * Time stamp formatter for intervals in the days range. | |
120 | */ | |
121 | protected static final LamiTimeStampFormat DAYS_FORMATTER = new LamiTimeStampFormat("dd HH:mm"); //$NON-NLS-1$ | |
122 | ||
123 | /** | |
124 | * Time stamp formatter for intervals in the hours range. | |
125 | */ | |
126 | protected static final LamiTimeStampFormat HOURS_FORMATTER = new LamiTimeStampFormat("HH:mm"); //$NON-NLS-1$ | |
127 | ||
128 | /** | |
129 | * Time stamp formatter for intervals in the minutes range. | |
130 | */ | |
131 | protected static final LamiTimeStampFormat MINUTES_FORMATTER = new LamiTimeStampFormat("mm:ss"); //$NON-NLS-1$ | |
132 | ||
133 | /** | |
134 | * Time stamp formatter for intervals in the seconds range. | |
135 | */ | |
136 | protected static final LamiTimeStampFormat SECONDS_FORMATTER = new LamiTimeStampFormat("ss"); //$NON-NLS-1$ | |
137 | ||
138 | /** | |
139 | * Time stamp formatter for intervals in the milliseconds range. | |
140 | */ | |
141 | protected static final LamiTimeStampFormat MILLISECONDS_FORMATTER = new LamiTimeStampFormat("ss.SSS"); //$NON-NLS-1$ | |
142 | ||
143 | /** | |
144 | * Decimal formatter to display nanoseconds as seconds. | |
145 | */ | |
146 | protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new DecimalUnitFormat(0.000000001); | |
147 | ||
148 | /** | |
149 | * Default decimal formatter. | |
150 | */ | |
151 | protected static final DecimalUnitFormat DECIMAL_FORMATTER = new DecimalUnitFormat(); | |
152 | ||
153 | private final Listener fResizeListener = event -> { | |
154 | /* Refresh the titles to fit the current chart size */ | |
155 | refreshDisplayTitles(); | |
156 | ||
157 | /* Refresh the Axis labels to fit the current chart size */ | |
158 | refreshDisplayLabels(); | |
159 | }; | |
160 | ||
161 | private final LamiResultTable fResultTable; | |
162 | private final LamiChartModel fChartModel; | |
163 | ||
164 | private final Chart fChart; | |
165 | ||
166 | private final String fChartTitle; | |
167 | private final String fXTitle; | |
168 | private final String fYTitle; | |
169 | ||
170 | private boolean fSelected; | |
171 | private Set<Integer> fSelection; | |
172 | ||
7710e6ed JR |
173 | private final ToolBar fToolBar; |
174 | ||
4208b510 AM |
175 | /** |
176 | * Creates a Viewer instance based on SWTChart. | |
177 | * | |
178 | * @param parent | |
179 | * The parent composite to draw in. | |
180 | * @param resultTable | |
181 | * The result table containing the data from which to build the | |
182 | * chart | |
183 | * @param chartModel | |
184 | * The information about the chart to build | |
185 | */ | |
186 | public LamiXYChartViewer(Composite parent, LamiResultTable resultTable, LamiChartModel chartModel) { | |
187 | super(parent); | |
188 | ||
189 | fParent = parent; | |
190 | fResultTable = resultTable; | |
191 | fChartModel = chartModel; | |
192 | fSelection = new HashSet<>(); | |
193 | ||
194 | fChart = new Chart(parent, SWT.NONE); | |
195 | fChart.addListener(SWT.Resize, fResizeListener); | |
196 | ||
197 | /* Set Chart title */ | |
198 | fChartTitle = fResultTable.getTableClass().getTableTitle(); | |
199 | ||
200 | /* Set X axis title */ | |
201 | if (fChartModel.getXSeriesColumns().size() == 1) { | |
202 | /* | |
203 | * There is only 1 series in the chart, we will use its name as the | |
204 | * Y axis (and hide the legend). | |
205 | */ | |
206 | String seriesName = getChartModel().getXSeriesColumns().get(0); | |
207 | // The time duration formatter converts ns to s on the axis | |
208 | if (NANOSECONDS_SYMBOL.equals(getXAxisAspects().get(0).getUnits())) { | |
209 | seriesName = getXAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$ | |
210 | } | |
211 | fXTitle = seriesName; | |
212 | } else { | |
213 | /* | |
214 | * There are multiple series in the chart, if they all share the same | |
215 | * units, display that. | |
216 | */ | |
8d9b1c04 | 217 | long nbDiffAspectsUnits = getXAxisAspects().stream() |
4208b510 AM |
218 | .map(aspect -> aspect.getUnits()) |
219 | .distinct() | |
220 | .count(); | |
221 | ||
8d9b1c04 JR |
222 | long nbDiffAspectName = getXAxisAspects().stream() |
223 | .map(aspect -> aspect.getName()) | |
224 | .distinct() | |
225 | .count(); | |
226 | ||
227 | String xBaseTitle = Messages.LamiViewer_DefaultValueName; | |
228 | if (nbDiffAspectName == 1) { | |
229 | xBaseTitle = getXAxisAspects().get(0).getName(); | |
230 | } | |
231 | ||
4208b510 | 232 | String units = getXAxisAspects().get(0).getUnits(); |
8d9b1c04 | 233 | if (nbDiffAspectsUnits == 1 && units != null) { |
4208b510 AM |
234 | /* All aspects use the same unit type */ |
235 | ||
236 | // The time duration formatter converts ns to s on the axis | |
237 | if (NANOSECONDS_SYMBOL.equals(units)) { | |
238 | units = SECONDS_SYMBOL; | |
239 | } | |
8d9b1c04 | 240 | fXTitle = xBaseTitle + " (" + units + ')'; //$NON-NLS-1$ |
4208b510 AM |
241 | } else { |
242 | /* Various unit types, just say "Value" */ | |
8d9b1c04 | 243 | fXTitle = nullToEmptyString(xBaseTitle); |
4208b510 AM |
244 | } |
245 | } | |
246 | ||
247 | /* Set Y axis title */ | |
248 | if (fChartModel.getYSeriesColumns().size() == 1) { | |
249 | /* | |
250 | * There is only 1 series in the chart, we will use its name as the | |
251 | * Y axis (and hide the legend). | |
252 | */ | |
253 | String seriesName = getChartModel().getYSeriesColumns().get(0); | |
254 | // The time duration formatter converts ns to s on the axis | |
255 | if (NANOSECONDS_SYMBOL.equals(getYAxisAspects().get(0).getUnits())) { | |
256 | seriesName = getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$ | |
257 | } | |
258 | fYTitle = seriesName; | |
259 | fChart.getLegend().setVisible(false); | |
260 | } else { | |
261 | /* | |
262 | * There are multiple series in the chart, if they all share the same | |
263 | * units, display that. | |
264 | */ | |
8d9b1c04 | 265 | long nbDiffAspectsUnits = getYAxisAspects().stream() |
4208b510 AM |
266 | .map(aspect -> aspect.getUnits()) |
267 | .distinct() | |
268 | .count(); | |
269 | ||
8d9b1c04 JR |
270 | long nbDiffAspectName = getYAxisAspects().stream() |
271 | .map(aspect -> aspect.getName()) | |
272 | .distinct() | |
273 | .count(); | |
274 | ||
275 | String yBaseTitle = Messages.LamiViewer_DefaultValueName; | |
276 | if (nbDiffAspectName == 1) { | |
277 | yBaseTitle = getYAxisAspects().get(0).getName(); | |
278 | } | |
279 | ||
4208b510 | 280 | String units = getYAxisAspects().get(0).getUnits(); |
8d9b1c04 | 281 | if (nbDiffAspectsUnits == 1 && units != null) { |
4208b510 AM |
282 | /* All aspects use the same unit type */ |
283 | ||
284 | // The time duration formatter converts ns to s on the axis | |
285 | if (NANOSECONDS_SYMBOL.equals(units)) { | |
286 | units = SECONDS_SYMBOL; | |
287 | } | |
8d9b1c04 | 288 | fYTitle = yBaseTitle + " (" + units + ')'; //$NON-NLS-1$ |
4208b510 | 289 | } else { |
8d9b1c04 JR |
290 | /* Various unit types, don't display any units */ |
291 | fYTitle = nullToEmptyString(yBaseTitle); | |
4208b510 AM |
292 | } |
293 | ||
294 | /* Put legend at the bottom */ | |
295 | fChart.getLegend().setPosition(SWT.BOTTOM); | |
296 | } | |
297 | ||
298 | /* Set all titles and labels font color to black */ | |
299 | fChart.getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
300 | fChart.getAxisSet().getXAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
301 | fChart.getAxisSet().getYAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
302 | fChart.getAxisSet().getXAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
303 | fChart.getAxisSet().getYAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
304 | ||
305 | /* Set X label 90 degrees */ | |
306 | fChart.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90); | |
307 | ||
308 | /* Refresh the titles to fit the current chart size */ | |
309 | refreshDisplayTitles(); | |
310 | ||
7710e6ed JR |
311 | fToolBar = createChartToolBar(); |
312 | ||
4208b510 AM |
313 | fChart.addDisposeListener(e -> { |
314 | /* Dispose resources of this class */ | |
315 | LamiXYChartViewer.super.dispose(); | |
316 | }); | |
317 | } | |
318 | ||
319 | /** | |
320 | * Util method to check if a list of aspects are all continuous. | |
321 | * | |
322 | * @param axisAspects | |
323 | * The list of aspects to check. | |
324 | * @return true is all aspects are continuous, otherwise false. | |
325 | */ | |
326 | protected static boolean areAspectsContinuous(List<LamiTableEntryAspect> axisAspects) { | |
327 | return axisAspects.stream().allMatch(aspect -> aspect.isContinuous()); | |
328 | } | |
329 | ||
330 | /** | |
331 | * Util method to check if a list of aspects are all time stamps. | |
332 | * | |
333 | * @param axisAspects | |
334 | * The list of aspects to check. | |
335 | * @return true is all aspects are time stamps, otherwise false. | |
336 | */ | |
337 | protected static boolean areAspectsTimeStamp(List<LamiTableEntryAspect> axisAspects) { | |
338 | return axisAspects.stream().allMatch(aspect -> aspect.isTimeStamp()); | |
339 | } | |
340 | ||
341 | /** | |
342 | * Util method to check if a list of aspects are all time durations. | |
343 | * | |
344 | * @param axisAspects | |
345 | * The list of aspects to check. | |
346 | * @return true is all aspects are time durations, otherwise false. | |
347 | */ | |
348 | protected static boolean areAspectsTimeDuration(List<LamiTableEntryAspect> axisAspects) { | |
349 | return axisAspects.stream().allMatch(aspect -> aspect.isTimeDuration()); | |
350 | } | |
351 | ||
352 | /** | |
353 | * Util method that will return a formatter based on the aspects linked to an axis | |
354 | * | |
355 | * If all aspects are time stamps, return a timestamp formatter tuned to the interval. | |
356 | * If all aspects are time durations, return the nanoseconds to seconds formatter. | |
357 | * Otherwise, return the generic decimal formatter. | |
358 | * | |
359 | * @param axisAspects | |
360 | * The list of aspects of the axis. | |
361 | * @param entries | |
362 | * The list of entries of the chart. | |
363 | * @return a formatter for the axis. | |
364 | */ | |
365 | protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries) { | |
366 | ||
367 | if (areAspectsTimeStamp(axisAspects)) { | |
368 | /* Set a TimeStamp formatter depending on the duration between the first and last value */ | |
369 | double max = Double.MIN_VALUE; | |
370 | double min = Double.MAX_VALUE; | |
371 | ||
372 | for (LamiTableEntry entry : entries) { | |
373 | for (LamiTableEntryAspect aspect : axisAspects) { | |
374 | Double current = aspect.resolveDouble(entry); | |
375 | if (current != null) { | |
376 | max = Math.max(max, current); | |
377 | min = Math.min(min, current); | |
378 | } | |
379 | } | |
380 | } | |
381 | long duration = (long) max - (long) min; | |
382 | ||
383 | if (duration > TimeUnit.DAYS.toNanos(1)) { | |
384 | return DAYS_FORMATTER; | |
385 | } else if (duration > TimeUnit.HOURS.toNanos(1)) { | |
386 | return HOURS_FORMATTER; | |
387 | } else if (duration > TimeUnit.MINUTES.toNanos(1)) { | |
388 | return MINUTES_FORMATTER; | |
389 | } else if (duration > TimeUnit.SECONDS.toNanos(15)) { | |
390 | return SECONDS_FORMATTER; | |
391 | } else { | |
392 | return MILLISECONDS_FORMATTER; | |
393 | } | |
394 | } else if (areAspectsTimeDuration(axisAspects)) { | |
395 | /* Set the time duration formatter */ | |
396 | return NANO_TO_SECS_FORMATTER; | |
397 | ||
398 | } else { | |
399 | /* For other numeric aspects, use the default decimal unit formatter */ | |
400 | return DECIMAL_FORMATTER; | |
401 | } | |
402 | } | |
403 | ||
404 | /** | |
405 | * Get the chart result table. | |
406 | * | |
407 | * @return The chart result table. | |
408 | */ | |
409 | protected LamiResultTable getResultTable() { | |
410 | return fResultTable; | |
411 | } | |
412 | ||
413 | /** | |
414 | * Get the chart model. | |
415 | * | |
416 | * @return The chart model. | |
417 | */ | |
418 | protected LamiChartModel getChartModel() { | |
419 | return fChartModel; | |
420 | } | |
421 | ||
422 | /** | |
423 | * Get the chart object. | |
424 | * @return The chart object. | |
425 | */ | |
426 | protected Chart getChart() { | |
427 | return fChart; | |
428 | } | |
429 | ||
7710e6ed JR |
430 | /** |
431 | * @return the toolBar | |
432 | */ | |
433 | public ToolBar getToolBar() { | |
434 | return fToolBar; | |
435 | } | |
436 | ||
4208b510 AM |
437 | /** |
438 | * Is a selection made in the chart. | |
439 | * | |
440 | * @return true if there is a selection. | |
441 | */ | |
442 | protected boolean isSelected() { | |
443 | return fSelected; | |
444 | } | |
445 | ||
446 | /** | |
447 | * Set the selection index. | |
448 | * | |
449 | * @param selection the index to select. | |
450 | */ | |
451 | protected void setSelection(Set<Integer> selection) { | |
452 | fSelection = selection; | |
453 | fSelected = !selection.isEmpty(); | |
454 | } | |
455 | ||
456 | /** | |
457 | * Unset the chart selection. | |
458 | */ | |
459 | protected void unsetSelection() { | |
460 | fSelection.clear(); | |
461 | fSelected = false; | |
462 | } | |
463 | ||
464 | /** | |
465 | * Get the current selection index. | |
466 | * | |
467 | * @return the current selection index. | |
468 | */ | |
469 | protected Set<Integer> getSelection() { | |
470 | return fSelection; | |
471 | } | |
472 | ||
473 | @Override | |
474 | public @Nullable Control getControl() { | |
475 | return fChart.getParent(); | |
476 | } | |
477 | ||
478 | @Override | |
479 | public void refresh() { | |
480 | Display.getDefault().asyncExec(() -> { | |
481 | if (!fChart.isDisposed()) { | |
482 | fChart.redraw(); | |
483 | } | |
484 | }); | |
485 | } | |
486 | ||
487 | @Override | |
488 | public void dispose() { | |
489 | fChart.dispose(); | |
490 | /* The control's DisposeListener will call super.dispose() */ | |
491 | } | |
492 | ||
493 | /** | |
494 | * Get a list of all the aspect of the Y axis. | |
495 | * | |
496 | * @return The aspects for the Y axis | |
497 | */ | |
498 | protected List<LamiTableEntryAspect> getYAxisAspects() { | |
499 | ||
500 | List<LamiTableEntryAspect> yAxisAspects = new ArrayList<>(); | |
501 | ||
502 | for (String colName : getChartModel().getYSeriesColumns()) { | |
503 | yAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); | |
504 | } | |
505 | ||
506 | return yAxisAspects; | |
507 | } | |
508 | ||
509 | /** | |
510 | * Get a list of all the aspect of the X axis. | |
511 | * | |
512 | * @return The aspects for the X axis | |
513 | */ | |
514 | protected List<LamiTableEntryAspect> getXAxisAspects() { | |
515 | ||
516 | List<LamiTableEntryAspect> xAxisAspects = new ArrayList<>(); | |
517 | ||
518 | for (String colName : getChartModel().getXSeriesColumns()) { | |
519 | xAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); | |
520 | } | |
521 | ||
522 | return xAxisAspects; | |
523 | } | |
524 | ||
525 | /** | |
526 | * Set the ITitle object text to a substring of canonicalTitle that when | |
527 | * rendered in the chart will fit maxPixelLength. | |
528 | */ | |
529 | private void refreshDisplayTitle(ITitle title, String canonicalTitle, int maxPixelLength) { | |
530 | if (title.isVisible()) { | |
531 | ||
532 | String newTitle = canonicalTitle; | |
533 | ||
534 | /* Get the title font */ | |
535 | Font font = title.getFont(); | |
536 | ||
537 | GC gc = new GC(fParent); | |
538 | gc.setFont(font); | |
539 | ||
540 | /* Get the length and height of the canonical title in pixels */ | |
541 | Point pixels = gc.stringExtent(canonicalTitle); | |
542 | ||
543 | /* | |
544 | * If the title is too long, generate a shortened version based on the | |
545 | * average character width of the current font. | |
546 | */ | |
547 | if (pixels.x > maxPixelLength) { | |
548 | int charwidth = gc.getFontMetrics().getAverageCharWidth(); | |
549 | ||
550 | int minimum = 3; | |
551 | ||
552 | int strLen = ((maxPixelLength / charwidth) - minimum); | |
553 | ||
554 | if (strLen > minimum) { | |
555 | newTitle = canonicalTitle.substring(0, strLen) + ELLIPSIS; | |
556 | } else { | |
557 | newTitle = ELLIPSIS; | |
558 | } | |
559 | } | |
560 | ||
561 | title.setText(newTitle); | |
562 | ||
563 | // Cleanup | |
564 | gc.dispose(); | |
565 | } | |
566 | } | |
567 | ||
568 | /** | |
569 | * Refresh the Chart, XAxis and YAxis titles to fit the current | |
570 | * chart size. | |
571 | */ | |
572 | private void refreshDisplayTitles() { | |
573 | Rectangle chartRect = fChart.getClientArea(); | |
574 | Rectangle plotRect = fChart.getPlotArea().getClientArea(); | |
575 | ||
576 | ITitle chartTitle = checkNotNull(fChart.getTitle()); | |
577 | refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width); | |
578 | ||
579 | ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle()); | |
580 | refreshDisplayTitle(xTitle, fXTitle, plotRect.width); | |
581 | ||
582 | ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle()); | |
583 | refreshDisplayTitle(yTitle, fYTitle, plotRect.height); | |
584 | } | |
585 | ||
586 | /** | |
587 | * Get the aspect with the given name | |
588 | * | |
589 | * @param aspects | |
590 | * The list of aspects to search into | |
591 | * @param aspectName | |
592 | * The name of the aspect we are looking for | |
593 | * @return The corresponding aspect | |
594 | */ | |
595 | protected static @Nullable LamiTableEntryAspect getAspectFromName(List<LamiTableEntryAspect> aspects, String aspectName) { | |
596 | for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { | |
597 | ||
598 | if (lamiTableEntryAspect.getLabel().equals(aspectName)) { | |
599 | return lamiTableEntryAspect; | |
600 | } | |
601 | } | |
602 | ||
603 | return null; | |
604 | } | |
605 | ||
606 | /** | |
607 | * Refresh the axis labels to fit the current chart size. | |
608 | */ | |
609 | protected abstract void refreshDisplayLabels(); | |
610 | ||
611 | /** | |
612 | * Redraw the chart. | |
613 | */ | |
614 | protected void redraw() { | |
615 | refresh(); | |
616 | } | |
617 | ||
618 | /** | |
619 | * Signal handler for selection update. | |
620 | * | |
621 | * @param signal | |
622 | * The selection update signal | |
623 | */ | |
624 | @TmfSignalHandler | |
625 | public void updateSelection(LamiSelectionUpdateSignal signal) { | |
626 | if (getResultTable().hashCode() != signal.getSignalHash() || equals(signal.getSource())) { | |
627 | /* The signal is not for us */ | |
628 | return; | |
629 | } | |
630 | setSelection(signal.getEntryIndex()); | |
631 | ||
632 | redraw(); | |
633 | } | |
7710e6ed JR |
634 | |
635 | /** | |
636 | * Create a tool bar on top right of the chart. Contained actions: | |
637 | * <ul> | |
638 | * <li>Dispose the current viewer, also known as "Close the chart"</li> | |
639 | * </ul> | |
640 | * | |
641 | * This tool bar should only appear when the mouse enters the composite. | |
642 | * | |
643 | * @return the tool bar | |
644 | */ | |
645 | protected ToolBar createChartToolBar() { | |
646 | Image removeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE); | |
647 | ToolBar toolBar = new ToolBar(getChart(), SWT.HORIZONTAL); | |
648 | ||
649 | /* Default state */ | |
650 | toolBar.moveAbove(null); | |
651 | toolBar.setVisible(false); | |
652 | ||
653 | /* | |
654 | * Close chart button | |
655 | */ | |
656 | ToolItem closeButton = new ToolItem(toolBar, SWT.PUSH); | |
657 | closeButton.setImage(removeImage); | |
658 | closeButton.setToolTipText(Messages.LamiXYChartViewer_CloseChartToolTip); | |
659 | closeButton.addSelectionListener(new SelectionListener() { | |
660 | @Override | |
661 | public void widgetSelected(@Nullable SelectionEvent e) { | |
662 | Composite parent = getParent(); | |
663 | dispose(); | |
664 | parent.layout(); | |
665 | } | |
666 | ||
667 | @Override | |
668 | public void widgetDefaultSelected(@Nullable SelectionEvent e) { | |
669 | } | |
670 | }); | |
671 | ||
672 | toolBar.pack(); | |
673 | toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); | |
674 | ||
675 | /* Visibility toggle filter */ | |
676 | Listener toolBarVisibilityToggleListener = e -> { | |
677 | if (e.widget instanceof Control) { | |
678 | Control control = (Control) e.widget; | |
679 | Point display = control.toDisplay(e.x, e.y); | |
680 | Point location = getChart().getParent().toControl(display); | |
681 | ||
682 | /* | |
683 | * Only set to visible if we are at the right location, in the | |
684 | * right shell. | |
685 | */ | |
686 | boolean visible = getChart().getBounds().contains(location) && | |
687 | control.getShell().equals(getChart().getShell()); | |
688 | getToolBar().setVisible(visible); | |
689 | } | |
690 | }; | |
691 | ||
692 | /* Filter to make sure we hide the toolbar if we exit the window */ | |
693 | Listener hideToolBarListener = (e -> getToolBar().setVisible(false)); | |
694 | ||
695 | /* | |
696 | * Add the filters to the main Display, and remove them when we dispose | |
697 | * the chart. | |
698 | */ | |
699 | Display display = getChart().getDisplay(); | |
700 | display.addFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); | |
701 | display.addFilter(SWT.MouseExit, hideToolBarListener); | |
702 | ||
703 | getChart().addDisposeListener(e -> { | |
704 | display.removeFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); | |
705 | display.removeFilter(SWT.MouseExit, hideToolBarListener); | |
706 | }); | |
707 | ||
708 | /* Reposition the tool bar on resize */ | |
709 | getChart().addListener(SWT.Resize, new Listener() { | |
710 | @Override | |
711 | public void handleEvent(@Nullable Event event) { | |
712 | toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); | |
713 | } | |
714 | }); | |
715 | ||
716 | return toolBar; | |
717 | } | |
4208b510 | 718 | } |