/*******************************************************************************
* Copyright (c) 2014 Bruno Medeiros and other Contributors.
* 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
*
* Contributors:
* Bruno Medeiros - initial API and implementation
*******************************************************************************/
package melnorme.lang.ide.ui.navigator;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertFail;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.navigator.ICommonContentExtensionSite;
import org.eclipse.ui.navigator.ICommonContentProvider;
import melnorme.lang.ide.core.EclipseCore;
import melnorme.lang.ide.core.LangCore;
import melnorme.lang.ide.core.LangCore_Actual;
import melnorme.lang.ide.core.operations.build.ProjectBuildInfo;
import melnorme.lang.ide.core.operations.build.BuildManager.BuildModel;
import melnorme.lang.ide.core.project_model.IProjectModelListener;
import melnorme.lang.ide.core.project_model.LangBundleModel;
import melnorme.lang.ide.core.project_model.UpdateEvent;
import melnorme.lang.ide.core.project_model.view.IBundleModelElement;
import melnorme.lang.ide.core.utils.EclipseUtils;
import melnorme.lang.ide.ui.navigator.NavigatorElementsSwitcher;
import melnorme.lang.tooling.bundle.AbstractBundleInfo;
import melnorme.util.swt.SWTUtil;
import melnorme.util.swt.jface.AbstractTreeContentProvider;
import melnorme.utilbox.collections.ArrayList2;
import melnorme.utilbox.collections.Indexable;
import melnorme.utilbox.misc.CollectionUtil;
import melnorme.utilbox.ownership.IDisposable;
public abstract class AbstractNavigatorContentProvider extends AbstractTreeContentProvider
implements ICommonContentProvider {
public AbstractNavigatorContentProvider() {
super();
}
@Override
public void saveState(IMemento aMemento) {
}
@Override
public void restoreState(IMemento aMemento) {
}
@Override
public void init(ICommonContentExtensionSite aConfig) {
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
assertTrue(viewer instanceof StructuredViewer);
super.inputChanged(viewer, oldInput, newInput);
}
protected StructuredViewer getViewer() {
return (StructuredViewer) viewer;
}
protected BuildModel getBuildModel() {
return LangCore.getBuildManager().getBuildModel();
}
public LangBundleModel getBundleModel() {
return LangCore_Actual.getBundleModel();
}
@Override
protected void viewerInitialized() {
super.viewerInitialized();
getBuildModel().addListener(buildModelListener);
getBundleModel().addListener(bundleModelListener);
}
@Override
public void dispose() {
getBundleModel().removeListener(bundleModelListener);
getBuildModel().removeListener(buildModelListener);
super.dispose();
}
protected final NavigatorBundleModelListener bundleModelListener = new NavigatorBundleModelListener();
/* ----------------- ----------------- */
@Override
public boolean hasChildren(Object element) {
return hasChildren_switcher().switchElement(element);
}
protected abstract LangNavigatorSwitcher_HasChildren hasChildren_switcher();
protected static interface LangNavigatorSwitcher_HasChildren extends NavigatorElementsSwitcher<Boolean> {
@Override
default Boolean visitProject(IProject project) {
return project.isAccessible();
}
@Override
default Boolean visitBundleElement(IBundleModelElement bundleElement) {
return bundleElement.hasChildren();
}
@Override
default Boolean visitBuildTargetsElement(BuildTargetsContainer buildTargetsElement) {
return true;
}
@Override
default Boolean visitBuildTarget(BuildTargetElement buildTarget) {
return false;
}
@Override
default Boolean visitManifestFile(IFile element) {
return false;
}
@Override
default Boolean visitOther2(Object element) {
return false;
}
}
@Override
public Object[] getChildren(Object parent) {
return getChildren_switcher().switchElement(parent);
}
protected abstract LangNavigatorSwitcher_GetChildren getChildren_switcher();
public abstract class LangNavigatorSwitcher_GetChildren implements NavigatorElementsSwitcher<Object[]> {
@Override
public Object[] visitProject(IProject project) {
return getProjectChildren(project);
}
@Override
public Object[] visitBundleElement(IBundleModelElement bundleElement) {
return bundleElement.getChildren();
}
@Override
public Object[] visitBuildTargetsElement(BuildTargetsContainer buildTargetsElement) {
return buildTargetsElement.getChildren_toArray();
}
@Override
public Object[] visitBuildTarget(BuildTargetElement buildTarget) {
return null;
}
@Override
public Object[] visitManifestFile(IFile element) {
return null;
}
public Object[] getProjectChildren(IProject project) {
ArrayList2<Object> projectChildren = new ArrayList2<>();
if(project.isAccessible()) {
addFirstProjectChildren(project, projectChildren);
addBuildTargetsContainer(project, projectChildren);
addProjectResourceChildren(project, projectChildren);
}
return projectChildren.toArray();
}
protected void addProjectResourceChildren(IProject project, ArrayList2<Object> projectChildren) {
// Add project children ourselves: this is so that children will be sorted by our own sorter.
// (otherwise only Platform Navigator sorter will be used)
// Navigator ResourceExtension will also add this, but they will not appear duplicated because they
// are equal elements.
try {
projectChildren.addAll(CollectionUtil.createArrayList(project.members()));
} catch (CoreException e) {
// ignore, leave empty
}
}
@SuppressWarnings("unused")
public void addFirstProjectChildren(IProject project, ArrayList2<Object> projectChildren) {
}
}
protected void addBuildTargetsContainer(IProject project, ArrayList2<Object> projectChildren) {
ProjectBuildInfo targets = LangCore.getBuildManager().getBuildInfo(project);
if(targets != null) {
projectChildren.add(new BuildTargetsContainer(targets));
}
}
@Override
public Object getParent(Object element) {
return getParent_switcher().switchElement(element);
}
protected abstract LangNavigatorSwitcher_GetParent getParent_switcher();
public static interface LangNavigatorSwitcher_GetParent extends NavigatorElementsSwitcher<Object> {
@Override
default Object visitResource(IResource resource) {
return resource.getParent();
}
@Override
default Object visitBundleElement(IBundleModelElement dubElement) {
return dubElement.getParent();
}
@Override
default Object visitBuildTargetsElement(BuildTargetsContainer buildTargetsElement) {
return buildTargetsElement.buildInfo.getProject();
}
@Override
default Object visitBuildTarget(BuildTargetElement buildTarget) {
return buildTarget.getParent();
}
@Override
default Object visitManifestFile(IFile element) {
throw assertFail();
}
}
/* ----------------- ----------------- */
protected final BuildModelListener buildModelListener = new BuildModelListener();
protected class BuildModelListener implements IProjectModelListener<ProjectBuildInfo> {
@Override
public void notifyUpdateEvent(UpdateEvent<ProjectBuildInfo> updateEvent) {
Display.getDefault().asyncExec(
() -> getViewer().refresh(updateEvent.project));
}
}
/* ----------------- ----------------- */
// useful mostly to workaround bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=430005
/**
* Helper to throttle some code, that is, to prevent some recorring code to run too soon after each other
* within a given a time interval.
*/
public abstract class ThrottleCodeJob extends Job {
protected final int throttleDelayMillis;
protected long lastRequestMillis;
protected boolean isScheduled = false;
public ThrottleCodeJob(int throttleDelayMillis) {
super("throttle job");
this.throttleDelayMillis = throttleDelayMillis;
setSystem(true);
}
/** Schedule this job to run again. Will run throttled code immediatly if past time delay,
* schedule otherwise.
* Multiple schedule requests within the delay period will be squashed into just one request.
*/
public void scheduleRefreshJob() {
synchronized (this) {
if(isScheduled) {
return;
}
assertTrue(getState() == Job.NONE || getState() == Job.RUNNING);
isScheduled = true;
long runningTimeMillis = getRunningTimeMillis();
long nextPeriod = lastRequestMillis + throttleDelayMillis;
long deltaToNext = nextPeriod - runningTimeMillis;
if(deltaToNext > 0) {
//System.out.println(" schedule delta to next:" + deltaToNext);
schedule(deltaToNext);
return;
} else {
// continue and run immediately
}
}
runThrottledCode();
}
protected long getRunningTimeMillis() {
return System.nanoTime() / 1000_000;
}
@Override
protected final IStatus run(IProgressMonitor monitor) {
//System.out.println(getRunningTimeMillis() + " :job#run");
runThrottledCode();
return EclipseCore.createOkStatus("ok");
}
public void markRequestFinished() {
synchronized (this) {
isScheduled = false;
lastRequestMillis = getRunningTimeMillis();
}
//System.out.println(lastRequestMillis + " lastRequestFinished");
}
protected abstract void runThrottledCode();
}
public class NavigatorBundleModelListener implements IDisposable, IProjectModelListener<AbstractBundleInfo> {
@Override
public void notifyUpdateEvent(UpdateEvent<AbstractBundleInfo> updateEvent) {
viewerRefreshThrottleJob.scheduleRefreshJob();
}
@Override
public void dispose() {
}
// we use throttle Job as a workaround to to ensure label is updated, due to bug:
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=430005
protected final ThrottleCodeJob viewerRefreshThrottleJob = new ThrottleCodeJob(1200) {
@Override
protected void runThrottledCode() {
postRefreshEventToUI(this, getElementsToRefresh());
}
};
protected Indexable<Object> getElementsToRefresh() {
ArrayList2<Object> elementsToRefresh = new ArrayList2<>();
for(String projectName : getBundleModel().getModelProjects()) {
IProject project = EclipseUtils.getWorkspaceRoot().getProject(projectName);
elementsToRefresh.add(project);
}
return elementsToRefresh;
}
protected void postRefreshEventToUI(ThrottleCodeJob throttleCodeJob, Indexable<Object> elementsToRefresh) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
throttleCodeJob.markRequestFinished();
if(SWTUtil.isOkToUse(getViewer().getControl())) {
for (Object element : elementsToRefresh) {
getViewer().refresh(element);
}
}
}
});
}
}
}