Commit | Line | Data |
---|---|---|
dc4fa715 PT |
1 | /******************************************************************************* |
2 | * Copyright (c) 2016 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 | * Patrick Tasse - Initial API and implementation | |
11 | *******************************************************************************/ | |
12 | ||
13 | package org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets; | |
14 | ||
a924e2ed | 15 | import java.util.ArrayList; |
dc4fa715 | 16 | import java.util.Collections; |
f0a9cee1 | 17 | import java.util.LinkedHashSet; |
dc4fa715 | 18 | import java.util.List; |
f0a9cee1 | 19 | import java.util.Set; |
dc4fa715 PT |
20 | |
21 | import org.eclipse.jdt.annotation.NonNull; | |
22 | import org.eclipse.swt.SWT; | |
23 | import org.eclipse.swt.events.MouseAdapter; | |
24 | import org.eclipse.swt.events.MouseEvent; | |
25 | import org.eclipse.swt.events.PaintEvent; | |
26 | import org.eclipse.swt.graphics.Color; | |
27 | import org.eclipse.swt.graphics.GC; | |
f0a9cee1 | 28 | import org.eclipse.swt.graphics.Image; |
dc4fa715 | 29 | import org.eclipse.swt.graphics.Point; |
968b0216 | 30 | import org.eclipse.swt.graphics.RGB; |
dc4fa715 PT |
31 | import org.eclipse.swt.graphics.Rectangle; |
32 | import org.eclipse.swt.widgets.Composite; | |
33 | import org.eclipse.swt.widgets.Display; | |
f0a9cee1 | 34 | import org.eclipse.tracecompass.internal.tmf.ui.Activator; |
dc4fa715 PT |
35 | import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.IMarkerEvent; |
36 | ||
37 | import com.google.common.collect.LinkedHashMultimap; | |
dc4fa715 PT |
38 | import com.google.common.collect.Multimap; |
39 | ||
40 | /** | |
41 | * A control that shows marker labels on a time axis. | |
42 | * | |
43 | * @since 2.0 | |
44 | */ | |
45 | public class TimeGraphMarkerAxis extends TimeGraphBaseControl { | |
46 | ||
f0a9cee1 PT |
47 | private static final Image COLLAPSED = Activator.getDefault().getImageFromPath("icons/ovr16/collapsed_ovr.gif"); //$NON-NLS-1$ |
48 | private static final Image EXPANDED = Activator.getDefault().getImageFromPath("icons/ovr16/expanded_ovr.gif"); //$NON-NLS-1$ | |
49 | private static final Image HIDE = Activator.getDefault().getImageFromPath("icons/etool16/hide.gif"); //$NON-NLS-1$ | |
50 | private static final int HIDE_BORDER = 4; // transparent border of the hide icon | |
51 | ||
dc4fa715 PT |
52 | private static final int HEIGHT; |
53 | static { | |
54 | GC gc = new GC(Display.getDefault()); | |
55 | HEIGHT = gc.getFontMetrics().getHeight() + 1; | |
56 | gc.dispose(); | |
57 | } | |
58 | ||
59 | private static final int TOP_MARGIN = 1; | |
60 | private static final int MAX_LABEL_LENGTH = 256; | |
61 | private static final int TEXT_MARGIN = 2; | |
62 | private static final int MAX_GAP = 5; | |
63 | private static final int X_LIMIT = Integer.MAX_VALUE / 256; | |
64 | ||
65 | private @NonNull ITimeDataProvider fTimeProvider; | |
f0a9cee1 | 66 | private final Set<IMarkerAxisListener> fListeners = new LinkedHashSet<>(); |
dc4fa715 | 67 | private Multimap<String, IMarkerEvent> fMarkers = LinkedHashMultimap.create(); |
a924e2ed | 68 | private @NonNull List<String> fCategories = Collections.EMPTY_LIST; |
f0a9cee1 | 69 | private boolean fCollapsed = false; |
dc4fa715 PT |
70 | |
71 | /** | |
72 | * Contructor | |
73 | * | |
74 | * @param parent | |
75 | * The parent composite object | |
76 | * @param colorScheme | |
77 | * The color scheme to use | |
78 | * @param timeProvider | |
79 | * The time data provider | |
80 | */ | |
81 | public TimeGraphMarkerAxis(Composite parent, @NonNull TimeGraphColorScheme colorScheme, @NonNull ITimeDataProvider timeProvider) { | |
82 | super(parent, colorScheme, SWT.NO_BACKGROUND | SWT.NO_FOCUS | SWT.DOUBLE_BUFFERED); | |
83 | fTimeProvider = timeProvider; | |
84 | addMouseListener(new MouseAdapter() { | |
85 | @Override | |
86 | public void mouseDown(MouseEvent e) { | |
f0a9cee1 PT |
87 | Point size = getSize(); |
88 | Rectangle bounds = new Rectangle(0, 0, size.x, size.y); | |
89 | TimeGraphMarkerAxis.this.mouseDown(e, bounds, fTimeProvider.getNameSpace()); | |
dc4fa715 PT |
90 | } |
91 | }); | |
92 | } | |
93 | ||
94 | @Override | |
95 | public Point computeSize(int wHint, int hHint, boolean changed) { | |
96 | int height = 0; | |
97 | if (!fMarkers.isEmpty() && fTimeProvider.getTime0() != fTimeProvider.getTime1()) { | |
f0a9cee1 PT |
98 | if (fCollapsed) { |
99 | height = COLLAPSED.getBounds().height; | |
100 | } else { | |
101 | height = TOP_MARGIN + fMarkers.keySet().size() * HEIGHT; | |
102 | } | |
dc4fa715 PT |
103 | } |
104 | return super.computeSize(wHint, height, changed); | |
105 | } | |
106 | ||
f0a9cee1 PT |
107 | /** |
108 | * Add a marker axis listener. | |
109 | * | |
110 | * @param listener | |
111 | * the listener | |
112 | */ | |
113 | public void addMarkerAxisListener(IMarkerAxisListener listener) { | |
114 | fListeners.add(listener); | |
115 | } | |
116 | ||
117 | /** | |
118 | * Remove a marker axis listener. | |
119 | * | |
120 | * @param listener | |
121 | * the listener | |
122 | */ | |
123 | public void removeMarkerAxisListener(IMarkerAxisListener listener) { | |
124 | fListeners.remove(listener); | |
125 | } | |
126 | ||
dc4fa715 PT |
127 | /** |
128 | * Set the time provider | |
129 | * | |
130 | * @param timeProvider | |
131 | * The provider to use | |
132 | */ | |
133 | public void setTimeProvider(@NonNull ITimeDataProvider timeProvider) { | |
134 | fTimeProvider = timeProvider; | |
135 | } | |
136 | ||
a924e2ed PT |
137 | /** |
138 | * Set the list of marker categories. | |
139 | * | |
140 | * @param categories | |
141 | * The list of marker categories, or null | |
142 | */ | |
143 | public void setMarkerCategories(List<String> categories) { | |
144 | if (categories == null) { | |
145 | fCategories = Collections.EMPTY_LIST; | |
146 | } else { | |
147 | fCategories = categories; | |
148 | } | |
149 | } | |
150 | ||
f0a9cee1 PT |
151 | /** |
152 | * Handle a mouseDown event. | |
153 | * | |
154 | * @param e | |
155 | * the mouse event | |
156 | * @param bounds | |
157 | * the bounds of the marker axis in the mouse event's coordinates | |
158 | * @param nameSpace | |
159 | * the width of the marker name area | |
160 | */ | |
161 | public void mouseDown(MouseEvent e, Rectangle bounds, int nameSpace) { | |
162 | if (bounds.isEmpty()) { | |
163 | return; | |
164 | } | |
165 | if (fCollapsed || (e.x < bounds.x + Math.min(nameSpace, EXPANDED.getBounds().width))) { | |
166 | fCollapsed = !fCollapsed; | |
167 | getParent().layout(); | |
168 | redraw(); | |
169 | return; | |
170 | } | |
171 | if (e.x < bounds.x + nameSpace) { | |
172 | String category = getHiddenCategoryForEvent(e, bounds); | |
173 | if (category != null) { | |
174 | for (IMarkerAxisListener listener : fListeners) { | |
175 | listener.setMarkerCategoryVisible(category, false); | |
176 | } | |
177 | } | |
178 | return; | |
179 | } | |
180 | IMarkerEvent marker = getMarkerForEvent(e); | |
181 | if (marker != null) { | |
182 | fTimeProvider.setSelectionRangeNotify(marker.getTime(), marker.getTime() + marker.getDuration(), false); | |
183 | } | |
184 | } | |
185 | ||
dc4fa715 PT |
186 | /** |
187 | * Set the markers list. | |
188 | * | |
189 | * @param markers | |
190 | * The markers list | |
191 | */ | |
192 | public void setMarkers(List<IMarkerEvent> markers) { | |
193 | Multimap<String, IMarkerEvent> map = LinkedHashMultimap.create(); | |
194 | for (IMarkerEvent marker : markers) { | |
195 | map.put(marker.getCategory(), marker); | |
196 | } | |
dc4fa715 | 197 | Display.getDefault().asyncExec(() -> { |
d1709ced PT |
198 | if (isDisposed()) { |
199 | return; | |
200 | } | |
dc4fa715 | 201 | fMarkers = map; |
dc4fa715 PT |
202 | getParent().layout(); |
203 | redraw(); | |
204 | }); | |
205 | } | |
206 | ||
207 | @Override | |
208 | void paint(Rectangle bounds, PaintEvent e) { | |
209 | drawMarkerAxis(bounds, fTimeProvider.getNameSpace(), e.gc); | |
210 | } | |
211 | ||
212 | /** | |
213 | * Draw the marker axis | |
214 | * | |
215 | * @param bounds | |
216 | * the bounds of the marker axis | |
217 | * @param nameSpace | |
218 | * the width of the marker name area | |
219 | * @param gc | |
220 | * the GC instance | |
221 | */ | |
222 | protected void drawMarkerAxis(Rectangle bounds, int nameSpace, GC gc) { | |
f0a9cee1 PT |
223 | if (bounds.isEmpty()) { |
224 | return; | |
225 | } | |
dc4fa715 PT |
226 | // draw background |
227 | gc.fillRectangle(bounds); | |
228 | ||
f0a9cee1 PT |
229 | if (!fCollapsed) { |
230 | Rectangle rect = new Rectangle(bounds.x, bounds.y + TOP_MARGIN, bounds.width, HEIGHT); | |
231 | for (String category : getVisibleCategories()) { | |
232 | rect.x = bounds.x; | |
233 | rect.width = nameSpace; | |
234 | drawMarkerCategory(category, rect, gc); | |
235 | rect.x = nameSpace; | |
236 | rect.width = bounds.width - nameSpace; | |
237 | drawMarkerLabels(category, rect, gc); | |
238 | rect.y += HEIGHT; | |
239 | } | |
dc4fa715 | 240 | } |
f0a9cee1 PT |
241 | |
242 | Rectangle rect = new Rectangle(bounds.x, bounds.y, nameSpace, bounds.height); | |
243 | gc.setClipping(rect); | |
244 | drawToolbar(rect, nameSpace, gc); | |
dc4fa715 PT |
245 | } |
246 | ||
247 | /** | |
248 | * Draw the marker category | |
249 | * | |
250 | * @param category | |
251 | * the category | |
252 | * @param rect | |
253 | * the bounds of the marker name area | |
254 | * @param gc | |
255 | * the GC instance | |
256 | */ | |
257 | protected void drawMarkerCategory(String category, Rectangle rect, GC gc) { | |
258 | if (rect.isEmpty()) { | |
259 | return; | |
260 | } | |
261 | // draw marker category | |
262 | gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_FOREGROUND)); | |
263 | gc.setClipping(rect); | |
264 | int width = gc.textExtent(category).x + TEXT_MARGIN; | |
f0a9cee1 PT |
265 | int x = rect.x + EXPANDED.getBounds().width + HIDE.getBounds().width; |
266 | gc.drawText(category, Math.max(x, rect.x + rect.width - width), rect.y, true); | |
dc4fa715 PT |
267 | } |
268 | ||
269 | /** | |
270 | * Draw the marker labels for the specified category | |
271 | * | |
272 | * @param category | |
273 | * the category | |
274 | * @param rect | |
275 | * the bounds of the marker time area | |
276 | * @param gc | |
277 | * the GC instance | |
278 | */ | |
279 | protected void drawMarkerLabels(String category, Rectangle rect, GC gc) { | |
280 | if (rect.isEmpty()) { | |
281 | return; | |
282 | } | |
283 | long time0 = fTimeProvider.getTime0(); | |
284 | long time1 = fTimeProvider.getTime1(); | |
285 | if (time0 == time1) { | |
286 | return; | |
287 | } | |
288 | int timeSpace = fTimeProvider.getTimeSpace(); | |
289 | double pixelsPerNanoSec = (timeSpace <= RIGHT_MARGIN) ? 0 : | |
290 | (double) (timeSpace - RIGHT_MARGIN) / (time1 - time0); | |
291 | ||
292 | gc.setClipping(rect); | |
293 | for (IMarkerEvent markerEvent : fMarkers.get(category)) { | |
294 | Color color = getColorScheme().getColor(markerEvent.getColor()); | |
295 | gc.setForeground(color); | |
968b0216 | 296 | gc.setBackground(color); |
dc4fa715 PT |
297 | int x1 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime()); |
298 | if (x1 > rect.x + rect.width) { | |
299 | return; | |
300 | } | |
301 | if (markerEvent.getEntry() != null) { | |
302 | continue; | |
303 | } | |
304 | int x2 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime() + markerEvent.getDuration()) - 1; | |
305 | String label = getTrimmedLabel(markerEvent); | |
306 | if (label != null) { | |
307 | int width = gc.textExtent(label).x + TEXT_MARGIN; | |
308 | if (x1 < rect.x && x1 + width < x2) { | |
309 | int gap = Math.min(rect.x - x1, MAX_GAP); | |
310 | x1 = Math.min(rect.x + gap, x2 - width); | |
311 | if (x1 > rect.x) { | |
312 | int y = rect.y + rect.height / 2; | |
313 | gc.drawLine(rect.x, y, x1, y); | |
314 | } | |
315 | } | |
316 | gc.fillRectangle(x1, rect.y, width, rect.height - 1); | |
dc4fa715 PT |
317 | gc.drawRectangle(x1, rect.y, width, rect.height - 1); |
318 | if (x2 > x1 + width) { | |
319 | int y = rect.y + rect.height / 2; | |
320 | gc.drawLine(x1 + width, y, x2, y); | |
321 | } | |
968b0216 PT |
322 | gc.setForeground(getDistinctForeground(color.getRGB())); |
323 | Utils.drawText(gc, label, x1 + TEXT_MARGIN, rect.y, true); | |
dc4fa715 PT |
324 | } else { |
325 | int y = rect.y + rect.height / 2; | |
326 | gc.drawLine(x1, y, x2, y); | |
327 | } | |
328 | } | |
329 | } | |
330 | ||
968b0216 PT |
331 | private static Color getDistinctForeground(RGB rgb) { |
332 | /* Calculate the relative luminance of the color, high value is bright */ | |
333 | final int luminanceThreshold = 128; | |
334 | /* Relative luminance (Y) coefficients as defined in ITU.R Rec. 709 */ | |
335 | final double redCoefficient = 0.2126; | |
336 | final double greenCoefficient = 0.7152; | |
337 | final double blueCoefficient = 0.0722; | |
338 | int luminance = (int) (redCoefficient * rgb.red + greenCoefficient * rgb.green + blueCoefficient * rgb.blue); | |
339 | /* Use black over bright colors and white over dark colors */ | |
340 | return Display.getDefault().getSystemColor( | |
341 | luminance > luminanceThreshold ? SWT.COLOR_BLACK : SWT.COLOR_WHITE); | |
342 | } | |
343 | ||
f0a9cee1 PT |
344 | /** |
345 | * Draw the toolbar | |
346 | * | |
347 | * @param bounds | |
348 | * the bounds of the marker axis | |
349 | * @param nameSpace | |
350 | * the width of the marker name area | |
351 | * @param gc | |
352 | * the GC instance | |
353 | */ | |
354 | protected void drawToolbar(Rectangle bounds, int nameSpace, GC gc) { | |
355 | if (bounds.isEmpty()) { | |
356 | return; | |
357 | } | |
358 | if (fCollapsed) { | |
359 | gc.drawImage(COLLAPSED, bounds.x, bounds.y); | |
360 | } else { | |
361 | gc.drawImage(EXPANDED, bounds.x, bounds.y); | |
362 | int x = bounds.x + EXPANDED.getBounds().width; | |
363 | for (int i = 0; i < fMarkers.keySet().size(); i++) { | |
364 | int y = bounds.y + TOP_MARGIN + i * HEIGHT; | |
365 | gc.drawImage(HIDE, x, y); | |
366 | } | |
367 | } | |
368 | } | |
369 | ||
dc4fa715 PT |
370 | private static String getTrimmedLabel(IMarkerEvent marker) { |
371 | String label = marker.getLabel(); | |
372 | if (label == null) { | |
373 | return null; | |
374 | } | |
375 | return label.substring(0, Math.min(label.indexOf(SWT.LF) != -1 ? label.indexOf(SWT.LF) : label.length(), MAX_LABEL_LENGTH)); | |
376 | } | |
377 | ||
378 | private static int getXForTime(Rectangle rect, long time0, double pixelsPerNanoSec, long time) { | |
379 | int x = rect.x + (int) (Math.floor((time - time0) * pixelsPerNanoSec)); | |
380 | return Math.min(Math.max(x, -X_LIMIT), X_LIMIT); | |
381 | } | |
382 | ||
383 | private IMarkerEvent getMarkerForEvent(MouseEvent event) { | |
384 | long time0 = fTimeProvider.getTime0(); | |
385 | long time1 = fTimeProvider.getTime1(); | |
386 | if (time0 == time1) { | |
387 | return null; | |
388 | } | |
389 | int timeSpace = fTimeProvider.getTimeSpace(); | |
390 | double pixelsPerNanoSec = (timeSpace <= RIGHT_MARGIN) ? 0 : | |
391 | (double) (timeSpace - RIGHT_MARGIN) / (time1 - time0); | |
392 | ||
393 | int categoryIndex = Math.max((event.y - TOP_MARGIN) / HEIGHT, 0); | |
f0a9cee1 | 394 | String category = getVisibleCategories().get(categoryIndex); |
dc4fa715 PT |
395 | |
396 | IMarkerEvent marker = null; | |
397 | GC gc = new GC(Display.getDefault()); | |
398 | Rectangle rect = getBounds(); | |
399 | rect.x += fTimeProvider.getNameSpace(); | |
400 | rect.width -= fTimeProvider.getNameSpace(); | |
401 | ||
402 | for (IMarkerEvent markerEvent : fMarkers.get(category)) { | |
403 | String label = getTrimmedLabel(markerEvent); | |
404 | if (markerEvent.getEntry() == null) { | |
405 | int x1 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime()); | |
406 | if (x1 <= event.x) { | |
407 | if (label != null) { | |
408 | int width = gc.textExtent(label).x + TEXT_MARGIN; | |
409 | if (event.x <= x1 + width) { | |
410 | marker = markerEvent; | |
411 | continue; | |
412 | } | |
413 | } | |
414 | int x2 = getXForTime(rect, time0, pixelsPerNanoSec, markerEvent.getTime() + markerEvent.getDuration()) - 1; | |
415 | if (event.x <= x2) { | |
416 | marker = markerEvent; | |
417 | } | |
418 | } else { | |
419 | break; | |
420 | } | |
421 | } | |
422 | } | |
423 | gc.dispose(); | |
424 | return marker; | |
425 | } | |
f0a9cee1 PT |
426 | |
427 | private String getHiddenCategoryForEvent(MouseEvent e, Rectangle bounds) { | |
428 | List<String> categories = getVisibleCategories(); | |
429 | Rectangle rect = HIDE.getBounds(); | |
430 | rect.x += bounds.x + EXPANDED.getBounds().width + HIDE_BORDER; | |
431 | rect.y += bounds.y + TOP_MARGIN + HIDE_BORDER; | |
432 | rect.width -= 2 * HIDE_BORDER; | |
433 | rect.height -= 2 * HIDE_BORDER; | |
434 | for (int i = 0; i < categories.size(); i++) { | |
435 | if (rect.contains(e.x, e.y)) { | |
436 | return categories.get(i); | |
437 | } | |
438 | rect.y += HEIGHT; | |
439 | } | |
440 | return null; | |
441 | } | |
442 | ||
443 | private List<String> getVisibleCategories() { | |
444 | List<String> categories = new ArrayList<>(fCategories); | |
445 | categories.retainAll(fMarkers.keySet()); | |
446 | return categories; | |
447 | } | |
dc4fa715 | 448 | } |