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 public void selectionChanged(SelectionChangedEvent event) { 265 managedForm.fireSelectionChanged(mMasterPart, event.getSelection()); 266 adjustTreeButtons(event.getSelection()); 267 } 268 }); 269 270 // Create three listeners: 271 // - One to refresh the tree viewer when the parent's node has been updated 272 // - One to refresh the tree viewer when the framework resources have changed 273 // - One to enable/disable the UI based on the application node's presence. 274 mUiRefreshListener = new IUiUpdateListener() { 275 public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { 276 mTreeViewer.refresh(); 277 } 278 }; 279 280 mUiEnableListener = new IUiUpdateListener() { 281 public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { 282 // The UiElementNode for the application XML node always exists, even 283 // if there is no corresponding XML node in the XML file. 284 // 285 // Normally, we enable the UI here if the XML node is not null. 286 // 287 // However if mAutoCreateRoot is true, the root node will be created on-demand 288 // so the tree/block is always enabled. 289 boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null); 290 if (mMasterPart != null) { 291 Section section = mMasterPart.getSection(); 292 if (section.getEnabled() != exists) { 293 section.setEnabled(exists); 294 for (Control c : section.getChildren()) { 295 c.setEnabled(exists); 296 } 297 } 298 } 299 } 300 }; 301 302 /** Listener to update the root node if the target of the file is changed because of a 303 * SDK location change or a project target change */ 304 final ITargetChangeListener targetListener = new TargetChangeListener() { 305 @Override 306 public IProject getProject() { 307 if (mEditor != null) { 308 return mEditor.getProject(); 309 } 310 311 return null; 312 } 313 314 @Override 315 public void reload() { 316 // If a details part has been created, we need to "refresh" it too. 317 if (mDetailsPart != null) { 318 // The details part does not directly expose access to its internal 319 // page book. Instead it is possible to resize the page book to 0 and then 320 // back to its original value, which has the side effect of removing all 321 // existing cached pages. 322 int limit = mDetailsPart.getPageLimit(); 323 mDetailsPart.setPageLimit(0); 324 mDetailsPart.setPageLimit(limit); 325 } 326 // Refresh the tree, preserving the selection if possible. 327 mTreeViewer.refresh(); 328 } 329 }; 330 331 // Setup the listeners 332 changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */); 333 334 // Listen on resource framework changes to refresh the tree 335 AdtPlugin.getDefault().addTargetListener(targetListener); 336 337 // Remove listeners when the tree widget gets disposed. 338 tree.addDisposeListener(new DisposeListener() { 339 public void widgetDisposed(DisposeEvent e) { 340 if (mUiRootNode != null) { 341 UiElementNode node = mUiRootNode.getUiParent() != null ? 342 mUiRootNode.getUiParent() : 343 mUiRootNode; 344 345 if (node != null) { 346 node.removeUpdateListener(mUiRefreshListener); 347 } 348 mUiRootNode.removeUpdateListener(mUiEnableListener); 349 } 350 351 AdtPlugin.getDefault().removeTargetListener(targetListener); 352 if (mClipboard != null) { 353 mClipboard.dispose(); 354 mClipboard = null; 355 } 356 } 357 }); 358 359 // Get a new clipboard reference. It is disposed when the tree is disposed. 360 mClipboard = new Clipboard(tree.getDisplay()); 361 362 return tree; 363 } 364 365 /** 366 * Changes the UI root node and the descriptor filters of the tree. 367 * <p/> 368 * This removes the listeners attached to the old root node and reattaches them to the 369 * new one. 370 * 371 * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are 372 * to be manipulated by this tree view. In general this is the manifest UI node or the 373 * application UI node. This cannot be null. 374 * @param descriptorFilters A list of descriptors of the elements to be displayed as root in 375 * this tree view. Use null or an empty list to accept any kind of node. 376 * @param forceRefresh If tree, forces the tree to refresh 377 */ 378 public void changeRootAndDescriptors(UiElementNode uiRootNode, 379 ElementDescriptor[] descriptorFilters, boolean forceRefresh) { 380 UiElementNode node; 381 382 // Remove previous listeners if any 383 if (mUiRootNode != null) { 384 node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; 385 node.removeUpdateListener(mUiRefreshListener); 386 mUiRootNode.removeUpdateListener(mUiEnableListener); 387 } 388 389 mUiRootNode = uiRootNode; 390 mDescriptorFilters = descriptorFilters; 391 392 mTreeViewer.setContentProvider( 393 new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters)); 394 395 // Listen on structural changes on the root node of the tree 396 // If the node has a parent, listen on the parent instead. 397 if (mUiRootNode != null) { 398 node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; 399 400 if (node != null) { 401 node.addUpdateListener(mUiRefreshListener); 402 } 403 404 // Use the root node to listen to its presence. 405 mUiRootNode.addUpdateListener(mUiEnableListener); 406 407 // Initialize the enabled/disabled state 408 mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */); 409 } 410 411 if (forceRefresh) { 412 mTreeViewer.refresh(); 413 } 414 415 createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit()); 416 } 417 418 /** 419 * Creates the buttons next to the tree. 420 */ 421 private void createButtons(FormToolkit toolkit, Composite grid) { 422 423 mUiTreeActions = new UiTreeActions(); 424 425 Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1); 426 button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING)); 427 mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH); 428 SectionHelper.addControlTooltip(mAddButton, "Adds a new element."); 429 mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | 430 GridData.VERTICAL_ALIGN_BEGINNING)); 431 432 mAddButton.addSelectionListener(new SelectionAdapter() { 433 @Override 434 public void widgetSelected(SelectionEvent e) { 435 super.widgetSelected(e); 436 doTreeAdd(); 437 } 438 }); 439 440 mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH); 441 SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element."); 442 mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 443 444 mRemoveButton.addSelectionListener(new SelectionAdapter() { 445 @Override 446 public void widgetSelected(SelectionEvent e) { 447 super.widgetSelected(e); 448 doTreeRemove(); 449 } 450 }); 451 452 mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH); 453 SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up."); 454 mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 455 456 mUpButton.addSelectionListener(new SelectionAdapter() { 457 @Override 458 public void widgetSelected(SelectionEvent e) { 459 super.widgetSelected(e); 460 doTreeUp(); 461 } 462 }); 463 464 mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH); 465 SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down."); 466 mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 467 468 mDownButton.addSelectionListener(new SelectionAdapter() { 469 @Override 470 public void widgetSelected(SelectionEvent e) { 471 super.widgetSelected(e); 472 doTreeDown(); 473 } 474 }); 475 476 adjustTreeButtons(TreeSelection.EMPTY); 477 } 478 479 private void createTreeContextMenu(Tree tree) { 480 MenuManager menuManager = new MenuManager(); 481 menuManager.setRemoveAllWhenShown(true); 482 menuManager.addMenuListener(new IMenuListener() { 483 /** 484 * The menu is about to be shown. The menu manager has already been 485 * requested to remove any existing menu item. This method gets the 486 * tree selection and if it is of the appropriate type it re-creates 487 * the necessary actions. 488 */ 489 public void menuAboutToShow(IMenuManager manager) { 490 ISelection selection = mTreeViewer.getSelection(); 491 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 492 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 493 doCreateMenuAction(manager, selected); 494 return; 495 } 496 doCreateMenuAction(manager, null /* ui_node */); 497 } 498 }); 499 Menu contextMenu = menuManager.createContextMenu(tree); 500 tree.setMenu(contextMenu); 501 } 502 503 /** 504 * Adds the menu actions to the context menu when the given UI node is selected in 505 * the tree view. 506 * 507 * @param manager The context menu manager 508 * @param selected The UI nodes selected in the tree. Can be null, in which case the root 509 * is to be modified. 510 */ 511 private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) { 512 if (selected != null) { 513 boolean hasXml = false; 514 for (UiElementNode uiNode : selected) { 515 if (uiNode.getXmlNode() != null) { 516 hasXml = true; 517 break; 518 } 519 } 520 521 if (hasXml) { 522 manager.add(new CopyCutAction(getEditor(), getClipboard(), 523 null, selected, true /* cut */)); 524 manager.add(new CopyCutAction(getEditor(), getClipboard(), 525 null, selected, false /* cut */)); 526 527 // Can't paste with more than one element selected (the selection is the target) 528 if (selected.size() <= 1) { 529 // Paste is not valid if it would add a second element on a terminal element 530 // which parent is a document -- an XML document can only have one child. This 531 // means paste is valid if the current UI node can have children or if the 532 // parent is not a document. 533 UiElementNode ui_root = selected.get(0).getUiRoot(); 534 if (ui_root.getDescriptor().hasChildren() || 535 !(ui_root.getUiParent() instanceof UiDocumentNode)) { 536 manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0))); 537 } 538 } 539 manager.add(new Separator()); 540 } 541 } 542 543 // Append "add" and "remove" actions. They do the same thing as the add/remove 544 // buttons on the side. 545 IconFactory factory = IconFactory.getInstance(); 546 547 // "Add" makes sense only if there's 0 or 1 item selected since the 548 // one selected item becomes the target. 549 if (selected == null || selected.size() <= 1) { 550 manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$ 551 @Override 552 public void run() { 553 super.run(); 554 doTreeAdd(); 555 } 556 }); 557 } 558 559 if (selected != null) { 560 if (selected != null) { 561 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$ 562 @Override 563 public void run() { 564 super.run(); 565 doTreeRemove(); 566 } 567 }); 568 } 569 manager.add(new Separator()); 570 571 manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$ 572 @Override 573 public void run() { 574 super.run(); 575 doTreeUp(); 576 } 577 }); 578 manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$ 579 @Override 580 public void run() { 581 super.run(); 582 doTreeDown(); 583 } 584 }); 585 } 586 } 587 588 589 /** 590 * This is called by the tree when a selection is made. 591 * It enables/disables the buttons associated with the tree depending on the current 592 * selection. 593 * 594 * @param selection The current tree selection (same as mTreeViewer.getSelection()) 595 */ 596 private void adjustTreeButtons(ISelection selection) { 597 mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection); 598 mUpButton.setEnabled(canDoTreeUp(selection)); 599 mDownButton.setEnabled(canDoTreeDown(selection)); 600 } 601 602 /** 603 * An adapter/wrapper to use the add/remove/up/down tree edit actions. 604 */ 605 private class UiTreeActions extends UiActions { 606 @Override 607 protected UiElementNode getRootNode() { 608 return mUiRootNode; 609 } 610 611 @Override 612 protected void selectUiNode(UiElementNode uiNodeToSelect) { 613 // Select the new item 614 if (uiNodeToSelect != null) { 615 LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>(); 616 for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode; 617 ui_node = ui_node.getUiParent()) { 618 segments.add(0, ui_node); 619 } 620 if (segments.size() > 0) { 621 mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray()))); 622 } else { 623 mTreeViewer.setSelection(null); 624 } 625 } 626 } 627 628 @Override 629 public void commitPendingXmlChanges() { 630 commitManagedForm(); 631 } 632 } 633 634 /** 635 * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's 636 * something else in there). 637 * 638 * @return A new list of {@link UiElementNode} with at least one item or null. 639 */ 640 @SuppressWarnings("unchecked") 641 private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) { 642 ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); 643 644 for (Iterator it = selection.iterator(); it.hasNext(); ) { 645 Object selectedObj = it.next(); 646 647 if (selectedObj instanceof UiElementNode) { 648 selected.add((UiElementNode) selectedObj); 649 } 650 } 651 652 return selected.size() > 0 ? selected : null; 653 } 654 655 /** 656 * Called when the "Add..." button next to the tree view is selected. 657 * 658 * Displays a selection dialog that lets the user select which kind of node 659 * to create, depending on the current selection. 660 */ 661 private void doTreeAdd() { 662 UiElementNode ui_node = mUiRootNode; 663 ISelection selection = mTreeViewer.getSelection(); 664 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 665 ITreeSelection tree_selection = (ITreeSelection) selection; 666 Object first = tree_selection.getFirstElement(); 667 if (first != null && first instanceof UiElementNode) { 668 ui_node = (UiElementNode) first; 669 } 670 } 671 672 mUiTreeActions.doAdd( 673 ui_node, 674 mDescriptorFilters, 675 mTreeViewer.getControl().getShell(), 676 (ILabelProvider) mTreeViewer.getLabelProvider()); 677 } 678 679 /** 680 * Called when the "Remove" button is selected. 681 * 682 * If the tree has a selection, remove it. 683 * This simply deletes the XML node attached to the UI node: when the XML model fires the 684 * update event, the tree will get refreshed. 685 */ 686 protected void doTreeRemove() { 687 ISelection selection = mTreeViewer.getSelection(); 688 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 689 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 690 mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell()); 691 } 692 } 693 694 /** 695 * Called when the "Up" button is selected. 696 * <p/> 697 * If the tree has a selection, move it up, either in the child list or as the last child 698 * of the previous parent. 699 */ 700 protected void doTreeUp() { 701 ISelection selection = mTreeViewer.getSelection(); 702 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 703 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 704 mUiTreeActions.doUp(selected, mDescriptorFilters); 705 } 706 } 707 708 /** 709 * Checks whether the "up" action can be done on the current selection. 710 * 711 * @param selection The current tree selection. 712 * @return True if all the selected nodes can be moved up. 713 */ 714 protected boolean canDoTreeUp(ISelection selection) { 715 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 716 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 717 return mUiTreeActions.canDoUp(selected, mDescriptorFilters); 718 } 719 720 return false; 721 } 722 723 /** 724 * Called when the "Down" button is selected. 725 * 726 * If the tree has a selection, move it down, either in the same child list or as the 727 * first child of the next parent. 728 */ 729 protected void doTreeDown() { 730 ISelection selection = mTreeViewer.getSelection(); 731 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 732 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 733 mUiTreeActions.doDown(selected, mDescriptorFilters); 734 } 735 } 736 737 /** 738 * Checks whether the "down" action can be done on the current selection. 739 * 740 * @param selection The current tree selection. 741 * @return True if all the selected nodes can be moved down. 742 */ 743 protected boolean canDoTreeDown(ISelection selection) { 744 if (!selection.isEmpty() && selection instanceof ITreeSelection) { 745 ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); 746 return mUiTreeActions.canDoDown(selected, mDescriptorFilters); 747 } 748 749 return false; 750 } 751 752 /** 753 * Commits the current managed form (the one associated with our master part). 754 * As a side effect, this will commit the current UiElementDetails page. 755 */ 756 void commitManagedForm() { 757 if (mManagedForm != null) { 758 mManagedForm.commit(false /* onSave */); 759 } 760 } 761 762 /* Implements ICommitXml for CopyCutAction */ 763 public void commitPendingXmlChanges() { 764 commitManagedForm(); 765 } 766 767 @Override 768 protected void createToolBarActions(IManagedForm managedForm) { 769 // Pass. Not used, toolbar actions are defined by createSectionActions(). 770 } 771 772 @Override 773 protected void registerPages(DetailsPart inDetailsPart) { 774 // Keep a reference on the details part (the super class doesn't provide a getter 775 // for it.) 776 mDetailsPart = inDetailsPart; 777 778 // The page selection mechanism does not use pages registered by association with 779 // a node class. Instead it uses a custom details page provider that provides a 780 // new UiElementDetail instance for each node instance. A limit of 5 pages is 781 // then set (the value is arbitrary but should be reasonable) for the internal 782 // page book. 783 inDetailsPart.setPageLimit(5); 784 785 final UiTreeBlock tree = this; 786 787 inDetailsPart.setPageProvider(new IDetailsPageProvider() { 788 public IDetailsPage getPage(Object key) { 789 if (key instanceof UiElementNode) { 790 return new UiElementDetail(tree); 791 } 792 return null; 793 } 794 795 public Object getPageKey(Object object) { 796 return object; // use node object as key 797 } 798 }); 799 } 800 801 /** 802 * An alphabetic sort action for the tree viewer. 803 */ 804 private class TreeSortAction extends Action { 805 806 private ViewerComparator mComparator; 807 808 public TreeSortAction() { 809 super("Sorts elements alphabetically.", AS_CHECK_BOX); 810 setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$ 811 812 if (mTreeViewer != null) { 813 boolean is_sorted = mTreeViewer.getComparator() != null; 814 setChecked(is_sorted); 815 } 816 } 817 818 /** 819 * Called when the button is selected. Toggles the tree viewer comparator. 820 */ 821 @Override 822 public void run() { 823 if (mTreeViewer == null) { 824 notifyResult(false /*success*/); 825 return; 826 } 827 828 ViewerComparator comp = mTreeViewer.getComparator(); 829 if (comp != null) { 830 // Tree is currently sorted. 831 // Save currently comparator and remove it 832 mComparator = comp; 833 mTreeViewer.setComparator(null); 834 } else { 835 // Tree is not currently sorted. 836 // Reuse or add a new comparator. 837 if (mComparator == null) { 838 mComparator = new ViewerComparator(); 839 } 840 mTreeViewer.setComparator(mComparator); 841 } 842 843 notifyResult(true /*success*/); 844 } 845 } 846 847 /** 848 * A filter on descriptor for the tree viewer. 849 * <p/> 850 * The tree viewer will contain many of these actions and only one can be enabled at a 851 * given time. When no action is selected, everything is displayed. 852 * <p/> 853 * Since "radio"-like actions do not allow for unselecting all of them, we manually 854 * handle the exclusive radio button-like property: when an action is selected, it manually 855 * removes all other actions as needed. 856 */ 857 private class DescriptorFilterAction extends Action { 858 859 private final ElementDescriptor mDescriptor; 860 private ViewerFilter mFilter; 861 862 public DescriptorFilterAction(ElementDescriptor descriptor) { 863 super(String.format("Displays only %1$s elements.", descriptor.getUiName()), 864 AS_CHECK_BOX); 865 866 mDescriptor = descriptor; 867 setImageDescriptor(descriptor.getImageDescriptor()); 868 } 869 870 /** 871 * Called when the button is selected. 872 * <p/> 873 * Find any existing {@link DescriptorFilter}s and remove them. Install ours. 874 */ 875 @Override 876 public void run() { 877 super.run(); 878 879 if (isChecked()) { 880 if (mFilter == null) { 881 // create filter when required 882 mFilter = new DescriptorFilter(this); 883 } 884 885 // we add our filter first, otherwise the UI might show the full list 886 mTreeViewer.addFilter(mFilter); 887 888 // Then remove the any other filters except ours. There should be at most 889 // one other filter, since that's how the actions are made to look like 890 // exclusive radio buttons. 891 for (ViewerFilter filter : mTreeViewer.getFilters()) { 892 if (filter instanceof DescriptorFilter && filter != mFilter) { 893 DescriptorFilterAction action = ((DescriptorFilter) filter).getAction(); 894 action.setChecked(false); 895 mTreeViewer.removeFilter(filter); 896 } 897 } 898 } else if (mFilter != null){ 899 mTreeViewer.removeFilter(mFilter); 900 } 901 } 902 903 /** 904 * Filters the tree viewer for the given descriptor. 905 * <p/> 906 * The filter is linked to the action so that an action can iterate through the list 907 * of filters and un-select the actions. 908 */ 909 private class DescriptorFilter extends ViewerFilter { 910 911 private final DescriptorFilterAction mAction; 912 913 public DescriptorFilter(DescriptorFilterAction action) { 914 mAction = action; 915 } 916 917 public DescriptorFilterAction getAction() { 918 return mAction; 919 } 920 921 /** 922 * Returns true if an element should be displayed, that if the element or 923 * any of its parent matches the requested descriptor. 924 */ 925 @Override 926 public boolean select(Viewer viewer, Object parentElement, Object element) { 927 while (element instanceof UiElementNode) { 928 UiElementNode uiNode = (UiElementNode)element; 929 if (uiNode.getDescriptor() == mDescriptor) { 930 return true; 931 } 932 element = uiNode.getUiParent(); 933 } 934 return false; 935 } 936 } 937 } 938 939 } 940