aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
blob: fc71272787121c71e4137e0cf01b4204db41364c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.editors.layout.gle2;

import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW;
import static com.android.SdkConstants.FQCN_IMAGE_VIEW;
import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT;
import static com.android.SdkConstants.FQCN_TEXT_VIEW;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.SPINNER;
import static com.android.SdkConstants.VIEW_FRAGMENT;

import com.android.SdkConstants;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.RuleAction;
import com.android.ide.common.api.RuleAction.Choices;
import com.android.ide.common.api.RuleAction.NestedAction;
import com.android.ide.common.api.RuleAction.Toggle;
import com.android.ide.common.layout.BaseViewRule;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeViewAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractStyleAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UnwrapAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UseCompoundDrawableAction;
import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInAction;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.ContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Menu;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Helper class that is responsible for adding and managing the dynamic menu items
 * contributed by the {@link IViewRule} instances, based on the current selection
 * on the {@link LayoutCanvas}.
 * <p/>
 * This class is tied to a specific {@link LayoutCanvas} instance and a root {@link MenuManager}.
 * <p/>
 * Two instances of this are used: one created by {@link LayoutCanvas} and the other one
 * created by {@link OutlinePage}. Different root {@link MenuManager}s are populated, however
 * they are both linked to the current selection state of the {@link LayoutCanvas}.
 */
class DynamicContextMenu {
    public static String DEFAULT_ACTION_SHORTCUT = "F2"; //$NON-NLS-1$
    public static int DEFAULT_ACTION_KEY = SWT.F2;

    /** The XML layout editor that contains the canvas that uses this menu. */
    private final LayoutEditorDelegate mEditorDelegate;

    /** The layout canvas that displays this context menu. */
    private final LayoutCanvas mCanvas;

    /** The root menu manager of the context menu. */
    private final MenuManager mMenuManager;

    /**
     * Creates a new helper responsible for adding and managing the dynamic menu items
     * contributed by the {@link IViewRule} instances, based on the current selection
     * on the {@link LayoutCanvas}.
     * @param editorDelegate the editor owning the menu
     * @param canvas The {@link LayoutCanvas} providing the selection, the node factory and
     *   the rules engine.
     * @param rootMenu The root of the context menu displayed. In practice this may be the
     *   context menu manager of the {@link LayoutCanvas} or the one from {@link OutlinePage}.
     */
    public DynamicContextMenu(
            LayoutEditorDelegate editorDelegate,
            LayoutCanvas canvas,
            MenuManager rootMenu) {
        mEditorDelegate = editorDelegate;
        mCanvas = canvas;
        mMenuManager = rootMenu;

        setupDynamicMenuActions();
    }

    /**
     * Setups the menu manager to receive dynamic menu contributions from the {@link IViewRule}s
     * when it's about to be shown.
     */
    private void setupDynamicMenuActions() {
        // Remember how many static actions we have. Then each time the menu is
        // shown, find dynamic contributions based on the current selection and insert
        // them at the beginning of the menu.
        final int numStaticActions = mMenuManager.getSize();
        mMenuManager.addMenuListener(new IMenuListener() {
            @Override
            public void menuAboutToShow(IMenuManager manager) {

                // Remove any previous dynamic contributions to keep only the
                // default static items.
                int n = mMenuManager.getSize() - numStaticActions;
                if (n > 0) {
                    IContributionItem[] items = mMenuManager.getItems();
                    for (int i = 0; i < n; i++) {
                        mMenuManager.remove(items[i]);
                    }
                }

                // Now add all the dynamic menu actions depending on the current selection.
                populateDynamicContextMenu();
            }
        });

    }

    /**
     * This method is invoked by <code>menuAboutToShow</code> on {@link #mMenuManager}.
     * All previous dynamic menu actions have been removed and this method can now insert
     * any new actions that depend on the current selection.
     */
    private void populateDynamicContextMenu() {
        //  Create the actual menu contributions
        String endId = mMenuManager.getItems()[0].getId();

        Separator sep = new Separator();
        sep.setId("-dyn-gle-sep");  //$NON-NLS-1$
        mMenuManager.insertBefore(endId, sep);
        endId = sep.getId();

        List<SelectionItem> selections = mCanvas.getSelectionManager().getSelections();
        if (selections.size() == 0) {
            return;
        }
        List<INode> nodes = new ArrayList<INode>(selections.size());
        for (SelectionItem item : selections) {
            nodes.add(item.getNode());
        }

        List<IContributionItem> menuItems = getMenuItems(nodes);
        for (IContributionItem menuItem : menuItems) {
            mMenuManager.insertBefore(endId, menuItem);
        }

        insertTagSpecificMenus(endId);
        insertVisualRefactorings(endId);
        insertParentItems(endId);
    }

    /**
     * Returns the list of node-specific actions applicable to the given
     * collection of nodes
     *
     * @param nodes the collection of nodes to look up actions for
     * @return a list of contribution items applicable for all the nodes
     */
    private List<IContributionItem> getMenuItems(List<INode> nodes) {
        Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
        for (INode node : nodes) {
            List<RuleAction> actionList = getMenuActions((NodeProxy) node);
            allActions.put(node, actionList);
        }

        Set<String> availableIds = computeApplicableActionIds(allActions);

        // +10: Make room for separators too
        List<IContributionItem> items = new ArrayList<IContributionItem>(availableIds.size() + 10);

        // We'll use the actions returned by the first node. Even when there
        // are multiple items selected, we'll use the first action, but pass
        // the set of all selected nodes to that first action. Actions are required
        // to work this way to facilitate multi selection and actions which apply
        // to multiple nodes.
        NodeProxy first = (NodeProxy) nodes.get(0);
        List<RuleAction> firstSelectedActions = allActions.get(first);
        String defaultId = getDefaultActionId(first);
        for (RuleAction action : firstSelectedActions) {
            if (!availableIds.contains(action.getId())
                    && !(action instanceof RuleAction.Separator)) {
                // This action isn't supported by all selected items.
                continue;
            }

            items.add(createContributionItem(action, nodes, defaultId));
        }

        return items;
    }

    private void insertParentItems(String endId) {
        List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
        if (selection.size() == 1) {
            mMenuManager.insertBefore(endId, new Separator());
            INode parent = selection.get(0).getNode().getParent();
            while (parent != null) {
                String id = parent.getStringAttr(ANDROID_URI, ATTR_ID);
                String label;
                if (id != null && id.length() > 0) {
                    label = BaseViewRule.stripIdPrefix(id);
                } else {
                    // Use the view name, such as "Button", as the label
                    label = parent.getFqcn();
                    // Strip off package
                    label = label.substring(label.lastIndexOf('.') + 1);
                }
                mMenuManager.insertBefore(endId, new NestedParentMenu(label, parent));
                parent = parent.getParent();
            }
            mMenuManager.insertBefore(endId, new Separator());
        }
    }

    private void insertVisualRefactorings(String endId) {
        // Extract As <include> refactoring, Wrap In Refactoring, etc.
        List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
        if (selection.size() == 0) {
            return;
        }
        // Only include the menu item if you are not right clicking on a root,
        // or on an included view, or on a non-contiguous selection
        mMenuManager.insertBefore(endId, new Separator());
        if (selection.size() == 1 && selection.get(0).getViewInfo() != null
                && selection.get(0).getViewInfo().getName().equals(FQCN_LINEAR_LAYOUT)) {
            CanvasViewInfo info = selection.get(0).getViewInfo();
            List<CanvasViewInfo> children = info.getChildren();
            if (children.size() == 2) {
                String first = children.get(0).getName();
                String second = children.get(1).getName();
                if ((first.equals(FQCN_IMAGE_VIEW) && second.equals(FQCN_TEXT_VIEW))
                        || (first.equals(FQCN_TEXT_VIEW) && second.equals(FQCN_IMAGE_VIEW))) {
                    mMenuManager.insertBefore(endId, UseCompoundDrawableAction.create(
                            mEditorDelegate));
                }
            }
        }
        mMenuManager.insertBefore(endId, ExtractIncludeAction.create(mEditorDelegate));
        mMenuManager.insertBefore(endId, ExtractStyleAction.create(mEditorDelegate));
        mMenuManager.insertBefore(endId, WrapInAction.create(mEditorDelegate));
        if (selection.size() == 1 && !(selection.get(0).isRoot())) {
            mMenuManager.insertBefore(endId, UnwrapAction.create(mEditorDelegate));
        }
        if (selection.size() == 1 && (selection.get(0).isLayout() ||
                selection.get(0).getViewInfo().getName().equals(FQCN_GESTURE_OVERLAY_VIEW))) {
            mMenuManager.insertBefore(endId, ChangeLayoutAction.create(mEditorDelegate));
        } else {
            mMenuManager.insertBefore(endId, ChangeViewAction.create(mEditorDelegate));
        }
        mMenuManager.insertBefore(endId, new Separator());
    }

    /** "Preview List Content" pull-right menu for lists, "Preview Fragment" for fragments, etc. */
    private void insertTagSpecificMenus(String endId) {

        List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
        if (selection.size() == 0) {
            return;
        }
        for (SelectionItem item : selection) {
            UiViewElementNode node = item.getViewInfo().getUiViewNode();
            String name = node.getDescriptor().getXmlLocalName();
            boolean isGrid = name.equals(GRID_VIEW);
            boolean isSpinner = name.equals(SPINNER);
            if (name.equals(LIST_VIEW) || name.equals(EXPANDABLE_LIST_VIEW)
                    || isGrid || isSpinner) {
                mMenuManager.insertBefore(endId, new Separator());
                mMenuManager.insertBefore(endId, new ListViewTypeMenu(mCanvas, isGrid, isSpinner));
                return;
            } else if (name.equals(VIEW_FRAGMENT) && selection.size() == 1) {
                mMenuManager.insertBefore(endId, new Separator());
                mMenuManager.insertBefore(endId, new FragmentMenu(mCanvas));
                return;
            }
        }
    }

    /**
     * Given a map from selection items to list of applicable actions (produced
     * by {@link #computeApplicableActions()}) this method computes the set of
     * common actions and returns the action ids of these actions.
     *
     * @param actions a map from selection item to list of actions applicable to
     *            that selection item
     * @return set of action ids for the actions that are present in the action
     *         lists for all selected items
     */
    private Set<String> computeApplicableActionIds(Map<INode, List<RuleAction>> actions) {
        if (actions.size() > 1) {
            // More than one view is selected, so we have to filter down the available
            // actions such that only those actions that are defined for all the views
            // are shown
            Map<String, Integer> idCounts = new HashMap<String, Integer>();
            for (Map.Entry<INode, List<RuleAction>> entry : actions.entrySet()) {
                List<RuleAction> actionList = entry.getValue();
                for (RuleAction action : actionList) {
                    if (!action.supportsMultipleNodes()) {
                        continue;
                    }
                    String id = action.getId();
                    if (id != null) {
                        assert id != null : action;
                        Integer count = idCounts.get(id);
                        if (count == null) {
                            idCounts.put(id, Integer.valueOf(1));
                        } else {
                            idCounts.put(id, count + 1);
                        }
                    }
                }
            }
            Integer selectionCount = Integer.valueOf(actions.size());
            Set<String> validIds = new HashSet<String>(idCounts.size());
            for (Map.Entry<String, Integer> entry : idCounts.entrySet()) {
                Integer count = entry.getValue();
                if (selectionCount.equals(count)) {
                    String id = entry.getKey();
                    validIds.add(id);
                }
            }
            return validIds;
        } else {
            List<RuleAction> actionList = actions.values().iterator().next();
            Set<String> validIds = new HashSet<String>(actionList.size());
            for (RuleAction action : actionList) {
                String id = action.getId();
                validIds.add(id);
            }
            return validIds;
        }
    }

    /**
     * Returns the menu actions computed by the rule associated with this node.
     *
     * @param node the canvas node we need menu actions for
     * @return a list of {@link RuleAction} objects applicable to the node
     */
    private List<RuleAction> getMenuActions(NodeProxy node) {
        List<RuleAction> actions = mCanvas.getRulesEngine().callGetContextMenu(node);
        if (actions == null || actions.size() == 0) {
            return null;
        }

        return actions;
    }

    /**
     * Returns the default action id, or null
     *
     * @param node the node to look up the default action for
     * @return the action id, or null
     */
    private String getDefaultActionId(NodeProxy node) {
        return mCanvas.getRulesEngine().callGetDefaultActionId(node);
    }

    /**
     * Creates a {@link ContributionItem} for the given {@link RuleAction}.
     *
     * @param action the action to create a {@link ContributionItem} for
     * @param nodes the set of nodes the action should be applied to
     * @param defaultId if not non null, the id of an action which should be considered default
     * @return a new {@link ContributionItem} which implements the given action
     *         on the given nodes
     */
    private ContributionItem createContributionItem(final RuleAction action,
            final List<INode> nodes, final String defaultId) {
        if (action instanceof RuleAction.Separator) {
            return new Separator();
        } else if (action instanceof NestedAction) {
            NestedAction parentAction = (NestedAction) action;
            return new ActionContributionItem(new NestedActionMenu(parentAction, nodes));
        } else if (action instanceof Choices) {
            Choices parentAction = (Choices) action;
            return new ActionContributionItem(new NestedChoiceMenu(parentAction, nodes));
        } else if (action instanceof Toggle) {
            return new ActionContributionItem(createToggleAction(action, nodes));
        } else {
            return new ActionContributionItem(createPlainAction(action, nodes, defaultId));
        }
    }

    private Action createToggleAction(final RuleAction action, final List<INode> nodes) {
        Toggle toggleAction = (Toggle) action;
        final boolean isChecked = toggleAction.isChecked();
        Action a = new Action(action.getTitle(), IAction.AS_CHECK_BOX) {
            @Override
            public void run() {
                String label = createActionLabel(action, nodes);
                mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
                    @Override
                    public void run() {
                        action.getCallback().action(action, nodes,
                                null/* no valueId for a toggle */, !isChecked);
                        applyPendingChanges();
                    }
                });
            }
        };
        a.setId(action.getId());
        a.setChecked(isChecked);
        return a;
    }

    private IAction createPlainAction(final RuleAction action, final List<INode> nodes,
            final String defaultId) {
        IAction a = new Action(action.getTitle(), IAction.AS_PUSH_BUTTON) {
            @Override
            public void run() {
                String label = createActionLabel(action, nodes);
                mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
                    @Override
                    public void run() {
                        action.getCallback().action(action, nodes, null,
                                Boolean.TRUE);
                        applyPendingChanges();
                    }
                });
            }
        };

        String id = action.getId();
        if (defaultId != null && id.equals(defaultId)) {
            a.setAccelerator(DEFAULT_ACTION_KEY);
            String text = a.getText();
            text = text + '\t' + DEFAULT_ACTION_SHORTCUT;
            a.setText(text);

        } else if (ATTR_ID.equals(id)) {
            // Keep in sync with {@link LayoutCanvas#handleKeyPressed}
            if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
                a.setAccelerator('R' | SWT.MOD1 | SWT.MOD3);
                // Option+Command
                a.setText(a.getText().trim() + "\t\u2325\u2318R"); //$NON-NLS-1$
            } else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX) {
                a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
                a.setText(a.getText() + "\tShift+Alt+R"); //$NON-NLS-1$
            } else {
                a.setAccelerator('R' | SWT.MOD2 | SWT.MOD3);
                a.setText(a.getText() + "\tAlt+Shift+R"); //$NON-NLS-1$
            }
        }
        a.setId(id);
        return a;
    }

    private static String createActionLabel(final RuleAction action, final List<INode> nodes) {
        String label = action.getTitle();
        if (nodes.size() > 1) {
            label += String.format(" (%d elements)", nodes.size());
        }
        return label;
    }

    /**
     * The {@link NestedParentMenu} provides submenu content which adds actions
     * available on one of the selected node's parent nodes. This will be
     * similar to the menu content for the selected node, except the parent
     * menus will not be embedded within the nested menu.
     */
    private class NestedParentMenu extends SubmenuAction {
        INode mParent;

        NestedParentMenu(String title, INode parent) {
            super(title);
            mParent = parent;
        }

        @Override
        protected void addMenuItems(Menu menu) {
            List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
            if (selection.size() == 0) {
                return;
            }

            List<IContributionItem> menuItems = getMenuItems(Collections.singletonList(mParent));
            for (IContributionItem menuItem : menuItems) {
                menuItem.fill(menu, -1);
            }
        }
    }

    /**
     * The {@link NestedActionMenu} creates a lazily populated pull-right menu
     * where the children are {@link RuleAction}'s themselves.
     */
    private class NestedActionMenu extends SubmenuAction {
        private final NestedAction mParentAction;
        private final List<INode> mNodes;

        NestedActionMenu(NestedAction parentAction, List<INode> nodes) {
            super(parentAction.getTitle());
            mParentAction = parentAction;
            mNodes = nodes;

            assert mNodes.size() > 0;
        }

        @Override
        protected void addMenuItems(Menu menu) {
            Map<INode, List<RuleAction>> allActions = new HashMap<INode, List<RuleAction>>();
            for (INode node : mNodes) {
                List<RuleAction> actionList = mParentAction.getNestedActions(node);
                allActions.put(node, actionList);
            }

            Set<String> availableIds = computeApplicableActionIds(allActions);

            NodeProxy first = (NodeProxy) mNodes.get(0);
            String defaultId = getDefaultActionId(first);
            List<RuleAction> firstSelectedActions = allActions.get(first);

            int count = 0;
            for (RuleAction firstAction : firstSelectedActions) {
                if (!availableIds.contains(firstAction.getId())
                        && !(firstAction instanceof RuleAction.Separator)) {
                    // This action isn't supported by all selected items.
                    continue;
                }

                createContributionItem(firstAction, mNodes, defaultId).fill(menu, -1);
                count++;
            }

            if (count == 0) {
                addDisabledMessageItem("<Empty>");
            }
        }
    }

    private void applyPendingChanges() {
        LayoutCanvas canvas = mEditorDelegate.getGraphicalEditor().getCanvasControl();
        CanvasViewInfo root = canvas.getViewHierarchy().getRoot();
        if (root != null) {
            UiViewElementNode uiViewNode = root.getUiViewNode();
            NodeFactory nodeFactory = canvas.getNodeFactory();
            NodeProxy rootNode = nodeFactory.create(uiViewNode);
            if (rootNode != null) {
                rootNode.applyPendingChanges();
            }
        }
    }

    /**
     * The {@link NestedChoiceMenu} creates a lazily populated pull-right menu
     * where the items in the menu are strings
     */
    private class NestedChoiceMenu extends SubmenuAction {
        private final Choices mParentAction;
        private final List<INode> mNodes;

        NestedChoiceMenu(Choices parentAction, List<INode> nodes) {
            super(parentAction.getTitle());
            mParentAction = parentAction;
            mNodes = nodes;
        }

        @Override
        protected void addMenuItems(Menu menu) {
            List<String> titles = mParentAction.getTitles();
            List<String> ids = mParentAction.getIds();
            String current = mParentAction.getCurrent();
            assert titles.size() == ids.size();
            String[] currentValues = current != null
                    && current.indexOf(RuleAction.CHOICE_SEP) != -1 ?
                    current.split(RuleAction.CHOICE_SEP_PATTERN) : null;
            for (int i = 0, n = Math.min(titles.size(), ids.size()); i < n; i++) {
                final String id = ids.get(i);
                if (id == null || id.equals(RuleAction.SEPARATOR)) {
                    new Separator().fill(menu, -1);
                    continue;
                }

                // Find out whether this item is selected
                boolean select = false;
                if (current != null) {
                    // The current choice has a separator, so it's a flag with
                    // multiple values selected. Compare keys with the split
                    // values.
                    if (currentValues != null) {
                        if (current.indexOf(id) >= 0) {
                            for (String value : currentValues) {
                                if (id.equals(value)) {
                                    select = true;
                                    break;
                                }
                            }
                        }
                    } else {
                        // current choice has no separator, simply compare to the key
                        select = id.equals(current);
                    }
                }

                String title = titles.get(i);
                IAction a = new Action(title,
                        current != null ? IAction.AS_CHECK_BOX : IAction.AS_PUSH_BUTTON) {
                    @Override
                    public void runWithEvent(Event event) {
                        run();
                    }
                    @Override
                    public void run() {
                        String label = createActionLabel(mParentAction, mNodes);
                        mEditorDelegate.getEditor().wrapUndoEditXmlModel(label, new Runnable() {
                            @Override
                            public void run() {
                                mParentAction.getCallback().action(mParentAction, mNodes, id,
                                        Boolean.TRUE);
                                applyPendingChanges();
                            }
                        });
                    }
                };
                a.setId(id);
                a.setEnabled(true);
                if (select) {
                    a.setChecked(true);
                }

                new ActionContributionItem(a).fill(menu, -1);
            }
        }
    }
}