1 /* 2 * Copyright (C) 2007 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.ui.tree; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 21 import com.android.ide.eclipse.adt.internal.editors.IconFactory; 22 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 23 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper; 24 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; 25 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener; 26 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 27 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 28 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; 29 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; 30 31 import org.eclipse.core.resources.IProject; 32 import org.eclipse.jface.action.Action; 33 import org.eclipse.jface.action.IMenuListener; 34 import org.eclipse.jface.action.IMenuManager; 35 import org.eclipse.jface.action.MenuManager; 36 import org.eclipse.jface.action.Separator; 37 import org.eclipse.jface.action.ToolBarManager; 38 import org.eclipse.jface.viewers.ILabelProvider; 39 import org.eclipse.jface.viewers.ISelection; 40 import org.eclipse.jface.viewers.ISelectionChangedListener; 41 import org.eclipse.jface.viewers.ITreeSelection; 42 import org.eclipse.jface.viewers.SelectionChangedEvent; 43 import org.eclipse.jface.viewers.TreePath; 44 import org.eclipse.jface.viewers.TreeSelection; 45 import org.eclipse.jface.viewers.TreeViewer; 46 import org.eclipse.jface.viewers.Viewer; 47 import org.eclipse.jface.viewers.ViewerComparator; 48 import org.eclipse.jface.viewers.ViewerFilter; 49 import org.eclipse.swt.SWT; 50 import org.eclipse.swt.dnd.Clipboard; 51 import org.eclipse.swt.events.DisposeEvent; 52 import org.eclipse.swt.events.DisposeListener; 53 import org.eclipse.swt.events.SelectionAdapter; 54 import org.eclipse.swt.events.SelectionEvent; 55 import org.eclipse.swt.layout.GridData; 56 import org.eclipse.swt.layout.GridLayout; 57 import org.eclipse.swt.widgets.Button; 58 import org.eclipse.swt.widgets.Composite; 59 import org.eclipse.swt.widgets.Control; 60 import org.eclipse.swt.widgets.Menu; 61 import org.eclipse.swt.widgets.ToolBar; 62 import org.eclipse.swt.widgets.Tree; 63 import org.eclipse.ui.forms.DetailsPart; 64 import org.eclipse.ui.forms.IDetailsPage; 65 import org.eclipse.ui.forms.IDetailsPageProvider; 66 import org.eclipse.ui.forms.IManagedForm; 67 import org.eclipse.ui.forms.MasterDetailsBlock; 68 import org.eclipse.ui.forms.widgets.FormToolkit; 69 import org.eclipse.ui.forms.widgets.Section; 70 71 import java.util.ArrayList; 72 import java.util.Iterator; 73 import java.util.LinkedList; 74 75 /** 76 * {@link UiTreeBlock} is a {@link MasterDetailsBlock} which displays a tree view for 77 * a specific set of {@link UiElementNode}. 78 * <p/> 79 * For a given UI element node, the tree view displays all first-level children that 80 * match a given type (given by an {@link ElementDescriptor}. All children from these 81 * nodes are also displayed. 82 * <p/> 83 * In the middle next to the tree are some controls to add or delete tree nodes. 84 * On the left is a details part that displays all the visible UI attributes for a given 85 * selected UI element node. 86 */ 87 public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml { 88 89 /** Height hint for the tree view. Helps the grid layout resize properly on smaller screens. */ 90 private static final int TREE_HEIGHT_HINT = 50; 91 92 /** Container editor */ 93 AndroidXmlEditor mEditor; 94 /** The root {@link UiElementNode} which contains all the elements that are to be 95 * manipulated by this tree view. In general this is the manifest UI node. */ 96 private UiElementNode mUiRootNode; 97 /** The descriptor of the elements to be displayed as root in this tree view. All elements 98 * of the same type in the root will be displayed. Can be null or empty to mean everything 99 * can be displayed. */ 100 private ElementDescriptor[] mDescriptorFilters; 101 /** The title for the master-detail part (displayed on the top "tab" on top of the tree) */ 102 private String mTitle; 103 /** The description for the master-detail part (displayed on top of the tree view) */ 104 private String mDescription; 105 /** The master-detail part, composed of a main tree and an auxiliary detail part */ 106 private ManifestSectionPart mMasterPart; 107 /** The tree viewer in the master-detail part */ 108 private TreeViewer mTreeViewer; 109 /** The "add" button for the tree view */ 110 private Button mAddButton; 111 /** The "remove" button for the tree view */ 112 private Button mRemoveButton; 113 /** The "up" button for the tree view */ 114 private Button mUpButton; 115 /** The "down" button for the tree view */ 116 private Button mDownButton; 117 /** The Managed Form used to create the master part */ 118 private IManagedForm mManagedForm; 119 /** Reference to the details part of the tree master block. */ 120 private DetailsPart mDetailsPart; 121 /** Reference to the clipboard for copy-paste */ 122 private Clipboard mClipboard; 123 /** Listener to refresh the tree viewer when the parent's node has been updated */ 124 private IUiUpdateListener mUiRefreshListener; 125 /** Listener to enable/disable the UI based on the application node's presence */ 126 private IUiUpdateListener mUiEnableListener; 127 /** An adapter/wrapper to use the add/remove/up/down tree edit actions. */ 128 private UiTreeActions mUiTreeActions; 129 /** 130 * True if the root node can be created on-demand (i.e. as needed as 131 * soon as children exist). False if an external entity controls the existence of the 132 * root node. In practise, this is false for the manifest application page (the actual 133 * "application" node is managed by the ApplicationToggle part) whereas it is true 134 * for all other tree pages. 135 */ 136 private final boolean mAutoCreateRoot; 137 138 139 /** 140 * Creates a new {@link MasterDetailsBlock} that will display all UI nodes matching the 141 * given filter in the given root node. 142 * 143 * @param editor The parent manifest editor. 144 * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are 145 * to be manipulated by this tree view. In general this is the manifest UI node or the 146 * application UI node. This cannot be null. 147 * @param autoCreateRoot True if the root node can be created on-demand (i.e. as needed as 148 * soon as children exist). False if an external entity controls the existence of the 149 * root node. In practise, this is false for the manifest application page (the actual 150 * "application" node is managed by the ApplicationToggle part) whereas it is true 151 * for all other tree pages. 152 * @param descriptorFilters A list of descriptors of the elements to be displayed as root in 153 * this tree view. Use null or an empty list to accept any kind of node. 154 * @param title Title for the section 155 * @param description Description for the section 156 */ 157 public UiTreeBlock(AndroidXmlEditor editor, 158 UiElementNode uiRootNode, 159 boolean autoCreateRoot, 160 ElementDescriptor[] descriptorFilters, 161 String title, 162 String description) { 163 mEditor = editor; 164 mUiRootNode = uiRootNode; 165 mAutoCreateRoot = autoCreateRoot; 166 mDescriptorFilters = descriptorFilters; 167 mTitle = title; 168 mDescription = description; 169 } 170 171 /** @returns The container editor */ 172 AndroidXmlEditor getEditor() { 173 return mEditor; 174 } 175 176 /** @returns The reference to the clipboard for copy-paste */ 177 Clipboard getClipboard() { 178 return mClipboard; 179 } 180 181 /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */ 182 ManifestSectionPart getMasterPart() { 183 return mMasterPart; 184 } 185 186 /** 187 * Returns the {@link UiElementNode} for the current model. 188 * <p/> 189 * This is used by the content provider attached to {@link #mTreeViewer} since 190 * the uiRootNode changes after each call to 191 * {@link #changeRootAndDescriptors(UiElementNode, ElementDescriptor[], boolean)}. 192 */ 193 public UiElementNode getRootNode() { 194 return mUiRootNode; 195 } 196 197 @Override 198 protected void createMasterPart(final IManagedForm managedForm, Composite parent) { 199 FormToolkit toolkit = managedForm.getToolkit(); 200 201 mManagedForm = managedForm; 202 mMasterPart = new ManifestSectionPart(parent, toolkit); 203 Section section = mMasterPart.getSection(); 204 section.setText(mTitle); 205 section.setDescription(mDescription); 206 section.setLayout(new GridLayout()); 207 section.setLayoutData(new GridData(GridData.FILL_BOTH)); 208 209 Composite grid = SectionHelper.createGridLayout(section, toolkit, 2); 210 211 Tree tree = createTreeViewer(toolkit, grid, managedForm); 212 createButtons(toolkit, grid); 213 createTreeContextMenu(tree); 214 createSectionActions(section, toolkit); 215 } 216 217 private void createSectionActions(Section section, FormToolkit toolkit) { 218 ToolBarManager manager = new ToolBarManager(SWT.FLAT); 219 manager.removeAll(); 220 221 ToolBar toolbar = manager.createControl(section); 222 section.setTextClient(toolbar); 223 224 ElementDescriptor[] descs = mDescriptorFilters; 225 if (descs == null && mUiRootNode != null) { 226 descs = mUiRootNode.getDescriptor().getChildren(); 227 } 228 229 if (descs != null && descs.length > 1) { 230 for (ElementDescriptor desc : descs) { 231 manager.add(new DescriptorFilterAction(desc)); 232 } 233 } 234 235 manager.add(new TreeSortAction()); 236 237 manager.update(true /*force*/); 238 } 239 240 /** 241 * Creates the tree and its viewer 242 * @return The tree control 243 */ 244 private Tree createTreeViewer(FormToolkit toolkit, Composite grid, 245 final IManagedForm managedForm) { 246 // Note: we *could* use a FilteredTree instead of the Tree+TreeViewer here. 247 // However the class must be adapted to create an adapted toolkit tree. 248 final Tree tree = toolkit.createTree(grid, SWT.MULTI); 249 GridData gd = new GridData(GridData.FILL_BOTH); 250 gd.widthHint = AndroidXmlEditor.TEXT_WIDTH_HINT; 251 gd.heightHint = TREE_HEIGHT_HINT; 252 tree.setLayoutData(gd); 253 254 mTreeViewer = new TreeViewer(tree); 255 mTreeViewer.setContentProvider(new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters)); 256 mTreeViewer.setLabelProvider(new UiModelTreeLabelProvider()); 257 mTreeViewer.setInput("unused"); //$NON-NLS-1$ 258 259 // Create a listener that reacts to selections on the tree viewer. 260 // When a selection is made, ask the managed form to propagate an event to 261 // all parts in the managed form. 262 // This is picked up by UiElementDetail.selectionChanged(). 263 mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { 264 @Override 265 public void selectionChanged(SelectionChangedEvent event) { 266 managedForm.fireSelectionChanged(mMasterPart, event.getSelection()); 267 adjustTreeButtons(event.getSelection()); 268 } 269 }); 270 271 // Create three listeners: 272 // - One to refresh the tree viewer when the parent's node has been updated 273 // - One to refresh the tree viewer when the framework resources have changed 274 // - One to enable/disable the UI based on the application node's presence. 275 mUiRefreshListener = new IUiUpdateListener() { 276 @Override 277 public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { 278 mTreeViewer.refresh(); 279 } 280 }; 281 282 mUiEnableListener = new IUiUpdateListener() { 283 @Override 284 public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { 285 // The UiElementNode for the application XML node always exists, even 286 // if there is no corresponding XML node in the XML file. 287 // 288 // Normally, we enable the UI here if the XML node is not null. 289 // 290 // However if mAutoCreateRoot is true, the root node will be created on-demand 291 // so the tree/block is always enabled. 292 boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null); 293 if (mMasterPart != null) { 294 Section section = mMasterPart.getSection(); 295 if (section.getEnabled() != exists) { 296 section.setEnabled(exists); 297 for (Control c : section.getChildren()) { 298 c.setEnabled(exists); 299 } 300 } 301 } 302 } 303 }; 304 305 /** Listener to update the root node if the target of the file is changed because of a 306 * SDK location change or a project target change */ 307 final ITargetChangeListener targetListener = new TargetChangeListener() { 308 @Override 309 public IProject getProject() { 310 if (mEditor != null) { 311 return mEditor.getProject(); 312 } 313 314 return null; 315 } 316 317 @Override 318 public void reload() { 319 // If a details part has been created, we need to "refresh" it too. 320 if (mDetailsPart != null) { 321 // The details part does not directly expose access to its internal 322 // page book. Instead it is possible to resize the page book to 0 and then 323 // back to its original value, which has the side effect of removing all 324 // existing cached pages. 325 int limit = mDetailsPart.getPageLimit(); 326 mDetailsPart.setPageLimit(0); 327 mDetailsPart.setPageLimit(limit); 328 } 329 // Refresh the tree, preserving the selection if possible. 330 mTreeViewer.refresh(); 331 } 332 }; 333 334 // Setup the listeners 335 changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */); 336 337 // Listen on resource framework changes to refresh the tree 338 AdtPlugin.getDefault().addTargetListener(targetListener); 339 340 // Remove listeners when the tree widget gets disposed. 341 tree.addDisposeListener(new DisposeListener() { 342 @Override 343 public void widgetDisposed(DisposeEvent e) { 344 if (mUiRootNode != null) { 345 UiElementNode node = mUiRootNode.getUiParent() != null ? 346 mUiRootNode.getUiParent() : 347 mUiRootNode; 348 349 if (node != null) { 350 node.removeUpdateListener(mUiRefreshListener); 351 } 352 mUiRootNode.removeUpdateListener(mUiEnableListener); 353 } 354 355 AdtPlugin.getDefault().removeTargetListener(targetListener); 356 if (mClipboard != null) { 357 mClipboard.dispose(); 358 mClipboard = null; 359 } 360 } 361 }); 362 363 // Get a new clipboard reference. It is disposed when the tree is disposed. 364 mClipboard = new Clipboard(tree.getDisplay()); 365 366 return tree; 367 } 368 369 /** 370 * Changes the UI root node and the descriptor filters of the tree. 371 * <p/> 372 * This removes the listeners attached to the old root node and reattaches them to the 373 * new one. 374 * 375 * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are 376 * to be manipulated by this tree view. In general this is the manifest UI node or the 377 * application UI node. This cannot be null. 378 * @param descriptorFilters A list of descriptors of the elements to be displayed as root in 379 * this tree view. Use null or an empty list to accept any kind of node. 380 * @param forceRefresh If tree, forces the tree to refresh 381 */ 382 public void changeRootAndDescriptors(UiElementNode uiRootNode, 383 ElementDescriptor[] descriptorFilters, boolean forceRefresh) { 384 UiElementNode node; 385 386 // Remove previous listeners if any 387 if (mUiRootNode != null) { 388 node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; 389 node.removeUpdateListener(mUiRefreshListener); 390 mUiRootNode.removeUpdateListener(mUiEnableListener); 391 } 392 393 mUiRootNode = uiRootNode; 394 mDescriptorFilters = descriptorFilters; 395 396 mTreeViewer.setContentProvider( 397 new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters)); 398 399 // Listen on structural changes on the root node of the tree 400 // If the node has a parent, listen on the parent instead. 401 if (mUiRootNode != null) { 402 node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; 403 404 if (node != null) { 405 node.addUpdateListener(mUiRefreshListener); 406 } 407 408 // Use the root node to listen to its presence. 409 mUiRootNode.addUpdateListener(mUiEnableListener); 410 411 // Initialize the enabled/disabled state 412 mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */); 413 } 414 415 if (forceRefresh) { 416 mTreeViewer.refresh(); 417 } 418 419 createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit()); 420 } 421 422 /** 423 * Creates the buttons next to the tree. 424 */ 425 private void createButtons(FormToolkit toolkit, Composite grid) { 426 427 mUiTreeActions = new UiTreeActions(); 428 429 Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1); 430 button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING)); 431 mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH); 432 SectionHelper.addControlTooltip(mAddButton, "Adds a new element."); 433 mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | 434 GridData.VERTICAL_ALIGN_BEGINNING)); 435 436 mAddButton.addSelectionListener(new SelectionAdapter() { 437 @Override 438 public void widgetSelected(SelectionEvent e) { 439 super.widgetSelected(e); 440 doTreeAdd(); 441 } 442 }); 443 444 mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH); 445 SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element."); 446 mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 447 448 mRemoveButton.addSelectionListener(new SelectionAdapter() { 449 @Override 450 public void widgetSelected(SelectionEvent e) { 451 super.widgetSelected(e); 452 doTreeRemove(); 453 } 454 }); 455 456 mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH); 457 SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up."); 458 mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 459 460 mUpButton.addSelectionListener(new SelectionAdapter() { 461 @Override 462 public void widgetSelected(SelectionEvent e) { 463 super.widgetSelected(e); 464 doTreeUp(); 465 } 466 }); 467 468 mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH); 469 SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down."); 470 mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 471 472 mDownButton.addSelectionListener(new SelectionAdapter() { 473 @Override 474 public void widgetSelected(SelectionEvent e) { 475 super.widgetSelected(e); 476 doTreeDown(); 477 } 478 }); 479 480 adjustTreeButtons(TreeSelection.EMPTY); 481 } 482 483 private void createTreeContextMenu(Tree tree) { 484 MenuManager menuManager = new MenuManager(); 485 menuManager.setRemoveAllWhenShown(true); 486 menuManager.addMenuListener(new IMenuListener() { 487 /** 488 * The menu is about to be shown. The menu manager has already been 489 * requested to remove any existing menu item. This method gets the 490 * tree selection and if it is of the appropriate type it re-creates 491 * the necessary actions. 492 */ 493 @Override 494 public void menuAboutToShow(IMenuManager manager) { 495 ISelection selection = mTreeViewer.getSelection(); 496 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 497 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 498 doCreateMenuAction(manager, selected); 499 return; 500 } 501 doCreateMenuAction(manager, null /* ui_node */); 502 } 503 }); 504 Menu contextMenu = menuManager.createContextMenu(tree); 505 tree.setMenu(contextMenu); 506 } 507 508 /** 509 * Adds the menu actions to the context menu when the given UI node is selected in 510 * the tree view. 511 * 512 * @param manager The context menu manager 513 * @param selected The UI nodes selected in the tree. Can be null, in which case the root 514 * is to be modified. 515 */ 516 private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) { 517 if (selected != null) { 518 boolean hasXml = false; 519 for (UiElementNode uiNode : selected) { 520 if (uiNode.getXmlNode() != null) { 521 hasXml = true; 522 break; 523 } 524 } 525 526 if (hasXml) { 527 manager.add(new CopyCutAction(getEditor(), getClipboard(), 528 null, selected, true /* cut */)); 529 manager.add(new CopyCutAction(getEditor(), getClipboard(), 530 null, selected, false /* cut */)); 531 532 // Can't paste with more than one element selected (the selection is the target) 533 if (selected.size() <= 1) { 534 // Paste is not valid if it would add a second element on a terminal element 535 // which parent is a document -- an XML document can only have one child. This 536 // means paste is valid if the current UI node can have children or if the 537 // parent is not a document. 538 UiElementNode ui_root = selected.get(0).getUiRoot(); 539 if (ui_root.getDescriptor().hasChildren() || 540 !(ui_root.getUiParent() instanceof UiDocumentNode)) { 541 manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0))); 542 } 543 } 544 manager.add(new Separator()); 545 } 546 } 547 548 // Append "add" and "remove" actions. They do the same thing as the add/remove 549 // buttons on the side. 550 IconFactory factory = IconFactory.getInstance(); 551 552 // "Add" makes sense only if there's 0 or 1 item selected since the 553 // one selected item becomes the target. 554 if (selected == null || selected.size() <= 1) { 555 manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$ 556 @Override 557 public void run() { 558 super.run(); 559 doTreeAdd(); 560 } 561 }); 562 } 563 564 if (selected != null) { 565 if (selected != null) { 566 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$ 567 @Override 568 public void run() { 569 super.run(); 570 doTreeRemove(); 571 } 572 }); 573 } 574 manager.add(new Separator()); 575 576 manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$ 577 @Override 578 public void run() { 579 super.run(); 580 doTreeUp(); 581 } 582 }); 583 manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$ 584 @Override 585 public void run() { 586 super.run(); 587 doTreeDown(); 588 } 589 }); 590 } 591 } 592 593 594 /** 595 * This is called by the tree when a selection is made. 596 * It enables/disables the buttons associated with the tree depending on the current 597 * selection. 598 * 599 * @param selection The current tree selection (same as mTreeViewer.getSelection()) 600 */ 601 private void adjustTreeButtons(ISelection selection) { 602 mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection); 603 mUpButton.setEnabled(canDoTreeUp(selection)); 604 mDownButton.setEnabled(canDoTreeDown(selection)); 605 } 606 607 /** 608 * An adapter/wrapper to use the add/remove/up/down tree edit actions. 609 */ 610 private class UiTreeActions extends UiActions { 611 @Override 612 protected UiElementNode getRootNode() { 613 return mUiRootNode; 614 } 615 616 @Override 617 protected void selectUiNode(UiElementNode uiNodeToSelect) { 618 // Select the new item 619 if (uiNodeToSelect != null) { 620 LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>(); 621 for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode; 622 ui_node = ui_node.getUiParent()) { 623 segments.add(0, ui_node); 624 } 625 if (segments.size() > 0) { 626 mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray()))); 627 } else { 628 mTreeViewer.setSelection(null); 629 } 630 } 631 } 632 633 @Override 634 public void commitPendingXmlChanges() { 635 commitManagedForm(); 636 } 637 } 638 639 /** 640 * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's 641 * something else in there). 642 * 643 * @return A new list of {@link UiElementNode} with at least one item or null. 644 */ 645 private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) { 646 ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); 647 648 for (Iterator<Object> it = selection.iterator(); it.hasNext(); ) { 649 Object selectedObj = it.next(); 650 651 if (selectedObj instanceof UiElementNode) { 652 selected.add((UiElementNode) selectedObj); 653 } 654 } 655 656 return selected.size() > 0 ? selected : null; 657 } 658 659 /** 660 * Called when the "Add..." button next to the tree view is selected. 661 * 662 * Displays a selection dialog that lets the user select which kind of node 663 * to create, depending on the current selection. 664 */ 665 private void doTreeAdd() { 666 UiElementNode ui_node = mUiRootNode; 667 ISelection selection = mTreeViewer.getSelection(); 668 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 669 ITreeSelection tree_selection = (ITreeSelection) selection; 670 Object first = tree_selection.getFirstElement(); 671 if (first != null && first instanceof UiElementNode) { 672 ui_node = (UiElementNode) first; 673 } 674 } 675 676 mUiTreeActions.doAdd( 677 ui_node, 678 mDescriptorFilters, 679 mTreeViewer.getControl().getShell(), 680 (ILabelProvider) mTreeViewer.getLabelProvider()); 681 } 682 683 /** 684 * Called when the "Remove" button is selected. 685 * 686 * If the tree has a selection, remove it. 687 * This simply deletes the XML node attached to the UI node: when the XML model fires the 688 * update event, the tree will get refreshed. 689 */ 690 protected void doTreeRemove() { 691 ISelection selection = mTreeViewer.getSelection(); 692 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 693 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 694 mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell()); 695 } 696 } 697 698 /** 699 * Called when the "Up" button is selected. 700 * <p/> 701 * If the tree has a selection, move it up, either in the child list or as the last child 702 * of the previous parent. 703 */ 704 protected void doTreeUp() { 705 ISelection selection = mTreeViewer.getSelection(); 706 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 707 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 708 mUiTreeActions.doUp(selected, mDescriptorFilters); 709 } 710 } 711 712 /** 713 * Checks whether the "up" action can be done on the current selection. 714 * 715 * @param selection The current tree selection. 716 * @return True if all the selected nodes can be moved up. 717 */ 718 protected boolean canDoTreeUp(ISelection selection) { 719 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 720 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 721 return mUiTreeActions.canDoUp(selected, mDescriptorFilters); 722 } 723 724 return false; 725 } 726 727 /** 728 * Called when the "Down" button is selected. 729 * 730 * If the tree has a selection, move it down, either in the same child list or as the 731 * first child of the next parent. 732 */ 733 protected void doTreeDown() { 734 ISelection selection = mTreeViewer.getSelection(); 735 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 736 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 737 mUiTreeActions.doDown(selected, mDescriptorFilters); 738 } 739 } 740 741 /** 742 * Checks whether the "down" action can be done on the current selection. 743 * 744 * @param selection The current tree selection. 745 * @return True if all the selected nodes can be moved down. 746 */ 747 protected boolean canDoTreeDown(ISelection selection) { 748 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 749 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 750 return mUiTreeActions.canDoDown(selected, mDescriptorFilters); 751 } 752 753 return false; 754 } 755 756 /** 757 * Commits the current managed form (the one associated with our master part). 758 * As a side effect, this will commit the current UiElementDetails page. 759 */ 760 void commitManagedForm() { 761 if (mManagedForm != null) { 762 mManagedForm.commit(false /* onSave */); 763 } 764 } 765 766 /* Implements ICommitXml for CopyCutAction */ 767 @Override 768 public void commitPendingXmlChanges() { 769 commitManagedForm(); 770 } 771 772 @Override 773 protected void createToolBarActions(IManagedForm managedForm) { 774 // Pass. Not used, toolbar actions are defined by createSectionActions(). 775 } 776 777 @Override 778 protected void registerPages(DetailsPart inDetailsPart) { 779 // Keep a reference on the details part (the super class doesn't provide a getter 780 // for it.) 781 mDetailsPart = inDetailsPart; 782 783 // The page selection mechanism does not use pages registered by association with 784 // a node class. Instead it uses a custom details page provider that provides a 785 // new UiElementDetail instance for each node instance. A limit of 5 pages is 786 // then set (the value is arbitrary but should be reasonable) for the internal 787 // page book. 788 inDetailsPart.setPageLimit(5); 789 790 final UiTreeBlock tree = this; 791 792 inDetailsPart.setPageProvider(new IDetailsPageProvider() { 793 @Override 794 public IDetailsPage getPage(Object key) { 795 if (key instanceof UiElementNode) { 796 return new UiElementDetail(tree); 797 } 798 return null; 799 } 800 801 @Override 802 public Object getPageKey(Object object) { 803 return object; // use node object as key 804 } 805 }); 806 } 807 808 /** 809 * An alphabetic sort action for the tree viewer. 810 */ 811 private class TreeSortAction extends Action { 812 813 private ViewerComparator mComparator; 814 815 public TreeSortAction() { 816 super("Sorts elements alphabetically.", AS_CHECK_BOX); 817 setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$ 818 819 if (mTreeViewer != null) { 820 boolean is_sorted = mTreeViewer.getComparator() != null; 821 setChecked(is_sorted); 822 } 823 } 824 825 /** 826 * Called when the button is selected. Toggles the tree viewer comparator. 827 */ 828 @Override 829 public void run() { 830 if (mTreeViewer == null) { 831 notifyResult(false /*success*/); 832 return; 833 } 834 835 ViewerComparator comp = mTreeViewer.getComparator(); 836 if (comp != null) { 837 // Tree is currently sorted. 838 // Save currently comparator and remove it 839 mComparator = comp; 840 mTreeViewer.setComparator(null); 841 } else { 842 // Tree is not currently sorted. 843 // Reuse or add a new comparator. 844 if (mComparator == null) { 845 mComparator = new ViewerComparator(); 846 } 847 mTreeViewer.setComparator(mComparator); 848 } 849 850 notifyResult(true /*success*/); 851 } 852 } 853 854 /** 855 * A filter on descriptor for the tree viewer. 856 * <p/> 857 * The tree viewer will contain many of these actions and only one can be enabled at a 858 * given time. When no action is selected, everything is displayed. 859 * <p/> 860 * Since "radio"-like actions do not allow for unselecting all of them, we manually 861 * handle the exclusive radio button-like property: when an action is selected, it manually 862 * removes all other actions as needed. 863 */ 864 private class DescriptorFilterAction extends Action { 865 866 private final ElementDescriptor mDescriptor; 867 private ViewerFilter mFilter; 868 869 public DescriptorFilterAction(ElementDescriptor descriptor) { 870 super(String.format("Displays only %1$s elements.", descriptor.getUiName()), 871 AS_CHECK_BOX); 872 873 mDescriptor = descriptor; 874 setImageDescriptor(descriptor.getImageDescriptor()); 875 } 876 877 /** 878 * Called when the button is selected. 879 * <p/> 880 * Find any existing {@link DescriptorFilter}s and remove them. Install ours. 881 */ 882 @Override 883 public void run() { 884 super.run(); 885 886 if (isChecked()) { 887 if (mFilter == null) { 888 // create filter when required 889 mFilter = new DescriptorFilter(this); 890 } 891 892 // we add our filter first, otherwise the UI might show the full list 893 mTreeViewer.addFilter(mFilter); 894 895 // Then remove the any other filters except ours. There should be at most 896 // one other filter, since that's how the actions are made to look like 897 // exclusive radio buttons. 898 for (ViewerFilter filter : mTreeViewer.getFilters()) { 899 if (filter instanceof DescriptorFilter && filter != mFilter) { 900 DescriptorFilterAction action = ((DescriptorFilter) filter).getAction(); 901 action.setChecked(false); 902 mTreeViewer.removeFilter(filter); 903 } 904 } 905 } else if (mFilter != null){ 906 mTreeViewer.removeFilter(mFilter); 907 } 908 } 909 910 /** 911 * Filters the tree viewer for the given descriptor. 912 * <p/> 913 * The filter is linked to the action so that an action can iterate through the list 914 * of filters and un-select the actions. 915 */ 916 private class DescriptorFilter extends ViewerFilter { 917 918 private final DescriptorFilterAction mAction; 919 920 public DescriptorFilter(DescriptorFilterAction action) { 921 mAction = action; 922 } 923 924 public DescriptorFilterAction getAction() { 925 return mAction; 926 } 927 928 /** 929 * Returns true if an element should be displayed, that if the element or 930 * any of its parent matches the requested descriptor. 931 */ 932 @Override 933 public boolean select(Viewer viewer, Object parentElement, Object element) { 934 while (element instanceof UiElementNode) { 935 UiElementNode uiNode = (UiElementNode)element; 936 if (uiNode.getDescriptor() == mDescriptor) { 937 return true; 938 } 939 element = uiNode.getUiParent(); 940 } 941 return false; 942 } 943 } 944 } 945 946 } 947