| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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.intellij.ide.util.treeView; |
| |
| import com.intellij.openapi.util.ActionCallback; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.util.Conditions; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.Function; |
| import com.intellij.util.containers.ContainerUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.tree.DefaultMutableTreeNode; |
| import javax.swing.tree.TreePath; |
| import java.util.*; |
| |
| public class UpdaterTreeState { |
| |
| private final AbstractTreeUi myUi; |
| protected WeakHashMap<Object, Object> myToSelect = new WeakHashMap<Object, Object>(); |
| protected WeakHashMap<Object, Condition> myAdjustedSelection = new WeakHashMap<Object, Condition>(); |
| protected WeakHashMap<Object, Object> myToExpand = new WeakHashMap<Object, Object>(); |
| private int myProcessingCount; |
| |
| private boolean myCanRunRestore = true; |
| |
| private final WeakHashMap<Object, Object> myAdjustmentCause2Adjustment = new WeakHashMap<Object, Object>(); |
| |
| public UpdaterTreeState(AbstractTreeUi ui) { |
| this(ui, false); |
| } |
| |
| public UpdaterTreeState(AbstractTreeUi ui, boolean isEmpty) { |
| myUi = ui; |
| |
| if (!isEmpty) { |
| final JTree tree = myUi.getTree(); |
| putAll(addPaths(tree.getSelectionPaths()), myToSelect); |
| putAll(addPaths(tree.getExpandedDescendants(new TreePath(tree.getModel().getRoot()))), myToExpand); |
| } |
| } |
| |
| public boolean isQueuedForSelection(Object element) { |
| return myToSelect.containsKey(element); |
| } |
| |
| private static void putAll(final Set<Object> source, final Map<Object, Object> target) { |
| for (Object o : source) { |
| target.put(o, o); |
| } |
| } |
| |
| private Set<Object> addPaths(Object[] elements) { |
| Set<Object> set = new HashSet<Object>(); |
| if (elements != null) { |
| ContainerUtil.addAll(set, elements); |
| } |
| |
| return addPaths(set); |
| } |
| |
| private Set<Object> addPaths(Enumeration elements) { |
| ArrayList<Object> elementArray = new ArrayList<Object>(); |
| if (elements != null) { |
| while (elements.hasMoreElements()) { |
| Object each = elements.nextElement(); |
| elementArray.add(each); |
| } |
| } |
| |
| return addPaths(elementArray); |
| } |
| |
| private Set<Object> addPaths(Collection elements) { |
| Set<Object> target = new HashSet<Object>(); |
| |
| if (elements != null) { |
| for (Object each : elements) { |
| final Object node = ((TreePath)each).getLastPathComponent(); |
| if (node instanceof DefaultMutableTreeNode) { |
| final Object descriptor = ((DefaultMutableTreeNode)node).getUserObject(); |
| if (descriptor instanceof NodeDescriptor) { |
| final Object element = myUi.getElementFromDescriptor((NodeDescriptor)descriptor); |
| if (element != null) { |
| target.add(element); |
| } |
| } |
| } |
| } |
| } |
| return target; |
| } |
| |
| @NotNull |
| public Object[] getToSelect() { |
| return myToSelect.keySet().toArray(new Object[myToSelect.size()]); |
| } |
| |
| @NotNull |
| public Object[] getToExpand() { |
| return myToExpand.keySet().toArray(new Object[myToExpand.size()]); |
| } |
| |
| public boolean process(@NotNull Runnable runnable) { |
| try { |
| setProcessingNow(true); |
| runnable.run(); |
| } |
| finally { |
| setProcessingNow(false); |
| } |
| |
| return isEmpty(); |
| } |
| |
| public boolean isEmpty() { |
| return myToExpand.isEmpty() && myToSelect.isEmpty() && myAdjustedSelection.isEmpty(); |
| } |
| |
| |
| public boolean isProcessingNow() { |
| return myProcessingCount > 0; |
| } |
| |
| public void addAll(@NotNull UpdaterTreeState state) { |
| myToExpand.putAll(state.myToExpand); |
| |
| Object[] toSelect = state.getToSelect(); |
| for (Object each : toSelect) { |
| if (!myAdjustedSelection.containsKey(each)) { |
| myToSelect.put(each, each); |
| } |
| } |
| |
| myCanRunRestore = state.myCanRunRestore; |
| } |
| |
| public boolean restore(@Nullable DefaultMutableTreeNode actionNode) { |
| if (isProcessingNow() || !myCanRunRestore || myUi.hasNodesToUpdate()) { |
| return false; |
| } |
| |
| invalidateToSelectWithRefsToParent(actionNode); |
| |
| setProcessingNow(true); |
| |
| final Object[] toSelect = getToSelect(); |
| final Object[] toExpand = getToExpand(); |
| |
| |
| final Map<Object, Condition> adjusted = new WeakHashMap<Object, Condition>(); |
| adjusted.putAll(myAdjustedSelection); |
| |
| clearSelection(); |
| clearExpansion(); |
| |
| final Set<Object> originallySelected = myUi.getSelectedElements(); |
| |
| myUi._select(toSelect, new Runnable() { |
| @Override |
| public void run() { |
| processUnsuccessfulSelections(toSelect, new Function<Object, Object>() { |
| @Override |
| public Object fun(final Object o) { |
| if (myUi.getTree().isRootVisible() || !myUi.getTreeStructure().getRootElement().equals(o)) { |
| addSelection(o); |
| } |
| return o; |
| } |
| }, originallySelected); |
| |
| processAjusted(adjusted, originallySelected).doWhenDone(new Runnable() { |
| @Override |
| public void run() { |
| myUi.expand(toExpand, new Runnable() { |
| @Override |
| public void run() { |
| myUi.clearUpdaterState(); |
| setProcessingNow(false); |
| } |
| }, true); |
| } |
| }); |
| } |
| }, false, true, true, false); |
| |
| return true; |
| } |
| |
| private void invalidateToSelectWithRefsToParent(DefaultMutableTreeNode actionNode) { |
| if (actionNode != null) { |
| Object readyElement = myUi.getElementFor(actionNode); |
| if (readyElement != null) { |
| Iterator<Object> toSelect = myToSelect.keySet().iterator(); |
| while (toSelect.hasNext()) { |
| Object eachToSelect = toSelect.next(); |
| if (readyElement.equals(myUi.getTreeStructure().getParentElement(eachToSelect))) { |
| List<Object> children = myUi.getLoadedChildrenFor(readyElement); |
| if (!children.contains(eachToSelect)) { |
| toSelect.remove(); |
| if (!myToSelect.containsKey(readyElement) && !myUi.getSelectedElements().contains(eachToSelect)) { |
| addAdjustedSelection(eachToSelect, Conditions.alwaysFalse(), null); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| void beforeSubtreeUpdate() { |
| myCanRunRestore = true; |
| } |
| |
| private void processUnsuccessfulSelections(final Object[] toSelect, Function<Object, Object> restore, Set<Object> originallySelected) { |
| final Set<Object> selected = myUi.getSelectedElements(); |
| |
| boolean wasFullyRejected = false; |
| if (toSelect.length > 0 && !selected.isEmpty() && !originallySelected.containsAll(selected)) { |
| final Set<Object> successfulSelections = new HashSet<Object>(); |
| ContainerUtil.addAll(successfulSelections, toSelect); |
| |
| successfulSelections.retainAll(selected); |
| wasFullyRejected = successfulSelections.isEmpty(); |
| } else if (selected.isEmpty() && originallySelected.isEmpty()) { |
| wasFullyRejected = true; |
| } |
| |
| if (wasFullyRejected && !selected.isEmpty()) return; |
| |
| for (Object eachToSelect : toSelect) { |
| if (!selected.contains(eachToSelect)) { |
| restore.fun(eachToSelect); |
| } |
| } |
| } |
| |
| private ActionCallback processAjusted(final Map<Object, Condition> adjusted, final Set<Object> originallySelected) { |
| final ActionCallback result = new ActionCallback(); |
| |
| final Set<Object> allSelected = myUi.getSelectedElements(); |
| |
| Set<Object> toSelect = new HashSet<Object>(); |
| for (Map.Entry<Object, Condition> entry : adjusted.entrySet()) { |
| if (entry.getValue().value(entry.getKey())) continue; |
| |
| for (final Object eachSelected : allSelected) { |
| if (isParentOrSame(entry.getKey(), eachSelected)) continue; |
| toSelect.add(entry.getKey()); |
| } |
| if (allSelected.isEmpty()) { |
| toSelect.add(entry.getKey()); |
| } |
| } |
| |
| final Object[] newSelection = ArrayUtil.toObjectArray(toSelect); |
| |
| if (newSelection.length > 0) { |
| myUi._select(newSelection, new Runnable() { |
| @Override |
| public void run() { |
| final Set<Object> hangByParent = new HashSet<Object> (); |
| processUnsuccessfulSelections(newSelection, new Function<Object, Object>() { |
| @Override |
| public Object fun(final Object o) { |
| if (myUi.isInStructure(o) && !adjusted.get(o).value(o)) { |
| hangByParent.add(o); |
| } else { |
| addAdjustedSelection(o, adjusted.get(o), null); |
| } |
| return null; |
| } |
| }, originallySelected); |
| |
| processHangByParent(hangByParent).notify(result); |
| } |
| }, false, true, true); |
| } else { |
| result.setDone(); |
| } |
| |
| return result; |
| } |
| |
| private ActionCallback processHangByParent(Set<Object> elements) { |
| if (elements.isEmpty()) return new ActionCallback.Done(); |
| |
| ActionCallback result = new ActionCallback(elements.size()); |
| for (Object hangElement : elements) { |
| if (!myAdjustmentCause2Adjustment.containsKey(hangElement)) { |
| processHangByParent(hangElement).notify(result); |
| } |
| else { |
| result.setDone(); |
| } |
| } |
| return result; |
| } |
| |
| private ActionCallback processHangByParent(Object each) { |
| ActionCallback result = new ActionCallback(); |
| processNextHang(each, result); |
| return result; |
| } |
| |
| private void processNextHang(Object element, final ActionCallback callback) { |
| if (element == null || myUi.getSelectedElements().contains(element)) { |
| callback.setDone(); |
| } else { |
| final Object nextElement = myUi.getTreeStructure().getParentElement(element); |
| if (nextElement == null) { |
| callback.setDone(); |
| } else { |
| myUi.select(nextElement, new Runnable() { |
| @Override |
| public void run() { |
| processNextHang(nextElement, callback); |
| } |
| }, true); |
| } |
| } |
| } |
| |
| private boolean isParentOrSame(Object parent, Object child) { |
| Object eachParent = child; |
| while (eachParent != null) { |
| if (parent.equals(eachParent)) return true; |
| eachParent = myUi.getTreeStructure().getParentElement(eachParent); |
| } |
| |
| return false; |
| } |
| |
| public void clearExpansion() { |
| myToExpand.clear(); |
| } |
| |
| public void clearSelection() { |
| myToSelect.clear(); |
| myAdjustedSelection = new WeakHashMap<Object, Condition>(); |
| } |
| |
| public void addSelection(final Object element) { |
| myToSelect.put(element, element); |
| } |
| |
| public void addAdjustedSelection(final Object element, Condition isExpired, @Nullable Object adjustmentCause) { |
| myAdjustedSelection.put(element, isExpired); |
| if (adjustmentCause != null) { |
| myAdjustmentCause2Adjustment.put(adjustmentCause, element); |
| } |
| } |
| |
| @NonNls |
| @Override |
| public String toString() { |
| return "UpdaterState toSelect" + |
| myToSelect + " toExpand=" + |
| myToExpand + " processingNow=" + isProcessingNow() + " canRun=" + myCanRunRestore; |
| } |
| |
| public void setProcessingNow(boolean processingNow) { |
| if (processingNow) { |
| myProcessingCount++; |
| } else { |
| myProcessingCount--; |
| } |
| if (!isProcessingNow()) { |
| myUi.maybeReady(); |
| } |
| } |
| |
| public void removeFromSelection(Object element) { |
| myToSelect.remove(element); |
| myAdjustedSelection.remove(element); |
| } |
| } |