/******************************************************************************* * Copyright (c) 2000, 2006 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.rubypeople.rdt.internal.ui.packageview; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.viewers.IBasicPropertyConstants; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.widgets.Control; import org.eclipse.ui.IWorkingSet; import org.rubypeople.rdt.core.ElementChangedEvent; import org.rubypeople.rdt.core.IElementChangedListener; import org.rubypeople.rdt.core.ILoadpathEntry; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyElementDelta; import org.rubypeople.rdt.core.IRubyModel; import org.rubypeople.rdt.core.IRubyProject; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.ISourceFolder; import org.rubypeople.rdt.core.ISourceFolderRoot; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.internal.core.ERBScript; import org.rubypeople.rdt.internal.core.RubyProject; import org.rubypeople.rdt.internal.corext.util.RubyModelUtil; import org.rubypeople.rdt.internal.ui.RubyPlugin; import org.rubypeople.rdt.internal.ui.workingsets.WorkingSetModel; import org.rubypeople.rdt.ui.StandardRubyElementContentProvider; /** * Content provider for the PackageExplorer. * * <p> * Since 2.1 this content provider can provide the children for flat or hierarchical * layout. The hierarchical layout is done by delegating to the <code>SourceFolderProvider</code>. * </p> * * @see org.rubypeople.rdt.ui.StandardRubyElementContentProvider * @see org.rubypeople.rdt.internal.ui.packageview.SourceFolderProvider */ public class PackageExplorerContentProvider extends StandardRubyElementContentProvider implements ITreeContentProvider, IElementChangedListener { protected static final int ORIGINAL= 0; protected static final int PARENT= 1 << 0; protected static final int GRANT_PARENT= 1 << 1; protected static final int PROJECT= 1 << 2; private TreeViewer fViewer; private Object fInput; private boolean fIsFlatLayout; private SourceFolderProvider fSourceFolderProvider; private int fPendingChanges; /** * Creates a new content provider for Ruby elements. */ public PackageExplorerContentProvider(boolean provideMembers) { super(provideMembers); fSourceFolderProvider= new SourceFolderProvider(); } /* package */ SourceFolderProvider getSourceFolderProvider() { return fSourceFolderProvider; } protected Object getViewerInput() { return fInput; } /* (non-Rubydoc) * Method declared on IElementChangedListener. */ public void elementChanged(final ElementChangedEvent event) { try { // 58952 delete project does not update Package Explorer [package explorer] // if the input to the viewer is deleted then refresh to avoid the display of stale elements if (inputDeleted()) return; processDelta(event.getDelta()); } catch(RubyModelException e) { RubyPlugin.log(e); } } private boolean inputDeleted() { if (fInput == null) return false; if ((fInput instanceof IRubyElement) && ((IRubyElement) fInput).exists()) return false; if ((fInput instanceof IResource) && ((IResource) fInput).exists()) return false; if (fInput instanceof WorkingSetModel) return false; if (fInput instanceof IWorkingSet) // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=156239 return false; postRefresh(fInput, ORIGINAL, fInput); return true; } /* (non-Rubydoc) * Method declared on IContentProvider. */ public void dispose() { super.dispose(); RubyCore.removeElementChangedListener(this); fSourceFolderProvider.dispose(); } // ------ Code which delegates to SourceFolderProvider ------ private boolean needsToDelegateGetChildren(Object element) { int type= -1; if (element instanceof IFolder) { IFolder folder = (IFolder) element; if (RubyProject.hasRubyNature(folder.getProject())) return true; return false; } if (element instanceof IRubyElement) type= ((IRubyElement)element).getElementType(); return (!fIsFlatLayout && (type == IRubyElement.SOURCE_FOLDER || type == IRubyElement.SOURCE_FOLDER_ROOT || type == IRubyElement.RUBY_PROJECT)); } public Object[] getChildren(Object parentElement) { Object[] children= NO_CHILDREN; try { if (parentElement instanceof IRubyModel) return concatenate(getRubyProjects((IRubyModel)parentElement), getNonRubyProjects((IRubyModel)parentElement)); if (parentElement instanceof LoadPathContainer) return getContainerSourceFolderRoots((LoadPathContainer)parentElement); if (parentElement instanceof IProject) return ((IProject)parentElement).members(); if (needsToDelegateGetChildren(parentElement)) { Object[] packageFragments= fSourceFolderProvider.getChildren(parentElement); children= getWithParentsResources(packageFragments, parentElement); } else { children= super.getChildren(parentElement); } if (parentElement instanceof IRubyProject) { IRubyProject project= (IRubyProject)parentElement; return rootsAndContainers(project, children); } else return children; } catch (CoreException e) { return NO_CHILDREN; } } private Object[] rootsAndContainers(IRubyProject project, Object[] roots) throws RubyModelException { List result= new ArrayList(roots.length); Set containers= new HashSet(roots.length); Set containedRoots= new HashSet(roots.length); ILoadpathEntry[] entries= project.getRawLoadpath(); for (int i= 0; i < entries.length; i++) { ILoadpathEntry entry= entries[i]; if (entry != null && entry.getEntryKind() == ILoadpathEntry.CPE_CONTAINER) { ISourceFolderRoot[] roots1= project.findSourceFolderRoots(entry); containedRoots.addAll(Arrays.asList(roots1)); containers.add(entry); } } for (int i= 0; i < roots.length; i++) { if (roots[i] instanceof ISourceFolderRoot) { if (!containedRoots.contains(roots[i])) { result.add(roots[i]); } } else { result.add(roots[i]); } } for (Iterator each= containers.iterator(); each.hasNext();) { ILoadpathEntry element= (ILoadpathEntry) each.next(); result.add(new LoadPathContainer(project, element)); } return result.toArray(); } private Object[] getContainerSourceFolderRoots(LoadPathContainer container) { return container.getChildren(container); } private Object[] getNonRubyProjects(IRubyModel model) throws RubyModelException { return model.getNonRubyResources(); } public Object getParent(Object child) { if (needsToDelegateGetParent(child)) { return fSourceFolderProvider.getParent(child); } else return super.getParent(child); } protected Object internalGetParent(Object element) { // since we insert logical package containers we have to fix // up the parent for package fragment roots so that they refer // to the container and containers refere to the project // if (element instanceof ISourceFolderRoot) { ISourceFolderRoot root= (ISourceFolderRoot)element; IRubyProject project= root.getRubyProject(); try { ILoadpathEntry[] entries= project.getRawLoadpath(); for (int i= 0; i < entries.length; i++) { ILoadpathEntry entry= entries[i]; if (entry.getEntryKind() == ILoadpathEntry.CPE_CONTAINER) { if (LoadPathContainer.contains(project, entry, root)) return new LoadPathContainer(project, entry); } } } catch (RubyModelException e) { // fall through } } if (element instanceof LoadPathContainer) { return ((LoadPathContainer)element).getRubyProject(); } return super.internalGetParent(element); } private boolean needsToDelegateGetParent(Object element) { int type= -1; if (element instanceof IRubyElement) type= ((IRubyElement)element).getElementType(); return (!fIsFlatLayout && type == IRubyElement.SOURCE_FOLDER); } /** * Returns the given objects with the resources of the parent. */ private Object[] getWithParentsResources(Object[] existingObject, Object parent) { Object[] objects= super.getChildren(parent); List list= new ArrayList(); // Add everything that is not a SourceFolder (Files) for (int i= 0; i < objects.length; i++) { Object object= objects[i]; if (!(object instanceof ISourceFolder)) { if (!list.contains(object)) { list.add(object); } } } if (existingObject != null) list.addAll(Arrays.asList(existingObject)); return list.toArray(); } /* (non-Rubydoc) * Method declared on IContentProvider. */ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { super.inputChanged(viewer, oldInput, newInput); fSourceFolderProvider.inputChanged(viewer, oldInput, newInput); fViewer= (TreeViewer)viewer; if (oldInput == null && newInput != null) { RubyCore.addElementChangedListener(this); } else if (oldInput != null && newInput == null) { RubyCore.removeElementChangedListener(this); } fInput= newInput; } // ------ delta processing ------ /** * Processes a delta recursively. When more than two children are affected the * tree is fully refreshed starting at this node. The delta is processed in the * current thread but the viewer updates are posted to the UI thread. */ private void processDelta(IRubyElementDelta delta) throws RubyModelException { int kind= delta.getKind(); int flags= delta.getFlags(); IRubyElement element= delta.getElement(); int elementType= element.getElementType(); if (elementType != IRubyElement.RUBY_MODEL && elementType != IRubyElement.RUBY_PROJECT) { IRubyProject proj= element.getRubyProject(); if (proj == null || !proj.getProject().isOpen()) // TODO: Not needed if parent already did the 'open' check! return; } if (!fIsFlatLayout && elementType == IRubyElement.SOURCE_FOLDER) { fSourceFolderProvider.processDelta(delta); if (processResourceDeltas(delta.getResourceDeltas(), element)) return; handleAffectedChildren(delta, element); return; } if (elementType == IRubyElement.SCRIPT) { IRubyScript cu= (IRubyScript) element; if (!RubyModelUtil.isPrimary(cu)) { return; } if (!getProvideMembers() && cu.isWorkingCopy() && kind == IRubyElementDelta.CHANGED) { return; } if ((kind == IRubyElementDelta.CHANGED) && !isStructuralCUChange(flags)) { return; // test moved ahead } if (!isOnClassPath(cu)) { // TODO: isOnClassPath expensive! Should be put after all cheap tests return; } } if (elementType == IRubyElement.RUBY_PROJECT) { // handle open and closing of a project if ((flags & (IRubyElementDelta.F_CLOSED | IRubyElementDelta.F_OPENED)) != 0) { postRefresh(element, ORIGINAL, element); return; } // if the raw class path has changed we refresh the entire project if ((flags & IRubyElementDelta.F_CLASSPATH_CHANGED) != 0) { postRefresh(element, ORIGINAL, element); return; } } if (kind == IRubyElementDelta.REMOVED) { Object parent= internalGetParent(element); if (element instanceof ISourceFolder) { // refresh package fragment root to allow filtering empty (parent) packages: bug 72923 if (fViewer.testFindItem(parent) != null) postRefresh(parent, PARENT, element); return; } postRemove(element); if (parent instanceof ISourceFolder) postUpdateIcon((ISourceFolder)parent); // we are filtering out empty subpackages, so we // a package becomes empty we remove it from the viewer. if (isSourceFolderEmpty(element.getParent())) { if (fViewer.testFindItem(parent) != null) postRefresh(internalGetParent(parent), GRANT_PARENT, element); } return; } if (kind == IRubyElementDelta.ADDED) { Object parent= internalGetParent(element); // we are filtering out empty subpackages, so we // have to handle additions to them specially. if (parent instanceof ISourceFolder) { Object grandparent= internalGetParent(parent); if (((ISourceFolder)parent).isDefaultPackage()) { parent = grandparent; grandparent = internalGetParent(parent); } // 1GE8SI6: ITPJUI:WIN98 - Rename is not shown in Packages View // avoid posting a refresh to an unvisible parent if (parent.equals(fInput)) { postRefresh(parent, PARENT, element); } else { // refresh from grandparent if parent isn't visible yet if (fViewer.testFindItem(parent) == null) postRefresh(grandparent, GRANT_PARENT, element); else { postRefresh(parent, PARENT, element); } } return; } else { if ((flags & IRubyElementDelta.F_MOVED_FROM) != 0) { postRemove(delta.getMovedFromElement()); } postAdd(parent, element); } } if (elementType == IRubyElement.SCRIPT) { if (kind == IRubyElementDelta.CHANGED) { // isStructuralCUChange already performed above postRefresh(element, ORIGINAL, element); updateSelection(delta); } return; } if (elementType == IRubyElement.SOURCE_FOLDER_ROOT) { // the contents of an external JAR has changed if ((flags & IRubyElementDelta.F_ARCHIVE_CONTENT_CHANGED) != 0) { postRefresh(element, ORIGINAL, element); return; } // the source attachment of a JAR has changed if ((flags & (IRubyElementDelta.F_SOURCEATTACHED | IRubyElementDelta.F_SOURCEDETACHED)) != 0) postUpdateIcon(element); if (isClassPathChange(delta)) { // throw the towel and do a full refresh of the affected java project. postRefresh(element.getRubyProject(), PROJECT, element); return; } } if (processResourceDeltas(delta.getResourceDeltas(), element)) return; handleAffectedChildren(delta, element); } private static boolean isStructuralCUChange(int flags) { // No refresh on working copy creation (F_PRIMARY_WORKING_COPY) return ((flags & IRubyElementDelta.F_CHILDREN) != 0) || ((flags & (IRubyElementDelta.F_CONTENT | IRubyElementDelta.F_FINE_GRAINED)) == IRubyElementDelta.F_CONTENT); } /* package */ void handleAffectedChildren(IRubyElementDelta delta, IRubyElement element) throws RubyModelException { IRubyElementDelta[] affectedChildren= delta.getAffectedChildren(); if (affectedChildren.length > 1) { // a package fragment might become non empty refresh from the parent if (element instanceof ISourceFolder) { IRubyElement parent= (IRubyElement)internalGetParent(element); if (parent instanceof ISourceFolderRoot) { parent = (IRubyElement)internalGetParent(parent); } // 1GE8SI6: ITPJUI:WIN98 - Rename is not shown in Packages View // avoid posting a refresh to an unvisible parent if (element.equals(fInput)) { postRefresh(element, ORIGINAL, element); } else { postRefresh(parent, PARENT, element); } return; } // more than one child changed, refresh from here downwards if (element instanceof ISourceFolderRoot) { Object toRefresh= skipProjectSourceFolderRoot((ISourceFolderRoot)element); postRefresh(toRefresh, ORIGINAL, toRefresh); } else { postRefresh(element, ORIGINAL, element); } return; } processAffectedChildren(affectedChildren); } protected void processAffectedChildren(IRubyElementDelta[] affectedChildren) throws RubyModelException { for (int i= 0; i < affectedChildren.length; i++) { processDelta(affectedChildren[i]); } } private boolean isOnClassPath(IRubyScript element) { IRubyProject project= element.getRubyProject(); if (project == null || !project.exists()) return false; return project.isOnLoadpath(element); } /** * Updates the selection. It finds newly added elements * and selects them. */ private void updateSelection(IRubyElementDelta delta) { final IRubyElement addedElement= findAddedElement(delta); if (addedElement != null) { final StructuredSelection selection= new StructuredSelection(addedElement); postRunnable(new Runnable() { public void run() { Control ctrl= fViewer.getControl(); if (ctrl != null && !ctrl.isDisposed()) { // 19431 // if the item is already visible then select it if (fViewer.testFindItem(addedElement) != null) fViewer.setSelection(selection); } } }); } } private IRubyElement findAddedElement(IRubyElementDelta delta) { if (delta.getKind() == IRubyElementDelta.ADDED) return delta.getElement(); IRubyElementDelta[] affectedChildren= delta.getAffectedChildren(); for (int i= 0; i < affectedChildren.length; i++) return findAddedElement(affectedChildren[i]); return null; } /** * Updates the package icon */ private void postUpdateIcon(final IRubyElement element) { postRunnable(new Runnable() { public void run() { // 1GF87WR: ITPUI:ALL - SWTEx + NPE closing a workbench window. Control ctrl= fViewer.getControl(); if (ctrl != null && !ctrl.isDisposed()) fViewer.update(element, new String[]{IBasicPropertyConstants.P_IMAGE}); } }); } /** * Process a resource delta. * * @return true if the parent got refreshed */ private boolean processResourceDelta(IResourceDelta delta, Object parent) { int status= delta.getKind(); int flags= delta.getFlags(); IResource resource= delta.getResource(); // filter out changes affecting the output folder if (resource == null) return false; // this could be optimized by handling all the added children in the parent if ((status & IResourceDelta.REMOVED) != 0) { if (parent instanceof ISourceFolder) { Object grandparent= internalGetParent(parent); // if grandparent is src folder root that is equal to project, refresh project if (grandparent instanceof ISourceFolderRoot && ((ISourceFolderRoot)grandparent).getResource().equals(((ISourceFolderRoot)grandparent).getRubyProject().getProject())) { parent = grandparent; grandparent = internalGetParent(parent); } // refresh one level above to deal with empty package filtering properly postRefresh(grandparent, PARENT, parent); return true; } else postRemove(resource); } if ((status & IResourceDelta.ADDED) != 0) { if (parent instanceof ISourceFolder) { Object grandparent= internalGetParent(parent); // if grandparent is src folder root that is equal to project, refresh project if (grandparent instanceof ISourceFolderRoot && ((ISourceFolderRoot)grandparent).getResource().equals(((ISourceFolderRoot)grandparent).getRubyProject().getProject())) { parent = grandparent; grandparent = internalGetParent(parent); } // refresh one level above to deal with empty package filtering properly postRefresh(grandparent, PARENT, parent); return true; } else postAdd(parent, resource); } // open/close state change of a project if ((flags & IResourceDelta.OPEN) != 0) { postProjectStateChanged(internalGetParent(parent)); return true; } processResourceDeltas(delta.getAffectedChildren(), resource); return false; } public void setIsFlatLayout(boolean state) { fIsFlatLayout= state; } /** * Process resource deltas. * * @return true if the parent got refreshed */ private boolean processResourceDeltas(IResourceDelta[] deltas, Object parent) { if (deltas == null) return false; if (deltas.length > 1) { // more than one child changed, refresh from here downwards postRefresh(parent, ORIGINAL, parent); return true; } for (int i= 0; i < deltas.length; i++) { if (processResourceDelta(deltas[i], parent)) return true; } return false; } private void postRefresh(Object root, int relation, Object affectedElement) { // JFace doesn't refresh when object isn't part of the viewer // Therefore move the refresh start down to the viewer's input if (isParent(root, fInput)) root= fInput; List toRefresh= new ArrayList(1); toRefresh.add(root); augmentElementToRefresh(toRefresh, relation, affectedElement); postRefresh(toRefresh, true); } protected void augmentElementToRefresh(List toRefresh, int relation, Object affectedElement) { } boolean isParent(Object root, Object child) { Object parent= getParent(child); if (parent == null) return false; if (parent.equals(root)) return true; return isParent(root, parent); } protected void postRefresh(final List toRefresh, final boolean updateLabels) { postRunnable(new Runnable() { public void run() { Control ctrl= fViewer.getControl(); if (ctrl != null && !ctrl.isDisposed()) { for (Iterator iter= toRefresh.iterator(); iter.hasNext();) { fViewer.refresh(iter.next(), updateLabels); } } } }); } protected void postAdd(final Object parent, final Object element) { postRunnable(new Runnable() { public void run() { Control ctrl= fViewer.getControl(); if (ctrl != null && !ctrl.isDisposed()){ // TODO workaround for 39754 New projects being added to the TreeViewer twice if (fViewer.testFindItem(element) == null) fViewer.add(parent, element); } } }); } protected void postRemove(final Object element) { postRunnable(new Runnable() { public void run() { Control ctrl= fViewer.getControl(); if (ctrl != null && !ctrl.isDisposed()) { fViewer.remove(element); } } }); } protected void postProjectStateChanged(final Object root) { postRunnable(new Runnable() { public void run() { //fPart.projectStateChanged(root); Control ctrl= fViewer.getControl(); if (ctrl != null && !ctrl.isDisposed()) { fViewer.refresh(root, true); // trigger a syntetic selection change so that action refresh their // enable state. fViewer.setSelection(fViewer.getSelection()); } } }); } /* package */ void postRunnable(final Runnable r) { Control ctrl= fViewer.getControl(); final Runnable trackedRunnable= new Runnable() { public void run() { try { r.run(); } finally { removePendingChange(); } } }; if (ctrl != null && !ctrl.isDisposed()) { addPendingChange(); try { ctrl.getDisplay().asyncExec(trackedRunnable); } catch (RuntimeException e) { removePendingChange(); throw e; } catch (Error e) { removePendingChange(); throw e; } } } // ------ Pending change management due to the use of asyncExec in postRunnable. public synchronized boolean hasPendingChanges() { return fPendingChanges > 0; } private synchronized void addPendingChange() { fPendingChanges++; } synchronized void removePendingChange() { fPendingChanges--; if (fPendingChanges < 0) fPendingChanges= 0; } }