blob: 8178c68718692a83404b6890711dcec0ecb35069 [file] [log] [blame]
/*
* 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_COLUMN_COUNT;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
import static com.android.SdkConstants.ATTR_ROW_COUNT;
import static com.android.SdkConstants.ATTR_SRC;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.AUTO_URI;
import static com.android.SdkConstants.DRAWABLE_PREFIX;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.URI_PREFIX;
import static org.eclipse.jface.viewers.StyledString.COUNTER_STYLER;
import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;
import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.InsertType;
import com.android.ide.common.layout.BaseLayoutRule;
import com.android.ide.common.layout.GridLayoutRule;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtUtils;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertySheetPage;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.utils.Pair;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
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.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.IElementComparer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MenuDetectEvent;
import org.eclipse.swt.events.MenuDetectListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.INullSelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
import org.eclipse.wb.core.controls.SelfOrientingSashForm;
import org.eclipse.wb.internal.core.editor.structure.IPage;
import org.eclipse.wb.internal.core.editor.structure.PageSiteComposite;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An outline page for the layout canvas view.
* <p/>
* The page is created by {@link LayoutEditorDelegate#delegateGetAdapter(Class)}. This means
* we have *one* instance of the outline page per open canvas editor.
* <p/>
* It sets itself as a listener on the site's selection service in order to be
* notified of the canvas' selection changes.
* The underlying page is also a selection provider (via IContentOutlinePage)
* and as such it will broadcast selection changes to the site's selection service
* (on which both the layout editor part and the property sheet page listen.)
*/
public class OutlinePage extends ContentOutlinePage
implements INullSelectionListener, IPage {
/** Label which separates outline text from additional attributes like text prefix or url */
private static final String LABEL_SEPARATOR = " - ";
/** Max character count in labels, used for truncation */
private static final int LABEL_MAX_WIDTH = 50;
/**
* The graphical editor that created this outline.
*/
private final GraphicalEditorPart mGraphicalEditorPart;
/**
* RootWrapper is a workaround: we can't set the input of the TreeView to its root
* element, so we introduce a fake parent.
*/
private final RootWrapper mRootWrapper = new RootWrapper();
/**
* Menu manager for the context menu actions.
* The actions delegate to the current GraphicalEditorPart.
*/
private MenuManager mMenuManager;
private Composite mControl;
private PropertySheetPage mPropertySheet;
private PageSiteComposite mPropertySheetComposite;
private boolean mShowPropertySheet;
private boolean mShowHeader;
private boolean mIgnoreSelection;
private boolean mActive = true;
/** Action to Select All in the tree */
private final Action mTreeSelectAllAction = new Action() {
@Override
public void run() {
getTreeViewer().getTree().selectAll();
OutlinePage.this.fireSelectionChanged(getSelection());
}
@Override
public String getId() {
return ActionFactory.SELECT_ALL.getId();
}
};
/** Action for moving items up in the tree */
private Action mMoveUpAction = new Action("Move Up\t-",
IconFactory.getInstance().getImageDescriptor("up")) { //$NON-NLS-1$
@Override
public String getId() {
return "adt.outline.moveup"; //$NON-NLS-1$
}
@Override
public boolean isEnabled() {
return canMove(false);
}
@Override
public void run() {
move(false);
}
};
/** Action for moving items down in the tree */
private Action mMoveDownAction = new Action("Move Down\t+",
IconFactory.getInstance().getImageDescriptor("down")) { //$NON-NLS-1$
@Override
public String getId() {
return "adt.outline.movedown"; //$NON-NLS-1$
}
@Override
public boolean isEnabled() {
return canMove(true);
}
@Override
public void run() {
move(true);
}
};
/**
* Creates a new {@link OutlinePage} associated with the given editor
*
* @param graphicalEditorPart the editor associated with this outline
*/
public OutlinePage(GraphicalEditorPart graphicalEditorPart) {
super();
mGraphicalEditorPart = graphicalEditorPart;
}
@Override
public Control getControl() {
// We've injected some controls between the root of the outline page
// and the tree control, so return the actual root (a sash form) rather
// than the superclass' implementation which returns the tree. If we don't
// do this, various checks in the outline page which checks that getControl().getParent()
// is the outline window itself will ignore this page.
return mControl;
}
void setActive(boolean active) {
if (active != mActive) {
mActive = active;
// Outlines are by default active when they are created; this is intended
// for deactivating a hidden outline and later reactivating it
assert mControl != null;
if (active) {
getSite().getPage().addSelectionListener(this);
setModel(mGraphicalEditorPart.getCanvasControl().getViewHierarchy().getRoot());
} else {
getSite().getPage().removeSelectionListener(this);
mRootWrapper.setRoot(null);
if (mPropertySheet != null) {
mPropertySheet.selectionChanged(null, TreeSelection.EMPTY);
}
}
}
}
/** Refresh all the icon state */
public void refreshIcons() {
TreeViewer treeViewer = getTreeViewer();
if (treeViewer != null) {
Tree tree = treeViewer.getTree();
if (tree != null && !tree.isDisposed()) {
treeViewer.refresh();
}
}
}
/**
* Set whether the outline should be shown in the header
*
* @param show whether a header should be shown
*/
public void setShowHeader(boolean show) {
mShowHeader = show;
}
/**
* Set whether the property sheet should be shown within this outline
*
* @param show whether the property sheet should show
*/
public void setShowPropertySheet(boolean show) {
if (show != mShowPropertySheet) {
mShowPropertySheet = show;
if (mControl == null) {
return;
}
if (show && mPropertySheet == null) {
createPropertySheet();
} else if (!show) {
mPropertySheetComposite.dispose();
mPropertySheetComposite = null;
mPropertySheet.dispose();
mPropertySheet = null;
}
mControl.layout();
}
}
@Override
public void createControl(Composite parent) {
mControl = new SelfOrientingSashForm(parent, SWT.VERTICAL);
if (mShowHeader) {
PageSiteComposite mOutlineComposite = new PageSiteComposite(mControl, SWT.BORDER);
mOutlineComposite.setTitleText("Outline");
mOutlineComposite.setTitleImage(IconFactory.getInstance().getIcon("components_view"));
mOutlineComposite.setPage(new IPage() {
@Override
public void createControl(Composite outlineParent) {
createOutline(outlineParent);
}
@Override
public void dispose() {
}
@Override
public Control getControl() {
return getTreeViewer().getTree();
}
@Override
public void setToolBar(IToolBarManager toolBarManager) {
makeContributions(null, toolBarManager, null);
toolBarManager.update(false);
}
@Override
public void setFocus() {
getControl().setFocus();
}
});
} else {
createOutline(mControl);
}
if (mShowPropertySheet) {
createPropertySheet();
}
}
private void createOutline(Composite parent) {
if (AdtUtils.isEclipse4()) {
// This is a workaround for the focus behavior in Eclipse 4 where
// the framework ends up calling setFocus() on the first widget in the outline
// AFTER a mouse click has been received. Specifically, if the user clicks in
// the embedded property sheet to for example give a Text property editor focus,
// then after the mouse click, the Outline window activation event is processed,
// and this event causes setFocus() to be called first on the PageBookView (which
// ends up calling setFocus on the first control, normally the TreeViewer), and
// then on the Page itself. We're dealing with the page setFocus() in the override
// of that method in the class, such that it does nothing.
// However, we have to also disable the setFocus on the first control in the
// outline page. To deal with that, we create our *own* first control in the
// outline, and make its setFocus() a no-op. We also make it invisible, since we
// don't actually want anything but the tree viewer showing in the outline.
Text text = new Text(parent, SWT.NONE) {
@Override
public boolean setFocus() {
// Focus no-op
return true;
}
@Override
protected void checkSubclass() {
// Disable the check that prevents subclassing of SWT components
}
};
text.setVisible(false);
}
super.createControl(parent);
TreeViewer tv = getTreeViewer();
tv.setAutoExpandLevel(2);
tv.setContentProvider(new ContentProvider());
tv.setLabelProvider(new LabelProvider());
tv.setInput(mRootWrapper);
tv.expandToLevel(mRootWrapper.getRoot(), 2);
int supportedOperations = DND.DROP_COPY | DND.DROP_MOVE;
Transfer[] transfers = new Transfer[] {
SimpleXmlTransfer.getInstance()
};
tv.addDropSupport(supportedOperations, transfers, new OutlineDropListener(this, tv));
tv.addDragSupport(supportedOperations, transfers, new OutlineDragListener(this, tv));
// The tree viewer will hold CanvasViewInfo instances, however these
// change each time the canvas is reloaded. OTOH layoutlib gives us
// constant UiView keys which we can use to perform tree item comparisons.
tv.setComparer(new IElementComparer() {
@Override
public int hashCode(Object element) {
if (element instanceof CanvasViewInfo) {
UiViewElementNode key = ((CanvasViewInfo) element).getUiViewNode();
if (key != null) {
return key.hashCode();
}
}
if (element != null) {
return element.hashCode();
}
return 0;
}
@Override
public boolean equals(Object a, Object b) {
if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) {
UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewNode();
UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewNode();
if (keyA != null) {
return keyA.equals(keyB);
}
}
if (a != null) {
return a.equals(b);
}
return false;
}
});
tv.addDoubleClickListener(new IDoubleClickListener() {
@Override
public void doubleClick(DoubleClickEvent event) {
// This used to open the property view, but now that properties are docked
// let's use it for something else -- such as showing the editor source
/*
// Front properties panel; its selection is already linked
IWorkbenchPage page = getSite().getPage();
try {
page.showView(IPageLayout.ID_PROP_SHEET, null, IWorkbenchPage.VIEW_ACTIVATE);
} catch (PartInitException e) {
AdtPlugin.log(e, "Could not activate property sheet");
}
*/
TreeItem[] selection = getTreeViewer().getTree().getSelection();
if (selection.length > 0) {
CanvasViewInfo vi = getViewInfo(selection[0].getData());
if (vi != null) {
LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
canvas.show(vi);
}
}
}
});
setupContextMenu();
// Listen to selection changes from the layout editor
getSite().getPage().addSelectionListener(this);
getControl().addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
dispose();
}
});
Tree tree = tv.getTree();
tree.addKeyListener(new KeyListener() {
@Override
public void keyPressed(KeyEvent e) {
if (e.character == '-') {
if (mMoveUpAction.isEnabled()) {
mMoveUpAction.run();
}
} else if (e.character == '+') {
if (mMoveDownAction.isEnabled()) {
mMoveDownAction.run();
}
}
}
@Override
public void keyReleased(KeyEvent e) {
}
});
setupTooltip();
}
/**
* This flag is true when the mouse button is being pressed somewhere inside
* the property sheet
*/
private boolean mPressInPropSheet;
private void createPropertySheet() {
mPropertySheetComposite = new PageSiteComposite(mControl, SWT.BORDER);
mPropertySheetComposite.setTitleText("Properties");
mPropertySheetComposite.setTitleImage(IconFactory.getInstance().getIcon("properties_view"));
mPropertySheet = new PropertySheetPage(mGraphicalEditorPart);
mPropertySheetComposite.setPage(mPropertySheet);
if (AdtUtils.isEclipse4()) {
mPropertySheet.getControl().addMouseListener(new MouseListener() {
@Override
public void mouseDown(MouseEvent e) {
mPressInPropSheet = true;
}
@Override
public void mouseUp(MouseEvent e) {
mPressInPropSheet = false;
}
@Override
public void mouseDoubleClick(MouseEvent e) {
}
});
}
}
@Override
public void setFocus() {
// Only call setFocus on the tree viewer if the mouse click isn't in the property
// sheet area
if (!mPressInPropSheet) {
super.setFocus();
}
}
@Override
public void dispose() {
mRootWrapper.setRoot(null);
getSite().getPage().removeSelectionListener(this);
super.dispose();
if (mPropertySheet != null) {
mPropertySheet.dispose();
mPropertySheet = null;
}
}
/**
* Invoked by {@link LayoutCanvas} to set the model (a.k.a. the root view info).
*
* @param rootViewInfo The root of the view info hierarchy. Can be null.
*/
public void setModel(CanvasViewInfo rootViewInfo) {
if (!mActive) {
return;
}
mRootWrapper.setRoot(rootViewInfo);
TreeViewer tv = getTreeViewer();
if (tv != null && !tv.getTree().isDisposed()) {
Object[] expanded = tv.getExpandedElements();
tv.refresh();
tv.setExpandedElements(expanded);
// Ensure that the root is expanded
tv.expandToLevel(rootViewInfo, 2);
}
}
/**
* Returns the current tree viewer selection. Shouldn't be null,
* although it can be {@link TreeSelection#EMPTY}.
*/
@Override
public ISelection getSelection() {
return super.getSelection();
}
/**
* Sets the outline selection.
*
* @param selection Only {@link ITreeSelection} will be used, otherwise the
* selection will be cleared (including a null selection).
*/
@Override
public void setSelection(ISelection selection) {
// TreeViewer should be able to deal with a null selection, but let's make it safe
if (selection == null) {
selection = TreeSelection.EMPTY;
}
if (selection.equals(TreeSelection.EMPTY)) {
return;
}
super.setSelection(selection);
TreeViewer tv = getTreeViewer();
if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) {
return;
}
// auto-reveal the selection
ITreeSelection treeSel = (ITreeSelection) selection;
for (TreePath p : treeSel.getPaths()) {
tv.expandToLevel(p, 1);
}
}
@Override
protected void fireSelectionChanged(ISelection selection) {
super.fireSelectionChanged(selection);
if (mPropertySheet != null && !mIgnoreSelection) {
mPropertySheet.selectionChanged(null, selection);
}
}
/**
* Listens to a workbench selection.
* Only listen on selection coming from {@link LayoutEditorDelegate}, which avoid
* picking up our own selections.
*/
@Override
public void selectionChanged(IWorkbenchPart part, ISelection selection) {
if (mIgnoreSelection) {
return;
}
if (part instanceof IEditorPart) {
LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor((IEditorPart) part);
if (delegate != null) {
try {
mIgnoreSelection = true;
setSelection(selection);
if (mPropertySheet != null) {
mPropertySheet.selectionChanged(part, selection);
}
} finally {
mIgnoreSelection = false;
}
}
}
}
@Override
public void selectionChanged(SelectionChangedEvent event) {
if (!mIgnoreSelection) {
super.selectionChanged(event);
}
}
// ----
/**
* In theory, the root of the model should be the input of the {@link TreeViewer},
* which would be the root {@link CanvasViewInfo}.
* That means in theory {@link ContentProvider#getElements(Object)} should return
* its own input as the single root node.
* <p/>
* However as described in JFace Bug 9262, this case is not properly handled by
* a {@link TreeViewer} and leads to an infinite recursion in the tree viewer.
* See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262
* <p/>
* The solution is to wrap the tree viewer input in a dummy root node that acts
* as a parent. This class does just that.
*/
private static class RootWrapper {
private CanvasViewInfo mRoot;
public void setRoot(CanvasViewInfo root) {
mRoot = root;
}
public CanvasViewInfo getRoot() {
return mRoot;
}
}
/** Return the {@link CanvasViewInfo} associated with the given TreeItem's data field */
/* package */ static CanvasViewInfo getViewInfo(Object viewData) {
if (viewData instanceof RootWrapper) {
return ((RootWrapper) viewData).getRoot();
}
if (viewData instanceof CanvasViewInfo) {
return (CanvasViewInfo) viewData;
}
return null;
}
// --- Content and Label Providers ---
/**
* Content provider for the Outline model.
* Objects are going to be {@link CanvasViewInfo}.
*/
private static class ContentProvider implements ITreeContentProvider {
@Override
public Object[] getChildren(Object element) {
if (element instanceof RootWrapper) {
CanvasViewInfo root = ((RootWrapper)element).getRoot();
if (root != null) {
return new Object[] { root };
}
}
if (element instanceof CanvasViewInfo) {
List<CanvasViewInfo> children = ((CanvasViewInfo) element).getUniqueChildren();
if (children != null) {
return children.toArray();
}
}
return new Object[0];
}
@Override
public Object getParent(Object element) {
if (element instanceof CanvasViewInfo) {
return ((CanvasViewInfo) element).getParent();
}
return null;
}
@Override
public boolean hasChildren(Object element) {
if (element instanceof CanvasViewInfo) {
List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
if (children != null) {
return children.size() > 0;
}
}
return false;
}
/**
* Returns the root element.
* Semantically, the root element is the single top-level XML element of the XML layout.
*/
@Override
public Object[] getElements(Object inputElement) {
return getChildren(inputElement);
}
@Override
public void dispose() {
// pass
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
// pass
}
}
/**
* Label provider for the Outline model.
* Objects are going to be {@link CanvasViewInfo}.
*/
private class LabelProvider extends StyledCellLabelProvider {
/**
* Returns the element's logo with a fallback on the android logo.
*
* @param element the tree element
* @return the image to be used as a logo
*/
public Image getImage(Object element) {
if (element instanceof CanvasViewInfo) {
element = ((CanvasViewInfo) element).getUiViewNode();
}
if (element instanceof UiViewElementNode) {
UiViewElementNode v = (UiViewElementNode) element;
return v.getIcon();
}
return AdtPlugin.getAndroidLogo();
}
/**
* Uses {@link UiElementNode#getStyledDescription} for the label for this tree item.
*/
@Override
public void update(ViewerCell cell) {
Object element = cell.getElement();
StyledString styledString = null;
CanvasViewInfo vi = null;
if (element instanceof CanvasViewInfo) {
vi = (CanvasViewInfo) element;
element = vi.getUiViewNode();
}
Image image = getImage(element);
if (element instanceof UiElementNode) {
UiElementNode node = (UiElementNode) element;
styledString = node.getStyledDescription();
Node xmlNode = node.getXmlNode();
if (xmlNode instanceof Element) {
Element e = (Element) xmlNode;
// Temporary diagnostics code when developing GridLayout
if (GridLayoutRule.sDebugGridLayout) {
String namespace;
if (e.getNodeName().equals(GRID_LAYOUT) ||
e.getParentNode() != null
&& e.getParentNode().getNodeName().equals(GRID_LAYOUT)) {
namespace = ANDROID_URI;
} else {
// Else: probably a v7 gridlayout
IProject project = mGraphicalEditorPart.getProject();
ProjectState projectState = Sdk.getProjectState(project);
if (projectState != null && projectState.isLibrary()) {
namespace = AUTO_URI;
} else {
ManifestInfo info = ManifestInfo.get(project);
namespace = URI_PREFIX + info.getPackage();
}
}
if (e.getNodeName() != null && e.getNodeName().endsWith(GRID_LAYOUT)) {
// Attach rowCount/columnCount info
String rowCount = e.getAttributeNS(namespace, ATTR_ROW_COUNT);
if (rowCount.length() == 0) {
rowCount = "?";
}
String columnCount = e.getAttributeNS(namespace, ATTR_COLUMN_COUNT);
if (columnCount.length() == 0) {
columnCount = "?";
}
styledString.append(" - columnCount=", QUALIFIER_STYLER);
styledString.append(columnCount, QUALIFIER_STYLER);
styledString.append(", rowCount=", QUALIFIER_STYLER);
styledString.append(rowCount, QUALIFIER_STYLER);
} else if (e.getParentNode() != null
&& e.getParentNode().getNodeName() != null
&& e.getParentNode().getNodeName().endsWith(GRID_LAYOUT)) {
// Attach row/column info
String row = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW);
if (row.length() == 0) {
row = "?";
}
Styler colStyle = QUALIFIER_STYLER;
String column = e.getAttributeNS(namespace, ATTR_LAYOUT_COLUMN);
if (column.length() == 0) {
column = "?";
} else {
String colCount = ((Element) e.getParentNode()).getAttributeNS(
namespace, ATTR_COLUMN_COUNT);
if (colCount.length() > 0 && Integer.parseInt(colCount) <=
Integer.parseInt(column)) {
colStyle = StyledString.createColorRegistryStyler(
JFacePreferences.ERROR_COLOR, null);
}
}
String rowSpan = e.getAttributeNS(namespace, ATTR_LAYOUT_ROW_SPAN);
String columnSpan = e.getAttributeNS(namespace,
ATTR_LAYOUT_COLUMN_SPAN);
if (rowSpan.length() == 0) {
rowSpan = "1";
}
if (columnSpan.length() == 0) {
columnSpan = "1";
}
styledString.append(" - cell (row=", QUALIFIER_STYLER);
styledString.append(row, QUALIFIER_STYLER);
styledString.append(',', QUALIFIER_STYLER);
styledString.append("col=", colStyle);
styledString.append(column, colStyle);
styledString.append(')', colStyle);
styledString.append(", span=(", QUALIFIER_STYLER);
styledString.append(columnSpan, QUALIFIER_STYLER);
styledString.append(',', QUALIFIER_STYLER);
styledString.append(rowSpan, QUALIFIER_STYLER);
styledString.append(')', QUALIFIER_STYLER);
String gravity = e.getAttributeNS(namespace, ATTR_LAYOUT_GRAVITY);
if (gravity != null && gravity.length() > 0) {
styledString.append(" : ", COUNTER_STYLER);
styledString.append(gravity, COUNTER_STYLER);
}
}
}
if (e.hasAttributeNS(ANDROID_URI, ATTR_TEXT)) {
// Show the text attribute
String text = e.getAttributeNS(ANDROID_URI, ATTR_TEXT);
if (text != null && text.length() > 0
&& !text.contains(node.getDescriptor().getUiName())) {
if (text.charAt(0) == '@') {
String resolved = mGraphicalEditorPart.findString(text);
if (resolved != null) {
text = resolved;
}
}
if (styledString.length() < LABEL_MAX_WIDTH - LABEL_SEPARATOR.length()
- 2) {
styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
styledString.append('"', QUALIFIER_STYLER);
styledString.append(truncate(text, styledString), QUALIFIER_STYLER);
styledString.append('"', QUALIFIER_STYLER);
}
}
} else if (e.hasAttributeNS(ANDROID_URI, ATTR_SRC)) {
// Show ImageView source attributes etc
String src = e.getAttributeNS(ANDROID_URI, ATTR_SRC);
if (src != null && src.length() > 0) {
if (src.startsWith(DRAWABLE_PREFIX)) {
src = src.substring(DRAWABLE_PREFIX.length());
}
styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
}
} else if (e.getTagName().equals(SdkConstants.VIEW_INCLUDE)) {
// Show the include reference.
// Note: the layout attribute is NOT in the Android namespace
String src = e.getAttribute(SdkConstants.ATTR_LAYOUT);
if (src != null && src.length() > 0) {
if (src.startsWith(LAYOUT_RESOURCE_PREFIX)) {
src = src.substring(LAYOUT_RESOURCE_PREFIX.length());
}
styledString.append(LABEL_SEPARATOR, QUALIFIER_STYLER);
styledString.append(truncate(src, styledString), QUALIFIER_STYLER);
}
}
}
} else if (element == null && vi != null) {
// It's an inclusion-context: display it
Reference includedWithin = mGraphicalEditorPart.getIncludedWithin();
if (includedWithin != null) {
styledString = new StyledString();
styledString.append(includedWithin.getDisplayName(), QUALIFIER_STYLER);
image = IconFactory.getInstance().getIcon(SdkConstants.VIEW_INCLUDE);
}
}
if (styledString == null) {
styledString = new StyledString();
styledString.append(element == null ? "(null)" : element.toString());
}
cell.setText(styledString.toString());
cell.setStyleRanges(styledString.getStyleRanges());
cell.setImage(image);
super.update(cell);
}
@Override
public boolean isLabelProperty(Object element, String property) {
return super.isLabelProperty(element, property);
}
}
// --- Context Menu ---
/**
* This viewer uses its own actions that delegate to the ones given
* by the {@link LayoutCanvas}. All the processing is actually handled
* directly by the canvas and this viewer only gets refreshed as a
* consequence of the canvas changing the XML model.
*/
private void setupContextMenu() {
mMenuManager = new MenuManager();
mMenuManager.removeAll();
mMenuManager.add(mMoveUpAction);
mMenuManager.add(mMoveDownAction);
mMenuManager.add(new Separator());
mMenuManager.add(new SelectionManager.SelectionMenu(mGraphicalEditorPart));
mMenuManager.add(new Separator());
final String prefix = LayoutCanvas.PREFIX_CANVAS_ACTION;
mMenuManager.add(new DelegateAction(prefix + ActionFactory.CUT.getId()));
mMenuManager.add(new DelegateAction(prefix + ActionFactory.COPY.getId()));
mMenuManager.add(new DelegateAction(prefix + ActionFactory.PASTE.getId()));
mMenuManager.add(new Separator());
mMenuManager.add(new DelegateAction(prefix + ActionFactory.DELETE.getId()));
mMenuManager.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(IMenuManager manager) {
// Update all actions to match their LayoutCanvas counterparts
for (IContributionItem contrib : manager.getItems()) {
if (contrib instanceof ActionContributionItem) {
IAction action = ((ActionContributionItem) contrib).getAction();
if (action instanceof DelegateAction) {
((DelegateAction) action).updateFromEditorPart(mGraphicalEditorPart);
}
}
}
}
});
new DynamicContextMenu(
mGraphicalEditorPart.getEditorDelegate(),
mGraphicalEditorPart.getCanvasControl(),
mMenuManager);
getTreeViewer().getTree().setMenu(mMenuManager.createContextMenu(getControl()));
// Update Move Up/Move Down state only when the menu is opened
getTreeViewer().getTree().addMenuDetectListener(new MenuDetectListener() {
@Override
public void menuDetected(MenuDetectEvent e) {
mMenuManager.update(IAction.ENABLED);
}
});
}
/**
* An action that delegates its properties and behavior to a target action.
* The target action can be null or it can change overtime, typically as the
* layout canvas' editor part is activated or closed.
*/
private static class DelegateAction extends Action {
private IAction mTargetAction;
private final String mCanvasActionId;
public DelegateAction(String canvasActionId) {
super(canvasActionId);
setId(canvasActionId);
mCanvasActionId = canvasActionId;
}
// --- Methods form IAction ---
/** Returns the target action's {@link #isEnabled()} if defined, or false. */
@Override
public boolean isEnabled() {
return mTargetAction == null ? false : mTargetAction.isEnabled();
}
/** Returns the target action's {@link #isChecked()} if defined, or false. */
@Override
public boolean isChecked() {
return mTargetAction == null ? false : mTargetAction.isChecked();
}
/** Returns the target action's {@link #isHandled()} if defined, or false. */
@Override
public boolean isHandled() {
return mTargetAction == null ? false : mTargetAction.isHandled();
}
/** Runs the target action if defined. */
@Override
public void run() {
if (mTargetAction != null) {
mTargetAction.run();
}
super.run();
}
/**
* Updates this action to delegate to its counterpart in the given editor part
*
* @param editorPart The editor being updated
*/
public void updateFromEditorPart(GraphicalEditorPart editorPart) {
LayoutCanvas canvas = editorPart == null ? null : editorPart.getCanvasControl();
if (canvas == null) {
mTargetAction = null;
} else {
mTargetAction = canvas.getAction(mCanvasActionId);
}
if (mTargetAction != null) {
setText(mTargetAction.getText());
setId(mTargetAction.getId());
setDescription(mTargetAction.getDescription());
setImageDescriptor(mTargetAction.getImageDescriptor());
setHoverImageDescriptor(mTargetAction.getHoverImageDescriptor());
setDisabledImageDescriptor(mTargetAction.getDisabledImageDescriptor());
setToolTipText(mTargetAction.getToolTipText());
setActionDefinitionId(mTargetAction.getActionDefinitionId());
setHelpListener(mTargetAction.getHelpListener());
setAccelerator(mTargetAction.getAccelerator());
setChecked(mTargetAction.isChecked());
setEnabled(mTargetAction.isEnabled());
} else {
setEnabled(false);
}
}
}
/** Returns the associated editor with this outline */
/* package */GraphicalEditorPart getEditor() {
return mGraphicalEditorPart;
}
@Override
public void setActionBars(IActionBars actionBars) {
super.setActionBars(actionBars);
// Map Outline actions to canvas actions such that they share Undo context etc
LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
canvas.updateGlobalActions(actionBars);
// Special handling for Select All since it's different than the canvas (will
// include selecting the root etc)
actionBars.setGlobalActionHandler(mTreeSelectAllAction.getId(), mTreeSelectAllAction);
actionBars.updateActionBars();
}
// ---- Move Up/Down Support ----
/** Returns true if the current selected item can be moved */
private boolean canMove(boolean forward) {
CanvasViewInfo viewInfo = getSingleSelectedItem();
if (viewInfo != null) {
UiViewElementNode node = viewInfo.getUiViewNode();
if (forward) {
return findNext(node) != null;
} else {
return findPrevious(node) != null;
}
}
return false;
}
/** Moves the current selected item down (forward) or up (not forward) */
private void move(boolean forward) {
CanvasViewInfo viewInfo = getSingleSelectedItem();
if (viewInfo != null) {
final Pair<UiViewElementNode, Integer> target;
UiViewElementNode selected = viewInfo.getUiViewNode();
if (forward) {
target = findNext(selected);
} else {
target = findPrevious(selected);
}
if (target != null) {
final LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
final SelectionManager selectionManager = canvas.getSelectionManager();
final ArrayList<SelectionItem> dragSelection = new ArrayList<SelectionItem>();
dragSelection.add(selectionManager.createSelection(viewInfo));
SelectionManager.sanitize(dragSelection);
if (!dragSelection.isEmpty()) {
final SimpleElement[] elements = SelectionItem.getAsElements(dragSelection);
UiViewElementNode parentNode = target.getFirst();
final NodeProxy targetNode = canvas.getNodeFactory().create(parentNode);
// Record children of the target right before the drop (such that we
// can find out after the drop which exact children were inserted)
Set<INode> children = new HashSet<INode>();
for (INode node : targetNode.getChildren()) {
children.add(node);
}
String label = MoveGesture.computeUndoLabel(targetNode,
elements, DND.DROP_MOVE);
canvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
@Override
public void run() {
InsertType insertType = InsertType.MOVE_INTO;
if (dragSelection.get(0).getNode().getParent() == targetNode) {
insertType = InsertType.MOVE_WITHIN;
}
canvas.getRulesEngine().setInsertType(insertType);
int index = target.getSecond();
BaseLayoutRule.insertAt(targetNode, elements, false, index);
targetNode.applyPendingChanges();
canvas.getClipboardSupport().deleteSelection("Remove", dragSelection);
}
});
// Now find out which nodes were added, and look up their
// corresponding CanvasViewInfos
final List<INode> added = new ArrayList<INode>();
for (INode node : targetNode.getChildren()) {
if (!children.contains(node)) {
added.add(node);
}
}
selectionManager.setOutlineSelection(added);
}
}
}
}
/**
* Returns the {@link CanvasViewInfo} for the currently selected item, or null if
* there are no or multiple selected items
*
* @return the current selected item if there is exactly one item selected
*/
private CanvasViewInfo getSingleSelectedItem() {
TreeItem[] selection = getTreeViewer().getTree().getSelection();
if (selection.length == 1) {
return getViewInfo(selection[0].getData());
}
return null;
}
/** Returns the pair [parent,index] of the next node (when iterating forward) */
@VisibleForTesting
/* package */ static Pair<UiViewElementNode, Integer> findNext(UiViewElementNode node) {
UiElementNode parent = node.getUiParent();
if (parent == null) {
return null;
}
UiElementNode next = node.getUiNextSibling();
if (next != null) {
if (DescriptorsUtils.canInsertChildren(next.getDescriptor(), null)) {
return getFirstPosition(next);
} else {
return getPositionAfter(next);
}
}
next = parent.getUiNextSibling();
if (next != null) {
return getPositionBefore(next);
} else {
UiElementNode grandParent = parent.getUiParent();
if (grandParent != null) {
return getLastPosition(grandParent);
}
}
return null;
}
/** Returns the pair [parent,index] of the previous node (when iterating backward) */
@VisibleForTesting
/* package */ static Pair<UiViewElementNode, Integer> findPrevious(UiViewElementNode node) {
UiElementNode prev = node.getUiPreviousSibling();
if (prev != null) {
UiElementNode curr = prev;
while (true) {
List<UiElementNode> children = curr.getUiChildren();
if (children.size() > 0) {
curr = children.get(children.size() - 1);
continue;
}
if (DescriptorsUtils.canInsertChildren(curr.getDescriptor(), null)) {
return getFirstPosition(curr);
} else {
if (curr == prev) {
return getPositionBefore(curr);
} else {
return getPositionAfter(curr);
}
}
}
}
return getPositionBefore(node.getUiParent());
}
/** Returns the pair [parent,index] of the position immediately before the given node */
private static Pair<UiViewElementNode, Integer> getPositionBefore(UiElementNode node) {
if (node != null) {
UiElementNode parent = node.getUiParent();
if (parent != null && parent instanceof UiViewElementNode) {
return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex());
}
}
return null;
}
/** Returns the pair [parent,index] of the position immediately following the given node */
private static Pair<UiViewElementNode, Integer> getPositionAfter(UiElementNode node) {
if (node != null) {
UiElementNode parent = node.getUiParent();
if (parent != null && parent instanceof UiViewElementNode) {
return Pair.of((UiViewElementNode) parent, node.getUiSiblingIndex() + 1);
}
}
return null;
}
/** Returns the pair [parent,index] of the first position inside the given parent */
private static Pair<UiViewElementNode, Integer> getFirstPosition(UiElementNode parent) {
if (parent != null && parent instanceof UiViewElementNode) {
return Pair.of((UiViewElementNode) parent, 0);
}
return null;
}
/**
* Returns the pair [parent,index] of the last position after the given node's
* children
*/
private static Pair<UiViewElementNode, Integer> getLastPosition(UiElementNode parent) {
if (parent != null && parent instanceof UiViewElementNode) {
return Pair.of((UiViewElementNode) parent, parent.getUiChildren().size());
}
return null;
}
/**
* Truncates the given text such that it will fit into the given {@link StyledString}
* up to a maximum length of {@link #LABEL_MAX_WIDTH}.
*
* @param text the text to truncate
* @param string the existing string to be appended to
* @return the truncated string
*/
private static String truncate(String text, StyledString string) {
int existingLength = string.length();
if (text.length() + existingLength > LABEL_MAX_WIDTH) {
int truncatedLength = LABEL_MAX_WIDTH - existingLength - 3;
if (truncatedLength > 0) {
return String.format("%1$s...", text.substring(0, truncatedLength));
} else {
return ""; //$NON-NLS-1$
}
}
return text;
}
@Override
public void setToolBar(IToolBarManager toolBarManager) {
makeContributions(null, toolBarManager, null);
toolBarManager.update(false);
}
/**
* Sets up a custom tooltip when hovering over tree items. It currently displays the error
* message for the lint warning associated with each node, if any (and only if the hover
* is over the icon portion).
*/
private void setupTooltip() {
final Tree tree = getTreeViewer().getTree();
// This is based on SWT Snippet 125
final Listener listener = new Listener() {
Shell mTip = null;
Label mLabel = null;
@Override
public void handleEvent(Event event) {
switch(event.type) {
case SWT.Dispose:
case SWT.KeyDown:
case SWT.MouseExit:
case SWT.MouseDown:
case SWT.MouseMove:
if (mTip != null) {
mTip.dispose();
mTip = null;
mLabel = null;
}
break;
case SWT.MouseHover:
if (mTip != null) {
mTip.dispose();
mTip = null;
mLabel = null;
}
String tooltip = null;
TreeItem item = tree.getItem(new Point(event.x, event.y));
if (item != null) {
Rectangle rect = item.getBounds(0);
if (event.x - rect.x > 16) { // 16: Standard width of our outline icons
return;
}
Object data = item.getData();
if (data != null && data instanceof CanvasViewInfo) {
LayoutEditorDelegate editor = mGraphicalEditorPart.getEditorDelegate();
CanvasViewInfo vi = (CanvasViewInfo) data;
IMarker marker = editor.getIssueForNode(vi.getUiViewNode());
if (marker != null) {
tooltip = marker.getAttribute(IMarker.MESSAGE, null);
}
}
if (tooltip != null) {
Shell shell = tree.getShell();
Display display = tree.getDisplay();
Color fg = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
Color bg = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
mTip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
mTip.setBackground(bg);
FillLayout layout = new FillLayout();
layout.marginWidth = 1;
layout.marginHeight = 1;
mTip.setLayout(layout);
mLabel = new Label(mTip, SWT.WRAP);
mLabel.setForeground(fg);
mLabel.setBackground(bg);
mLabel.setText(tooltip);
mLabel.addListener(SWT.MouseExit, this);
mLabel.addListener(SWT.MouseDown, this);
Point pt = tree.toDisplay(rect.x, rect.y + rect.height);
Rectangle displayBounds = display.getBounds();
// -10: Don't extend -all- the way to the edge of the screen
// which would make it look like it has been cropped
int availableWidth = displayBounds.x + displayBounds.width - pt.x - 10;
if (availableWidth < 80) {
availableWidth = 80;
}
Point size = mTip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
if (size.x > availableWidth) {
size = mTip.computeSize(availableWidth, SWT.DEFAULT);
}
mTip.setBounds(pt.x, pt.y, size.x, size.y);
mTip.setVisible(true);
}
}
}
}
};
tree.addListener(SWT.Dispose, listener);
tree.addListener(SWT.KeyDown, listener);
tree.addListener(SWT.MouseMove, listener);
tree.addListener(SWT.MouseHover, listener);
}
}