IDEADEV-35207 Mouse clicks are recognized only after mouse up in some places [fix...
[idea/community.git] / platform / platform-api / src / com / intellij / ide / util / treeView / UpdaterTreeState.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ide.util.treeView;
17
18 import com.intellij.openapi.util.ActionCallback;
19 import com.intellij.openapi.util.Condition;
20 import com.intellij.util.ArrayUtil;
21 import com.intellij.util.Function;
22 import org.jetbrains.annotations.Nullable;
23
24 import javax.swing.*;
25 import javax.swing.tree.DefaultMutableTreeNode;
26 import javax.swing.tree.TreePath;
27 import java.util.*;
28
29 public class UpdaterTreeState {
30
31   private final AbstractTreeUi myUi;
32   protected WeakHashMap<Object, Object> myToSelect = new WeakHashMap<Object, Object>();
33   protected WeakHashMap<Object, Condition> myAdjustedSelection = new WeakHashMap<Object, Condition>();
34   protected WeakHashMap<Object, Object> myDisposedElements = new WeakHashMap<Object, Object>();
35   protected WeakHashMap<Object, Object> myToExpand = new WeakHashMap<Object, Object>();
36   private int myProcessingCount;
37
38   private boolean myCanRunRestore = true;
39
40   private WeakHashMap<Object, Object> myAdjustmentCause2Adjustment = new WeakHashMap<Object, Object>();
41
42   public UpdaterTreeState(AbstractTreeUi ui) {
43     myUi = ui;
44
45     final JTree tree = myUi.getTree();
46     putAll(addPaths(tree.getSelectionPaths()), myToSelect);
47     putAll(addPaths(tree.getExpandedDescendants(new TreePath(tree.getModel().getRoot()))), myToExpand);
48   }
49
50   private static void putAll(final Set<Object> source, final Map<Object, Object> target) {
51     for (Object o : source) {
52       target.put(o, o);
53     }
54   }
55
56   private Set<Object> addPaths(Object[] elements) {
57     Set<Object> set = new HashSet<Object>();
58     if (elements != null) {
59       set.addAll(Arrays.asList(elements));
60     }
61
62     return addPaths(set);
63   }
64
65   private Set<Object> addPaths(Enumeration elements) {
66     ArrayList<Object> elementArray = new ArrayList<Object>();
67     if (elements != null) {
68       while (elements.hasMoreElements()) {
69         Object each = elements.nextElement();
70         elementArray.add(each);
71       }
72     }
73
74     return addPaths(elementArray);
75   }
76
77   private Set<Object> addPaths(Collection elements) {
78     Set<Object> target = new HashSet<Object>();
79
80     if (elements != null) {
81       for (Object each : elements) {
82         final Object node = ((TreePath)each).getLastPathComponent();
83         if (node instanceof DefaultMutableTreeNode) {
84           final Object descriptor = ((DefaultMutableTreeNode)node).getUserObject();
85           if (descriptor instanceof NodeDescriptor) {
86             final Object element = myUi.getElementFromDescriptor((NodeDescriptor)descriptor);
87             if (element != null) {
88               target.add(element);
89             }
90           }
91         }
92       }
93     }
94     return target;
95   }
96
97   public Object[] getToSelect() {
98     return myToSelect.keySet().toArray(new Object[myToSelect.size()]);
99   }
100
101   public Object[] getToExpand() {
102     return myToExpand.keySet().toArray(new Object[myToExpand.size()]);
103   }
104
105   public boolean process(Runnable runnable) {
106     try {
107       setProcessingNow(true);
108       runnable.run();
109     }
110     finally {
111       setProcessingNow(false);
112     }
113
114     return isEmpty();
115   }
116
117   public boolean isEmpty() {
118     return myToExpand.isEmpty() && myToSelect.isEmpty() && myAdjustedSelection.isEmpty();
119   }
120
121
122   public boolean isProcessingNow() {
123     return myProcessingCount > 0;
124   }
125
126   public void addAll(final UpdaterTreeState state) {
127     myToExpand.putAll(state.myToExpand);
128
129     Object[] toSelect = state.getToSelect();
130     for (Object each : toSelect) {
131       if (!myAdjustedSelection.containsKey(each)) {
132         myToSelect.put(each, each);
133       }
134     }
135
136     myCanRunRestore = state.myCanRunRestore;
137   }
138
139   public boolean restore(@Nullable DefaultMutableTreeNode actionNode) {
140     if (isProcessingNow() || !myCanRunRestore || myUi.hasNodesToUpdate()) return false;
141
142     invalidateToSelectWithRefsToParent(actionNode);
143
144     setProcessingNow(true);
145
146     final Object[] toSelect = getToSelect();
147     final Object[] toExpand = getToExpand();
148
149
150     final Map<Object, Condition> adjusted = new WeakHashMap<Object, Condition>();
151     adjusted.putAll(myAdjustedSelection);
152
153     clearSelection();
154     clearExpansion();
155
156     final Set<Object> originallySelected = myUi.getSelectedElements();
157
158     myUi._select(toSelect, new Runnable() {
159       public void run() {
160         processUnsuccessfulSelections(toSelect, new Function<Object, Object>() {
161           public Object fun(final Object o) {
162             if (myUi.getTree().isRootVisible() || !myUi.getTreeStructure().getRootElement().equals(o)) {
163               addSelection(o);
164             }
165             return o;
166           }
167         }, originallySelected);
168
169         processAjusted(adjusted, originallySelected).doWhenDone(new Runnable() {
170           public void run() {
171             myUi.expand(toExpand, new Runnable() {
172               public void run() {
173                 myUi.clearUpdaterState();
174                 setProcessingNow(false);
175               }
176             }, true);
177           }
178         });
179       }
180     }, false, true, true, false);
181
182     return true;
183   }
184
185   private void invalidateToSelectWithRefsToParent(DefaultMutableTreeNode actionNode) {
186     if (actionNode != null) {
187       Object readyElement = myUi.getElementFor(actionNode);
188       if (readyElement != null) {
189         Iterator<Object> toSelect = myToSelect.keySet().iterator();
190         while (toSelect.hasNext()) {
191           Object eachToSelect = toSelect.next();
192           if (readyElement.equals(myUi.getTreeStructure().getParentElement(eachToSelect))) {
193             List<Object> children = myUi.getLoadedChildrenFor(readyElement);
194             if (!children.contains(eachToSelect)) {
195               toSelect.remove();
196               if (!myToSelect.containsKey(readyElement) && !myUi.getSelectedElements().contains(eachToSelect)) {
197                 addAdjustedSelection(eachToSelect, Condition.FALSE, null);
198               }
199             }
200           }
201         }
202       }
203     }
204   }
205
206   void beforeSubtreeUpdate() {
207     myCanRunRestore = true;
208   }
209
210   private void processUnsuccessfulSelections(final Object[] toSelect, Function<Object, Object> restore, Set<Object> originallySelected) {
211     final Set<Object> selected = myUi.getSelectedElements();
212
213     boolean wasFullyRejected = false;
214     if (toSelect.length > 0 && selected.size() > 0 && !originallySelected.containsAll(selected)) {
215       final Set<Object> successfulSelections = new HashSet<Object>();
216       successfulSelections.addAll(Arrays.asList(toSelect));
217
218       successfulSelections.retainAll(selected);
219       wasFullyRejected = successfulSelections.size() == 0;
220     } else if (selected.size() == 0 && originallySelected.size() == 0) {
221       wasFullyRejected = true;
222     }
223
224     if (wasFullyRejected && selected.size() > 0) return;
225
226     for (Object eachToSelect : toSelect) {
227       if (!selected.contains(eachToSelect)) {
228         restore.fun(eachToSelect);
229       }
230     }
231   }
232
233   private ActionCallback processAjusted(final Map<Object, Condition> adjusted, final Set<Object> originallySelected) {
234     final ActionCallback result = new ActionCallback();
235
236     final Set<Object> allSelected = myUi.getSelectedElements();
237
238     Set<Object> toSelect = new HashSet<Object>();
239     for (Object each : adjusted.keySet()) {
240       if (adjusted.get(each).value(each)) continue;
241
242       for (final Object eachSelected : allSelected) {
243         if (isParentOrSame(each, eachSelected)) continue;
244         toSelect.add(each);
245       }
246       if (allSelected.size() == 0) {
247         toSelect.add(each);
248       }
249     }
250
251     final Object[] newSelection = ArrayUtil.toObjectArray(toSelect);
252
253     if (newSelection.length > 0) {
254       myUi._select(newSelection, new Runnable() {
255         public void run() {
256           final Set<Object> hangByParent = new HashSet<Object> ();
257           processUnsuccessfulSelections(newSelection, new Function<Object, Object>() {
258             public Object fun(final Object o) {
259               if (myUi.isInStructure(o) && !adjusted.get(o).value(o)) {
260                 hangByParent.add(o);
261               } else {
262                 addAdjustedSelection(o, adjusted.get(o), null);
263               }
264               return null;
265             }
266           }, originallySelected);
267
268           processHangByParent(hangByParent).notify(result);
269         }
270       }, true, true, true);
271     } else {
272       result.setDone();
273     }
274
275     return result;
276   }
277
278   private ActionCallback processHangByParent(Set<Object> elements) {
279     if (elements.size() == 0) return new ActionCallback.Done();
280
281     ActionCallback result = new ActionCallback(elements.size());
282     for (Iterator<Object> iterator = elements.iterator(); iterator.hasNext();) {
283       Object hangElement = iterator.next();
284       if (!myAdjustmentCause2Adjustment.containsKey(hangElement)) {
285         processHangByParent(hangElement).notify(result);
286       } else {
287         result.setDone();
288       }
289     }
290     return result;
291   }
292
293   private ActionCallback processHangByParent(Object each) {
294     ActionCallback result = new ActionCallback();
295     processNextHang(each, result);
296     return result;
297   }
298
299   private void processNextHang(Object element, final ActionCallback callback) {
300     if (element == null || myUi.getSelectedElements().contains(element)) {
301       callback.setDone();
302     } else {
303       final Object nextElement = myUi.getTreeStructure().getParentElement(element);
304       if (nextElement == null) {
305         callback.setDone();
306       } else {
307        myUi.select(nextElement, new Runnable() {
308           public void run() {
309             processNextHang(nextElement, callback);
310           }
311         }, true);
312       }
313     }
314   }
315
316   private boolean isParentOrSame(Object parent, Object child) {
317     Object eachParent = child;
318     while (eachParent != null) {
319       if (parent.equals(eachParent)) return true;
320       eachParent = myUi.getTreeStructure().getParentElement(eachParent);
321     }
322
323     return false;
324   }
325
326   public void clearExpansion() {
327     myToExpand.clear();
328   }
329
330   public void clearSelection() {
331     myToSelect.clear();
332     myAdjustedSelection = new WeakHashMap<Object, Condition>();
333   }
334
335   public void addSelection(final Object element) {
336     myToSelect.put(element, element);
337   }
338
339   public void addAdjustedSelection(final Object element, Condition isExpired, @Nullable Object adjustmentCause) {
340     myAdjustedSelection.put(element, isExpired);
341     if (adjustmentCause != null) {
342       myAdjustmentCause2Adjustment.put(adjustmentCause, element);
343     }
344   }
345
346   @Override
347   public String toString() {
348     return "UpdaterState toSelect" + Arrays.asList(myToSelect) + " toExpand=" + Arrays.asList(myToExpand) + " processingNow=" + isProcessingNow() + " canRun=" + myCanRunRestore;
349   }
350
351   public void setProcessingNow(boolean processingNow) {
352     if (processingNow) {
353       myProcessingCount++;
354     } else {
355       myProcessingCount--;
356     }
357     if (!isProcessingNow()) {
358       myUi.maybeReady();
359     }
360   }
361
362   public void removeFromSelection(Object element) {
363     myToSelect.remove(element);
364     myAdjustedSelection.remove(element);
365   }
366 }