blob: 8257d870d2f0fc69ddd146d2c615e6daf2287628 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2019 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Marc-Andre Laperle - Initial API and implementation
*******************************************************************************/
package org.eclipse.tracecompass.internal.tmf.ui.views;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.internal.tmf.ui.ITmfUIPreferences;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentInfo;
import org.eclipse.tracecompass.tmf.ui.signal.TmfTimeViewAlignmentSignal;
import org.eclipse.tracecompass.tmf.ui.views.ITmfTimeAligned;
import org.eclipse.tracecompass.tmf.ui.views.TmfView;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import com.google.common.annotations.VisibleForTesting;
/**
* Receives various notifications for realignment and
* performs the alignment on the appropriate views.
*
* @since 1.0
*/
public class TmfAlignmentSynchronizer {
private static final long THROTTLE_DELAY = 500;
private static final int NEAR_THRESHOLD = 10;
/** Singleton instance */
private static TmfAlignmentSynchronizer fInstance = null;
private final Timer fTimer;
private final List<AlignmentOperation> fPendingOperations = Collections.synchronizedList(new ArrayList<AlignmentOperation>());
private TimerTask fCurrentTask;
/**
* Constructor
*/
private TmfAlignmentSynchronizer() {
TmfSignalManager.register(this);
fTimer = new Timer();
createPreferenceListener();
fCurrentTask = new TimerTask() {
@Override
public void run() {
/* Do nothing */
}
};
}
/**
* Get the alignment synchronizer's instance
*
* @return The singleton instance
*/
public static synchronized TmfAlignmentSynchronizer getInstance() {
if (fInstance == null) {
fInstance = new TmfAlignmentSynchronizer();
}
return fInstance;
}
/**
* Disposes the alignment synchronizer
*/
public void dispose() {
TmfSignalManager.deregister(this);
synchronized (fPendingOperations) {
fTimer.cancel();
fCurrentTask.cancel();
}
}
private IPreferenceChangeListener createPreferenceListener() {
IPreferenceChangeListener listener = event -> {
if (event.getKey().equals(ITmfUIPreferences.PREF_ALIGN_VIEWS)) {
Object oldValue = event.getOldValue();
Object newValue = event.getNewValue();
if (Boolean.toString(false).equals(oldValue) && Boolean.toString(true).equals(newValue)) {
realignViews();
} else if (Boolean.toString(true).equals(oldValue) && Boolean.toString(false).equals(newValue)) {
restoreViews();
}
}
};
InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID).addPreferenceChangeListener(listener);
return listener;
}
private class AlignmentOperation {
final TmfView fView;
final TmfTimeViewAlignmentInfo fAlignmentInfo;
public AlignmentOperation(TmfView view, TmfTimeViewAlignmentInfo timeViewAlignmentInfo) {
fView = view;
fAlignmentInfo = timeViewAlignmentInfo;
}
}
private class AlignTask extends TimerTask {
private boolean fComplete = false;
@Override
public void run() {
final List<AlignmentOperation> fCopy;
synchronized (fPendingOperations) {
fCopy = new ArrayList<>(fPendingOperations);
fPendingOperations.clear();
}
Display.getDefault().syncExec(() -> performAllAlignments(fCopy));
fComplete = true;
}
}
/**
* Handle a view that was just resized.
*
* @param view
* the view that was resized
*/
public void handleViewResized(TmfView view) {
if (view.getParentComposite().isDisposed()) {
return;
}
TmfTimeViewAlignmentInfo alignmentInfo = new TmfTimeViewAlignmentInfo(view.getParentComposite().getShell(), getViewLocation(view), 0);
// Don't use a view that was just resized as a reference view.
// Otherwise, a view that was just
// created might use itself as a reference but we want to
// keep the existing alignment from the other views.
ITmfTimeAligned referenceView = getReferenceView(alignmentInfo, view);
if (referenceView != null) {
queueAlignment(referenceView.getTimeViewAlignmentInfo(), false);
}
}
/**
* Handle a view that was just closed.
*
* @param view
* the view that was closed
*/
public void handleViewClosed(TmfView view) {
// Realign views so that they can use the maximum available width in the
// event that a narrow view was just closed
realignViews(view.getSite().getPage());
}
/**
* Process signal for alignment.
*
* @param signal the alignment signal
*/
@TmfSignalHandler
public void timeViewAlignmentUpdated(TmfTimeViewAlignmentSignal signal) {
queueAlignment(signal.getTimeViewAlignmentInfo(), signal.IsSynchronous());
}
/**
* Returns true if an alignment operation is pending or running, false
* otherwise.
*
* @return true if an alignment operation is pending or running, false
* otherwise
*/
@VisibleForTesting
public boolean isBusy() {
synchronized (fPendingOperations) {
TimerTask currentTask = fCurrentTask;
return !fPendingOperations.isEmpty() || (currentTask instanceof AlignTask &&
!((AlignTask) currentTask).fComplete);
}
}
/**
* Perform all alignment operations for the specified alignment
* informations.
*
* <pre>
* - The alignment algorithm chooses the narrowest width to accommodate all views.
* - View positions are recomputed for extra accuracy since the views could have been moved or resized.
* - Based on the up-to-date view positions, only views that are near and aligned with each other
* </pre>
*/
private static void performAllAlignments(final List<AlignmentOperation> alignments) {
for (final AlignmentOperation info : alignments) {
performAlignment(info);
}
}
private static void performAlignment(AlignmentOperation info) {
TmfView referenceView = info.fView;
if (isDisposedView(referenceView)) {
return;
}
TmfTimeViewAlignmentInfo alignmentInfo = info.fAlignmentInfo;
// The location of the view might have changed (resize, etc). Update the alignment info.
alignmentInfo = new TmfTimeViewAlignmentInfo(alignmentInfo.getShell(), getViewLocation(referenceView), getClampedTimeAxisOffset(alignmentInfo));
TmfView narrowestView = getNarrowestView(alignmentInfo);
if (narrowestView == null) {
// 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.
return;
}
int narrowestWidth = ((ITmfTimeAligned) narrowestView).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo));
narrowestWidth = getClampedTimeAxisWidth(alignmentInfo, narrowestWidth);
IViewReference[] viewReferences = referenceView.getSite().getPage().getViewReferences();
for (IViewReference ref : viewReferences) {
IViewPart view = ref.getView(false);
if (isTimeAlignedView(view)) {
TmfView tmfView = (TmfView) view;
ITmfTimeAligned alignedView = (ITmfTimeAligned) view;
if (!isDisposedView(tmfView) && isViewLocationNear(getViewLocation(tmfView), alignmentInfo.getViewLocation())) {
alignedView.performAlign(getClampedTimeAxisOffset(alignmentInfo), narrowestWidth);
}
}
}
}
/**
* Realign all views
*/
private void realignViews() {
for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
for (IWorkbenchPage page : window.getPages()) {
realignViews(page);
}
}
}
/**
* Realign views inside a given page
*
* @param page
* the workbench page
*/
private void realignViews(IWorkbenchPage page) {
IViewReference[] viewReferences = page.getViewReferences();
for (IViewReference ref : viewReferences) {
IViewPart view = ref.getView(false);
if (isTimeAlignedView(view)) {
queueAlignment(((ITmfTimeAligned) view).getTimeViewAlignmentInfo(), false);
}
}
}
/**
* Restore the views to their respective maximum widths
*/
private static void restoreViews() {
// We set the width to Integer.MAX_VALUE so that the
// views remove any "filler" space they might have.
for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
for (IWorkbenchPage page : window.getPages()) {
for (IViewReference ref : page.getViewReferences()) {
restoreView(ref);
}
}
}
}
private static void restoreView(IViewReference ref) {
IViewPart view = ref.getView(false);
if (isTimeAlignedView(view)) {
ITmfTimeAligned alignedView = (ITmfTimeAligned) view;
alignedView.performAlign(getClampedTimeAxisOffset(alignedView.getTimeViewAlignmentInfo()), Integer.MAX_VALUE);
}
}
private static boolean isTimeAlignedView(IViewPart view) {
if (view instanceof TmfView && view instanceof ITmfTimeAligned) {
Composite parentComposite = ((TmfView) view).getParentComposite();
if (parentComposite != null && !parentComposite.isDisposed()) {
return true;
}
}
return view instanceof TmfView && view instanceof ITmfTimeAligned;
}
private static boolean isDisposedView(TmfView view) {
Composite parentComposite = (view).getParentComposite();
return parentComposite != null && parentComposite.isDisposed();
}
/**
* Queue the operation for processing. If an operation is considered the
* same alignment (shell, location) as a previously queued one, it will
* replace the old one. This way, only one up-to-date alignment operation is
* kept per set of time-axis aligned views. The processing of the operation
* is also throttled (TimerTask).
*
* @param operation
* the operation to queue
*/
private void queue(AlignmentOperation operation) {
synchronized(fPendingOperations) {
fCurrentTask.cancel();
for (AlignmentOperation pendingOperation : fPendingOperations) {
if (!isDisposedView(pendingOperation.fView) && isSameAlignment(operation, pendingOperation)) {
fPendingOperations.remove(pendingOperation);
break;
}
}
fPendingOperations.add(operation);
fCurrentTask = new AlignTask();
fTimer.schedule(fCurrentTask, THROTTLE_DELAY);
}
}
/**
* Two operations are considered to be for the same set of time-axis aligned
* views if they are on the same Shell and near the same location.
*/
private static boolean isSameAlignment(AlignmentOperation operation1, AlignmentOperation operation2) {
if (operation1.fView == operation2.fView) {
return true;
}
if (operation1.fAlignmentInfo.getShell() != operation2.fAlignmentInfo.getShell()) {
return false;
}
return (isViewLocationNear(getViewLocation(operation1.fView), getViewLocation(operation2.fView)));
}
private static boolean isViewLocationNear(Point location1, Point location2) {
return Math.abs(location1.x - location2.x) < NEAR_THRESHOLD;
}
private static Point getViewLocation(TmfView view) {
return view.getParentComposite().toDisplay(0, 0);
}
private void queueAlignment(TmfTimeViewAlignmentInfo timeViewAlignmentInfo, boolean synchronous) {
if (isAlignViewsPreferenceEnabled()) {
IWorkbenchWindow workbenchWindow = getWorkbenchWindow(timeViewAlignmentInfo.getShell());
if (workbenchWindow == null || workbenchWindow.getActivePage() == null) {
// Only time aligned views that are part of a workbench window are supported
return;
}
// We need a view so that we can compute position right as we are
// about to realign the views. The view could have been resized,
// moved, etc.
TmfView view = (TmfView) getReferenceView(timeViewAlignmentInfo, null);
if (view == null) {
// No valid view found for this alignment
return;
}
AlignmentOperation operation = new AlignmentOperation(view, timeViewAlignmentInfo);
if (synchronous) {
performAlignment(operation);
} else {
queue(operation);
}
}
}
private static boolean isAlignViewsPreferenceEnabled() {
return InstanceScope.INSTANCE.getNode(Activator.PLUGIN_ID).getBoolean(ITmfUIPreferences.PREF_ALIGN_VIEWS, true);
}
/**
* Get a view that corresponds to the alignment information. The view is
* meant to be used as a "reference" for other views to align on. Heuristics
* are applied to choose the best view. For example, the view has to be
* visible. It also will prioritize the view with lowest time axis offset
* because most of the interesting data should be in the time widget.
*
* @param alignmentInfo
* alignment information
* @param blackListedView
* an optional black listed view that will not be used as
* reference (useful for a view that just got created)
* @return the reference view
*/
private static ITmfTimeAligned getReferenceView(TmfTimeViewAlignmentInfo alignmentInfo, TmfView blackListedView) {
IWorkbenchWindow workbenchWindow = getWorkbenchWindow(alignmentInfo.getShell());
if (workbenchWindow == null || workbenchWindow.getActivePage() == null) {
// Only time aligned views that are part of a workbench window are supported
return null;
}
IWorkbenchPage page = workbenchWindow.getActivePage();
int lowestTimeAxisOffset = Integer.MAX_VALUE;
ITmfTimeAligned referenceView = null;
for (IViewReference ref : page.getViewReferences()) {
IViewPart view = ref.getView(false);
if (view != blackListedView && isTimeAlignedView(view)) {
if (isCandidateForReferenceView((TmfView) view, alignmentInfo, lowestTimeAxisOffset)) {
referenceView = (ITmfTimeAligned) view;
lowestTimeAxisOffset = getClampedTimeAxisOffset(referenceView.getTimeViewAlignmentInfo());
}
}
}
return referenceView;
}
private static boolean isCandidateForReferenceView(TmfView tmfView, TmfTimeViewAlignmentInfo alignmentInfo, int lowestTimeAxisOffset) {
ITmfTimeAligned alignedView = (ITmfTimeAligned) tmfView;
TmfTimeViewAlignmentInfo timeViewAlignmentInfo = alignedView.getTimeViewAlignmentInfo();
if (timeViewAlignmentInfo == null) {
return false;
}
if (isDisposedView(tmfView)) {
return false;
}
Composite parentComposite = tmfView.getParentComposite();
boolean isVisible = parentComposite != null && parentComposite.isVisible();
if (isVisible) {
boolean isViewLocationNear = isViewLocationNear(alignmentInfo.getViewLocation(), getViewLocation(tmfView));
boolean isLowestTimeAxisOffset = getClampedTimeAxisOffset(timeViewAlignmentInfo) < lowestTimeAxisOffset;
if (isViewLocationNear && isLowestTimeAxisOffset) {
int availableWidth = alignedView.getAvailableWidth(getClampedTimeAxisOffset(timeViewAlignmentInfo));
availableWidth = getClampedTimeAxisWidth(timeViewAlignmentInfo, availableWidth);
if (availableWidth > 0) {
return true;
}
}
}
return false;
}
/**
* Get the narrowest view that corresponds to the given alignment information.
*/
private static TmfView getNarrowestView(TmfTimeViewAlignmentInfo alignmentInfo) {
IWorkbenchWindow workbenchWindow = getWorkbenchWindow(alignmentInfo.getShell());
if (workbenchWindow == null || workbenchWindow.getActivePage() == null) {
// Only time aligned views that are part of a workbench window are supported
return null;
}
IWorkbenchPage page = workbenchWindow.getActivePage();
int narrowestWidth = Integer.MAX_VALUE;
TmfView narrowestView = null;
for (IViewReference ref : page.getViewReferences()) {
IViewPart view = ref.getView(false);
if (isTimeAlignedView(view)) {
TmfView tmfView = (TmfView) view;
if (isCandidateForNarrowestView(tmfView, alignmentInfo, narrowestWidth)) {
narrowestWidth = ((ITmfTimeAligned) tmfView).getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo));
narrowestWidth = getClampedTimeAxisWidth(alignmentInfo, narrowestWidth);
narrowestView = tmfView;
}
}
}
return narrowestView;
}
private static int getClampedTimeAxisWidth(TmfTimeViewAlignmentInfo alignmentInfo, int width) {
int max = getMaxInt(alignmentInfo.getShell());
if (validateInt(width, max)) {
Activator.getDefault().logError("Time-axis width out of range (" + width + ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
}
return Math.min(max, Math.max(0, width));
}
private static int getClampedTimeAxisOffset(TmfTimeViewAlignmentInfo alignmentInfo) {
int timeAxisOffset = alignmentInfo.getTimeAxisOffset();
int max = getMaxInt(alignmentInfo.getShell());
if (validateInt(timeAxisOffset, max)) {
Activator.getDefault().logError("Time-axis offset out of range (" + timeAxisOffset + ")", new Throwable()); //$NON-NLS-1$//$NON-NLS-2$
}
return Math.min(max, Math.max(0, timeAxisOffset));
}
private static boolean validateInt(int value, int max) {
return value < 0 || value > max;
}
private static int getMaxInt(Shell shell) {
// Consider an integer to be buggy if it's bigger than 10 times the
// width of *all* monitors combined.
final int DISPLAY_WIDTH_FACTOR = 10;
return shell.getDisplay().getBounds().width * DISPLAY_WIDTH_FACTOR;
}
private static boolean isCandidateForNarrowestView(TmfView tmfView, TmfTimeViewAlignmentInfo alignmentInfo, int narrowestWidth) {
ITmfTimeAligned alignedView = (ITmfTimeAligned) tmfView;
TmfTimeViewAlignmentInfo timeViewAlignmentInfo = alignedView.getTimeViewAlignmentInfo();
if (timeViewAlignmentInfo == null) {
return false;
}
if (isDisposedView(tmfView)) {
return false;
}
Composite parentComposite = tmfView.getParentComposite();
boolean isVisible = parentComposite != null && parentComposite.isVisible();
if (isVisible) {
if (isViewLocationNear(getViewLocation(tmfView), alignmentInfo.getViewLocation())) {
int availableWidth = alignedView.getAvailableWidth(getClampedTimeAxisOffset(alignmentInfo));
availableWidth = getClampedTimeAxisWidth(alignmentInfo, availableWidth);
boolean isNarrower = availableWidth < narrowestWidth && availableWidth > 0;
return isNarrower;
}
}
return false;
}
private static IWorkbenchWindow getWorkbenchWindow(Shell shell) {
for (IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) {
if (window.getShell() != null && window.getShell().equals(shell)) {
return window;
}
}
return null;
}
}