1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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 
17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
18 
19 import static com.android.SdkConstants.ANDROID_URI;
20 import static com.android.SdkConstants.ATTR_ID;
21 import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
22 import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
23 import static com.android.SdkConstants.FQCN_IMAGE_VIEW;
24 import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
25 import static com.android.SdkConstants.FQCN_TEXT_VIEW;
26 import static com.android.SdkConstants.GRID_VIEW;
27 import static com.android.SdkConstants.LIST_VIEW;
28 import static com.android.SdkConstants.SPINNER;
29 import static com.android.SdkConstants.VIEW_FRAGMENT;
30 
31 import com.android.SdkConstants;
32 import com.android.ide.common.api.INode;
33 import com.android.ide.common.api.IViewRule;
34 import com.android.ide.common.api.RuleAction;
35 import com.android.ide.common.api.RuleAction.Choices;
36 import com.android.ide.common.api.RuleAction.NestedAction;
37 import com.android.ide.common.api.RuleAction.Toggle;
38 import com.android.ide.common.layout.BaseViewRule;
39 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
40 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
41 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
42 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutAction;
43 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeViewAction;
44 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeAction;
45 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractStyleAction;
46 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UnwrapAction;
47 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UseCompoundDrawableAction;
48 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInAction;
49 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
50 
51 import org.eclipse.jface.action.Action;
52 import org.eclipse.jface.action.ActionContributionItem;
53 import org.eclipse.jface.action.ContributionItem;
54 import org.eclipse.jface.action.IAction;
55 import org.eclipse.jface.action.IContributionItem;
56 import org.eclipse.jface.action.IMenuListener;
57 import org.eclipse.jface.action.IMenuManager;
58 import org.eclipse.jface.action.MenuManager;
59 import org.eclipse.jface.action.Separator;
60 import org.eclipse.swt.SWT;
61 import org.eclipse.swt.widgets.Event;
62 import org.eclipse.swt.widgets.Menu;
63 
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.HashMap;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.Set;
71 
72 /**
73  * Helper class that is responsible for adding and managing the dynamic menu items
74  * contributed by the {@link IViewRule} instances, based on the current selection
75  * on the {@link LayoutCanvas}.
76  * <p/>
77  * This class is tied to a specific {@link LayoutCanvas} instance and a root {@link MenuManager}.
78  * <p/>
79  * Two instances of this are used: one created by {@link LayoutCanvas} and the other one
80  * created by {@link OutlinePage}. Different root {@link MenuManager}s are populated, however
81  * they are both linked to the current selection state of the {@link LayoutCanvas}.
82  */
83 class DynamicContextMenu {
84     public static String DEFAULT_ACTION_SHORTCUT = "F2"; //$NON-NLS-1$
85     public static int DEFAULT_ACTION_KEY = SWT.F2;
86 
87     /** The XML layout editor that contains the canvas that uses this menu. */
88     private final LayoutEditorDelegate mEditorDelegate;
89 
90     /** The layout canvas that displays this context menu. */
91     private final LayoutCanvas mCanvas;
92 
93     /** The root menu manager of the context menu. */
94     private final MenuManager mMenuManager;
95 
96     /**
97      * Creates a new helper responsible for adding and managing the dynamic menu items
98      * contributed by the {@link IViewRule} instances, based on the current selection
99      * on the {@link LayoutCanvas}.
100      * @param editorDelegate the editor owning the menu
101      * @param canvas The {@link LayoutCanvas} providing the selection, the node factory and
102      *   the rules engine.
103      * @param rootMenu The root of the context menu displayed. In practice this may be the
104      *   context menu manager of the {@link LayoutCanvas} or the one from {@link OutlinePage}.
105      */
DynamicContextMenu( LayoutEditorDelegate editorDelegate, LayoutCanvas canvas, MenuManager rootMenu)106     public DynamicContextMenu(
107             LayoutEditorDelegate editorDelegate,
108             LayoutCanvas canvas,
109             MenuManager rootMenu) {
110         mEditorDelegate = editorDelegate;
111         mCanvas = canvas;
112         mMenuManager = rootMenu;
113 
114         setupDynamicMenuActions();
115     }
116 
117     /**
118      * Setups the menu manager to receive dynamic menu contributions from the {@link IViewRule}s
119      * when it's about to be shown.
120      */
setupDynamicMenuActions()121     private void setupDynamicMenuActions() {
122         // Remember how many static actions we have. Then each time the menu is
123         // shown, find dynamic contributions based on the current selection and insert
124         // them at the beginning of the menu.
125         final int numStaticActions = mMenuManager.getSize();
126         mMenuManager.addMenuListener(new IMenuListener() {
127             @Override
128             public void menuAboutToShow(IMenuManager manager) {
129 
130                 // Remove any previous dynamic contributions to keep only the
131                 // default static items.
132                 int n = mMenuManager.getSize() - numStaticActions;
133                 if (n > 0) {
134                     IContributionItem[] items = mMenuManager.getItems();
135                     for (int i = 0; i < n; i++) {
136                         mMenuManager.remove(items[i]);
137                     }
138                 }
139 
140                 // Now add all the dynamic menu actions depending on the current selection.
141                 populateDynamicContextMenu();
142             }
143         });
144 
145     }
146 
147     /**
148      * This method is invoked by <code>menuAboutToShow</code> on {@link #mMenuManager}.
149      * All previous dynamic menu actions have been removed and this method can now insert
150      * any new actions that depend on the current selection.
151      */
populateDynamicContextMenu()152     private void populateDynamicContextMenu() {
153         //  Create the actual menu contributions
154         String endId = mMenuManager.getItems()[0].getId();
155 
156         Separator sep = new Separator();
157         sep.setId("-dyn-gle-sep");  //$NON-NLS-1$
158         mMenuManager.insertBefore(endId, sep);
159         endId = sep.getId();
160 
161         List<SelectionItem> selections = mCanvas.getSelectionManager().getSelections();
162         if (selections.size() == 0) {
163             return;
164         }
165         List<INode> nodes = new ArrayList<INode>(selections.size());
166         for (SelectionItem item : selections) {
167             nodes.add(item.getNode());
168         }
169 
170         List<IContributionItem> menuItems = getMenuItems(nodes);
171         for (IContributionItem menuItem : menuItems) {
172             mMenuManager.insertBefore(endId, menuItem);
173         }
174 
175         insertTagSpecificMenus(endId);
176         insertVisualRefactorings(endId);
177         insertParentItems(endId);
178     }
179 
180     /**
181      * Returns the list of node-specific actions applicable to the given
182      * collection of nodes
183      *
184      * @param nodes the collection of nodes to look up actions for
185      * @return a list of contribution items applicable for all the nodes
186      */
getMenuItems(List<INode> nodes)187     private List<IContributionItem> getMenuItems(List<INode> nodes) {
188         Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
189         for (INode node : nodes) {
190             List<RuleAction> actionList = getMenuActions((NodeProxy) node);
191             allActions.put(node, actionList);
192         }
193 
194         Set<String> availableIds = computeApplicableActionIds(allActions);
195 
196         // +10: Make room for separators too
197         List<IContributionItem> items = new ArrayList<IContributionItem>(availableIds.size() + 10);
198 
199         // We'll use the actions returned by the first node. Even when there
200         // are multiple items selected, we'll use the first action, but pass
201         // the set of all selected nodes to that first action. Actions are required
202         // to work this way to facilitate multi selection and actions which apply
203         // to multiple nodes.
204         NodeProxy first = (NodeProxy) nodes.get(0);
205         List<RuleAction> firstSelectedActions = allActions.get(first);
206         String defaultId = getDefaultActionId(first);
207         for (RuleAction action : firstSelectedActions) {
208             if (!availableIds.contains(action.getId())
209                     && !(action instanceof RuleAction.Separator)) {
210                 // This action isn't supported by all selected items.
211                 continue;
212             }
213 
214             items.add(createContributionItem(action, nodes, defaultId));
215         }
216 
217         return items;
218     }
219 
insertParentItems(String endId)220     private void insertParentItems(String endId) {
221         List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
222         if (selection.size() == 1) {
223             mMenuManager.insertBefore(endId, new Separator());
224             INode parent = selection.get(0).getNode().getParent();
225             while (parent != null) {
226                 String id = parent.getStringAttr(ANDROID_URI, ATTR_ID);
227                 String label;
228                 if (id != null && id.length() > 0) {
229                     label = BaseViewRule.stripIdPrefix(id);
230                 } else {
231                     // Use the view name, such as "Button", as the label
232                     label = parent.getFqcn();
233                     // Strip off package
234                     label = label.substring(label.lastIndexOf('.') + 1);
235                 }
236                 mMenuManager.insertBefore(endId, new NestedParentMenu(label, parent));
237                 parent = parent.getParent();
238             }
239             mMenuManager.insertBefore(endId, new Separator());
240         }
241     }
242 
insertVisualRefactorings(String endId)243     private void insertVisualRefactorings(String endId) {
244         // Extract As <include> refactoring, Wrap In Refactoring, etc.
245         List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
246         if (selection.size() == 0) {
247             return;
248         }
249         // Only include the menu item if you are not right clicking on a root,
250         // or on an included view, or on a non-contiguous selection
251         mMenuManager.insertBefore(endId, new Separator());
252         if (selection.size() == 1 && selection.get(0).getViewInfo() != null
253                 && selection.get(0).getViewInfo().getName().equals(FQCN_LINEAR_LAYOUT)) {
254             CanvasViewInfo info = selection.get(0).getViewInfo();
255             List<CanvasViewInfo> children = info.getChildren();
256             if (children.size() == 2) {
257                 String first = children.get(0).getName();
258                 String second = children.get(1).getName();
259                 if ((first.equals(FQCN_IMAGE_VIEW) && second.equals(FQCN_TEXT_VIEW))
260                         || (first.equals(FQCN_TEXT_VIEW) && second.equals(FQCN_IMAGE_VIEW))) {
261                     mMenuManager.insertBefore(endId, UseCompoundDrawableAction.create(
262                             mEditorDelegate));
263                 }
264             }
265         }
266         mMenuManager.insertBefore(endId, ExtractIncludeAction.create(mEditorDelegate));
267         mMenuManager.insertBefore(endId, ExtractStyleAction.create(mEditorDelegate));
268         mMenuManager.insertBefore(endId, WrapInAction.create(mEditorDelegate));
269         if (selection.size() == 1 && !(selection.get(0).isRoot())) {
270             mMenuManager.insertBefore(endId, UnwrapAction.create(mEditorDelegate));
271         }
272         if (selection.size() == 1 && (selection.get(0).isLayout() ||
273                 selection.get(0).getViewInfo().getName().equals(FQCN_GESTURE_OVERLAY_VIEW))) {
274             mMenuManager.insertBefore(endId, ChangeLayoutAction.create(mEditorDelegate));
275         } else {
276             mMenuManager.insertBefore(endId, ChangeViewAction.create(mEditorDelegate));
277         }
278         mMenuManager.insertBefore(endId, new Separator());
279     }
280 
281     /** "Preview List Content" pull-right menu for lists, "Preview Fragment" for fragments, etc. */
insertTagSpecificMenus(String endId)282     private void insertTagSpecificMenus(String endId) {
283 
284         List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
285         if (selection.size() == 0) {
286             return;
287         }
288         for (SelectionItem item : selection) {
289             UiViewElementNode node = item.getViewInfo().getUiViewNode();
290             String name = node.getDescriptor().getXmlLocalName();
291             boolean isGrid = name.equals(GRID_VIEW);
292             boolean isSpinner = name.equals(SPINNER);
293             if (name.equals(LIST_VIEW) || name.equals(EXPANDABLE_LIST_VIEW)
294                     || isGrid || isSpinner) {
295                 mMenuManager.insertBefore(endId, new Separator());
296                 mMenuManager.insertBefore(endId, new ListViewTypeMenu(mCanvas, isGrid, isSpinner));
297                 return;
298             } else if (name.equals(VIEW_FRAGMENT) && selection.size() == 1) {
299                 mMenuManager.insertBefore(endId, new Separator());
300                 mMenuManager.insertBefore(endId, new FragmentMenu(mCanvas));
301                 return;
302             }
303         }
304     }
305 
306     /**
307      * Given a map from selection items to list of applicable actions (produced
308      * by {@link #computeApplicableActions()}) this method computes the set of
309      * common actions and returns the action ids of these actions.
310      *
311      * @param actions a map from selection item to list of actions applicable to
312      *            that selection item
313      * @return set of action ids for the actions that are present in the action
314      *         lists for all selected items
315      */
computeApplicableActionIds(Map<INode, List<RuleAction>> actions)316     private Set<String> computeApplicableActionIds(Map<INode, List<RuleAction>> actions) {
317         if (actions.size() > 1) {
318             // More than one view is selected, so we have to filter down the available
319             // actions such that only those actions that are defined for all the views
320             // are shown
321             Map<String, Integer> idCounts = new HashMap<String, Integer>();
322             for (Map.Entry<INode, List<RuleAction>> entry : actions.entrySet()) {
323                 List<RuleAction> actionList = entry.getValue();
324                 for (RuleAction action : actionList) {
325                     if (!action.supportsMultipleNodes()) {
326                         continue;
327                     }
328                     String id = action.getId();
329                     if (id != null) {
330                         assert id != null : action;
331                         Integer count = idCounts.get(id);
332                         if (count == null) {
333                             idCounts.put(id, Integer.valueOf(1));
334                         } else {
335                             idCounts.put(id, count + 1);
336                         }
337                     }
338                 }
339             }
340             Integer selectionCount = Integer.valueOf(actions.size());
341             Set<String> validIds = new HashSet<String>(idCounts.size());
342             for (Map.Entry<String, Integer> entry : idCounts.entrySet()) {
343                 Integer count = entry.getValue();
344                 if (selectionCount.equals(count)) {
345                     String id = entry.getKey();
346                     validIds.add(id);
347                 }
348             }
349             return validIds;
350         } else {
351             List<RuleAction> actionList = actions.values().iterator().next();
352             Set<String> validIds = new HashSet<String>(actionList.size());
353             for (RuleAction action : actionList) {
354                 String id = action.getId();
355                 validIds.add(id);
356             }
357             return validIds;
358         }
359     }
360 
361     /**
362      * Returns the menu actions computed by the rule associated with this node.
363      *
364      * @param node the canvas node we need menu actions for
365      * @return a list of {@link RuleAction} objects applicable to the node
366      */
getMenuActions(NodeProxy node)367     private List<RuleAction> getMenuActions(NodeProxy node) {
368         List<RuleAction> actions = mCanvas.getRulesEngine().callGetContextMenu(node);
369         if (actions == null || actions.size() == 0) {
370             return null;
371         }
372 
373         return actions;
374     }
375 
376     /**
377      * Returns the default action id, or null
378      *
379      * @param node the node to look up the default action for
380      * @return the action id, or null
381      */
getDefaultActionId(NodeProxy node)382     private String getDefaultActionId(NodeProxy node) {
383         return mCanvas.getRulesEngine().callGetDefaultActionId(node);
384     }
385 
386     /**
387      * Creates a {@link ContributionItem} for the given {@link RuleAction}.
388      *
389      * @param action the action to create a {@link ContributionItem} for
390      * @param nodes the set of nodes the action should be applied to
391      * @param defaultId if not non null, the id of an action which should be considered default
392      * @return a new {@link ContributionItem} which implements the given action
393      *         on the given nodes
394      */
createContributionItem(final RuleAction action, final List<INode> nodes, final String defaultId)395     private ContributionItem createContributionItem(final RuleAction action,
396             final List<INode> nodes, final String defaultId) {
397         if (action instanceof RuleAction.Separator) {
398             return new Separator();
399         } else if (action instanceof NestedAction) {
400             NestedAction parentAction = (NestedAction) action;
401             return new ActionContributionItem(new NestedActionMenu(parentAction, nodes));
402         } else if (action instanceof Choices) {
403             Choices parentAction = (Choices) action;
404             return new ActionContributionItem(new NestedChoiceMenu(parentAction, nodes));
405         } else if (action instanceof Toggle) {
406             return new ActionContributionItem(createToggleAction(action, nodes));
407         } else {
408             return new ActionContributionItem(createPlainAction(action, nodes, defaultId));
409         }
410     }
411 
createToggleAction(final RuleAction action, final List<INode> nodes)412     private Action createToggleAction(final RuleAction action, final List<INode> nodes) {
413         Toggle toggleAction = (Toggle) action;
414         final boolean isChecked = toggleAction.isChecked();
415         Action a = new Action(action.getTitle(), IAction.AS_CHECK_BOX) {
416             @Override
417             public void run() {
418                 String label = createActionLabel(action, nodes);
419                 mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
420                     @Override
421                     public void run() {
422                         action.getCallback().action(action, nodes,
423                                 null/* no valueId for a toggle */, !isChecked);
424                         applyPendingChanges();
425                     }
426                 });
427             }
428         };
429         a.setId(action.getId());
430         a.setChecked(isChecked);
431         return a;
432     }
433 
createPlainAction(final RuleAction action, final List<INode> nodes, final String defaultId)434     private IAction createPlainAction(final RuleAction action, final List<INode> nodes,
435             final String defaultId) {
436         IAction a = new Action(action.getTitle(), IAction.AS_PUSH_BUTTON) {
437             @Override
438             public void run() {
439                 String label = createActionLabel(action, nodes);
440                 mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
441                     @Override
442                     public void run() {
443                         action.getCallback().action(action, nodes, null,
444                                 Boolean.TRUE);
445                         applyPendingChanges();
446                     }
447                 });
448             }
449         };
450 
451         String id = action.getId();
452         if (defaultId != null && id.equals(defaultId)) {
453             a.setAccelerator(DEFAULT_ACTION_KEY);
454             String text = a.getText();
455             text = text + '\t' + DEFAULT_ACTION_SHORTCUT;
456             a.setText(text);
457 
458         } else if (ATTR_ID.equals(id)) {
459             // Keep in sync with {@link LayoutCanvas#handleKeyPressed}
460             if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
461                 a.setAccelerator('R' | SWT.MOD1 | SWT.MOD3);
462                 // Option+Command
463                 a.setText(a.getText().trim() + "\t\u2325\u2318R"); //$NON-NLS-1$
464             } else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX) {
465                 a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
466                 a.setText(a.getText() + "\tShift+Alt+R"); //$NON-NLS-1$
467             } else {
468                 a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
469                 a.setText(a.getText() + "\tAlt+Shift+R"); //$NON-NLS-1$
470             }
471         }
472         a.setId(id);
473         return a;
474     }
475 
createActionLabel(final RuleAction action, final List<INode> nodes)476     private static String createActionLabel(final RuleAction action, final List<INode> nodes) {
477         String label = action.getTitle();
478         if (nodes.size() > 1) {
479             label += String.format(" (%d elements)", nodes.size());
480         }
481         return label;
482     }
483 
484     /**
485      * The {@link NestedParentMenu} provides submenu content which adds actions
486      * available on one of the selected node's parent nodes. This will be
487      * similar to the menu content for the selected node, except the parent
488      * menus will not be embedded within the nested menu.
489      */
490     private class NestedParentMenu extends SubmenuAction {
491         INode mParent;
492 
NestedParentMenu(String title, INode parent)493         NestedParentMenu(String title, INode parent) {
494             super(title);
495             mParent = parent;
496         }
497 
498         @Override
addMenuItems(Menu menu)499         protected void addMenuItems(Menu menu) {
500             List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
501             if (selection.size() == 0) {
502                 return;
503             }
504 
505             List<IContributionItem> menuItems = getMenuItems(Collections.singletonList(mParent));
506             for (IContributionItem menuItem : menuItems) {
507                 menuItem.fill(menu, -1);
508             }
509         }
510     }
511 
512     /**
513      * The {@link NestedActionMenu} creates a lazily populated pull-right menu
514      * where the children are {@link RuleAction}'s themselves.
515      */
516     private class NestedActionMenu extends SubmenuAction {
517         private final NestedAction mParentAction;
518         private final List<INode> mNodes;
519 
NestedActionMenu(NestedAction parentAction, List<INode> nodes)520         NestedActionMenu(NestedAction parentAction, List<INode> nodes) {
521             super(parentAction.getTitle());
522             mParentAction = parentAction;
523             mNodes = nodes;
524 
525             assert mNodes.size() > 0;
526         }
527 
528         @Override
addMenuItems(Menu menu)529         protected void addMenuItems(Menu menu) {
530             Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
531             for (INode node : mNodes) {
532                 List<RuleAction> actionList = mParentAction.getNestedActions(node);
533                 allActions.put(node, actionList);
534             }
535 
536             Set<String> availableIds = computeApplicableActionIds(allActions);
537 
538             NodeProxy first = (NodeProxy) mNodes.get(0);
539             String defaultId = getDefaultActionId(first);
540             List<RuleAction> firstSelectedActions = allActions.get(first);
541 
542             int count = 0;
543             for (RuleAction firstAction : firstSelectedActions) {
544                 if (!availableIds.contains(firstAction.getId())
545                         && !(firstAction instanceof RuleAction.Separator)) {
546                     // This action isn't supported by all selected items.
547                     continue;
548                 }
549 
550                 createContributionItem(firstAction, mNodes, defaultId).fill(menu, -1);
551                 count++;
552             }
553 
554             if (count == 0) {
555                 addDisabledMessageItem("<Empty>");
556             }
557         }
558     }
559 
applyPendingChanges()560     private void applyPendingChanges() {
561         LayoutCanvas canvas = mEditorDelegate.getGraphicalEditor().getCanvasControl();
562         CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
563         if (root != null) {
564             UiViewElementNode uiViewNode = root.getUiViewNode();
565             NodeFactory nodeFactory = canvas.getNodeFactory();
566             NodeProxy rootNode = nodeFactory.create(uiViewNode);
567             if (rootNode != null) {
568                 rootNode.applyPendingChanges();
569             }
570         }
571     }
572 
573     /**
574      * The {@link NestedChoiceMenu} creates a lazily populated pull-right menu
575      * where the items in the menu are strings
576      */
577     private class NestedChoiceMenu extends SubmenuAction {
578         private final Choices mParentAction;
579         private final List<INode> mNodes;
580 
NestedChoiceMenu(Choices parentAction, List<INode> nodes)581         NestedChoiceMenu(Choices parentAction, List<INode> nodes) {
582             super(parentAction.getTitle());
583             mParentAction = parentAction;
584             mNodes = nodes;
585         }
586 
587         @Override
addMenuItems(Menu menu)588         protected void addMenuItems(Menu menu) {
589             List<String> titles = mParentAction.getTitles();
590             List<String> ids = mParentAction.getIds();
591             String current = mParentAction.getCurrent();
592             assert titles.size() == ids.size();
593             String[] currentValues = current != null
594                     && current.indexOf(RuleAction.CHOICE_SEP) != -1 ?
595                     current.split(RuleAction.CHOICE_SEP_PATTERN) : null;
596             for (int i = 0, n = Math.min(titles.size(), ids.size()); i < n; i++) {
597                 final String id = ids.get(i);
598                 if (id == null || id.equals(RuleAction.SEPARATOR)) {
599                     new Separator().fill(menu, -1);
600                     continue;
601                 }
602 
603                 // Find out whether this item is selected
604                 boolean select = false;
605                 if (current != null) {
606                     // The current choice has a separator, so it's a flag with
607                     // multiple values selected. Compare keys with the split
608                     // values.
609                     if (currentValues != null) {
610                         if (current.indexOf(id) >= 0) {
611                             for (String value : currentValues) {
612                                 if (id.equals(value)) {
613                                     select = true;
614                                     break;
615                                 }
616                             }
617                         }
618                     } else {
619                         // current choice has no separator, simply compare to the key
620                         select = id.equals(current);
621                     }
622                 }
623 
624                 String title = titles.get(i);
625                 IAction a = new Action(title,
626                         current != null ? IAction.AS_CHECK_BOX : IAction.AS_PUSH_BUTTON) {
627                     @Override
628                     public void runWithEvent(Event event) {
629                         run();
630                     }
631                     @Override
632                     public void run() {
633                         String label = createActionLabel(mParentAction, mNodes);
634                         mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
635                             @Override
636                             public void run() {
637                                 mParentAction.getCallback().action(mParentAction, mNodes, id,
638                                         Boolean.TRUE);
639                                 applyPendingChanges();
640                             }
641                         });
642                     }
643                 };
644                 a.setId(id);
645                 a.setEnabled(true);
646                 if (select) {
647                     a.setChecked(true);
648                 }
649 
650                 new ActionContributionItem(a).fill(menu, -1);
651             }
652         }
653     }
654 }
655