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