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 com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 21 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; 22 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 23 import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite; 24 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 25 26 import org.eclipse.jface.viewers.IElementComparer; 27 import org.eclipse.jface.viewers.ILabelProvider; 28 import org.eclipse.jface.viewers.ILabelProviderListener; 29 import org.eclipse.jface.viewers.ISelection; 30 import org.eclipse.jface.viewers.ITreeContentProvider; 31 import org.eclipse.jface.viewers.ITreeSelection; 32 import org.eclipse.jface.viewers.TreePath; 33 import org.eclipse.jface.viewers.TreeSelection; 34 import org.eclipse.jface.viewers.TreeViewer; 35 import org.eclipse.jface.viewers.Viewer; 36 import org.eclipse.swt.graphics.Image; 37 import org.eclipse.swt.widgets.Composite; 38 import org.eclipse.ui.INullSelectionListener; 39 import org.eclipse.ui.ISelectionListener; 40 import org.eclipse.ui.IWorkbenchPart; 41 import org.eclipse.ui.views.contentoutline.ContentOutlinePage; 42 43 import java.util.ArrayList; 44 45 /* 46 * TODO -- missing features: 47 * - right-click context menu *shared* with the one from canvas (simply delegate action) 48 * - drag'n'drop initiated from Palette to Outline 49 * - drag'n'drop from Outline to Outline 50 * - drag'n'drop from Canvas to Outline 51 * - drag'n'drop from Outline to Canvas 52 * - => Check if we can handle all the d'n'd cases a simply delegating the action to the canvas. 53 * There's a *lot* of logic in the CanvasDropListener we don't want to replicate. 54 * That should be fairly trivial, except that we can't provide X/Y coordinates in the drop 55 * move. We could just fake them by using the topleft/middle point of the tree item's bounds 56 * or something like that. 57 */ 58 59 /** 60 * An outline page for the GLE2 canvas view. 61 * <p/> 62 * The page is created by {@link LayoutEditor#getAdapter(Class)}. 63 * It sets itself as a listener on the site's selection service in order to be 64 * notified of the canvas' selection changes. 65 * The underlying page is also a selection provider (via IContentOutlinePage) 66 * and as such it will broadcast selection changes to the site's selection service 67 * (on which both the layout editor part and the property sheet page listen.) 68 * 69 * @since GLE2 70 */ 71 public class OutlinePage2 extends ContentOutlinePage 72 implements ISelectionListener, INullSelectionListener { 73 74 /** 75 * RootWrapper is a workaround: we can't set the input of the treeview to its root 76 * element, so we introduce a fake parent. 77 */ 78 private final RootWrapper mRootWrapper = new RootWrapper(); 79 OutlinePage2()80 public OutlinePage2() { 81 super(); 82 } 83 84 @Override createControl(Composite parent)85 public void createControl(Composite parent) { 86 super.createControl(parent); 87 88 TreeViewer tv = getTreeViewer(); 89 tv.setAutoExpandLevel(2); 90 tv.setContentProvider(new ContentProvider()); 91 tv.setLabelProvider(new LabelProvider()); 92 tv.setInput(mRootWrapper); 93 94 // The tree viewer will hold CanvasViewInfo instances, however these 95 // change each time the canvas is reloaded. OTOH liblayout gives us 96 // constant UiView keys which we can use to perform tree item comparisons. 97 tv.setComparer(new IElementComparer() { 98 public int hashCode(Object element) { 99 if (element instanceof CanvasViewInfo) { 100 UiViewElementNode key = ((CanvasViewInfo) element).getUiViewKey(); 101 if (key != null) { 102 return key.hashCode(); 103 } 104 } 105 if (element != null) { 106 return element.hashCode(); 107 } 108 return 0; 109 } 110 111 public boolean equals(Object a, Object b) { 112 if (a instanceof CanvasViewInfo && b instanceof CanvasViewInfo) { 113 UiViewElementNode keyA = ((CanvasViewInfo) a).getUiViewKey(); 114 UiViewElementNode keyB = ((CanvasViewInfo) b).getUiViewKey(); 115 if (keyA != null) { 116 return keyA.equals(keyB); 117 } 118 } 119 if (a != null) { 120 return a.equals(b); 121 } 122 return false; 123 } 124 }); 125 126 // Listen to selection changes from the layout editor 127 getSite().getPage().addSelectionListener(this); 128 } 129 130 @Override dispose()131 public void dispose() { 132 mRootWrapper.setRoot(null); 133 getSite().getPage().removeSelectionListener(this); 134 super.dispose(); 135 } 136 137 /** 138 * Invoked by {@link LayoutCanvas} to set the model (aka the root view info). 139 * 140 * @param rootViewInfo The root of the view info hierarchy. Can be null. 141 */ setModel(CanvasViewInfo rootViewInfo)142 public void setModel(CanvasViewInfo rootViewInfo) { 143 mRootWrapper.setRoot(rootViewInfo); 144 145 TreeViewer tv = getTreeViewer(); 146 if (tv != null) { 147 Object[] expanded = tv.getExpandedElements(); 148 tv.refresh(); 149 tv.setExpandedElements(expanded); 150 } 151 } 152 153 /** 154 * Returns the current tree viewer selection. Shouldn't be null, 155 * although it can be {@link TreeSelection#EMPTY}. 156 */ 157 @Override getSelection()158 public ISelection getSelection() { 159 return super.getSelection(); 160 } 161 162 /** 163 * Sets the outline selection. 164 * 165 * @param selection Only {@link ITreeSelection} will be used, otherwise the 166 * selection will be cleared (including a null selection). 167 */ 168 @Override setSelection(ISelection selection)169 public void setSelection(ISelection selection) { 170 // TreeViewer should be able to deal with a null selection, but let's make it safe 171 if (selection == null) { 172 selection = TreeSelection.EMPTY; 173 } 174 175 super.setSelection(selection); 176 177 TreeViewer tv = getTreeViewer(); 178 if (tv == null || !(selection instanceof ITreeSelection) || selection.isEmpty()) { 179 return; 180 } 181 182 // auto-reveal the selection 183 ITreeSelection treeSel = (ITreeSelection) selection; 184 for (TreePath p : treeSel.getPaths()) { 185 tv.expandToLevel(p, 1); 186 } 187 } 188 189 /** 190 * Listens to a workbench selection. 191 * Only listen on selection coming from {@link LayoutEditor}, which avoid 192 * picking up our own selections. 193 */ selectionChanged(IWorkbenchPart part, ISelection selection)194 public void selectionChanged(IWorkbenchPart part, ISelection selection) { 195 if (part instanceof LayoutEditor) { 196 setSelection(selection); 197 } 198 } 199 200 // ---- 201 202 203 /** 204 * In theory, the root of the model should be the input of the {@link TreeViewer}, 205 * which would be the root {@link CanvasViewInfo}. 206 * That means in theory {@link ContentProvider#getElements(Object)} should return 207 * its own input as the single root node. 208 * <p/> 209 * However as described in JFace Bug 9262, this case is not properly handled by 210 * a {@link TreeViewer} and leads to an infinite recursion in the tree viewer. 211 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=9262 212 * <p/> 213 * The solution is to wrap the tree viewer input in a dummy root node that acts 214 * as a parent. This class does just that. 215 */ 216 private static class RootWrapper { 217 private CanvasViewInfo mRoot; 218 setRoot(CanvasViewInfo root)219 public void setRoot(CanvasViewInfo root) { 220 mRoot = root; 221 } 222 getRoot()223 public CanvasViewInfo getRoot() { 224 return mRoot; 225 } 226 } 227 228 /** 229 * Content provider for the Outline model. 230 * Objects are going to be {@link CanvasViewInfo}. 231 */ 232 private static class ContentProvider implements ITreeContentProvider { 233 getChildren(Object element)234 public Object[] getChildren(Object element) { 235 if (element instanceof RootWrapper) { 236 CanvasViewInfo root = ((RootWrapper)element).getRoot(); 237 if (root != null) { 238 return new Object[] { root }; 239 } 240 } 241 if (element instanceof CanvasViewInfo) { 242 ArrayList<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren(); 243 if (children != null) { 244 return children.toArray(); 245 } 246 } 247 return new Object[0]; 248 } 249 getParent(Object element)250 public Object getParent(Object element) { 251 if (element instanceof CanvasViewInfo) { 252 return ((CanvasViewInfo) element).getParent(); 253 } 254 return null; 255 } 256 hasChildren(Object element)257 public boolean hasChildren(Object element) { 258 if (element instanceof CanvasViewInfo) { 259 ArrayList<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren(); 260 if (children != null) { 261 return children.size() > 0; 262 } 263 } 264 return false; 265 } 266 267 /** 268 * Returns the root element. 269 * Semantically, the root element is the single top-level XML element of the XML layout. 270 */ getElements(Object inputElement)271 public Object[] getElements(Object inputElement) { 272 return getChildren(inputElement); 273 } 274 dispose()275 public void dispose() { 276 // pass 277 } 278 inputChanged(Viewer viewer, Object oldInput, Object newInput)279 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 280 // pass 281 } 282 } 283 284 /** 285 * Label provider for the Outline model. 286 * Objects are going to be {@link CanvasViewInfo}. 287 */ 288 private static class LabelProvider implements ILabelProvider { 289 290 /** 291 * Returns the element's logo with a fallback on the android logo. 292 */ getImage(Object element)293 public Image getImage(Object element) { 294 if (element instanceof CanvasViewInfo) { 295 element = ((CanvasViewInfo) element).getUiViewKey(); 296 } 297 298 if (element instanceof UiElementNode) { 299 UiElementNode node = (UiElementNode) element; 300 ElementDescriptor desc = node.getDescriptor(); 301 if (desc != null) { 302 Image img = desc.getIcon(); 303 if (img != null) { 304 if (node.hasError()) { 305 return new ErrorImageComposite(img).createImage(); 306 } else { 307 return img; 308 } 309 } 310 } 311 } 312 313 return AdtPlugin.getAndroidLogo(); 314 } 315 316 /** 317 * Uses UiElementNode.shortDescription for the label for this tree item. 318 */ getText(Object element)319 public String getText(Object element) { 320 if (element instanceof CanvasViewInfo) { 321 element = ((CanvasViewInfo) element).getUiViewKey(); 322 } 323 324 if (element instanceof UiElementNode) { 325 UiElementNode node = (UiElementNode) element; 326 return node.getShortDescription(); 327 } 328 329 return element == null ? "(null)" : element.toString(); //$NON-NLS-1$ 330 } 331 addListener(ILabelProviderListener listener)332 public void addListener(ILabelProviderListener listener) { 333 // pass 334 } 335 dispose()336 public void dispose() { 337 // pass 338 } 339 isLabelProperty(Object element, String property)340 public boolean isLabelProperty(Object element, String property) { 341 // pass 342 return false; 343 } 344 removeListener(ILabelProviderListener listener)345 public void removeListener(ILabelProviderListener listener) { 346 // pass 347 } 348 } 349 } 350