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