/*******************************************************************************
* Copyright 2011 Google Inc. All Rights Reserved.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.google.gwt.eclipse.oophm.views.hierarchical;
import com.google.gwt.eclipse.oophm.breadcrumbs.BreadcrumbViewer;
import com.google.gwt.eclipse.oophm.model.BreadcrumbContentProvider;
import com.google.gwt.eclipse.oophm.model.BrowserTab;
import com.google.gwt.eclipse.oophm.model.IModelNode;
import com.google.gwt.eclipse.oophm.model.IWebAppDebugModelListener;
import com.google.gwt.eclipse.oophm.model.LaunchConfiguration;
import com.google.gwt.eclipse.oophm.model.ModelLabelProvider;
import com.google.gwt.eclipse.oophm.model.Server;
import com.google.gwt.eclipse.oophm.model.WebAppDebugModel;
import com.google.gwt.eclipse.oophm.model.WebAppDebugModelEvent;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
/**
* The panel for the navigation setup using the breadcrumb. Note that this class
* registers a listener (itself) for model changes, as opposed to the
* BreadcrumbViewer's content provider registering a listener. This is due to
* the unconventional way in which the viewer's setInput method is used.
*/
public class BreadcrumbNavigationView extends SelectionProvidingComposite
implements IWebAppDebugModelListener {
private final BreadcrumbViewer breadcrumbViewer;
private final ContentPanel contentPanel;
private WebAppDebugModel model;
public BreadcrumbNavigationView(Composite parent, int style) {
super(parent, style);
addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
setInput(null);
}
});
GridLayout layout = new GridLayout();
layout.marginHeight = 0;
layout.marginWidth = 0;
layout.horizontalSpacing = 0;
layout.verticalSpacing = 0;
setLayout(layout);
setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
breadcrumbViewer = new BreadcrumbViewer(this, SWT.HORIZONTAL) {
@Override
protected void configureDropDownViewer(TreeViewer viewer, Object input) {
viewer.setContentProvider(new BreadcrumbContentProvider());
viewer.setLabelProvider(new ModelLabelProvider());
viewer.setComparator(new ModelNodeViewerComparator());
}
};
breadcrumbViewer.addOpenListener(new IOpenListener() {
public void open(OpenEvent event) {
setSelection(event.getSelection());
}
});
breadcrumbViewer.addSelectionChangedListener(new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
fireSelectionChangedEvent(event);
}
});
breadcrumbViewer.setLabelProvider(new ModelLabelProvider());
breadcrumbViewer.setContentProvider(new BreadcrumbContentProvider());
contentPanel = new ContentPanel(this, SWT.NONE);
}
public void browserTabCreated(final WebAppDebugModelEvent<BrowserTab> e) {
Display.getDefault().syncExec(new Runnable() {
public void run() {
setSelection(new StructuredSelection(e.getElement()));
breadcrumbViewer.refresh();
}
});
}
public void browserTabNeedsAttention(final WebAppDebugModelEvent<BrowserTab> e) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
/*
* If it is the case that the browser tab that needs attention is
* currently selected, then we need to reset the selection so that the
* text color update is picked up.
*
* We also need to handle the case where the parent of the tab that
* needs attention (i.e. the launch configuration) is selected, but the
* launch config itself is not. Since the launch config inherits its
* attention state from its children, its text color changes as well.
*/
if (!resetSelectionIfModelNodeIsEqualToCurrentSelection(e.getElement())) {
resetSelectionIfModelNodeIsChildOfCurrentSelection(e.getElement());
}
breadcrumbViewer.refresh();
}
});
}
public void browserTabRemoved(final WebAppDebugModelEvent<BrowserTab> e) {
Display.getDefault().syncExec(new Runnable() {
public void run() {
maybeClearSelectionAndSelectNextAvailable(e.getElement());
breadcrumbViewer.refresh();
}
});
}
public void browserTabTerminated(final WebAppDebugModelEvent<BrowserTab> e) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
resetSelectionIfModelNodeIsEqualToCurrentSelection(e.getElement());
breadcrumbViewer.refresh();
}
});
}
@Override
public ISelection getSelection() {
return breadcrumbViewer.getSelection();
}
public void launchConfigurationLaunched(
final WebAppDebugModelEvent<LaunchConfiguration> e) {
Display.getDefault().syncExec(new Runnable() {
public void run() {
setSelection(new StructuredSelection(e.getElement()));
breadcrumbViewer.refresh();
}
});
}
public void launchConfigurationLaunchUrlsChanged(WebAppDebugModelEvent<LaunchConfiguration> e) {
}
public void launchConfigurationRemoved(
final WebAppDebugModelEvent<LaunchConfiguration> e) {
Display.getDefault().syncExec(new Runnable() {
public void run() {
maybeClearSelectionAndSelectNextAvailable(e.getElement());
breadcrumbViewer.refresh();
}
});
}
public void launchConfigurationRestartWebServerStatusChanged(
WebAppDebugModelEvent<LaunchConfiguration> e) {
// Ignore
}
public void launchConfigurationTerminated(
final WebAppDebugModelEvent<LaunchConfiguration> e) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
resetSelectionIfModelNodeIsEqualToCurrentSelection(e.getElement());
breadcrumbViewer.refresh();
}
});
}
public void serverCreated(final WebAppDebugModelEvent<Server> e) {
Display.getDefault().syncExec(new Runnable() {
public void run() {
resetSelectionIfModelNodeIsChildOfCurrentSelection(e.getElement());
breadcrumbViewer.refresh();
}
});
}
public void serverNeedsAttention(final WebAppDebugModelEvent<Server> e) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
/*
* If it is the case that the server that needs attention is currently
* selected, then we need to reset the selection so that the text color
* update is picked up.
*
* We also need to handle the case where the parent of the server that
* needs attention (i.e. the launch configuration) is selected, but the
* launch config itself is not. Since the launch config inherits its
* attention state from its children, its text color changes as well.
*/
if (!resetSelectionIfModelNodeIsEqualToCurrentSelection(e.getElement())) {
resetSelectionIfModelNodeIsChildOfCurrentSelection(e.getElement());
}
breadcrumbViewer.refresh();
}
});
}
public void serverTerminated(final WebAppDebugModelEvent<Server> e) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
resetSelectionIfModelNodeIsEqualToCurrentSelection(e.getElement());
breadcrumbViewer.refresh();
}
});
}
public void setInput(WebAppDebugModel newModel) {
if (model != null) {
model.removeWebAppDebugModelListener(this);
}
this.model = newModel;
setSelection(new StructuredSelection(WebAppDebugModel.getInstance()));
if (model != null) {
model.addWebAppDebugModelListener(this);
}
}
@Override
public void setSelection(final ISelection selection) {
assert (selection != null && !selection.isEmpty());
final Object firstElement = ((IStructuredSelection) selection).getFirstElement();
/*
* breadcrumbViewer.setInput is called every time an item is selected.
* Because of this, it does not make sense for the content provider
* associated with the breadcrumbViewer to register a listener on the model
*/
breadcrumbViewer.setInput(firstElement);
breadcrumbViewer.setSelection(selection, true);
contentPanel.setSelection(firstElement);
contentPanel.setFocus();
}
private IModelNode getFirstElementInSelection() {
StructuredSelection currentSelection = (StructuredSelection) getSelection();
if (currentSelection == null || currentSelection.isEmpty()) {
return null;
}
return (IModelNode) currentSelection.getFirstElement();
}
/**
* Even though we don't have a lock on it, querying the model at this point is
* actually safe. We're looking for parent relationships, and parent
* relationships are set at construction time for each model object. So, it is
* not possible for a parent-child relationship to become invalid.
*
* The only possible issue is that a launch configuration could become
* disconnected from the model while this code is running. That in itself is
* not dangerous, because the possibleParent is a launch configuration.
*/
private boolean isParentOf(IModelNode node, IModelNode possibleParent) {
assert (!(possibleParent instanceof WebAppDebugModel));
if (possibleParent == null) {
return false;
}
while (node != null) {
if (node.getParent() == possibleParent) {
return true;
}
node = node.getParent();
}
return false;
}
private void maybeClearSelectionAndSelectNextAvailable(
BrowserTab removedBrowserTab) {
IModelNode currentSelection = getFirstElementInSelection();
if (currentSelection == null) {
return;
}
if (currentSelection == removedBrowserTab) {
/*
* Technically, a browser tab could have been added to the model (but not
* the view) at the point that we perform this query. In reality, this is
* very unlikely to happen. Even it it does, there will be no harm - this
* call will force a refresh of the data, and the new one that "sneaked"
* in will be selected. The notification event for the new launch will
* follow shortly afterwards.
*/
BrowserTab latestActiveBrowserTab = removedBrowserTab.getLaunchConfiguration().getLatestActiveBrowserTab();
if (latestActiveBrowserTab != null) {
setSelection(new StructuredSelection(latestActiveBrowserTab));
} else {
setSelection(new StructuredSelection(
removedBrowserTab.getLaunchConfiguration()));
}
}
}
private void maybeClearSelectionAndSelectNextAvailable(
LaunchConfiguration removedLaunchConfiguration) {
IModelNode currentSelection = getFirstElementInSelection();
if (currentSelection == null) {
return;
}
if (currentSelection == removedLaunchConfiguration
|| isParentOf(currentSelection, removedLaunchConfiguration)) {
/*
* Technically, a launch configuration could have been added to the model
* (but not the view) at the point that we perform this query. In reality,
* this is very unlikely to happen. Even it it does, there will be no harm
* - this call will force a refresh of the data, and the new one that
* "sneaked" in will be selected. The notification event for the new
* launch will follow shortly afterwards.
*/
LaunchConfiguration latestActiveLaunchConfiguration = WebAppDebugModel.getInstance().getLatestActiveLaunchConfiguration();
if (latestActiveLaunchConfiguration != null) {
setSelection(new StructuredSelection(latestActiveLaunchConfiguration));
} else {
setSelection(new StructuredSelection(WebAppDebugModel.getInstance()));
}
}
}
private boolean resetSelectionIfModelNodeIsChildOfCurrentSelection(
IModelNode modelNode) {
assert (modelNode instanceof BrowserTab || modelNode instanceof Server);
IModelNode selectionModelNode = getFirstElementInSelection();
if (isParentOf(modelNode, selectionModelNode)) {
/*
* We're basically resetting the current selection here. This has the
* effect of refreshing the top line of the breadcrumb view, thereby
* forcing an arrow next to the launch configuration to appear, indicating
* that a child of the launch configuration has just appeared.
*
* This hack is necessary because refreshing the breadcrumb viewer does
* not do this (due to the odd way in which BreadCrumb viewer is
* implemented; it is not a traditional JFace viewer, even though it looks
* like one from the outside).
*/
setSelection(getSelection());
return true;
}
return false;
}
private boolean resetSelectionIfModelNodeIsEqualToCurrentSelection(final IModelNode modelNode) {
if (getFirstElementInSelection() == modelNode) {
/*
* We're basically resetting the current selection here. This has the
* effect of refreshing the top line of the breadcrumb view, thereby
* forcing an arrow next to the launch configuration to appear, indicating
* that a child of the launch configuration has just appeared.
*
* This hack is necessary because refreshing the breadcrumb viewer does
* not do this (due to the odd way in which BreadCrumb viewer is
* implemented; it is not a traditional JFace viewer, even though it looks
* like one from the outside).
*/
setSelection(getSelection());
return true;
}
return false;
}
}