Home | History | Annotate | Download | only in gle2
      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      */
    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      */
    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      */
    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      */
    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 
    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 
    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. */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    493         NestedParentMenu(String title, INode parent) {
    494             super(title);
    495             mParent = parent;
    496         }
    497 
    498         @Override
    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 
    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
    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 
    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 
    581         NestedChoiceMenu(Choices parentAction, List<INode> nodes) {
    582             super(parentAction.getTitle());
    583             mParentAction = parentAction;
    584             mNodes = nodes;
    585         }
    586 
    587         @Override
    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