1 /*******************************************************************************
2 * Copyright (c) 2015 Ericsson
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
10 * Marc-Andre Laperle - Initial API and implementation
11 *******************************************************************************/
13 package org
.eclipse
.tracecompass
.internal
.tmf
.ui
.views
;
15 import java
.util
.ArrayList
;
16 import java
.util
.Collections
;
17 import java
.util
.List
;
18 import java
.util
.Timer
;
19 import java
.util
.TimerTask
;
21 import org
.eclipse
.core
.runtime
.preferences
.InstanceScope
;
22 import org
.eclipse
.core
.runtime
.preferences
.IEclipsePreferences
.IPreferenceChangeListener
;
23 import org
.eclipse
.core
.runtime
.preferences
.IEclipsePreferences
.PreferenceChangeEvent
;
24 import org
.eclipse
.swt
.graphics
.Point
;
25 import org
.eclipse
.swt
.widgets
.Composite
;
26 import org
.eclipse
.swt
.widgets
.Display
;
27 import org
.eclipse
.swt
.widgets
.Shell
;
28 import org
.eclipse
.tracecompass
.internal
.tmf
.ui
.Activator
;
29 import org
.eclipse
.tracecompass
.internal
.tmf
.ui
.ITmfUIPreferences
;
30 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalHandler
;
31 import org
.eclipse
.tracecompass
.tmf
.core
.signal
.TmfSignalManager
;
32 import org
.eclipse
.tracecompass
.tmf
.ui
.signal
.TmfTimeViewAlignmentInfo
;
33 import org
.eclipse
.tracecompass
.tmf
.ui
.signal
.TmfTimeViewAlignmentSignal
;
34 import org
.eclipse
.tracecompass
.tmf
.ui
.views
.ITmfTimeAligned
;
35 import org
.eclipse
.tracecompass
.tmf
.ui
.views
.TmfView
;
36 import org
.eclipse
.ui
.IViewPart
;
37 import org
.eclipse
.ui
.IViewReference
;
38 import org
.eclipse
.ui
.IWorkbenchPage
;
39 import org
.eclipse
.ui
.IWorkbenchWindow
;
40 import org
.eclipse
.ui
.PlatformUI
;
43 * Receives various notifications for realignment and
44 * performs the alignment on the appropriate views.
48 public class TmfAlignmentSynchronizer
{
50 private static final long THROTTLE_DELAY
= 500;
51 private static final int NEAR_THRESHOLD
= 10;
52 private final Timer fTimer
;
53 private final List
<AlignmentOperation
> fPendingOperations
= Collections
.synchronizedList(new ArrayList
<AlignmentOperation
>());
55 private TimerTask fCurrentTask
;
60 public TmfAlignmentSynchronizer() {
61 TmfSignalManager
.register(this);
63 createPreferenceListener();
64 fCurrentTask
= new TimerTask() {
72 private IPreferenceChangeListener
createPreferenceListener() {
73 IPreferenceChangeListener listener
= new IPreferenceChangeListener() {
76 public void preferenceChange(PreferenceChangeEvent event
) {
77 if (event
.getKey().equals(ITmfUIPreferences
.PREF_ALIGN_VIEWS
)) {
78 Object oldValue
= event
.getOldValue();
79 Object newValue
= event
.getNewValue();
80 if (Boolean
.toString(false).equals(oldValue
) && Boolean
.toString(true).equals(newValue
)) {
82 } else if (Boolean
.toString(true).equals(oldValue
) && Boolean
.toString(false).equals(newValue
)) {
88 InstanceScope
.INSTANCE
.getNode(Activator
.PLUGIN_ID
).addPreferenceChangeListener(listener
);
92 private class AlignmentOperation
{
94 final TmfTimeViewAlignmentInfo fAlignmentInfo
;
96 public AlignmentOperation(TmfView view
, TmfTimeViewAlignmentInfo timeViewAlignmentInfo
) {
98 fAlignmentInfo
= timeViewAlignmentInfo
;
102 private class AlignTask
extends TimerTask
{
106 final List
<AlignmentOperation
> fCopy
;
107 synchronized (fPendingOperations
) {
108 fCopy
= new ArrayList
<>(fPendingOperations
);
109 fPendingOperations
.clear();
111 Display
.getDefault().syncExec(new Runnable() {
114 performAllAlignments(fCopy
);
121 * Handle a view that was just resized.
124 * the view that was resized
126 public void handleViewResized(TmfView view
) {
127 TmfTimeViewAlignmentInfo alignmentInfo
= new TmfTimeViewAlignmentInfo(view
.getParentComposite().getShell(), getViewLocation(view
), 0);
129 // Don't use a view that was just resized as a reference view.
130 // Otherwise, a view that was just
131 // created might use itself as a reference but we want to
132 // keep the existing alignment from the other views.
133 ITmfTimeAligned referenceView
= getReferenceView(alignmentInfo
, view
);
134 if (referenceView
!= null) {
135 queueAlignment(referenceView
.getTimeViewAlignmentInfo(), false);
140 * Handle a view that was just closed.
143 * the view that was closed
145 public void handleViewClosed(TmfView view
) {
146 // Realign views so that they can use the maximum available width in the
147 // event that a narrow view was just closed
148 realignViews(view
.getSite().getPage());
152 * Process signal for alignment.
154 * @param signal the alignment signal
157 public void timeViewAlignmentUpdated(TmfTimeViewAlignmentSignal signal
) {
158 queueAlignment(signal
.getTimeViewAlignmentInfo(), signal
.IsSynchronous());
162 * Perform all alignment operations for the specified alignment
166 * - The alignment algorithm chooses the narrowest width to accommodate all views.
167 * - View positions are recomputed for extra accuracy since the views could have been moved or resized.
168 * - Based on the up-to-date view positions, only views that are near and aligned with each other
171 private static void performAllAlignments(final List
<AlignmentOperation
> alignments
) {
172 for (final AlignmentOperation info
: alignments
) {
173 performAlignment(info
);
177 private static void performAlignment(AlignmentOperation info
) {
179 TmfView referenceView
= info
.fView
;
180 if (isDisposedView(referenceView
)) {
184 TmfTimeViewAlignmentInfo alignmentInfo
= info
.fAlignmentInfo
;
185 // The location of the view might have changed (resize, etc). Update the alignment info.
186 alignmentInfo
= new TmfTimeViewAlignmentInfo(alignmentInfo
.getShell(), getViewLocation(referenceView
), getClampedTimeAxisOffset(alignmentInfo
));
188 TmfView narrowestView
= getNarrowestView(alignmentInfo
);
189 if (narrowestView
== null) {
190 // No valid view found for this alignment. This could mean that the views for this alignment are now too narrow (width == 0) or that shell is not a workbench window.
194 int narrowestWidth
= ((ITmfTimeAligned
) narrowestView
).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo
));
195 narrowestWidth
= getClampedTimeAxisWidth(alignmentInfo
, narrowestWidth
);
196 IViewReference
[] viewReferences
= referenceView
.getSite().getPage().getViewReferences();
197 for (IViewReference ref
: viewReferences
) {
198 IViewPart view
= ref
.getView(false);
199 if (isTimeAlignedView(view
)) {
200 TmfView tmfView
= (TmfView
) view
;
201 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) view
;
202 if (!isDisposedView(tmfView
) && isViewLocationNear(getViewLocation(tmfView
), alignmentInfo
.getViewLocation())) {
203 alignedView
.performAlign(getClampedTimeAxisOffset(alignmentInfo
), narrowestWidth
);
212 private void realignViews() {
213 for (IWorkbenchWindow window
: PlatformUI
.getWorkbench().getWorkbenchWindows()) {
214 for (IWorkbenchPage page
: window
.getPages()) {
221 * Realign views inside a given page
226 private void realignViews(IWorkbenchPage page
) {
227 IViewReference
[] viewReferences
= page
.getViewReferences();
228 for (IViewReference ref
: viewReferences
) {
229 IViewPart view
= ref
.getView(false);
230 if (isTimeAlignedView(view
)) {
231 queueAlignment(((ITmfTimeAligned
) view
).getTimeViewAlignmentInfo(), false);
237 * Restore the views to their respective maximum widths
239 private static void restoreViews() {
240 // We set the width to Integer.MAX_VALUE so that the
241 // views remove any "filler" space they might have.
242 for (IWorkbenchWindow window
: PlatformUI
.getWorkbench().getWorkbenchWindows()) {
243 for (IWorkbenchPage page
: window
.getPages()) {
244 for (IViewReference ref
: page
.getViewReferences()) {
251 private static void restoreView(IViewReference ref
) {
252 IViewPart view
= ref
.getView(false);
253 if (isTimeAlignedView(view
)) {
254 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) view
;
255 alignedView
.performAlign(getClampedTimeAxisOffset(alignedView
.getTimeViewAlignmentInfo()), Integer
.MAX_VALUE
);
259 private static boolean isTimeAlignedView(IViewPart view
) {
260 if (view
instanceof TmfView
&& view
instanceof ITmfTimeAligned
) {
261 Composite parentComposite
= ((TmfView
) view
).getParentComposite();
262 if (parentComposite
!= null && !parentComposite
.isDisposed()) {
266 return view
instanceof TmfView
&& view
instanceof ITmfTimeAligned
;
269 private static boolean isDisposedView(TmfView view
) {
270 Composite parentComposite
= (view
).getParentComposite();
271 return parentComposite
!= null && parentComposite
.isDisposed();
275 * Queue the operation for processing. If an operation is considered the
276 * same alignment (shell, location) as a previously queued one, it will
277 * replace the old one. This way, only one up-to-date alignment operation is
278 * kept per set of time-axis aligned views. The processing of the operation
279 * is also throttled (TimerTask).
282 * the operation to queue
284 private void queue(AlignmentOperation operation
) {
285 synchronized(fPendingOperations
) {
286 fCurrentTask
.cancel();
287 for (AlignmentOperation pendingOperation
: fPendingOperations
) {
288 if (isSameAlignment(operation
, pendingOperation
)) {
289 fPendingOperations
.remove(pendingOperation
);
293 fPendingOperations
.add(operation
);
294 fCurrentTask
= new AlignTask();
295 fTimer
.schedule(fCurrentTask
, THROTTLE_DELAY
);
300 * Two operations are considered to be for the same set of time-axis aligned
301 * views if they are on the same Shell and near the same location.
303 private static boolean isSameAlignment(AlignmentOperation operation1
, AlignmentOperation operation2
) {
304 if (operation1
.fView
== operation2
.fView
) {
308 if (operation1
.fAlignmentInfo
.getShell() != operation2
.fAlignmentInfo
.getShell()) {
312 if (isViewLocationNear(getViewLocation(operation1
.fView
), getViewLocation(operation2
.fView
))) {
319 private static boolean isViewLocationNear(Point location1
, Point location2
) {
320 return Math
.abs(location1
.x
- location2
.x
) < NEAR_THRESHOLD
;
323 private static Point
getViewLocation(TmfView view
) {
324 return view
.getParentComposite().toDisplay(0, 0);
327 private void queueAlignment(TmfTimeViewAlignmentInfo timeViewAlignmentInfo
, boolean synchronous
) {
328 if (isAlignViewsPreferenceEnabled()) {
329 IWorkbenchWindow workbenchWindow
= getWorkbenchWindow(timeViewAlignmentInfo
.getShell());
330 if (workbenchWindow
== null || workbenchWindow
.getActivePage() == null) {
331 // Only time aligned views that are part of a workbench window are supported
335 // We need a view so that we can compute position right as we are
336 // about to realign the views. The view could have been resized,
338 TmfView view
= (TmfView
) getReferenceView(timeViewAlignmentInfo
, null);
340 // No valid view found for this alignment
344 AlignmentOperation operation
= new AlignmentOperation(view
, timeViewAlignmentInfo
);
346 performAlignment(operation
);
353 private static boolean isAlignViewsPreferenceEnabled() {
354 return InstanceScope
.INSTANCE
.getNode(Activator
.PLUGIN_ID
).getBoolean(ITmfUIPreferences
.PREF_ALIGN_VIEWS
, true);
358 * Get a view that corresponds to the alignment information. The view is
359 * meant to be used as a "reference" for other views to align on. Heuristics
360 * are applied to choose the best view. For example, the view has to be
361 * visible. It also will prioritize the view with lowest time axis offset
362 * because most of the interesting data should be in the time widget.
364 * @param alignmentInfo
365 * alignment information
366 * @param blackListedView
367 * an optional black listed view that will not be used as
368 * reference (useful for a view that just got created)
369 * @return the reference view
371 private static ITmfTimeAligned
getReferenceView(TmfTimeViewAlignmentInfo alignmentInfo
, TmfView blackListedView
) {
372 IWorkbenchWindow workbenchWindow
= getWorkbenchWindow(alignmentInfo
.getShell());
373 if (workbenchWindow
== null || workbenchWindow
.getActivePage() == null) {
374 // Only time aligned views that are part of a workbench window are supported
377 IWorkbenchPage page
= workbenchWindow
.getActivePage();
379 int lowestTimeAxisOffset
= Integer
.MAX_VALUE
;
380 ITmfTimeAligned referenceView
= null;
381 for (IViewReference ref
: page
.getViewReferences()) {
382 IViewPart view
= ref
.getView(false);
383 if (view
!= blackListedView
&& isTimeAlignedView(view
)) {
384 if (isCandidateForReferenceView((TmfView
) view
, alignmentInfo
, lowestTimeAxisOffset
)) {
385 referenceView
= (ITmfTimeAligned
) view
;
386 lowestTimeAxisOffset
= getClampedTimeAxisOffset(referenceView
.getTimeViewAlignmentInfo());
391 return referenceView
;
394 private static boolean isCandidateForReferenceView(TmfView tmfView
, TmfTimeViewAlignmentInfo alignmentInfo
, int lowestTimeAxisOffset
) {
395 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) tmfView
;
396 TmfTimeViewAlignmentInfo timeViewAlignmentInfo
= alignedView
.getTimeViewAlignmentInfo();
397 if (timeViewAlignmentInfo
== null) {
401 if (isDisposedView(tmfView
)) {
405 Composite parentComposite
= tmfView
.getParentComposite();
406 boolean isVisible
= parentComposite
!= null && parentComposite
.isVisible();
408 boolean isViewLocationNear
= isViewLocationNear(alignmentInfo
.getViewLocation(), getViewLocation(tmfView
));
409 boolean isLowestTimeAxisOffset
= getClampedTimeAxisOffset(timeViewAlignmentInfo
) < lowestTimeAxisOffset
;
410 if (isViewLocationNear
&& isLowestTimeAxisOffset
) {
411 int availableWidth
= alignedView
.getAvailableWidth(getClampedTimeAxisOffset(timeViewAlignmentInfo
));
412 availableWidth
= getClampedTimeAxisWidth(timeViewAlignmentInfo
, availableWidth
);
413 if (availableWidth
> 0) {
423 * Get the narrowest view that corresponds to the given alignment information.
425 private static TmfView
getNarrowestView(TmfTimeViewAlignmentInfo alignmentInfo
) {
426 IWorkbenchWindow workbenchWindow
= getWorkbenchWindow(alignmentInfo
.getShell());
427 if (workbenchWindow
== null || workbenchWindow
.getActivePage() == null) {
428 // Only time aligned views that are part of a workbench window are supported
431 IWorkbenchPage page
= workbenchWindow
.getActivePage();
433 int narrowestWidth
= Integer
.MAX_VALUE
;
434 TmfView narrowestView
= null;
435 for (IViewReference ref
: page
.getViewReferences()) {
436 IViewPart view
= ref
.getView(false);
437 if (isTimeAlignedView(view
)) {
438 TmfView tmfView
= (TmfView
) view
;
439 if (isCandidateForNarrowestView(tmfView
, alignmentInfo
, narrowestWidth
)) {
440 narrowestWidth
= ((ITmfTimeAligned
) tmfView
).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo
));
441 narrowestWidth
= getClampedTimeAxisWidth(alignmentInfo
, narrowestWidth
);
442 narrowestView
= tmfView
;
447 return narrowestView
;
450 private static int getClampedTimeAxisWidth(TmfTimeViewAlignmentInfo alignmentInfo
, int width
) {
451 int max
= getMaxInt(alignmentInfo
.getShell());
452 if (validateInt(width
, max
)) {
453 Activator
.getDefault().logError("Time-axis width out of range (" + width
+ ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
455 return Math
.min(max
, Math
.max(0, width
));
458 private static int getClampedTimeAxisOffset(TmfTimeViewAlignmentInfo alignmentInfo
) {
459 int timeAxisOffset
= alignmentInfo
.getTimeAxisOffset();
460 int max
= getMaxInt(alignmentInfo
.getShell());
461 if (validateInt(timeAxisOffset
, max
)) {
462 Activator
.getDefault().logError("Time-axis offset out of range (" + timeAxisOffset
+ ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
464 return Math
.min(max
, Math
.max(0, timeAxisOffset
));
467 private static boolean validateInt(int value
, int max
) {
468 return value
< 0 || value
> max
;
471 private static int getMaxInt(Shell shell
) {
472 // Consider an integer to be buggy if it's bigger than 10 times the
473 // width of *all* monitors combined.
474 final int DISPLAY_WIDTH_FACTOR
= 10;
475 return shell
.getDisplay().getBounds().width
* DISPLAY_WIDTH_FACTOR
;
478 private static boolean isCandidateForNarrowestView(TmfView tmfView
, TmfTimeViewAlignmentInfo alignmentInfo
, int narrowestWidth
) {
479 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) tmfView
;
480 TmfTimeViewAlignmentInfo timeViewAlignmentInfo
= alignedView
.getTimeViewAlignmentInfo();
481 if (timeViewAlignmentInfo
== null) {
485 if (isDisposedView(tmfView
)) {
489 Composite parentComposite
= tmfView
.getParentComposite();
490 boolean isVisible
= parentComposite
!= null && parentComposite
.isVisible();
492 if (isViewLocationNear(getViewLocation(tmfView
), alignmentInfo
.getViewLocation())) {
493 int availableWidth
= alignedView
.getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo
));
494 availableWidth
= getClampedTimeAxisWidth(alignmentInfo
, availableWidth
);
495 boolean isNarrower
= availableWidth
< narrowestWidth
&& availableWidth
> 0;
503 private static IWorkbenchWindow
getWorkbenchWindow(Shell shell
) {
504 for (IWorkbenchWindow window
: PlatformUI
.getWorkbench().getWorkbenchWindows()) {
505 if (window
.getShell().equals(shell
)) {