diff options
9 files changed, 259 insertions, 17 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 3fe90d2..a53859d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -482,6 +482,17 @@ public class BaseViewRule extends AbstractViewRule { actions.add(properties); } + @Override + @Nullable + public String getDefaultActionId(@NonNull final INode selectedNode) { + IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT); + if (textAttribute != null) { + return PROP_PREFIX + ATTR_TEXT; + } + + return null; + } + private static String getPropertyMapKey(INode node) { // Compute the key for mAttributesMap. This depends on the type of this // node and its parent in the view hierarchy. 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 index 680f417..27c98d0 100644 --- 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 @@ -28,6 +28,7 @@ import static com.android.ide.common.layout.LayoutConstants.SPINNER; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT; import static com.android.utils.XmlUtils.ANDROID_URI; +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; @@ -56,6 +57,7 @@ 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; @@ -79,6 +81,8 @@ import java.util.Set; * 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; @@ -197,8 +201,9 @@ class DynamicContextMenu { // 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. - List<RuleAction> firstSelectedActions = allActions.get(nodes.get(0)); - + 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)) { @@ -206,7 +211,7 @@ class DynamicContextMenu { continue; } - items.add(createContributionItem(action, nodes)); + items.add(createContributionItem(action, nodes, defaultId)); } return items; @@ -369,15 +374,26 @@ class DynamicContextMenu { } /** + * 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 List<INode> nodes, final String defaultId) { if (action instanceof RuleAction.Separator) { return new Separator(); } else if (action instanceof NestedAction) { @@ -389,7 +405,7 @@ class DynamicContextMenu { } else if (action instanceof Toggle) { return new ActionContributionItem(createToggleAction(action, nodes)); } else { - return new ActionContributionItem(createPlainAction(action, nodes)); + return new ActionContributionItem(createPlainAction(action, nodes, defaultId)); } } @@ -415,7 +431,8 @@ class DynamicContextMenu { return a; } - private IAction createPlainAction(final RuleAction action, final List<INode> nodes) { + 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() { @@ -430,7 +447,29 @@ class DynamicContextMenu { }); } }; - a.setId(action.getId()); + + 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; } @@ -495,7 +534,10 @@ class DynamicContextMenu { } Set<String> availableIds = computeApplicableActionIds(allActions); - List<RuleAction> firstSelectedActions = allActions.get(mNodes.get(0)); + + NodeProxy first = (NodeProxy) mNodes.get(0); + String defaultId = getDefaultActionId(first); + List<RuleAction> firstSelectedActions = allActions.get(first); int count = 0; for (RuleAction firstAction : firstSelectedActions) { @@ -505,7 +547,7 @@ class DynamicContextMenu { continue; } - createContributionItem(firstAction, mNodes).fill(menu, -1); + createContributionItem(firstAction, mNodes, defaultId).fill(menu, -1); count++; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index 3aa81ae..78c8cba 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -396,6 +396,23 @@ public class LayoutCanvas extends Canvas { mDeleteAction.run(); } else if (e.keyCode == SWT.ESC) { mSelectionManager.selectParent(); + } else if (e.keyCode == DynamicContextMenu.DEFAULT_ACTION_KEY) { + mSelectionManager.performDefaultAction(); + } else if (e.keyCode == 'r') { + // Keep key bindings in sync with {@link DynamicContextMenu#createPlainAction} + // TODO: Find a way to look up the Eclipse key bindings and attempt + // to use the current keymap's rename action. + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { + // Command+Option+R + if ((e.stateMask & (SWT.MOD1 | SWT.MOD3)) == (SWT.MOD1 | SWT.MOD3)) { + mSelectionManager.performRename(); + } + } else { + // Alt+Shift+R + if ((e.stateMask & (SWT.MOD2 | SWT.MOD3)) == (SWT.MOD2 | SWT.MOD3)) { + mSelectionManager.performRename(); + } + } } else { // Zooming actions char c = e.character; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java index 8ad7085..6f6259c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java @@ -15,28 +15,40 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE; import static com.android.ide.common.layout.LayoutConstants.FQCN_SPACE_V7; +import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS; +import static com.android.utils.XmlUtils.ANDROID_URI; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.api.INode; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.layout.BaseViewRule; import com.android.ide.common.layout.GridLayoutRule; +import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 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.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.resources.ResourceType; import com.android.utils.Pair; +import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.Separator; +import org.eclipse.jface.dialogs.InputDialog; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; @@ -45,6 +57,7 @@ import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MenuDetectEvent; import org.eclipse.swt.events.MouseEvent; @@ -1108,4 +1121,102 @@ public class SelectionManager implements ISelectionProvider { } return null; } + + /** Performs the default action provided by the currently selected view */ + public void performDefaultAction() { + final List<SelectionItem> selections = getSelections(); + if (selections.size() > 0) { + NodeProxy primary = selections.get(0).getNode(); + if (primary != null) { + RulesEngine rulesEngine = mCanvas.getRulesEngine(); + final String id = rulesEngine.callGetDefaultActionId(primary); + if (id == null) { + return; + } + final List<RuleAction> actions = rulesEngine.callGetContextMenu(primary); + if (actions == null) { + return; + } + RuleAction matching = null; + for (RuleAction a : actions) { + if (id.equals(a.getId())) { + matching = a; + break; + } + } + if (matching == null) { + return; + } + final List<INode> selectedNodes = new ArrayList<INode>(); + for (SelectionItem item : selections) { + NodeProxy n = item.getNode(); + if (n != null) { + selectedNodes.add(n); + } + } + final RuleAction action = matching; + mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(action.getTitle(), + new Runnable() { + @Override + public void run() { + action.getCallback().action(action, selectedNodes, + action.getId(), null); + LayoutCanvas canvas = mCanvas; + 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(); + } + } + } + }); + } + } + } + + /** Performs renaming the selected views */ + public void performRename() { + final List<SelectionItem> selections = getSelections(); + if (selections.size() > 0) { + NodeProxy primary = selections.get(0).getNode(); + if (primary != null) { + String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID); + currentId = BaseViewRule.stripIdPrefix(currentId); + InputDialog d = new InputDialog( + AdtPlugin.getDisplay().getActiveShell(), + "Set ID", + "New ID:", + currentId, + ResourceNameValidator.create(false, (IProject) null, ResourceType.ID)); + if (d.open() == Window.OK) { + final String s = d.getValue(); + mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID", + new Runnable() { + @Override + public void run() { + String newId = s; + newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s); + for (SelectionItem item : selections) { + item.getNode().setAttribute(ANDROID_URI, ATTR_ID, newId); + } + + LayoutCanvas canvas = mCanvas; + 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(); + } + } + } + }); + } + } + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index 7dc908b..616d4ab 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -19,6 +19,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre; import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IGraphics; @@ -154,6 +156,7 @@ public class RulesEngine { * @return Null if the rule failed, there's no rule or the rule does not provide * any custom menu actions. Otherwise, a list of {@link RuleAction}. */ + @Nullable public List<RuleAction> callGetContextMenu(NodeProxy selectedNode) { // try to find a rule for this element's FQCN IViewRule rule = loadRule(selectedNode.getNode()); @@ -177,6 +180,30 @@ public class RulesEngine { } /** + * Calls the selected node to return its default action + * + * @param selectedNode the node to apply the action to + * @return the default action id + */ + public String callGetDefaultActionId(@NonNull NodeProxy selectedNode) { + // try to find a rule for this element's FQCN + IViewRule rule = loadRule(selectedNode.getNode()); + + if (rule != null) { + try { + mInsertType = InsertType.CREATE; + return rule.getDefaultActionId(selectedNode); + } catch (Exception e) { + AdtPlugin.log(e, "%s.getDefaultAction() failed: %s", + rule.getClass().getSimpleName(), + e.toString()); + } + } + + return null; + } + + /** * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule * matching the specified element. * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java index 5208ed8..be11cf7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidator.java @@ -18,6 +18,8 @@ package com.android.ide.eclipse.adt.internal.resources; import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.resources.ResourceItem; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; @@ -219,14 +221,18 @@ public class ResourceNameValidator implements IInputValidator { * @param type the resource type of the resource name being validated * @return a new {@link ResourceNameValidator} */ - public static ResourceNameValidator create(boolean allowXmlExtension, IProject project, - ResourceType type) { - Set<String> existing = new HashSet<String>(); - ResourceManager manager = ResourceManager.getInstance(); - ProjectResources projectResources = manager.getProjectResources(project); - Collection<ResourceItem> items = projectResources.getResourceItemsOfType(type); - for (ResourceItem item : items) { - existing.add(item.getName()); + public static ResourceNameValidator create(boolean allowXmlExtension, + @Nullable IProject project, + @NonNull ResourceType type) { + Set<String> existing = null; + if (project != null) { + existing = new HashSet<String>(); + ResourceManager manager = ResourceManager.getInstance(); + ProjectResources projectResources = manager.getProjectResources(project); + Collection<ResourceItem> items = projectResources.getResourceItemsOfType(type); + for (ResourceItem item : items) { + existing.add(item.getName()); + } } boolean isFileType = ResourceHelper.isFileBasedResourceType(type); diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidatorTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidatorTest.java index af0ba2b..2cc2c93 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidatorTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/resources/ResourceNameValidatorTest.java @@ -19,6 +19,8 @@ package com.android.ide.eclipse.adt.internal.resources; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; +import org.eclipse.core.resources.IProject; + import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -64,6 +66,15 @@ public class ResourceNameValidatorTest extends TestCase { .isValid("_foo") != null); } + public void testIds() throws Exception { + ResourceNameValidator validator = ResourceNameValidator.create(false, (IProject) null, + ResourceType.ID); + assertTrue(validator.isValid("foo") == null); + assertTrue(validator.isValid(" foo") != null); + assertTrue(validator.isValid("foo ") != null); + assertTrue(validator.isValid("foo@") != null); + } + public void testUniqueOrExists() throws Exception { Set<String> existing = new HashSet<String>(); existing.add("foo1"); diff --git a/rule_api/src/com/android/ide/common/api/AbstractViewRule.java b/rule_api/src/com/android/ide/common/api/AbstractViewRule.java index 068580f..e23a567 100644 --- a/rule_api/src/com/android/ide/common/api/AbstractViewRule.java +++ b/rule_api/src/com/android/ide/common/api/AbstractViewRule.java @@ -63,6 +63,12 @@ public class AbstractViewRule implements IViewRule { } @Override + @Nullable + public String getDefaultActionId(@NonNull INode node) { + return null; + } + + @Override public void paintSelectionFeedback(@NonNull IGraphics graphics, @NonNull INode parentNode, @NonNull List<? extends INode> childNodes, @Nullable Object view) { } diff --git a/rule_api/src/com/android/ide/common/api/IViewRule.java b/rule_api/src/com/android/ide/common/api/IViewRule.java index b16df26..88b795f 100644 --- a/rule_api/src/com/android/ide/common/api/IViewRule.java +++ b/rule_api/src/com/android/ide/common/api/IViewRule.java @@ -102,6 +102,17 @@ public interface IViewRule { void addContextMenuActions(@NonNull List<RuleAction> actions, @NonNull INode node); /** + * Returns the id of the default action to invoke for this view, typically when the + * user presses F2. The id should correspond to the {@link RuleAction#getId()} returned + * by one of the actions added by {@link #addContextMenuActions(List, INode)}. + * + * @param node the primary selected node + * @return the id of default action, or null if none is default + */ + @Nullable + String getDefaultActionId(@NonNull INode node); + + /** * Invoked by the Rules Engine to ask the parent layout for the set of layout actions * to display in the layout bar. The layout rule should add these into the provided * list. The order the items are added in does not matter; the |