diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java new file mode 100644 index 000000000..fc7127278 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java @@ -0,0 +1,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); + } + } + } +} |