1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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 18 package com.android.ide.eclipse.adt.internal.editors.layout; 19 20 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 21 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiDocumentTreeEditPart; 22 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementTreeEditPart; 23 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiElementTreeEditPartFactory; 24 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiLayoutTreeEditPart; 25 import com.android.ide.eclipse.adt.internal.editors.layout.parts.UiViewTreeEditPart; 26 import com.android.ide.eclipse.adt.internal.editors.ui.tree.CopyCutAction; 27 import com.android.ide.eclipse.adt.internal.editors.ui.tree.PasteAction; 28 import com.android.ide.eclipse.adt.internal.editors.ui.tree.UiActions; 29 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 30 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 31 import com.android.ide.eclipse.adt.internal.ui.EclipseUiHelper; 32 33 import org.eclipse.gef.EditPartViewer; 34 import org.eclipse.gef.ui.parts.ContentOutlinePage; 35 import org.eclipse.jface.action.Action; 36 import org.eclipse.jface.action.IMenuListener; 37 import org.eclipse.jface.action.IMenuManager; 38 import org.eclipse.jface.action.IToolBarManager; 39 import org.eclipse.jface.action.MenuManager; 40 import org.eclipse.jface.action.Separator; 41 import org.eclipse.jface.viewers.ISelection; 42 import org.eclipse.jface.viewers.ISelectionChangedListener; 43 import org.eclipse.jface.viewers.SelectionChangedEvent; 44 import org.eclipse.jface.viewers.StructuredSelection; 45 import org.eclipse.jface.viewers.TreePath; 46 import org.eclipse.jface.viewers.TreeSelection; 47 import org.eclipse.swt.SWT; 48 import org.eclipse.swt.graphics.Point; 49 import org.eclipse.swt.graphics.Rectangle; 50 import org.eclipse.swt.layout.FillLayout; 51 import org.eclipse.swt.widgets.Composite; 52 import org.eclipse.swt.widgets.Control; 53 import org.eclipse.swt.widgets.Display; 54 import org.eclipse.swt.widgets.Event; 55 import org.eclipse.swt.widgets.Label; 56 import org.eclipse.swt.widgets.Listener; 57 import org.eclipse.swt.widgets.Menu; 58 import org.eclipse.swt.widgets.Shell; 59 import org.eclipse.swt.widgets.Tree; 60 import org.eclipse.swt.widgets.TreeItem; 61 import org.eclipse.ui.IActionBars; 62 63 import java.util.ArrayList; 64 import java.util.Iterator; 65 import java.util.LinkedList; 66 import java.util.List; 67 68 /** 69 * Implementation of the {@link ContentOutlinePage} to display {@link UiElementNode}. 70 * 71 * @since GLE1 72 */ 73 class UiContentOutlinePage extends ContentOutlinePage { 74 75 private GraphicalLayoutEditor mEditor; 76 77 private Action mAddAction; 78 private Action mDeleteAction; 79 private Action mUpAction; 80 private Action mDownAction; 81 82 private UiOutlineActions mUiActions = new UiOutlineActions(); 83 UiContentOutlinePage(GraphicalLayoutEditor editor, final EditPartViewer viewer)84 public UiContentOutlinePage(GraphicalLayoutEditor editor, final EditPartViewer viewer) { 85 super(viewer); 86 mEditor = editor; 87 IconFactory factory = IconFactory.getInstance(); 88 89 mAddAction = new Action("Add...") { 90 @Override 91 public void run() { 92 List<UiElementNode> nodes = getModelSelections(); 93 UiElementNode node = nodes != null && nodes.size() > 0 ? nodes.get(0) : null; 94 95 mUiActions.doAdd(node, viewer.getControl().getShell()); 96 } 97 }; 98 mAddAction.setToolTipText("Adds a new element."); 99 mAddAction.setImageDescriptor(factory.getImageDescriptor("add")); //$NON-NLS-1$ 100 101 mDeleteAction = new Action("Remove...") { 102 @Override 103 public void run() { 104 List<UiElementNode> nodes = getModelSelections(); 105 106 mUiActions.doRemove(nodes, viewer.getControl().getShell()); 107 } 108 }; 109 mDeleteAction.setToolTipText("Removes an existing selected element."); 110 mDeleteAction.setImageDescriptor(factory.getImageDescriptor("delete")); //$NON-NLS-1$ 111 112 mUpAction = new Action("Up") { 113 @Override 114 public void run() { 115 List<UiElementNode> nodes = getModelSelections(); 116 117 mUiActions.doUp(nodes); 118 } 119 }; 120 mUpAction.setToolTipText("Moves the selected element up"); 121 mUpAction.setImageDescriptor(factory.getImageDescriptor("up")); //$NON-NLS-1$ 122 123 mDownAction = new Action("Down") { 124 @Override 125 public void run() { 126 List<UiElementNode> nodes = getModelSelections(); 127 128 mUiActions.doDown(nodes); 129 } 130 }; 131 mDownAction.setToolTipText("Moves the selected element down"); 132 mDownAction.setImageDescriptor(factory.getImageDescriptor("down")); //$NON-NLS-1$ 133 134 // all actions disabled by default. 135 mAddAction.setEnabled(false); 136 mDeleteAction.setEnabled(false); 137 mUpAction.setEnabled(false); 138 mDownAction.setEnabled(false); 139 140 addSelectionChangedListener(new ISelectionChangedListener() { 141 public void selectionChanged(SelectionChangedEvent event) { 142 ISelection selection = event.getSelection(); 143 144 // the selection is never empty. The least it'll contain is the 145 // UiDocumentTreeEditPart object. 146 if (selection instanceof StructuredSelection) { 147 StructuredSelection structSel = (StructuredSelection)selection; 148 149 if (structSel.size() == 1 && 150 structSel.getFirstElement() instanceof UiDocumentTreeEditPart) { 151 mDeleteAction.setEnabled(false); 152 mUpAction.setEnabled(false); 153 mDownAction.setEnabled(false); 154 } else { 155 mDeleteAction.setEnabled(true); 156 mUpAction.setEnabled(true); 157 mDownAction.setEnabled(true); 158 } 159 160 // the "add" button is always enabled, in order to be able to set the 161 // initial root node 162 mAddAction.setEnabled(true); 163 } 164 } 165 }); 166 } 167 168 169 /* (non-Javadoc) 170 * @see org.eclipse.ui.part.IPage#createControl(org.eclipse.swt.widgets.Composite) 171 */ 172 @Override createControl(Composite parent)173 public void createControl(Composite parent) { 174 // create outline viewer page 175 getViewer().createControl(parent); 176 177 // configure outline viewer 178 getViewer().setEditPartFactory(new UiElementTreeEditPartFactory()); 179 180 setupOutline(); 181 setupContextMenu(); 182 setupTooltip(); 183 setupDoubleClick(); 184 } 185 186 /* 187 * (non-Javadoc) 188 * @see org.eclipse.ui.part.Page#setActionBars(org.eclipse.ui.IActionBars) 189 * 190 * Called automatically after createControl 191 */ 192 @Override setActionBars(IActionBars actionBars)193 public void setActionBars(IActionBars actionBars) { 194 IToolBarManager toolBarManager = actionBars.getToolBarManager(); 195 toolBarManager.add(mAddAction); 196 toolBarManager.add(mDeleteAction); 197 toolBarManager.add(new Separator()); 198 toolBarManager.add(mUpAction); 199 toolBarManager.add(mDownAction); 200 201 IMenuManager menuManager = actionBars.getMenuManager(); 202 menuManager.add(mAddAction); 203 menuManager.add(mDeleteAction); 204 menuManager.add(new Separator()); 205 menuManager.add(mUpAction); 206 menuManager.add(mDownAction); 207 } 208 209 /* (non-Javadoc) 210 * @see org.eclipse.ui.part.IPage#dispose() 211 */ 212 @Override dispose()213 public void dispose() { 214 breakConnectionWithEditor(); 215 216 // dispose 217 super.dispose(); 218 } 219 220 /* (non-Javadoc) 221 * @see org.eclipse.ui.part.IPage#getControl() 222 */ 223 @Override getControl()224 public Control getControl() { 225 return getViewer().getControl(); 226 } 227 setNewEditor(GraphicalLayoutEditor editor)228 void setNewEditor(GraphicalLayoutEditor editor) { 229 mEditor = editor; 230 setupOutline(); 231 } 232 breakConnectionWithEditor()233 void breakConnectionWithEditor() { 234 // unhook outline viewer 235 mEditor.getSelectionSynchronizer().removeViewer(getViewer()); 236 } 237 setupOutline()238 private void setupOutline() { 239 240 getViewer().setEditDomain(mEditor.getEditDomain()); 241 242 // hook outline viewer 243 mEditor.getSelectionSynchronizer().addViewer(getViewer()); 244 245 // initialize outline viewer with model 246 getViewer().setContents(mEditor.getModel()); 247 } 248 setupContextMenu()249 private void setupContextMenu() { 250 MenuManager menuManager = new MenuManager(); 251 menuManager.setRemoveAllWhenShown(true); 252 menuManager.addMenuListener(new IMenuListener() { 253 /** 254 * The menu is about to be shown. The menu manager has already been 255 * requested to remove any existing menu item. This method gets the 256 * tree selection and if it is of the appropriate type it re-creates 257 * the necessary actions. 258 */ 259 public void menuAboutToShow(IMenuManager manager) { 260 List<UiElementNode> selected = getModelSelections(); 261 262 if (selected != null) { 263 doCreateMenuAction(manager, selected); 264 return; 265 } 266 doCreateMenuAction(manager, null /* ui_node */); 267 } 268 }); 269 Control control = getControl(); 270 Menu contextMenu = menuManager.createContextMenu(control); 271 control.setMenu(contextMenu); 272 } 273 274 /** 275 * Adds the menu actions to the context menu when the given UI node is selected in 276 * the tree view. 277 * 278 * @param manager The context menu manager 279 * @param selected The UI node selected in the tree. Can be null, in which case the root 280 * is to be modified. 281 */ doCreateMenuAction(IMenuManager manager, List<UiElementNode> selected)282 private void doCreateMenuAction(IMenuManager manager, List<UiElementNode> selected) { 283 284 if (selected != null) { 285 boolean hasXml = false; 286 for (UiElementNode uiNode : selected) { 287 if (uiNode.getXmlNode() != null) { 288 hasXml = true; 289 break; 290 } 291 } 292 293 if (hasXml) { 294 manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(), 295 null, selected, true /* cut */)); 296 manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(), 297 null, selected, false /* cut */)); 298 299 // Can't paste with more than one element selected (the selection is the target) 300 if (selected.size() <= 1) { 301 // Paste is not valid if it would add a second element on a terminal element 302 // which parent is a document -- an XML document can only have one child. This 303 // means paste is valid if the current UI node can have children or if the parent 304 // is not a document. 305 UiElementNode ui_root = selected.get(0).getUiRoot(); 306 if (ui_root.getDescriptor().hasChildren() || 307 !(ui_root.getUiParent() instanceof UiDocumentNode)) { 308 manager.add(new PasteAction(mEditor.getLayoutEditor(), 309 mEditor.getClipboard(), 310 selected.get(0))); 311 } 312 } 313 manager.add(new Separator()); 314 } 315 } 316 317 // Append "add" and "remove" actions. They do the same thing as the add/remove 318 // buttons on the side. 319 // 320 // "Add" makes sense only if there's 0 or 1 item selected since the 321 // one selected item becomes the target. 322 if (selected == null || selected.size() <= 1) { 323 manager.add(mAddAction); 324 } 325 326 if (selected != null) { 327 manager.add(mDeleteAction); 328 manager.add(new Separator()); 329 330 manager.add(mUpAction); 331 manager.add(mDownAction); 332 } 333 334 if (selected != null && selected.size() == 1) { 335 manager.add(new Separator()); 336 337 Action propertiesAction = new Action("Properties") { 338 @Override 339 public void run() { 340 EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, 341 true /* activate */); 342 } 343 }; 344 propertiesAction.setToolTipText("Displays properties of the selected element."); 345 manager.add(propertiesAction); 346 } 347 } 348 349 /** 350 * Updates the outline view with the model of the {@link IGraphicalLayoutEditor}. 351 * <p/> 352 * This attemps to preserve the selection, if any. 353 */ reloadModel()354 public void reloadModel() { 355 // Attemps to preserve the UiNode selection, if any 356 List<UiElementNode> uiNodes = null; 357 try { 358 // get current selection using the model rather than the edit part as 359 // reloading the content may change the actual edit part. 360 uiNodes = getModelSelections(); 361 362 // perform the update 363 getViewer().setContents(mEditor.getModel()); 364 365 } finally { 366 // restore selection 367 if (uiNodes != null) { 368 setModelSelection(uiNodes.get(0)); 369 } 370 } 371 } 372 373 /** 374 * Returns the currently selected element, if any, in the viewer. 375 * This returns the viewer's elements (i.e. an {@link UiElementTreeEditPart}) 376 * and not the underlying model node. 377 * <p/> 378 * When there is no actual selection, this might still return the root node, 379 * which is of type {@link UiDocumentTreeEditPart}. 380 */ 381 @SuppressWarnings("unchecked") getViewerSelections()382 private List<UiElementTreeEditPart> getViewerSelections() { 383 ISelection selection = getSelection(); 384 if (selection instanceof StructuredSelection) { 385 StructuredSelection structuredSelection = (StructuredSelection)selection; 386 387 if (structuredSelection.size() > 0) { 388 ArrayList<UiElementTreeEditPart> selected = new ArrayList<UiElementTreeEditPart>(); 389 390 for (Iterator it = structuredSelection.iterator(); it.hasNext(); ) { 391 Object selectedObj = it.next(); 392 393 if (selectedObj instanceof UiElementTreeEditPart) { 394 selected.add((UiElementTreeEditPart) selectedObj); 395 } 396 } 397 398 return selected.size() > 0 ? selected : null; 399 } 400 } 401 402 return null; 403 } 404 405 /** 406 * Returns the currently selected model element, which is either an 407 * {@link UiViewTreeEditPart} or an {@link UiLayoutTreeEditPart}. 408 * <p/> 409 * Returns null if there is no selection or if the implicit root is "selected" 410 * (which actually represents the lack of a real element selection.) 411 */ getModelSelections()412 private List<UiElementNode> getModelSelections() { 413 414 List<UiElementTreeEditPart> parts = getViewerSelections(); 415 416 if (parts != null) { 417 ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); 418 419 for (UiElementTreeEditPart part : parts) { 420 if (part instanceof UiViewTreeEditPart || part instanceof UiLayoutTreeEditPart) { 421 selected.add((UiElementNode) part.getModel()); 422 } 423 } 424 425 return selected.size() > 0 ? selected : null; 426 } 427 428 return null; 429 } 430 431 /** 432 * Selects the corresponding edit part in the tree viewer. 433 */ setViewerSelection(UiElementTreeEditPart selectedPart)434 private void setViewerSelection(UiElementTreeEditPart selectedPart) { 435 if (selectedPart != null && !(selectedPart instanceof UiDocumentTreeEditPart)) { 436 LinkedList<UiElementTreeEditPart> segments = new LinkedList<UiElementTreeEditPart>(); 437 for (UiElementTreeEditPart part = selectedPart; 438 !(part instanceof UiDocumentTreeEditPart); 439 part = (UiElementTreeEditPart) part.getParent()) { 440 segments.add(0, part); 441 } 442 setSelection(new TreeSelection(new TreePath(segments.toArray()))); 443 } 444 } 445 446 /** 447 * Selects the corresponding model element in the tree viewer. 448 */ setModelSelection(UiElementNode uiNodeToSelect)449 private void setModelSelection(UiElementNode uiNodeToSelect) { 450 if (uiNodeToSelect != null) { 451 452 // find an edit part that has the requested model element 453 UiElementTreeEditPart part = findPartForModel( 454 (UiElementTreeEditPart) getViewer().getContents(), 455 uiNodeToSelect); 456 457 // if we found a part, select it and reveal it 458 if (part != null) { 459 setViewerSelection(part); 460 getViewer().reveal(part); 461 } 462 } 463 } 464 465 /** 466 * Utility method that tries to find an edit part that matches a given model UI node. 467 * 468 * @param rootPart The root of the viewer edit parts 469 * @param uiNode The UI node model to find 470 * @return The part that matches the model or null if it's not in the sub tree. 471 */ findPartForModel(UiElementTreeEditPart rootPart, UiElementNode uiNode)472 private UiElementTreeEditPart findPartForModel(UiElementTreeEditPart rootPart, 473 UiElementNode uiNode) { 474 if (rootPart.getModel() == uiNode) { 475 return rootPart; 476 } 477 478 for (Object part : rootPart.getChildren()) { 479 if (part instanceof UiElementTreeEditPart) { 480 UiElementTreeEditPart found = findPartForModel( 481 (UiElementTreeEditPart) part, uiNode); 482 if (found != null) { 483 return found; 484 } 485 } 486 } 487 488 return null; 489 } 490 491 /** 492 * Sets up a custom tooltip when hovering over tree items. 493 * <p/> 494 * The tooltip will display the element's javadoc, if any, or the item's getText otherwise. 495 */ setupTooltip()496 private void setupTooltip() { 497 final Tree tree = (Tree) getControl(); 498 499 /* 500 * Reference: 501 * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup 502 */ 503 504 final Listener listener = new Listener() { 505 Shell tip = null; 506 Label label = null; 507 508 public void handleEvent(Event event) { 509 switch(event.type) { 510 case SWT.Dispose: 511 case SWT.KeyDown: 512 case SWT.MouseExit: 513 case SWT.MouseDown: 514 case SWT.MouseMove: 515 if (tip != null) { 516 tip.dispose(); 517 tip = null; 518 label = null; 519 } 520 break; 521 case SWT.MouseHover: 522 if (tip != null) { 523 tip.dispose(); 524 tip = null; 525 label = null; 526 } 527 528 String tooltip = null; 529 530 TreeItem item = tree.getItem(new Point(event.x, event.y)); 531 if (item != null) { 532 Object data = item.getData(); 533 if (data instanceof UiElementTreeEditPart) { 534 Object model = ((UiElementTreeEditPart) data).getModel(); 535 if (model instanceof UiElementNode) { 536 tooltip = ((UiElementNode) model).getDescriptor().getTooltip(); 537 } 538 } 539 540 if (tooltip == null) { 541 tooltip = item.getText(); 542 } else { 543 tooltip = item.getText() + ":\r" + tooltip; 544 } 545 } 546 547 548 if (tooltip != null) { 549 Shell shell = tree.getShell(); 550 Display display = tree.getDisplay(); 551 552 tip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL); 553 tip.setBackground(display .getSystemColor(SWT.COLOR_INFO_BACKGROUND)); 554 FillLayout layout = new FillLayout(); 555 layout.marginWidth = 2; 556 tip.setLayout(layout); 557 label = new Label(tip, SWT.NONE); 558 label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); 559 label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); 560 label.setData("_TABLEITEM", item); 561 label.setText(tooltip); 562 label.addListener(SWT.MouseExit, this); 563 label.addListener(SWT.MouseDown, this); 564 Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT); 565 Rectangle rect = item.getBounds(0); 566 Point pt = tree.toDisplay(rect.x, rect.y); 567 tip.setBounds(pt.x, pt.y, size.x, size.y); 568 tip.setVisible(true); 569 } 570 } 571 } 572 }; 573 574 tree.addListener(SWT.Dispose, listener); 575 tree.addListener(SWT.KeyDown, listener); 576 tree.addListener(SWT.MouseMove, listener); 577 tree.addListener(SWT.MouseHover, listener); 578 } 579 580 /** 581 * Sets up double-click action on the tree. 582 * <p/> 583 * By default, double-click (a.k.a. "default selection") on a valid list item will 584 * show the property view. 585 */ setupDoubleClick()586 private void setupDoubleClick() { 587 final Tree tree = (Tree) getControl(); 588 589 tree.addListener(SWT.DefaultSelection, new Listener() { 590 public void handleEvent(Event event) { 591 EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, 592 true /* activate */); 593 } 594 }); 595 } 596 597 // --------------- 598 599 private class UiOutlineActions extends UiActions { 600 601 @Override getRootNode()602 protected UiDocumentNode getRootNode() { 603 return mEditor.getModel(); // this is LayoutEditor.getUiRootNode() 604 } 605 606 // Select the new item 607 @Override selectUiNode(UiElementNode uiNodeToSelect)608 protected void selectUiNode(UiElementNode uiNodeToSelect) { 609 setModelSelection(uiNodeToSelect); 610 } 611 612 @Override commitPendingXmlChanges()613 public void commitPendingXmlChanges() { 614 // Pass. There is nothing to commit before the XML is changed here. 615 } 616 617 } 618 } 619