1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 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
.IEclipsePreferences
.IPreferenceChangeListener
;
22 import org
.eclipse
.core
.runtime
.preferences
.IEclipsePreferences
.PreferenceChangeEvent
;
23 import org
.eclipse
.core
.runtime
.preferences
.InstanceScope
;
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;
53 /** Singleton instance */
54 private static TmfAlignmentSynchronizer fInstance
= null;
56 private final Timer fTimer
;
57 private final List
<AlignmentOperation
> fPendingOperations
= Collections
.synchronizedList(new ArrayList
<AlignmentOperation
>());
59 private TimerTask fCurrentTask
;
64 private TmfAlignmentSynchronizer() {
65 TmfSignalManager
.register(this);
67 createPreferenceListener();
68 fCurrentTask
= new TimerTask() {
77 * Get the alignment synchronizer's instance
79 * @return The singleton instance
81 public static synchronized TmfAlignmentSynchronizer
getInstance() {
82 if (fInstance
== null) {
83 fInstance
= new TmfAlignmentSynchronizer();
89 * Disposes the alignment synchronizer
91 public void dispose() {
92 TmfSignalManager
.deregister(this);
93 synchronized (fPendingOperations
) {
95 fCurrentTask
.cancel();
99 private IPreferenceChangeListener
createPreferenceListener() {
100 IPreferenceChangeListener listener
= new IPreferenceChangeListener() {
103 public void preferenceChange(PreferenceChangeEvent event
) {
104 if (event
.getKey().equals(ITmfUIPreferences
.PREF_ALIGN_VIEWS
)) {
105 Object oldValue
= event
.getOldValue();
106 Object newValue
= event
.getNewValue();
107 if (Boolean
.toString(false).equals(oldValue
) && Boolean
.toString(true).equals(newValue
)) {
109 } else if (Boolean
.toString(true).equals(oldValue
) && Boolean
.toString(false).equals(newValue
)) {
115 InstanceScope
.INSTANCE
.getNode(Activator
.PLUGIN_ID
).addPreferenceChangeListener(listener
);
119 private class AlignmentOperation
{
121 final TmfTimeViewAlignmentInfo fAlignmentInfo
;
123 public AlignmentOperation(TmfView view
, TmfTimeViewAlignmentInfo timeViewAlignmentInfo
) {
125 fAlignmentInfo
= timeViewAlignmentInfo
;
129 private class AlignTask
extends TimerTask
{
133 final List
<AlignmentOperation
> fCopy
;
134 synchronized (fPendingOperations
) {
135 fCopy
= new ArrayList
<>(fPendingOperations
);
136 fPendingOperations
.clear();
138 Display
.getDefault().syncExec(new Runnable() {
141 performAllAlignments(fCopy
);
148 * Handle a view that was just resized.
151 * the view that was resized
153 public void handleViewResized(TmfView view
) {
154 TmfTimeViewAlignmentInfo alignmentInfo
= new TmfTimeViewAlignmentInfo(view
.getParentComposite().getShell(), getViewLocation(view
), 0);
156 // Don't use a view that was just resized as a reference view.
157 // Otherwise, a view that was just
158 // created might use itself as a reference but we want to
159 // keep the existing alignment from the other views.
160 ITmfTimeAligned referenceView
= getReferenceView(alignmentInfo
, view
);
161 if (referenceView
!= null) {
162 queueAlignment(referenceView
.getTimeViewAlignmentInfo(), false);
167 * Handle a view that was just closed.
170 * the view that was closed
172 public void handleViewClosed(TmfView view
) {
173 // Realign views so that they can use the maximum available width in the
174 // event that a narrow view was just closed
175 realignViews(view
.getSite().getPage());
179 * Process signal for alignment.
181 * @param signal the alignment signal
184 public void timeViewAlignmentUpdated(TmfTimeViewAlignmentSignal signal
) {
185 queueAlignment(signal
.getTimeViewAlignmentInfo(), signal
.IsSynchronous());
189 * Perform all alignment operations for the specified alignment
193 * - The alignment algorithm chooses the narrowest width to accommodate all views.
194 * - View positions are recomputed for extra accuracy since the views could have been moved or resized.
195 * - Based on the up-to-date view positions, only views that are near and aligned with each other
198 private static void performAllAlignments(final List
<AlignmentOperation
> alignments
) {
199 for (final AlignmentOperation info
: alignments
) {
200 performAlignment(info
);
204 private static void performAlignment(AlignmentOperation info
) {
206 TmfView referenceView
= info
.fView
;
207 if (isDisposedView(referenceView
)) {
211 TmfTimeViewAlignmentInfo alignmentInfo
= info
.fAlignmentInfo
;
212 // The location of the view might have changed (resize, etc). Update the alignment info.
213 alignmentInfo
= new TmfTimeViewAlignmentInfo(alignmentInfo
.getShell(), getViewLocation(referenceView
), getClampedTimeAxisOffset(alignmentInfo
));
215 TmfView narrowestView
= getNarrowestView(alignmentInfo
);
216 if (narrowestView
== null) {
217 // 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.
221 int narrowestWidth
= ((ITmfTimeAligned
) narrowestView
).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo
));
222 narrowestWidth
= getClampedTimeAxisWidth(alignmentInfo
, narrowestWidth
);
223 IViewReference
[] viewReferences
= referenceView
.getSite().getPage().getViewReferences();
224 for (IViewReference ref
: viewReferences
) {
225 IViewPart view
= ref
.getView(false);
226 if (isTimeAlignedView(view
)) {
227 TmfView tmfView
= (TmfView
) view
;
228 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) view
;
229 if (!isDisposedView(tmfView
) && isViewLocationNear(getViewLocation(tmfView
), alignmentInfo
.getViewLocation())) {
230 alignedView
.performAlign(getClampedTimeAxisOffset(alignmentInfo
), narrowestWidth
);
239 private void realignViews() {
240 for (IWorkbenchWindow window
: PlatformUI
.getWorkbench().getWorkbenchWindows()) {
241 for (IWorkbenchPage page
: window
.getPages()) {
248 * Realign views inside a given page
253 private void realignViews(IWorkbenchPage page
) {
254 IViewReference
[] viewReferences
= page
.getViewReferences();
255 for (IViewReference ref
: viewReferences
) {
256 IViewPart view
= ref
.getView(false);
257 if (isTimeAlignedView(view
)) {
258 queueAlignment(((ITmfTimeAligned
) view
).getTimeViewAlignmentInfo(), false);
264 * Restore the views to their respective maximum widths
266 private static void restoreViews() {
267 // We set the width to Integer.MAX_VALUE so that the
268 // views remove any "filler" space they might have.
269 for (IWorkbenchWindow window
: PlatformUI
.getWorkbench().getWorkbenchWindows()) {
270 for (IWorkbenchPage page
: window
.getPages()) {
271 for (IViewReference ref
: page
.getViewReferences()) {
278 private static void restoreView(IViewReference ref
) {
279 IViewPart view
= ref
.getView(false);
280 if (isTimeAlignedView(view
)) {
281 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) view
;
282 alignedView
.performAlign(getClampedTimeAxisOffset(alignedView
.getTimeViewAlignmentInfo()), Integer
.MAX_VALUE
);
286 private static boolean isTimeAlignedView(IViewPart view
) {
287 if (view
instanceof TmfView
&& view
instanceof ITmfTimeAligned
) {
288 Composite parentComposite
= ((TmfView
) view
).getParentComposite();
289 if (parentComposite
!= null && !parentComposite
.isDisposed()) {
293 return view
instanceof TmfView
&& view
instanceof ITmfTimeAligned
;
296 private static boolean isDisposedView(TmfView view
) {
297 Composite parentComposite
= (view
).getParentComposite();
298 return parentComposite
!= null && parentComposite
.isDisposed();
302 * Queue the operation for processing. If an operation is considered the
303 * same alignment (shell, location) as a previously queued one, it will
304 * replace the old one. This way, only one up-to-date alignment operation is
305 * kept per set of time-axis aligned views. The processing of the operation
306 * is also throttled (TimerTask).
309 * the operation to queue
311 private void queue(AlignmentOperation operation
) {
312 synchronized(fPendingOperations
) {
313 fCurrentTask
.cancel();
314 for (AlignmentOperation pendingOperation
: fPendingOperations
) {
315 if (isSameAlignment(operation
, pendingOperation
)) {
316 fPendingOperations
.remove(pendingOperation
);
320 fPendingOperations
.add(operation
);
321 fCurrentTask
= new AlignTask();
322 fTimer
.schedule(fCurrentTask
, THROTTLE_DELAY
);
327 * Two operations are considered to be for the same set of time-axis aligned
328 * views if they are on the same Shell and near the same location.
330 private static boolean isSameAlignment(AlignmentOperation operation1
, AlignmentOperation operation2
) {
331 if (operation1
.fView
== operation2
.fView
) {
335 if (operation1
.fAlignmentInfo
.getShell() != operation2
.fAlignmentInfo
.getShell()) {
339 if (isViewLocationNear(getViewLocation(operation1
.fView
), getViewLocation(operation2
.fView
))) {
346 private static boolean isViewLocationNear(Point location1
, Point location2
) {
347 return Math
.abs(location1
.x
- location2
.x
) < NEAR_THRESHOLD
;
350 private static Point
getViewLocation(TmfView view
) {
351 return view
.getParentComposite().toDisplay(0, 0);
354 private void queueAlignment(TmfTimeViewAlignmentInfo timeViewAlignmentInfo
, boolean synchronous
) {
355 if (isAlignViewsPreferenceEnabled()) {
356 IWorkbenchWindow workbenchWindow
= getWorkbenchWindow(timeViewAlignmentInfo
.getShell());
357 if (workbenchWindow
== null || workbenchWindow
.getActivePage() == null) {
358 // Only time aligned views that are part of a workbench window are supported
362 // We need a view so that we can compute position right as we are
363 // about to realign the views. The view could have been resized,
365 TmfView view
= (TmfView
) getReferenceView(timeViewAlignmentInfo
, null);
367 // No valid view found for this alignment
371 AlignmentOperation operation
= new AlignmentOperation(view
, timeViewAlignmentInfo
);
373 performAlignment(operation
);
380 private static boolean isAlignViewsPreferenceEnabled() {
381 return InstanceScope
.INSTANCE
.getNode(Activator
.PLUGIN_ID
).getBoolean(ITmfUIPreferences
.PREF_ALIGN_VIEWS
, true);
385 * Get a view that corresponds to the alignment information. The view is
386 * meant to be used as a "reference" for other views to align on. Heuristics
387 * are applied to choose the best view. For example, the view has to be
388 * visible. It also will prioritize the view with lowest time axis offset
389 * because most of the interesting data should be in the time widget.
391 * @param alignmentInfo
392 * alignment information
393 * @param blackListedView
394 * an optional black listed view that will not be used as
395 * reference (useful for a view that just got created)
396 * @return the reference view
398 private static ITmfTimeAligned
getReferenceView(TmfTimeViewAlignmentInfo alignmentInfo
, TmfView blackListedView
) {
399 IWorkbenchWindow workbenchWindow
= getWorkbenchWindow(alignmentInfo
.getShell());
400 if (workbenchWindow
== null || workbenchWindow
.getActivePage() == null) {
401 // Only time aligned views that are part of a workbench window are supported
404 IWorkbenchPage page
= workbenchWindow
.getActivePage();
406 int lowestTimeAxisOffset
= Integer
.MAX_VALUE
;
407 ITmfTimeAligned referenceView
= null;
408 for (IViewReference ref
: page
.getViewReferences()) {
409 IViewPart view
= ref
.getView(false);
410 if (view
!= blackListedView
&& isTimeAlignedView(view
)) {
411 if (isCandidateForReferenceView((TmfView
) view
, alignmentInfo
, lowestTimeAxisOffset
)) {
412 referenceView
= (ITmfTimeAligned
) view
;
413 lowestTimeAxisOffset
= getClampedTimeAxisOffset(referenceView
.getTimeViewAlignmentInfo());
417 return referenceView
;
420 private static boolean isCandidateForReferenceView(TmfView tmfView
, TmfTimeViewAlignmentInfo alignmentInfo
, int lowestTimeAxisOffset
) {
421 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) tmfView
;
422 TmfTimeViewAlignmentInfo timeViewAlignmentInfo
= alignedView
.getTimeViewAlignmentInfo();
423 if (timeViewAlignmentInfo
== null) {
427 if (isDisposedView(tmfView
)) {
431 Composite parentComposite
= tmfView
.getParentComposite();
432 boolean isVisible
= parentComposite
!= null && parentComposite
.isVisible();
434 boolean isViewLocationNear
= isViewLocationNear(alignmentInfo
.getViewLocation(), getViewLocation(tmfView
));
435 boolean isLowestTimeAxisOffset
= getClampedTimeAxisOffset(timeViewAlignmentInfo
) < lowestTimeAxisOffset
;
436 if (isViewLocationNear
&& isLowestTimeAxisOffset
) {
437 int availableWidth
= alignedView
.getAvailableWidth(getClampedTimeAxisOffset(timeViewAlignmentInfo
));
438 availableWidth
= getClampedTimeAxisWidth(timeViewAlignmentInfo
, availableWidth
);
439 if (availableWidth
> 0) {
449 * Get the narrowest view that corresponds to the given alignment information.
451 private static TmfView
getNarrowestView(TmfTimeViewAlignmentInfo alignmentInfo
) {
452 IWorkbenchWindow workbenchWindow
= getWorkbenchWindow(alignmentInfo
.getShell());
453 if (workbenchWindow
== null || workbenchWindow
.getActivePage() == null) {
454 // Only time aligned views that are part of a workbench window are supported
457 IWorkbenchPage page
= workbenchWindow
.getActivePage();
459 int narrowestWidth
= Integer
.MAX_VALUE
;
460 TmfView narrowestView
= null;
461 for (IViewReference ref
: page
.getViewReferences()) {
462 IViewPart view
= ref
.getView(false);
463 if (isTimeAlignedView(view
)) {
464 TmfView tmfView
= (TmfView
) view
;
465 if (isCandidateForNarrowestView(tmfView
, alignmentInfo
, narrowestWidth
)) {
466 narrowestWidth
= ((ITmfTimeAligned
) tmfView
).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo
));
467 narrowestWidth
= getClampedTimeAxisWidth(alignmentInfo
, narrowestWidth
);
468 narrowestView
= tmfView
;
473 return narrowestView
;
476 private static int getClampedTimeAxisWidth(TmfTimeViewAlignmentInfo alignmentInfo
, int width
) {
477 int max
= getMaxInt(alignmentInfo
.getShell());
478 if (validateInt(width
, max
)) {
479 Activator
.getDefault().logError("Time-axis width out of range (" + width
+ ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
481 return Math
.min(max
, Math
.max(0, width
));
484 private static int getClampedTimeAxisOffset(TmfTimeViewAlignmentInfo alignmentInfo
) {
485 int timeAxisOffset
= alignmentInfo
.getTimeAxisOffset();
486 int max
= getMaxInt(alignmentInfo
.getShell());
487 if (validateInt(timeAxisOffset
, max
)) {
488 Activator
.getDefault().logError("Time-axis offset out of range (" + timeAxisOffset
+ ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
490 return Math
.min(max
, Math
.max(0, timeAxisOffset
));
493 private static boolean validateInt(int value
, int max
) {
494 return value
< 0 || value
> max
;
497 private static int getMaxInt(Shell shell
) {
498 // Consider an integer to be buggy if it's bigger than 10 times the
499 // width of *all* monitors combined.
500 final int DISPLAY_WIDTH_FACTOR
= 10;
501 return shell
.getDisplay().getBounds().width
* DISPLAY_WIDTH_FACTOR
;
504 private static boolean isCandidateForNarrowestView(TmfView tmfView
, TmfTimeViewAlignmentInfo alignmentInfo
, int narrowestWidth
) {
505 ITmfTimeAligned alignedView
= (ITmfTimeAligned
) tmfView
;
506 TmfTimeViewAlignmentInfo timeViewAlignmentInfo
= alignedView
.getTimeViewAlignmentInfo();
507 if (timeViewAlignmentInfo
== null) {
511 if (isDisposedView(tmfView
)) {
515 Composite parentComposite
= tmfView
.getParentComposite();
516 boolean isVisible
= parentComposite
!= null && parentComposite
.isVisible();
518 if (isViewLocationNear(getViewLocation(tmfView
), alignmentInfo
.getViewLocation())) {
519 int availableWidth
= alignedView
.getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo
));
520 availableWidth
= getClampedTimeAxisWidth(alignmentInfo
, availableWidth
);
521 boolean isNarrower
= availableWidth
< narrowestWidth
&& availableWidth
> 0;
529 private static IWorkbenchWindow
getWorkbenchWindow(Shell shell
) {
530 for (IWorkbenchWindow window
: PlatformUI
.getWorkbench().getWorkbenchWindows()) {
531 if (window
.getShell().equals(shell
)) {