d6db42d238468d43e1422b0071c7647f04300714
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / actionSystem / DefaultActionGroup.java
1 /*
2  * Copyright 2000-2016 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.openapi.actionSystem;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.Pair;
20 import com.intellij.util.FunctionUtil;
21 import com.intellij.util.containers.ContainerUtil;
22 import org.jetbrains.annotations.NotNull;
23 import org.jetbrains.annotations.Nullable;
24
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.HashSet;
28 import java.util.List;
29
30 /**
31  * A default implementation of {@link ActionGroup}. Provides the ability
32  * to add children actions and separators between them. In most of the
33  * cases you will be using this implementation but note that there are
34  * cases (for example "Recent files" dialog) where children are determined
35  * on rules different than just positional constraints, that's when you need
36  * to implement your own <code>ActionGroup</code>.
37  *
38  * @see Constraints
39  *
40  * @see com.intellij.openapi.actionSystem.ComputableActionGroup
41  *
42  * @see com.intellij.ide.actions.NonEmptyActionGroup
43  * @see com.intellij.ide.actions.NonTrivialActionGroup
44  * @see com.intellij.ide.actions.SmartPopupActionGroup
45  *
46  */
47 public class DefaultActionGroup extends ActionGroup {
48   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.actionSystem.DefaultActionGroup");
49   private static final String CANT_ADD_ITSELF = "Cannot add a group to itself";
50   /**
51    * Contains instances of AnAction
52    */
53   private final List<AnAction> mySortedChildren = ContainerUtil.createLockFreeCopyOnWriteList();
54   /**
55    * Contains instances of Pair
56    */
57   private final List<Pair<AnAction, Constraints>> myPairs = ContainerUtil.createLockFreeCopyOnWriteList();
58
59   public DefaultActionGroup() {
60     this(null, false);
61   }
62
63   /**
64    * Creates an action group containing the specified actions.
65    *
66    * @param actions the actions to add to the group
67    * @since 9.0
68    */
69   public DefaultActionGroup(@NotNull AnAction... actions) {
70     this(Arrays.asList(actions));
71   }
72
73   /**
74    * Creates an action group containing the specified actions.
75    *
76    * @param actions the actions to add to the group
77    * @since 13.0
78    */
79   public DefaultActionGroup(@NotNull List<? extends AnAction> actions) {
80     this(null, actions);
81   }
82
83   public DefaultActionGroup(@Nullable String name, @NotNull List<? extends AnAction> actions) {
84     this(name, actions, true);
85   }
86
87   public DefaultActionGroup(@Nullable String name, @NotNull List<? extends AnAction> actions, boolean validate) {
88     this(name, false);
89     addActions(actions, validate);
90   }
91
92   public DefaultActionGroup(@Nullable String shortName, boolean popup) {
93     super(shortName, popup);
94   }
95
96   private void addActions(@NotNull List<? extends AnAction> actions, boolean validate) {
97     if (validate) {
98       HashSet<Object> actionSet = new HashSet<>();
99       for (AnAction action : actions) {
100         if (action == this) throw new IllegalArgumentException(CANT_ADD_ITSELF);
101         if (!(action instanceof Separator) && !actionSet.add(action)) throw new ActionDuplicationException(action);
102       }
103     }
104     mySortedChildren.addAll(actions);
105   }
106
107   /**
108    * Adds the specified action to the tail.
109    *
110    * @param action        Action to be added
111    * @param actionManager ActionManager instance
112    */
113   public final void add(@NotNull AnAction action, @NotNull ActionManager actionManager) {
114     add(action, Constraints.LAST, actionManager);
115   }
116
117   public final void add(@NotNull AnAction action) {
118     addAction(action, Constraints.LAST);
119   }
120
121   public final ActionInGroup addAction(@NotNull AnAction action) {
122     return addAction(action, Constraints.LAST);
123   }
124
125   /**
126    * Adds a separator to the tail.
127    */
128   public final void addSeparator() {
129     add(Separator.getInstance());
130   }
131
132   /**
133    * Adds the specified action with the specified constraint.
134    *
135    * @param action     Action to be added; cannot be null
136    * @param constraint Constraint to be used for determining action's position; cannot be null
137    * @throws IllegalArgumentException in case when:
138    *                                  <li>action is null
139    *                                  <li>constraint is null
140    *                                  <li>action is already in the group
141    */
142   public final void add(@NotNull AnAction action, @NotNull Constraints constraint) {
143     add(action, constraint, ActionManager.getInstance());
144   }
145
146   public final ActionInGroup addAction(@NotNull AnAction action, @NotNull Constraints constraint) {
147     return addAction(action, constraint, ActionManager.getInstance());
148   }
149
150   public final void add(@NotNull AnAction action, @NotNull Constraints constraint, @NotNull ActionManager actionManager) {
151     addAction(action, constraint, actionManager);
152   }
153
154   public final ActionInGroup addAction(@NotNull AnAction action, @NotNull Constraints constraint, @NotNull ActionManager actionManager) {
155     if (action == this) throw new IllegalArgumentException(CANT_ADD_ITSELF);
156     // Check that action isn't already registered
157     if (!(action instanceof Separator)) {
158       if (mySortedChildren.contains(action)) throw new ActionDuplicationException(action);
159       for (Pair<AnAction, Constraints> pair : myPairs) {
160         if (action.equals(pair.first)) throw new ActionDuplicationException(action);
161       }
162     }
163
164     constraint = (Constraints)constraint.clone();
165
166     if (constraint.myAnchor == Anchor.FIRST) {
167       mySortedChildren.add(0, action);
168     }
169     else if (constraint.myAnchor == Anchor.LAST) {
170       mySortedChildren.add(action);
171     }
172     else {
173       if (addToSortedList(action, constraint, actionManager)) {
174         actionAdded(action, actionManager);
175       }
176       else {
177         myPairs.add(Pair.create(action, constraint));
178       }
179     }
180
181     return new ActionInGroup(this, action);
182   }
183
184   private void actionAdded(AnAction addedAction, ActionManager actionManager) {
185     String addedActionId = addedAction instanceof ActionStub ? ((ActionStub)addedAction).getId() : actionManager.getId(addedAction);
186     if (addedActionId == null) {
187       return;
188     }
189     outer:
190     while (!myPairs.isEmpty()) {
191       for (int i = 0; i < myPairs.size(); i++) {
192         Pair<AnAction, Constraints> pair = myPairs.get(i);
193         if (addToSortedList(pair.first, pair.second, actionManager)) {
194           myPairs.remove(i);
195           continue outer;
196         }
197       }
198       break;
199     }
200   }
201
202   private boolean addToSortedList(@NotNull AnAction action, Constraints constraint, ActionManager actionManager) {
203     int index = findIndex(constraint.myRelativeToActionId, mySortedChildren, actionManager);
204     if (index == -1) {
205       return false;
206     }
207     if (constraint.myAnchor == Anchor.BEFORE) {
208       mySortedChildren.add(index, action);
209     }
210     else {
211       mySortedChildren.add(index + 1, action);
212     }
213     return true;
214   }
215
216   private static int findIndex(String actionId, List<? extends AnAction> actions, ActionManager actionManager) {
217     for (int i = 0; i < actions.size(); i++) {
218       AnAction action = actions.get(i);
219       if (action instanceof ActionStub) {
220         if (((ActionStub)action).getId().equals(actionId)) {
221           return i;
222         }
223       }
224       else {
225         String id = actionManager.getId(action);
226         if (id != null && id.equals(actionId)) {
227           return i;
228         }
229       }
230     }
231     return -1;
232   }
233
234   /**
235    * Removes specified action from group.
236    *
237    * @param action Action to be removed
238    */
239   public final void remove(AnAction action) {
240     if (!mySortedChildren.remove(action)) {
241       for (int i = 0; i < myPairs.size(); i++) {
242         Pair<AnAction, Constraints> pair = myPairs.get(i);
243         if (pair.first.equals(action)) {
244           myPairs.remove(i);
245           break;
246         }
247       }
248     }
249   }
250
251   /**
252    * Removes all children actions (separators as well) from the group.
253    */
254   public final void removeAll() {
255     mySortedChildren.clear();
256     myPairs.clear();
257   }
258
259
260   /**
261    * Replaces specified action with the a one.
262    */
263   public boolean replaceAction(@NotNull AnAction oldAction, @NotNull AnAction newAction) {
264     int index = mySortedChildren.indexOf(oldAction);
265     if (index >= 0) {
266       mySortedChildren.set(index, newAction);
267       return true;
268     }
269     else {
270       for (int i = 0; i < myPairs.size(); i++) {
271         Pair<AnAction, Constraints> pair = myPairs.get(i);
272         if (pair.first.equals(newAction)) {
273           myPairs.set(i, Pair.create(newAction, pair.second));
274           return true;
275         }
276       }
277     }
278     return false;
279   }
280
281   /**
282    * Copies content from <code>group</code>.
283    * @param other group to copy from
284    */
285   public void copyFromGroup(@NotNull DefaultActionGroup other) {
286     copyFrom(other);
287     setPopup(other.isPopup());
288
289     mySortedChildren.clear();
290     mySortedChildren.addAll(other.mySortedChildren);
291
292     myPairs.clear();
293     myPairs.addAll(other.myPairs);
294   }
295
296   /**
297    * Returns group's children in the order determined by constraints.
298    *
299    * @param e not used
300    * @return An array of children actions
301    */
302   @Override
303   @NotNull
304   public final AnAction[] getChildren(@Nullable AnActionEvent e) {
305     boolean hasNulls = false;
306
307     // Mix sorted actions and pairs
308     int sortedSize = mySortedChildren.size();
309     AnAction[] children = new AnAction[sortedSize + myPairs.size()];
310     for (int i = 0; i < sortedSize; i++) {
311       AnAction action = mySortedChildren.get(i);
312       if (action == null) {
313         LOG.error("Empty sorted child: " + this + ", " + getClass() + "; index=" + i);
314       }
315       if (action instanceof ActionStub) {
316         action = unStub(e, (ActionStub)action);
317         if (action == null) {
318           LOG.error("Can't unstub " + mySortedChildren.get(i));
319         }
320         else {
321           mySortedChildren.set(i, action);
322         }
323       }
324
325       hasNulls |= action == null;
326       children[i] = action;
327     }
328     for (int i = 0; i < myPairs.size(); i++) {
329       final Pair<AnAction, Constraints> pair = myPairs.get(i);
330       AnAction action = pair.first;
331       if (action == null) {
332         LOG.error("Empty pair child: " + this + ", " + getClass() + "; index=" + i);
333       }
334       else if (action instanceof ActionStub) {
335         action = unStub(e, (ActionStub)action);
336         if (action == null) {
337           LOG.error("Can't unstub " + pair);
338         }
339         else {
340           myPairs.set(i, Pair.create(action, pair.second));
341         }
342       }
343
344       hasNulls |= action == null;
345       children[i + sortedSize] = action;
346     }
347
348     if (hasNulls) {
349       return ContainerUtil.mapNotNull(children, FunctionUtil.id(), AnAction.EMPTY_ARRAY);
350     }
351     return children;
352   }
353
354   @Nullable
355   private AnAction unStub(@Nullable AnActionEvent e, final ActionStub stub) {
356     ActionManager actionManager = e != null ? e.getActionManager() : ActionManager.getInstance();
357     try {
358       AnAction action = actionManager.getAction(stub.getId());
359       if (action == null) {
360         LOG.error("Null child action in group " + this + " of class " + getClass() + ", id=" + stub.getId());
361         return null;
362       }
363       replace(stub, action);
364       return action;
365     }
366     catch (Throwable e1) {
367       LOG.error(e1);
368       return null;
369     }
370   }
371
372   /**
373    * Returns the number of contained children (including separators).
374    *
375    * @return number of children in the group
376    */
377   public final int getChildrenCount() {
378     return mySortedChildren.size() + myPairs.size();
379   }
380
381   @NotNull
382   public final AnAction[] getChildActionsOrStubs() {
383     // Mix sorted actions and pairs
384     int sortedSize = mySortedChildren.size();
385     AnAction[] children = new AnAction[sortedSize + myPairs.size()];
386     for (int i = 0; i < sortedSize; i++) {
387       children[i] = mySortedChildren.get(i);
388     }
389     for (int i = 0; i < myPairs.size(); i++) {
390       children[i + sortedSize] = myPairs.get(i).first;
391     }
392     return children;
393   }
394
395   public final void addAll(ActionGroup group) {
396     for (AnAction each : group.getChildren(null)) {
397       add(each);
398     }
399   }
400
401   public final void addAll(Collection<? extends AnAction> actionList) {
402     for (AnAction each : actionList) {
403       add(each);
404     }
405   }
406
407   public final void addAll(AnAction... actions) {
408     for (AnAction each : actions) {
409       add(each);
410     }
411   }
412
413   public void addSeparator(@Nullable String separatorText) {
414     add(new Separator(separatorText));
415   }
416
417   private static class ActionDuplicationException extends IllegalArgumentException {
418     public ActionDuplicationException(@NotNull AnAction action) {
419       super("cannot add an action twice: " + action);
420     }
421   }
422 }