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