/******************************************************************************* * 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); } } } }); } } }